This is an automated email from the ASF dual-hosted git repository.

sblackmon pushed a commit to branch feat-7-profile
in repository https://gitbox.apache.org/repos/asf/streams-activitypub.git

commit cacc07d026932e1caefa961c5a108315a09359f9
Author: Steve Blackmon <sblack...@apache.org>
AuthorDate: Sat Mar 2 18:01:19 2024 -0700

    feat: implement get profile endpoint
    
    resolves apache/streams-activitypub#7
    
    Signed-off-by: Steve Blackmon <sblack...@apache.org>
---
 doap.ttl                                           |  15 ++-
 pom.xml                                            |  59 +++++-----
 .../activitypub/api/pojo/ProfileQueryRequest.json  |  14 +++
 .../activitypub/api/pojo/ProfileQueryResponse.json |  78 +++++++++++++
 .../api/pojo/WebfingerQueryResponse.json           |   1 +
 .../streams/activitypub/api/ProfileApi.scala       |  18 +++
 streams-activitypub-dist/Dockerfile                |   2 +
 streams-activitypub-dist/pom.xml                   |  43 ++++++-
 streams-activitypub-graph/pom.xml                  |  34 +++++-
 .../graph/config/BaseGraphImplConfig.json}         |  15 ++-
 .../graph/config/ProfileGraphImplConfig.json       |  12 ++
 .../graph/config/WebfingerGraphImplConfig.json     |  12 +-
 .../resources/framing/ProfileQueryResponse.jsonld  |  17 +++
 .../framing/WebfingerQueryResponse.jsonld          |  18 +++
 .../src/main/resources/queries/profileAsk.sparql   |   7 ++
 .../main/resources/queries/profileConstruct.sparql |  16 +++
 .../src/main/resources/queries/webfingerAsk.sparql |   4 +
 .../resources/queries/webfingerConstruct.sparql    |   8 ++
 .../src/main/resources/reference.conf              |   8 +-
 .../activitypub/graph/impl/BaseGraphImpl.scala     |  61 ++++++++++
 .../activitypub/graph/impl/ProfileGraphImpl.scala  | 127 +++++++++++++++++++++
 .../graph/impl/WebfingerGraphImpl.scala            |  98 ++++++++++------
 .../ActivityPubGraphTestSuiteExtensionConfig.json  |  13 +--
 .../testGetProfileForKnownPersonOutput.jsonld      |  13 +++
 ...erQueryForKnownAbbreviatedResourceOutput.jsonld |   3 +
 ...tWebfingerQueryForKnownUriResourceOutput.jsonld |   3 +
 .../src/test/resources/application.conf            |  11 +-
 .../graph/test/ActivityPubGraphTestSuite.scala     |   6 +-
 .../test/ActivityPubGraphTestSuiteExtension.scala  |  45 +++++---
 .../graph/test/cases/JsonLdFrameValidityTest.scala |  43 +++++++
 .../graph/test/cases/JsonSchemaValidityTest.scala  |  44 +++++++
 .../graph/test/cases/ProfileGraphImplTest.scala    |  72 ++++++++++++
 .../graph/test/cases/WebfingerGraphImplTest.scala  |  34 +++---
 .../activitypub/servlets/WebfingerServlet.scala    |   1 +
 streams-activitypub-utils/pom.xml                  |  18 +++
 .../AcctPrefixResourceToResourceURISwap.scala      |  20 +++-
 .../streams/activitypub/utils/JsonLdHelper.scala   |  64 +++++++++++
 .../testProfileCleanupInput.jsonld                 |  47 ++++++++
 .../testProfileCleanupTemplate.jsonld              |  15 +++
 .../utils/test/ActivityPubUtilsTestSuite.scala     |  26 +++++
 .../cases}/AcctPrefixResourceURISwapTest.scala     |   0
 .../utils/test/cases/JsonLdHelperTest.scala        |  36 ++++++
 .../test/cases/WebappServerAvailableTest.scala     |   4 -
 .../webapp/test/cases/WebfingerServletTest.scala   |   2 -
 44 files changed, 1037 insertions(+), 150 deletions(-)

diff --git a/doap.ttl b/doap.ttl
index 83bff69..9e54b82 100644
--- a/doap.ttl
+++ b/doap.ttl
@@ -1,7 +1,6 @@
 @base         <http://streams.apache.org.org/rdf> .
 @prefix as:   <https://www.w3.org/ns/activitystreams#> .
 @prefix asfext: <http://projects.apache.org/ns/asfext#> .
-#@prefix dc:   <http://purl.org/dc/terms/> .
 @prefix doap: <http://usefulinc.com/ns/doap#> .
 @prefix foaf: <http://xmlns.com/foaf/0.1/> .
 @prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@@ -34,15 +33,15 @@
 .
 
 <https://mastodon.social/users/steveblackmon> a as:Person ;
-   as:id "https://mastodon.social/@steveblackmon"; ;
-   as:name "Steve Blackmon"@en ;
-   vcard:given-name "Steve"@en ;
-   vcard:family-name "Blackmon"@en ;
+   as:name "Steve Blackmon" ;
+   vcard:given-name "Steve" ;
+   vcard:family-name "Blackmon" ;
    vcard:email "sblack...@apache.org" ;
-   as:summary "Techie, Dad, Salesforce, PMC Chair of 
http://streams.apache.org"@en ;
-   as:preferredUsername "steveblackmon"@en ;
+   as:summary "Techie, Dad, Salesforce, PMC Chair of 
http://streams.apache.org"; ;
+   as:preferredUsername "steveblackmon" ;
+   as:url <https://people.apache.org/~sblackmon> ;
    as:inbox <https://mastodon.social/users/steveblackmon/inbox> ;
    as:outbox <https://mastodon.social/users/steveblackmon/outbox> ;
    as:followers <https://mastodon.social/users/steveblackmon/followers> ;
    as:following <https://mastodon.social/users/steveblackmon/following> ;
-.
+   .
diff --git a/pom.xml b/pom.xml
index 0da114e..fe1ffad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -364,6 +364,7 @@
         <httpcomponents.client.version>4.5.13</httpcomponents.client.version>
         <jackson.version>2.16.0</jackson.version>
         <jakarta.version>6.0.0</jakarta.version>
+        <jakarta-json.version>2.0.1</jakarta-json.version>
         <jakarta-ws.version>3.1.0</jakarta-ws.version>
         <jena.version>5.0.0-rc1</jena.version>
         <jetty.version>12.0.6</jetty.version>
@@ -378,6 +379,7 @@
         <jaxbutil.version>2.1.0</jaxbutil.version>
         <netty.version>4.1.105.Final</netty.version>
         <slf4j.version>2.0.9</slf4j.version>
+        <titanium.version>1.4.0</titanium.version>
         <log4j.version>2.22.0</log4j.version>
         <logback.version>1.4.14</logback.version>
         <protobuf.version>3.16.3</protobuf.version>
@@ -454,20 +456,28 @@
             </extension>
         </extensions>
         <plugins>
+            <plugin>
+                <groupId>com.googlecode.maven-download-plugin</groupId>
+                <artifactId>download-maven-plugin</artifactId>
+            </plugin>
             <plugin>
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
             </plugin>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
             </plugin>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-site-plugin</artifactId>
             </plugin>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-resources-plugin</artifactId>
             </plugin>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-remote-resources-plugin</artifactId>
             </plugin>
         </plugins>
@@ -567,6 +577,7 @@
                         <addCompileSourceRoot>true</addCompileSourceRoot>
                         <annotationStyle>none</annotationStyle>
                         
<customAnnotator>org.apache.streams.plugins.JuneauPojoAnnotator</customAnnotator>
+                        
<customDateTimePattern>yyyy-MM-dd'T'HH:mm:ssZ</customDateTimePattern>
                         <generateBuilders>true</generateBuilders>
                         
<includeGeneratedAnnotation>false</includeGeneratedAnnotation>
                     </configuration>
@@ -627,6 +638,7 @@
                 <plugin>
                     <groupId>org.codehaus.mojo</groupId>
                     <artifactId>build-helper-maven-plugin</artifactId>
+                    <version>${build-helper.plugin.version}</version>
                     <executions>
                         <execution>
                             <id>add-schemas</id>
@@ -690,15 +702,20 @@
                     </configuration>
                     <dependencies>
                         <dependency>
-                            <groupId>org.apache.maven.surefire</groupId>
-                            <artifactId>surefire-junit-platform</artifactId>
-                            <version>${surefire.version}</version>
+                            <groupId>org.junit.jupiter</groupId>
+                            <artifactId>junit-jupiter-engine</artifactId>
+                            <version>${junit.version}</version>
                         </dependency>
                         <dependency>
                             <groupId>org.junit.jupiter</groupId>
-                            <artifactId>junit-jupiter-engine</artifactId>
+                            <artifactId>junit-jupiter-params</artifactId>
                             <version>${junit.version}</version>
                         </dependency>
+                        <dependency>
+                            <groupId>org.apache.maven.surefire</groupId>
+                            <artifactId>surefire-junit-platform</artifactId>
+                            <version>${surefire.version}</version>
+                        </dependency>
                         <dependency>
                             <groupId>org.junit.platform</groupId>
                             
<artifactId>junit-platform-suite-engine</artifactId>
@@ -793,34 +810,6 @@
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-dependency-plugin</artifactId>
                     <version>${dependency.plugin.version}</version>
-                    <executions>
-                        <execution>
-                            <id>unpack-schemas</id>
-                            <phase>generate-sources</phase>
-                            <goals>
-                                <goal>unpack-dependencies</goal>
-                            </goals>
-                            <configuration>
-                                <excludeTransitive>true</excludeTransitive>
-                                
<includeGroupIds>org.apache.streams</includeGroupIds>
-                                <includeScope>compile</includeScope>
-                                <includeTypes>zip</includeTypes>
-                            </configuration>
-                        </execution>
-                        <execution>
-                            <id>unpack-test-resources</id>
-                            <phase>process-test-resources</phase>
-                            <goals>
-                                <goal>unpack-dependencies</goal>
-                            </goals>
-                            <configuration>
-                                <excludeTransitive>true</excludeTransitive>
-                                
<includeGroupIds>org.apache.streams</includeGroupIds>
-                                <includeScope>test</includeScope>
-                                <includeTypes>zip</includeTypes>
-                            </configuration>
-                        </execution>
-                    </executions>
                 </plugin>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
@@ -903,6 +892,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.platform</groupId>
             <artifactId>junit-platform-suite-engine</artifactId>
diff --git 
a/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/ProfileQueryRequest.json
 
b/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/ProfileQueryRequest.json
new file mode 100644
index 0000000..f045477
--- /dev/null
+++ 
b/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/ProfileQueryRequest.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema";,
+  "id": "#",
+  "type": "object",
+  "javaType": 
"org.apache.streams.activitypub.api.pojo.profile.ProfileQueryRequest",
+  "javaInterfaces": [
+    "java.io.Serializable"
+  ],
+  "properties": {
+    "username": {
+      "type": "string"
+    }
+  }
+}
diff --git 
a/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/ProfileQueryResponse.json
 
b/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/ProfileQueryResponse.json
new file mode 100755
index 0000000..85639b5
--- /dev/null
+++ 
b/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/ProfileQueryResponse.json
@@ -0,0 +1,78 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema";,
+  "id": "#",
+  "type": "object",
+  "$license": [
+    "http://www.apache.org/licenses/LICENSE-2.0";
+  ],
+  "javaType" : 
"org.apache.streams.activitypub.api.pojo.profile.ProfileQueryResponse",
+  "javaInterfaces": ["java.io.Serializable"],
+  "additionalProperties": false,
+  "properties": {
+    "@context": {
+      "type": "string",
+      "format": "uri"
+    },
+    "id": {
+      "type": "string",
+      "format": "uri"
+    },
+    "@type": {
+      "type": "string",
+        "enum": [
+          "Organization",
+          "Person",
+          "Profile"
+        ]
+    },
+    "following": {
+      "type": "string",
+      "format": "uri"
+    },
+    "followers": {
+      "type": "string",
+      "format": "uri"
+    },
+    "inbox": {
+      "type": "string",
+      "format": "uri"
+    },
+    "outbox": {
+      "type": "string",
+      "format": "uri"
+    },
+    "preferredUsername": {
+      "type": "string"
+    },
+    "name": {
+      "type": "string"
+    },
+    "summary": {
+      "type": "string"
+    },
+    "url": {
+      "type": "string",
+      "format": "uri"
+    },
+    "published": {
+      "type": "string",
+      "format": "date-time"
+    },
+    "publicKey": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uri"
+        },
+        "owner": {
+          "type": "string",
+          "format": "uri"
+        },
+        "publicKeyPem": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git 
a/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/WebfingerQueryResponse.json
 
b/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/WebfingerQueryResponse.json
index d8ee708..464ff5d 100755
--- 
a/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/WebfingerQueryResponse.json
+++ 
b/streams-activitypub-api/src/main/jsonschema/org/apache/streams/activitypub/api/pojo/WebfingerQueryResponse.json
@@ -7,6 +7,7 @@
   ],
   "javaType" : 
"org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryResponse",
   "javaInterfaces": ["java.io.Serializable"],
+  "additionalProperties": false,
   "properties": {
     "subject": {
       "type": "string"
diff --git 
a/streams-activitypub-api/src/main/scala/org/apache/streams/activitypub/api/ProfileApi.scala
 
b/streams-activitypub-api/src/main/scala/org/apache/streams/activitypub/api/ProfileApi.scala
new file mode 100644
index 0000000..94e60f8
--- /dev/null
+++ 
b/streams-activitypub-api/src/main/scala/org/apache/streams/activitypub/api/ProfileApi.scala
@@ -0,0 +1,18 @@
+package org.apache.streams.activitypub.api
+
+import org.apache.juneau.collections.JsonMap
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryRequest
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryResponse
+
+import java.net.URI
+
+trait ProfileApi {
+
+  /**
+   * Returns a JSON[-LD] representation of the profile page data.
+   *
+   * @return ProfileQueryResponse
+   */
+  def profile(request: ProfileQueryRequest): ProfileQueryResponse
+
+}
diff --git a/streams-activitypub-dist/Dockerfile 
b/streams-activitypub-dist/Dockerfile
index 8534e27..7f59e35 100644
--- a/streams-activitypub-dist/Dockerfile
+++ b/streams-activitypub-dist/Dockerfile
@@ -3,6 +3,8 @@ MAINTAINER d...@streams.apache.org
 LABEL Description="apache-streams-activitypub-dist"
 WORKDIR /
 RUN mkdir -p /etc/fuseki
+RUN mkdir -p /etc/rdf
+ADD target/classes/*.ttl /etc/rdf/
 ADD target/lib/* /usr/local/tomcat/lib/
 ADD target/webapps/streams-activitypub.war /usr/local/tomcat/webapps/ROOT.war
 ADD target/webapps/jena-fuseki.war /usr/local/tomcat/webapps/fuseki.war
diff --git a/streams-activitypub-dist/pom.xml b/streams-activitypub-dist/pom.xml
index be6cb27..3e77533 100755
--- a/streams-activitypub-dist/pom.xml
+++ b/streams-activitypub-dist/pom.xml
@@ -32,6 +32,18 @@ under the License.
 
     <dependencies>
 
+        <dependency>
+            <groupId>org.apache.streams.activitypub</groupId>
+            <artifactId>streams-activitypub-graph</artifactId>
+            <version>${project.version}</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.streams.activitypub</groupId>
+            <artifactId>streams-activitypub-graph</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+        </dependency>
         <dependency>
             <groupId>org.apache.streams.activitypub</groupId>
             <artifactId>streams-activitypub-webapp</artifactId>
@@ -57,6 +69,36 @@ under the License.
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
+                   <execution>
+                       <id>unpack-resources</id>
+                       <phase>prepare-package</phase>
+                       <goals>
+                           <goal>unpack-dependencies</goal>
+                       </goals>
+                       <configuration>
+                           <excludeTransitive>true</excludeTransitive>
+                           
<includeArtifactIds>streams-activitypub-graph</includeArtifactIds>
+                           
<includeGroupIds>org.apache.streams.activitypub</includeGroupIds>
+                           <includeTypes>jar</includeTypes>
+                           <includes>*.owl,*.rdf,*.ttl</includes>
+                           
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
+                       </configuration>
+                   </execution>
+                   <execution>
+                       <id>unpack-test-resources</id>
+                       <phase>prepare-package</phase>
+                       <goals>
+                           <goal>unpack-dependencies</goal>
+                       </goals>
+                       <configuration>
+                           <excludeTransitive>true</excludeTransitive>
+                           
<includeArtifactIds>streams-activitypub-graph</includeArtifactIds>
+                           
<includeGroupIds>org.apache.streams.activitypub</includeGroupIds>
+                           <includeTypes>test-jar</includeTypes>
+                           <includes>*.owl,*.rdf,*.ttl</includes>
+                           
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
+                       </configuration>
+                   </execution>
                    <execution>
                        <id>copy</id>
                        <phase>prepare-package</phase>
@@ -64,7 +106,6 @@ under the License.
                            <goal>copy</goal>
                        </goals>
                        <configuration>
-                           
<outputDirectory>${project.build.directory}/exploded</outputDirectory>
                            <artifactItems>
                                <artifactItem>
                                    
<groupId>org.apache.streams.activitypub</groupId>
diff --git a/streams-activitypub-graph/pom.xml 
b/streams-activitypub-graph/pom.xml
index 3049dba..b76d50c 100644
--- a/streams-activitypub-graph/pom.xml
+++ b/streams-activitypub-graph/pom.xml
@@ -40,6 +40,11 @@
             <artifactId>streams-activitypub-api</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.streams.activitypub</groupId>
+            <artifactId>streams-activitypub-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.streams.activitypub</groupId>
             <artifactId>streams-activitypub-utils</artifactId>
@@ -59,7 +64,6 @@
             <version>${jena.version}</version>
             <type>jar</type>
         </dependency>
-
         <!-- streams-graph test dependencies -->
         <dependency>
             <groupId>org.apache.jena</groupId>
@@ -79,6 +83,9 @@
         <sourceDirectory>src/main/scala</sourceDirectory>
         <testSourceDirectory>src/test/scala</testSourceDirectory>
         <resources>
+            <resource>
+                <directory>src/main/jsonschema</directory>
+            </resource>
             <resource>
                 <directory>src/main/resources</directory>
             </resource>
@@ -89,6 +96,29 @@
             </testResource>
         </testResources>
         <plugins>
+            <!--
+                  per w3c social working group mailing list discussion,
+                  this is the most up-to-date rdf/owl definition for
+                  activity streams 2 ontology
+                -->
+            <plugin>
+                <groupId>com.googlecode.maven-download-plugin</groupId>
+                <artifactId>download-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>download-activitystreams2-ontology</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>wget</goal>
+                        </goals>
+                        <configuration>
+                            
<url>https://raw.githubusercontent.com/steve-bate/activitypub-ontology/main/activitystreams2.ttl</url>
+                            <unpack>false</unpack>
+                            
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>org.jsonschema2pojo</groupId>
                 <artifactId>jsonschema2pojo-maven-plugin</artifactId>
@@ -167,7 +197,7 @@
                                 <resource>
                                     <directory>..</directory>
                                     <includes>
-                                        <include>*.ttl</include>
+                                        <include>doap.ttl</include>
                                     </includes>
                                 </resource>
                             </resources>
diff --git 
a/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
 
b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/BaseGraphImplConfig.json
similarity index 57%
copy from 
streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
copy to 
streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/BaseGraphImplConfig.json
index a1e2606..23b78bf 100755
--- 
a/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
+++ 
b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/BaseGraphImplConfig.json
@@ -2,7 +2,7 @@
   "$schema": "http://json-schema.org/draft-07/schema";,
   "id": "#",
   "type": "object",
-  "javaType": 
"org.apache.streams.activitypub.graph.test.config.ActivityPubGraphTestSuiteExtensionConfig",
+  "javaType": 
"org.apache.streams.activitypub.graph.config.BaseGraphImplConfig",
   "javaInterfaces": [
     "java.io.Serializable"
   ],
@@ -12,13 +12,20 @@
       "format": "uri",
       "required": true
     },
-    "testDatasetResource": {
+    "defaultDataset": {
       "type": "string",
-      "format": "uri",
       "required": true
     },
-    "testDatasetId": {
+    "defaultGraph": {
       "type": "string",
+      "required": false
+    },
+    "datasetResources": {
+      "type": "array",
+      "items": {
+        "type": "string",
+        "format": "uri"
+      },
       "required": true
     }
   }
diff --git 
a/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/ProfileGraphImplConfig.json
 
b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/ProfileGraphImplConfig.json
new file mode 100755
index 0000000..a8e53b9
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/ProfileGraphImplConfig.json
@@ -0,0 +1,12 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema";,
+  "id": "#",
+  "type": "object",
+  "javaType": 
"org.apache.streams.activitypub.graph.config.ProfileGraphImplConfig",
+  "javaInterfaces": [
+    "java.io.Serializable"
+  ],
+  "extends": {
+    "$ref": "BaseGraphImplConfig.json"
+  }
+}
diff --git 
a/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json
 
b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json
index 51250f8..9b0aac1 100755
--- 
a/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json
+++ 
b/streams-activitypub-graph/src/main/jsonschema/org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json
@@ -6,15 +6,7 @@
   "javaInterfaces": [
     "java.io.Serializable"
   ],
-  "properties": {
-    "fusekiEndpointURI": {
-      "type": "string",
-      "format": "uri",
-      "required": true
-    },
-    "defaultDatasetId": {
-      "type": "string",
-      "required": false
-    }
+  "extends": {
+    "$ref": "BaseGraphImplConfig.json"
   }
 }
diff --git 
a/streams-activitypub-graph/src/main/resources/framing/ProfileQueryResponse.jsonld
 
b/streams-activitypub-graph/src/main/resources/framing/ProfileQueryResponse.jsonld
new file mode 100644
index 0000000..7d2af72
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/resources/framing/ProfileQueryResponse.jsonld
@@ -0,0 +1,17 @@
+{
+  "@context": {
+      "@language": "en",
+      "@vocab": "https://www.w3.org/ns/activitystreams#";,
+      "summary": {"@id": "summary", "@language": "en"}
+  },
+  "id": {},
+  "@type": ["Profile", "Person", "Organization"],
+  "name": {},
+  "url": {},
+  "summary": {},
+  "preferredUsername": {},
+  "outbox": {},
+  "inbox": {},
+  "following": {},
+  "followers": {}
+}
diff --git 
a/streams-activitypub-graph/src/main/resources/framing/WebfingerQueryResponse.jsonld
 
b/streams-activitypub-graph/src/main/resources/framing/WebfingerQueryResponse.jsonld
new file mode 100644
index 0000000..ac73865
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/resources/framing/WebfingerQueryResponse.jsonld
@@ -0,0 +1,18 @@
+{
+  "@context": {
+      "@language": "en",
+      "@vocab": "https://www.w3.org/ns/activitystreams#";,
+      "subject": {"@id": "subject"},
+      "aliases": {"@id": "aliases", "@type": "@id"}
+  },
+  "subject": {},
+  "aliases": {},
+  "links": [
+    {
+      "rel": {},
+      "type": {},
+      "href": {},
+      "titles": {}
+    }
+  ]
+}
diff --git 
a/streams-activitypub-graph/src/main/resources/queries/profileAsk.sparql 
b/streams-activitypub-graph/src/main/resources/queries/profileAsk.sparql
new file mode 100644
index 0000000..e113f88
--- /dev/null
+++ b/streams-activitypub-graph/src/main/resources/queries/profileAsk.sparql
@@ -0,0 +1,7 @@
+BASE <>
+PREFIX as: <https://www.w3.org/ns/activitystreams#>
+PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+ASK WHERE {
+    ?resourceParam rdf:type ?type ;
+    FILTER(?type IN (as:Profile, as:Person, as:Organization)) .
+}
diff --git 
a/streams-activitypub-graph/src/main/resources/queries/profileConstruct.sparql 
b/streams-activitypub-graph/src/main/resources/queries/profileConstruct.sparql
new file mode 100644
index 0000000..48163b4
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/resources/queries/profileConstruct.sparql
@@ -0,0 +1,16 @@
+BASE <https://www.w3.org/ns/activitystreams#>
+PREFIX as: <https://www.w3.org/ns/activitystreams#>
+PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+CONSTRUCT {
+    ?subject as:id ?subject ;
+             rdf:type ?type ;
+             ?predicate ?object
+} WHERE {
+    BIND( ?subjectParam as ?subject ) {
+       SELECT * WHERE {
+           ?subject rdf:type ?type .
+           FILTER(?type IN (as:Profile, as:Person, as:Organization)) .
+           ?subject ?predicate ?object
+       }
+    }
+}
diff --git 
a/streams-activitypub-graph/src/main/resources/queries/webfingerAsk.sparql 
b/streams-activitypub-graph/src/main/resources/queries/webfingerAsk.sparql
new file mode 100644
index 0000000..72c64dc
--- /dev/null
+++ b/streams-activitypub-graph/src/main/resources/queries/webfingerAsk.sparql
@@ -0,0 +1,4 @@
+BASE <>
+ASK {
+    ?resourceParam ?p ?o
+}
diff --git 
a/streams-activitypub-graph/src/main/resources/queries/webfingerConstruct.sparql
 
b/streams-activitypub-graph/src/main/resources/queries/webfingerConstruct.sparql
new file mode 100644
index 0000000..6abbc2b
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/resources/queries/webfingerConstruct.sparql
@@ -0,0 +1,8 @@
+BASE <https://www.w3.org/ns/activitystreams#>
+PREFIX as: <https://www.w3.org/ns/activitystreams#>
+PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+CONSTRUCT {
+    <#response> <#subject> ?resource .
+} WHERE {
+    BIND( ?resourceParam as ?resource )
+}
diff --git a/streams-activitypub-graph/src/main/resources/reference.conf 
b/streams-activitypub-graph/src/main/resources/reference.conf
index b60d46c..908c6a0 100644
--- a/streams-activitypub-graph/src/main/resources/reference.conf
+++ b/streams-activitypub-graph/src/main/resources/reference.conf
@@ -1,3 +1,9 @@
-WebfingerGraphImpl = {
+BaseGraphImplConfig = {
   fusekiEndpointURI = "http://localhost:8080/fuseki/";
+  defaultDataset = "default"
+  datasetResources = [
+    "activitystreams2.ttl"
+  ]
 }
+ProfilePageGraphImplConfig = ${BaseGraphImplConfig}
+WebfingerGraphImplConfig = ${BaseGraphImplConfig}
diff --git 
a/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/BaseGraphImpl.scala
 
b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/BaseGraphImpl.scala
new file mode 100644
index 0000000..8ada4e8
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/BaseGraphImpl.scala
@@ -0,0 +1,61 @@
+package org.apache.streams.activitypub.graph.impl
+
+import org.apache.http.client.utils.URIBuilder
+import org.apache.jena.query.ResultSet
+import org.apache.jena.rdf.model.Model
+import org.apache.jena.riot.system.PrefixMap
+import org.apache.jena.riot.system.PrefixMapStd
+import org.apache.jena.shared.PrefixMapping
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTP
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTPBuilder
+import org.apache.juneau.json.JsonParser
+import org.apache.juneau.json.JsonSerializer
+import org.apache.streams.activitypub.graph.config.BaseGraphImplConfig
+import org.apache.streams.config.ComponentConfigurator
+
+import java.net.URI
+import scala.io.Source;
+
+/**
+ * Base class for jena/fuseki api implementations with common
+ * configuration, member variables, and member methods.
+ */
+object BaseGraphImpl {
+  private final val configurator: ComponentConfigurator[BaseGraphImplConfig] = 
new ComponentConfigurator(classOf[BaseGraphImplConfig])
+  final val config: BaseGraphImplConfig = configurator.detectConfiguration()
+  final val jsonParser: JsonParser = JsonParser.DEFAULT.copy()
+    .debug()
+    .ignoreUnknownBeanProperties()
+    .ignoreUnknownEnumValues()
+    .trimStrings()
+    .build()
+  final val jsonSerializer: JsonSerializer = 
JsonSerializer.DEFAULT_READABLE.copy()
+    .debug()
+    .ignoreUnknownBeanProperties()
+    .ignoreUnknownEnumValues()
+    .trimStrings()
+    .trimEmptyMaps()
+    .trimEmptyCollections()
+    .build()
+}
+
+abstract class BaseGraphImpl(config: BaseGraphImplConfig = 
BaseGraphImpl.config) {
+
+  private def serverUri = new URIBuilder(config.getFusekiEndpointURI).build()
+
+  private def datasetQueryUri = new 
URIBuilder(serverUri).setPath(s"${config.getDefaultDataset}/query").build()
+
+  final val sparqlBuilder: QueryExecutionHTTPBuilder = 
QueryExecutionHTTP.service(datasetQueryUri.toString).postQuery()
+
+  /**
+   * Check if the model returned from the query contains the requested resource
+   * and necessary details to generate a valid response.
+   *
+   * @param request
+   * @param model
+   * @return
+   */
+  def checkModelContainsSubject(resourceParamURI: URI, model: Model): Boolean 
= {
+    model.containsResource(model.getResource(resourceParamURI.toString))
+  }
+}
diff --git 
a/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/ProfileGraphImpl.scala
 
b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/ProfileGraphImpl.scala
new file mode 100644
index 0000000..96ca280
--- /dev/null
+++ 
b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/ProfileGraphImpl.scala
@@ -0,0 +1,127 @@
+package org.apache.streams.activitypub.graph.impl
+
+import org.apache.http.client.utils.URIBuilder
+import org.apache.jena.graph.Node
+import org.apache.jena.graph.NodeFactory
+import org.apache.jena.query.DatasetFactory
+import org.apache.jena.query.ParameterizedSparqlString
+import org.apache.jena.rdf.model.Resource
+import org.apache.jena.rdf.model.impl.ModelCom
+import org.apache.jena.riot.RDFFormat
+import org.apache.jena.riot.system.PrefixMap
+import org.apache.jena.riot.system.PrefixMapStd
+import org.apache.jena.riot.writer.JsonLD11Writer
+import org.apache.jena.shared.PrefixMapping
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTP
+import org.apache.jena.sparql.util.Context
+import org.apache.jena.sparql.util.PrefixMapping2
+import org.apache.juneau.collections.JsonMap
+import org.apache.juneau.json.JsonParser
+import org.apache.streams.activitypub.api.NodeinfoApi
+import org.apache.streams.activitypub.api.ProfileApi
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryRequest
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryResponse
+import org.apache.streams.activitypub.graph.config.ProfileGraphImplConfig
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl.jsonParser
+import org.apache.streams.activitypub.server.NodeinfoApiStaticImpl
+import org.apache.streams.activitypub.utils.JsonLdHelper
+import org.apache.streams.config.ComponentConfigurator
+
+import java.io.Reader
+import java.io.StringWriter
+import java.net.URI
+import scala.io.Source
+import scala.jdk.CollectionConverters.*
+
+/**
+ * Implementation of the Profile API using jena/fuseki as back-end.
+ */
+object ProfileGraphImpl {
+
+  private final val configurator: 
ComponentConfigurator[ProfileGraphImplConfig] = new 
ComponentConfigurator(classOf[ProfileGraphImplConfig])
+  final val config: ProfileGraphImplConfig = configurator.detectConfiguration()
+  final val DEFAULT: ProfileGraphImpl = new ProfileGraphImpl(config)
+
+  given nodeinfo : NodeinfoApi = NodeinfoApiStaticImpl.DEFAULT
+
+}
+
+class ProfileGraphImpl(config: ProfileGraphImplConfig) extends 
BaseGraphImpl(config) with ProfileApi {
+
+  import ProfileGraphImpl.nodeinfo
+
+  /**
+   * Get the profile page for a user
+   * @return
+   */
+  override def profile(request: ProfileQueryRequest): ProfileQueryResponse = {
+    doProfile(request)
+  }
+
+  def doProfile(request: ProfileQueryRequest)(using nodeinfo: NodeinfoApi): 
ProfileQueryResponse = {
+
+    val profileUri: URI = new 
URIBuilder(nodeinfo.nodeinfoQuery.getServer.getBaseUrl).setPathSegments("users",
 request.getUsername).build()
+    val profileNode: Node = NodeFactory.createURI(profileUri.toString)
+    val askQueryBody: String = 
Source.fromResource("queries/profileAsk.sparql").getLines.mkString
+    val askQueryTemplate: ParameterizedSparqlString = new 
ParameterizedSparqlString(askQueryBody)
+    askQueryTemplate.setParam("subjectParam", profileNode)
+    val askQuery = askQueryTemplate.asQuery()
+    val askExecution: QueryExecutionHTTP = 
sparqlBuilder.query(askQuery).build()
+    val askResult = askExecution.execAsk()
+    if (!askResult) throw new Exception("Requested resource not found in 
dataset.")
+
+//    val selectQueryBody: String = 
Source.fromResource("queries/profileSelect.sparql").getLines.mkString
+//    val selectQueryTemplate: ParameterizedSparqlString = new 
ParameterizedSparqlString(selectQueryBody)
+//    selectQueryTemplate.setIri("resourceParam", profileUri.toString)
+//    val selectQuery = selectQueryTemplate.asQuery()
+//    val selectExecution: QueryExecutionHTTP = 
sparqlBuilder.query(selectQuery).build()
+//    val resultSet: ResultSet = selectExecution.execSelect()
+//    val model: Model = RDFOutput.encodeAsModel(resultSet)
+
+    val constructQueryBody: String = 
Source.fromResource("queries/profileConstruct.sparql").getLines.mkString
+    val constructQueryTemplate: ParameterizedSparqlString = new 
ParameterizedSparqlString(constructQueryBody)
+    constructQueryTemplate.setIri("resourceParam", profileUri.toString)
+    val constructQuery = constructQueryTemplate.asQuery()
+    val constructExecution: QueryExecutionHTTP = 
sparqlBuilder.query(constructQuery).build()
+    val modelCom = new ModelCom(constructExecution.execConstruct().getGraph)
+
+    val subjectResource = modelCom.getResource(profileNode.getURI)
+
+    if (checkModelContainsSubject(profileUri, modelCom)) {
+      generateResponse(request, modelCom, subjectResource)
+    } else throw new Exception("Error generating response.")
+  }
+
+  /**
+   * Translate the constructed model into a profile response object.
+   *
+   * @param request
+   * @param model
+   * @param subjectResource
+   * @return
+   */
+  def generateResponse(request: ProfileQueryRequest, modelCom: ModelCom, 
subjectResource: Resource): ProfileQueryResponse = {
+    val jsonLdWriter = new JsonLD11Writer(RDFFormat.JSONLD11_PRETTY)
+    val baseUriString: String = "https://www.w3.org/ns/activitystreams#";
+    val baseUri: URI = new URI(baseUriString)
+    val datasetGraph = DatasetFactory.wrap(modelCom).asDatasetGraph()
+    val prefixMapping: PrefixMapping = new PrefixMapping2(modelCom)
+    val prefixMapValues: Map[String, String] = 
prefixMapping.getNsPrefixMap.asScala.toMap
+    val prefixMap: PrefixMap = {
+      val tmp = new PrefixMapStd()
+      tmp.putAll(prefixMapValues.asJava)
+      tmp
+    }
+    val jenaContext: Context = Context.fromDataset(datasetGraph)
+    val modelWriter = new StringWriter()
+    jsonLdWriter.write(modelWriter, datasetGraph, prefixMap, baseUri.toString, 
jenaContext)
+    val model = jsonParser.parse(modelWriter.toString, classOf[JsonMap])
+    val templateReader: Reader = 
Source.fromResource("framing/ProfileQueryResponse.jsonld").reader()
+    val template = jsonParser.parse(templateReader, classOf[JsonMap])
+    val result = JsonLdHelper.clean(model, template)
+    val responseJson = result.asJson()
+    val response = jsonParser.parse(responseJson, 
classOf[ProfileQueryResponse])
+    response
+  }
+
+}
diff --git 
a/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala
 
b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala
index 2f0e0f0..9b6d901 100644
--- 
a/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala
+++ 
b/streams-activitypub-graph/src/main/scala/org/apache/streams/activitypub/graph/impl/WebfingerGraphImpl.scala
@@ -1,21 +1,39 @@
 package org.apache.streams.activitypub.graph.impl
 
-import org.apache.http.client.utils.URIBuilder
+import org.apache.jena.query.DatasetFactory
 import org.apache.jena.query.ParameterizedSparqlString
 import org.apache.jena.rdf.model.Model
+import org.apache.jena.rdf.model.impl.ModelCom
+import org.apache.jena.riot.RDFFormat
+import org.apache.jena.riot.system.PrefixMap
 import org.apache.jena.riot.system.PrefixMapStd
+import org.apache.jena.riot.writer.JsonLD11Writer
+import org.apache.jena.shared.PrefixMapping
 import org.apache.jena.sparql.exec.http.QueryExecutionHTTP
-import org.apache.jena.sparql.exec.http.QueryExecutionHTTPBuilder
+import org.apache.jena.sparql.util.Context
+import org.apache.jena.sparql.util.PrefixMapping2
+import org.apache.jena.sparql.util.Symbol
+import org.apache.juneau.collections.JsonMap
+import org.apache.juneau.json.JsonParser
 import org.apache.streams.activitypub.api.WebfingerApi
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryResponse
 import org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryRequest
 import org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryResponse
 import org.apache.streams.activitypub.graph.config.WebfingerGraphImplConfig
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl.jsonParser
 import org.apache.streams.activitypub.utils.AcctPrefixResourceToResourceURISwap
+import org.apache.streams.activitypub.utils.JsonLdHelper
 import org.apache.streams.config.ComponentConfigurator
 
+import java.io.Reader
+import java.io.StringWriter
 import java.net.URI
-import scala.io.Source;
+import scala.io.Source
+import scala.jdk.CollectionConverters.*
 
+/**
+ * Implementation of the Webfinger API using jena/fuseki as back-end.
+ */
 object WebfingerGraphImpl {
 
   private final val configurator: 
ComponentConfigurator[WebfingerGraphImplConfig] = new 
ComponentConfigurator(classOf[WebfingerGraphImplConfig])
@@ -24,15 +42,7 @@ object WebfingerGraphImpl {
 
 }
 
-class WebfingerGraphImpl(config: WebfingerGraphImplConfig) extends 
WebfingerApi {
-
-  private def serverUri = new URIBuilder(config.getFusekiEndpointURI).build()
-
-  private def datasetQueryUri = new 
URIBuilder(serverUri).setPath(s"${config.getDefaultDatasetId}/query").build()
-
-  final val sparqlBuilder: QueryExecutionHTTPBuilder = 
QueryExecutionHTTP.service(datasetQueryUri.toString).postQuery()
-
-  val prefixMap = new PrefixMapStd()
+class WebfingerGraphImpl(config: WebfingerGraphImplConfig) extends 
BaseGraphImpl(config) with WebfingerApi {
 
   /**
    * Query the dataset for the requested resource.
@@ -40,53 +50,69 @@ class WebfingerGraphImpl(config: WebfingerGraphImplConfig) 
extends WebfingerApi
    * @return
    */
   override def webfingerQuery(request: WebfingerQueryRequest): 
WebfingerQueryResponse = {
+    doWebfingerQuery(request)
+  }
 
-    val resourceParamURI: URI = request.getResource match {
-      case s"acct:$x" => AcctPrefixResourceToResourceURISwap.doUnswap(x)
-      case _ => new URI(request.getResource)
+  def doWebfingerQuery(request: WebfingerQueryRequest): WebfingerQueryResponse 
= {
+    val resourceParamURI: URI = {
+      if (request.getResource.startsWith("acct:")) {
+        AcctPrefixResourceToResourceURISwap.doUnswap(request.getResource)
+      } else {
+        new URI(request.getResource)
+      }
     }
 
-    val askQueryBody: String = 
Source.fromResource("queries/webfingerAskByResource.sparql").getLines.mkString
+    val askQueryBody: String = 
Source.fromResource("queries/webfingerAsk.sparql").getLines.mkString
     val askQuery: ParameterizedSparqlString = new 
ParameterizedSparqlString(askQueryBody)
     askQuery.setIri("resourceParam", resourceParamURI.toString)
     val askExecution: QueryExecutionHTTP = 
sparqlBuilder.query(askQuery.asQuery()).build()
     val askResult = askExecution.execAsk()
     if( !askResult ) throw new Exception("Requested resource not found in 
dataset.")
 
-    val constructQueryBody: String = 
Source.fromResource("queries/webfingerGetByResource.sparql").getLines.mkString
+    val constructQueryBody: String = 
Source.fromResource("queries/webfingerConstruct.sparql").getLines.mkString
     val constructQuery: ParameterizedSparqlString = new 
ParameterizedSparqlString(constructQueryBody)
     constructQuery.setIri("resourceParam", resourceParamURI.toString)
     val constructExecution: QueryExecutionHTTP = 
sparqlBuilder.query(constructQuery.asQuery()).build()
-    val model: Model = constructExecution.execConstruct()
+    val modelCom = new ModelCom(constructExecution.execConstruct().getGraph)
 
     //    val result = writer.write(System.out, constructed, prefixMap, 
RDFFormat.JSONLD_FRAME_PRETTY, context);
-    if( checkModelContainsSubject( resourceParamURI, model ) ) {
-      generateResponse(request, model, resourceParamURI)
+    if( checkModelContainsSubject( resourceParamURI, modelCom ) ) {
+      generateResponse(request, modelCom, resourceParamURI)
     } else throw new Exception("Error generating response.")
   }
 
-  /**
-   * Check if the model returned from the query contains the requested resource
-   * and necessary details to generate a valid response.
-   * @param request
-   * @param model
-   * @return
-   */
-  def checkModelContainsSubject(resourceParamURI: URI, model: Model): Boolean 
= {
-    model.containsResource(model.getResource(resourceParamURI.toString))
-  }
-
   /**
    * Translate the model into a webfinger response object.
    * @param request
    * @param model
    * @return
    */
-  def generateResponse(request: WebfingerQueryRequest, model: Model, 
resourceParamURI: URI): WebfingerQueryResponse = {
-    val subject = model.getResource(resourceParamURI.toString)
-    val result = new WebfingerQueryResponse()
-      .withSubject(subject.getURI)
-    result
+  def generateResponse(request: WebfingerQueryRequest, modelCom: ModelCom, 
resourceParamURI: URI): WebfingerQueryResponse = {
+    val jsonLdWriter = new JsonLD11Writer(RDFFormat.JSONLD11_PRETTY)
+    val baseUriString: String = "https://www.w3.org/ns/activitystreams#";
+    val baseUri: URI = new URI(baseUriString)
+    val datasetGraph = DatasetFactory.wrap(modelCom).asDatasetGraph();
+    val prefixMapping: PrefixMapping = new PrefixMapping2(modelCom)
+    val prefixMapValues: Map[String, String] = 
prefixMapping.getNsPrefixMap.asScala.toMap
+    val prefixMap: PrefixMap = {
+      val tmp = new PrefixMapStd()
+      tmp.putAll(prefixMapValues.asJava)
+      tmp
+    }
+    val jenaContext: Context = Context.fromDataset(datasetGraph)
+    val modelWriter = new StringWriter()
+    jsonLdWriter.write(modelWriter, datasetGraph, prefixMap, baseUri.toString, 
jenaContext)
+    val model = jsonParser.parse(modelWriter.toString, classOf[JsonMap])
+    val templateReader: Reader = 
Source.fromResource("framing/WebfingerQueryResponse.jsonld").reader()
+    val template = jsonParser.parse(templateReader, classOf[JsonMap])
+    val result = JsonLdHelper.clean(model, template)
+    val responseJson = result.asJson()
+    val response = jsonParser.parse(responseJson, 
classOf[WebfingerQueryResponse])
+    if (!response.getSubject.startsWith("acct:")) {
+      val subjectUri = new URI(response.getSubject)
+      
response.setSubject(AcctPrefixResourceToResourceURISwap.doSwap(subjectUri))
+    }
+    response
   }
 
 }
diff --git 
a/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
 
b/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
index a1e2606..c469eed 100755
--- 
a/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
+++ 
b/streams-activitypub-graph/src/test/jsonschema/ActivityPubGraphTestSuiteExtensionConfig.json
@@ -12,13 +12,12 @@
       "format": "uri",
       "required": true
     },
-    "testDatasetResource": {
-      "type": "string",
-      "format": "uri",
-      "required": true
-    },
-    "testDatasetId": {
-      "type": "string",
+    "testDatasetResources": {
+      "type": "array",
+      "items": {
+          "type": "string",
+          "format": "uri"
+      },
       "required": true
     }
   }
diff --git 
a/streams-activitypub-graph/src/test/resources/ProfileGraphImplTest/testGetProfileForKnownPersonOutput.jsonld
 
b/streams-activitypub-graph/src/test/resources/ProfileGraphImplTest/testGetProfileForKnownPersonOutput.jsonld
new file mode 100644
index 0000000..744d988
--- /dev/null
+++ 
b/streams-activitypub-graph/src/test/resources/ProfileGraphImplTest/testGetProfileForKnownPersonOutput.jsonld
@@ -0,0 +1,13 @@
+{
+  "@context": "https://www.w3.org/ns/activitystreams#";,
+  "type": "Person",
+  "id": "https://mastodon.social/users/steveblackmon";,
+  "url": "https://people.apache.org/~sblackmon";,
+  "summary": "Techie, Dad, Salesforce, PMC Chair of http://streams.apache.org";,
+  "preferredUsername": "steveblackmon",
+  "outbox": "https://mastodon.social/users/steveblackmon/outbox";,
+  "name": "Steve Blackmon",
+  "inbox": "https://mastodon.social/users/steveblackmon/inbox";,
+  "following": "https://mastodon.social/users/steveblackmon/following";,
+  "followers": "https://mastodon.social/users/steveblackmon/followers";
+}
diff --git 
a/streams-activitypub-graph/src/test/resources/WebfingerGraphImplTest/testWebfingerQueryForKnownAbbreviatedResourceOutput.jsonld
 
b/streams-activitypub-graph/src/test/resources/WebfingerGraphImplTest/testWebfingerQueryForKnownAbbreviatedResourceOutput.jsonld
new file mode 100644
index 0000000..8d0967c
--- /dev/null
+++ 
b/streams-activitypub-graph/src/test/resources/WebfingerGraphImplTest/testWebfingerQueryForKnownAbbreviatedResourceOutput.jsonld
@@ -0,0 +1,3 @@
+{
+    "subject": "acct:steveblackmon@mastodon.social"
+}
diff --git 
a/streams-activitypub-graph/src/test/resources/WebfingerGraphImplTest/testWebfingerQueryForKnownUriResourceOutput.jsonld
 
b/streams-activitypub-graph/src/test/resources/WebfingerGraphImplTest/testWebfingerQueryForKnownUriResourceOutput.jsonld
new file mode 100644
index 0000000..8d0967c
--- /dev/null
+++ 
b/streams-activitypub-graph/src/test/resources/WebfingerGraphImplTest/testWebfingerQueryForKnownUriResourceOutput.jsonld
@@ -0,0 +1,3 @@
+{
+    "subject": "acct:steveblackmon@mastodon.social"
+}
diff --git a/streams-activitypub-graph/src/test/resources/application.conf 
b/streams-activitypub-graph/src/test/resources/application.conf
index 43e94e0..5e980d3 100644
--- a/streams-activitypub-graph/src/test/resources/application.conf
+++ b/streams-activitypub-graph/src/test/resources/application.conf
@@ -17,12 +17,17 @@ NodeinfoApiStaticImplConfig = {
 }
 ActivityPubGraphTestSuiteExtensionConfig = {
   fusekiEndpointURI = "http://localhost:13330/";
-  testDatasetResource = "doap.ttl"
-  testDatasetId = "doap.ttl"
+  testDatasetResources = [
+    "doap.ttl"
+  ]
 }
 BaseGraphImplConfig = {
   fusekiEndpointURI = "http://localhost:13330/";
-  defaultDatasetId = "doap.ttl"
+  defaultDataset = "default"
+  defaultGraph = "default"
+  datasetResources = [
+    "activitystreams2.ttl"
+  ]
 }
 ProfileGraphImplConfig = ${BaseGraphImplConfig}
 WebfingerGraphImplConfig = ${BaseGraphImplConfig}
diff --git 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala
 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala
index e8b32c5..e6a3426 100755
--- 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala
+++ 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuite.scala
@@ -8,6 +8,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
 
 /**
  * ActivityPubServletsTestSuite
+ * Runs all streams-activitypub-graph tests in order, with extension activated
  */
 
 object ActivityPubGraphTestSuite {
@@ -22,7 +23,10 @@ object ActivityPubGraphTestSuite {
 @Order(Order.DEFAULT)
 @SelectClasses(Array(
   
classOf[org.apache.streams.activitypub.graph.test.cases.GraphDatabaseServerAvailableTest],
-  
classOf[org.apache.streams.activitypub.graph.test.cases.WebfingerGraphImplTest]
+  
classOf[org.apache.streams.activitypub.graph.test.cases.JsonSchemaValidityTest],
+  
classOf[org.apache.streams.activitypub.graph.test.cases.JsonLdFrameValidityTest],
+  
classOf[org.apache.streams.activitypub.graph.test.cases.WebfingerGraphImplTest],
+  classOf[org.apache.streams.activitypub.graph.test.cases.ProfileGraphImplTest]
 ))
 class ActivityPubGraphTestSuite {
 
diff --git 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala
 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala
index a2273f9..ba395d3 100644
--- 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala
+++ 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/ActivityPubGraphTestSuiteExtension.scala
@@ -7,6 +7,7 @@ import org.apache.jena.query.Dataset
 import org.apache.jena.query.DatasetFactory
 import org.apache.jena.riot.RDFDataMgr
 import org.apache.juneau.rest.client.RestClient
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl
 import 
org.apache.streams.activitypub.graph.test.config.ActivityPubGraphTestSuiteExtensionConfig
 import org.apache.streams.config.ComponentConfigurator
 import org.junit.jupiter.api.extension.AfterAllCallback
@@ -26,28 +27,34 @@ object ActivityPubGraphTestSuiteExtension {
 
 class ActivityPubGraphTestSuiteExtension extends ParameterResolver with 
BeforeAllCallback with AfterAllCallback {
 
-  import ActivityPubGraphTestSuiteExtension.config
+  import ActivityPubGraphTestSuiteExtension.config as testConfig
+  import BaseGraphImpl.config as graphConfig
 
   private final val LOGGER: Logger = 
LoggerFactory.getLogger(classOf[ActivityPubGraphTestSuiteExtension])
 
-  private val dataset : Dataset = DatasetFactory.create()
-  private val serverBuilder : FusekiServer.Builder = FusekiServer.create()
-
-  RDFDataMgr.read(dataset, config.getTestDatasetResource.toString)
-
-  private val server: FusekiServer = serverBuilder
-    .add(config.getTestDatasetId, dataset)
-    .enablePing(true)
-    .loopback(true)
-    .port(config.getFusekiEndpointURI.getPort)
-    .verbose(true)
-    .build();
-  server.start();
-
-  val serverUrl = new URL(server.serverURL())
-  val datasetUrl = new URL(server.datasetURL(config.getTestDatasetId))
-  val serverUrlBuilder = new URIBuilder(serverUrl.toURI)
-  val datasetUrlBuilder = new URIBuilder(datasetUrl.toURI)
+  private val dataset: Dataset = DatasetFactory.create()
+
+  private final val server: FusekiServer = {
+    val builder: FusekiServer.Builder = FusekiServer.create()
+      .add(graphConfig.getDefaultDataset, dataset)
+      .enablePing(true)
+      .loopback(true)
+      .port(testConfig.getFusekiEndpointURI.getPort)
+      .verbose(true)
+    graphConfig.getDatasetResources.forEach { resource =>
+      
RDFDataMgr.read(builder.getDataset(graphConfig.getDefaultDataset).getDefaultGraph,
 resource.toString)
+    }
+    testConfig.getTestDatasetResources.forEach { resource =>
+      
RDFDataMgr.read(builder.getDataset(graphConfig.getDefaultDataset).getDefaultGraph,
 resource.toString)
+    }
+    builder.build()
+  }.start()
+
+  val serverUrlBuilder = new URIBuilder(server.serverURL())
+  val datasetUrlBuilder = new 
URIBuilder(server.datasetURL(graphConfig.getDefaultDataset))
+
+  val serverUrl: URL = serverUrlBuilder.build().toURL
+  val datasetUrl: URL = datasetUrlBuilder.build().toURL
 
   def restClientBuilder: RestClient.Builder = RestClient.create()
     .connectionReuseStrategy(new NoConnectionReuseStrategy())
diff --git 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/JsonLdFrameValidityTest.scala
 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/JsonLdFrameValidityTest.scala
new file mode 100644
index 0000000..c85b986
--- /dev/null
+++ 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/JsonLdFrameValidityTest.scala
@@ -0,0 +1,43 @@
+package org.apache.streams.activitypub.graph.test.cases
+
+import org.apache.juneau.collections.JsonMap
+import org.apache.juneau.json.JsonParser
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.slf4j.LoggerFactory
+
+import java.io.Reader
+import scala.io.Source
+import scala.util.Try
+
+/**
+ * Tests that all JSON-LD frame resources are valid
+ */
+class JsonLdFrameValidityTest {
+
+  private final val LOGGER = 
LoggerFactory.getLogger(classOf[JsonLdFrameValidityTest]);
+
+  private final val jsonParser: JsonParser = JsonParser.DEFAULT.copy()
+    .debug()
+    .ignoreUnknownBeanProperties()
+    .ignoreUnknownEnumValues()
+    .build()
+
+  @ParameterizedTest
+  @ValueSource(strings = Array(
+    "framing/ProfileQueryResponse.jsonld",
+    "framing/WebfingerQueryResponse.jsonld"
+  ))
+  @DisplayName("JsonLdFrameValidityTest")
+  @Order(2)
+  def testJsonLdFrameValid(frameResource : String): Unit = {
+    val frameReader : Reader = Source.fromResource(frameResource).reader()
+    val attempt = Try(jsonParser.parse(frameReader, classOf[JsonMap]))
+    Assertions.assertTrue(attempt.isSuccess)
+  }
+
+}
diff --git 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/JsonSchemaValidityTest.scala
 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/JsonSchemaValidityTest.scala
new file mode 100644
index 0000000..46b4aad
--- /dev/null
+++ 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/JsonSchemaValidityTest.scala
@@ -0,0 +1,44 @@
+package org.apache.streams.activitypub.graph.test.cases
+
+import org.apache.juneau.collections.JsonMap
+import org.apache.juneau.json.JsonParser
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.slf4j.LoggerFactory
+
+import java.io.Reader
+import scala.io.Source
+import scala.util.Try
+
+/**
+ * Tests that all JSON schema resources are valid
+ */
+class JsonSchemaValidityTest {
+
+  private final val LOGGER = 
LoggerFactory.getLogger(classOf[JsonSchemaValidityTest]);
+
+  private final val jsonParser: JsonParser = JsonParser.DEFAULT.copy()
+    .debug()
+    .ignoreUnknownBeanProperties()
+    .ignoreUnknownEnumValues()
+    .build()
+
+  @ParameterizedTest
+  @ValueSource(strings = Array(
+    "org/apache/streams/activitypub/graph/config/BaseGraphImplConfig.json",
+    "org/apache/streams/activitypub/graph/config/ProfileGraphImplConfig.json",
+    "org/apache/streams/activitypub/graph/config/WebfingerGraphImplConfig.json"
+  ))
+  @DisplayName("JsonSchemaValidityTest")
+  @Order(1)
+  def testJsonSchemaValid(schemaResource : String): Unit = {
+    val schemaReader : Reader = Source.fromResource(schemaResource).reader()
+    val attempt = Try(jsonParser.parse(schemaReader, classOf[JsonMap]))
+    Assertions.assertTrue(attempt.isSuccess)
+  }
+
+}
diff --git 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/ProfileGraphImplTest.scala
 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/ProfileGraphImplTest.scala
new file mode 100755
index 0000000..da88a45
--- /dev/null
+++ 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/ProfileGraphImplTest.scala
@@ -0,0 +1,72 @@
+package org.apache.streams.activitypub.graph.test.cases
+
+import org.apache.juneau.json.JsonParser
+import org.apache.juneau.collections.JsonMap
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryRequest
+import org.apache.streams.activitypub.api.pojo.profile.ProfileQueryResponse
+import org.apache.streams.activitypub.graph.impl.ProfileGraphImpl
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assumptions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.Test
+import org.slf4j.LoggerFactory
+
+import scala.io.Source
+import scala.util.Try;
+
+/**
+ * Test cases for ProfileGraphImpl
+ */
+class ProfileGraphImplTest {
+
+    private final val LOGGER = 
LoggerFactory.getLogger(classOf[ProfileGraphImplTest]);
+
+    private final val testProfileGraphImpl: ProfileGraphImpl = 
ProfileGraphImpl.DEFAULT
+
+    private final val jsonParser: JsonParser = JsonParser.DEFAULT.copy()
+      .debug()
+      .ignoreUnknownBeanProperties()
+      .ignoreUnknownEnumValues()
+      .build()
+
+    /**
+     * Test load profile page for an existing 'as:Person' type user
+     */
+    @Test
+    @DisplayName("ProfileQuery For Known Person")
+    @Order(5)
+    def testGetProfileForKnownPerson() : Unit = {
+        val expectedResponseAttempt: Try[ProfileQueryResponse] = {
+            val expectedJson = 
Try(Source.fromResource("ProfileGraphImplTest/testGetProfileForKnownPersonOutput.jsonld").getLines.mkString)
+            val expectedMap = Try(JsonMap.ofText(expectedJson.get, jsonParser))
+            val expectedResponse = 
Try(expectedMap.get.cast(classOf[ProfileQueryResponse]))
+            expectedResponse
+        }
+        Assumptions.assumeTrue(expectedResponseAttempt.isSuccess)
+        Assumptions.assumeTrue(expectedResponseAttempt.get != null)
+        val username = "steveblackmon"
+        val request = new ProfileQueryRequest().withUsername(username)
+        val attempt = Try(testProfileGraphImpl.profile(request))
+        Assertions.assertTrue(attempt.isSuccess)
+        val response: ProfileQueryResponse = attempt.get
+        Assertions.assertNotNull(response)
+        Assertions.assertNotNull(response.getId)
+        Assertions.assertNotNull(response.getType)
+
+    }
+
+    /**
+     * Test load profile page for a non-existent resource URI from a 
fake/wrong username
+     */
+    @Test
+    @DisplayName("ProfileQuery For Missing User")
+    @Order(5)
+    def testGetProfileForMissingUser(): Unit = {
+        val username = "elonmusk"
+        val request = new ProfileQueryRequest().withUsername(username)
+        val attempt = Try(testProfileGraphImpl.profile(request))
+        Assertions.assertFalse(attempt.isSuccess)
+    }
+
+}
diff --git 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala
 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala
index bcaf65a..9683996 100755
--- 
a/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala
+++ 
b/streams-activitypub-graph/src/test/scala/org/apache/streams/activitypub/graph/test/cases/WebfingerGraphImplTest.scala
@@ -2,6 +2,10 @@ package org.apache.streams.activitypub.graph.test.cases
 
 import org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryRequest
 import org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryResponse
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl.jsonParser
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl.jsonSerializer
 import org.apache.streams.activitypub.graph.impl.WebfingerGraphImpl
 import org.junit.jupiter.api.Assertions
 import org.junit.jupiter.api.Disabled
@@ -10,12 +14,18 @@ import org.junit.jupiter.api.Order
 import org.junit.jupiter.api.Test
 import org.slf4j.LoggerFactory
 
+import scala.io.Source
 import scala.util.Try;
 
+/**
+ * Test cases for WebfingerGraphImpl
+ */
 class WebfingerGraphImplTest {
 
     private final val LOGGER = 
LoggerFactory.getLogger(classOf[GraphDatabaseServerAvailableTest]);
 
+    private final val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
+
     val primaryTestResourceURI = "https://mastodon.social/users/steveblackmon";
 
     /**
@@ -24,15 +34,15 @@ class WebfingerGraphImplTest {
      */
     @Test
     @DisplayName("WebfingerQuery For Known Abbreviated Resource")
-    @Order(1)
+    @Order(3)
     def testWebfingerQueryForKnownAbbreviatedResource() : Unit = {
         val knownAbbreviatedResource = "acct:steveblackmon@mastodon.social"
-        val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
         val request = new WebfingerQueryRequest()
         request.setResource(knownAbbreviatedResource)
         val response: WebfingerQueryResponse = 
testWebfingerGraphImpl.webfingerQuery(request)
         Assertions.assertNotNull(response)
-        
Assertions.assertTrue(response.getSubject.equals(primaryTestResourceURI))
+        Assertions.assertEquals(knownAbbreviatedResource, response.getSubject)
+        Assertions.assertEquals(response.getSubject, 
"acct:steveblackmon@mastodon.social")
     }
 
     /**
@@ -42,15 +52,15 @@ class WebfingerGraphImplTest {
      */
     @Test
     @DisplayName("WebfingerQuery For Known URI Resource")
-    @Order(1)
+    @Order(3)
     def testWebfingerQueryForKnownUriResource(): Unit = {
         val knownPrimaryResourceUri = primaryTestResourceURI
-        val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
         val request = new WebfingerQueryRequest()
         request.setResource(knownPrimaryResourceUri)
         val response: WebfingerQueryResponse = 
testWebfingerGraphImpl.webfingerQuery(request)
         Assertions.assertNotNull(response)
-        
Assertions.assertTrue(response.getSubject.equals(primaryTestResourceURI))
+        Assertions.assertTrue(response.getSubject.startsWith("acct:"))
+        Assertions.assertEquals(response.getSubject, 
"acct:steveblackmon@mastodon.social")
     }
 
     /**
@@ -62,10 +72,9 @@ class WebfingerGraphImplTest {
     @Disabled("alias inference not yet implemented")
     @Test
     @DisplayName("WebfingerQuery For Known Alias")
-    @Order(1)
+    @Order(3)
     def testWebfingerQueryForKnownAlias(): Unit = {
         val knownAliasUri = "https://mastodon.social/@steveblackmon";
-        val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
         val request = new WebfingerQueryRequest()
         request.setResource(knownAliasUri)
         val response: WebfingerQueryResponse = 
testWebfingerGraphImpl.webfingerQuery(request)
@@ -79,9 +88,8 @@ class WebfingerGraphImplTest {
      */
     @Test
     @DisplayName("WebfingerQuery For Missing URI Resource")
-    @Order(2)
+    @Order(4)
     def testWebfingerQueryForMissingUriResource(): Unit = {
-        val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
         val request = new WebfingerQueryRequest()
         request.setResource("https://mastodon.social/users/joeschmo";)
         val attempt = Try(testWebfingerGraphImpl.webfingerQuery(request))
@@ -94,9 +102,8 @@ class WebfingerGraphImplTest {
      */
     @Test
     @DisplayName("WebfingerQuery For Missing Abbreviated Resource")
-    @Order(2)
+    @Order(4)
     def testWebfingerQueryForMissingAbbreviatedResource(): Unit = {
-        val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
         val request = new WebfingerQueryRequest()
         request.setResource("acct:joeschmo@mastodon.social")
         val attempt = Try(testWebfingerGraphImpl.webfingerQuery(request))
@@ -110,9 +117,8 @@ class WebfingerGraphImplTest {
      */
     @Test
     @DisplayName("WebfingerQuery For Atypical Prefixed Resource")
-    @Order(2)
+    @Order(4)
     def testWebfingerQueryForAtypicalPrefixedResource(): Unit = {
-        val testWebfingerGraphImpl = WebfingerGraphImpl.DEFAULT
         val request = new WebfingerQueryRequest()
         request.setResource("account:steveblackmon@mastodon.social")
         val attempt = Try(testWebfingerGraphImpl.webfingerQuery(request))
diff --git 
a/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala
 
b/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala
index d9bb10f..2f1a9cc 100755
--- 
a/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala
+++ 
b/streams-activitypub-servlets/src/main/scala/org/apache/streams/activitypub/servlets/WebfingerServlet.scala
@@ -13,6 +13,7 @@ import org.apache.juneau.rest.servlet.BasicRestServlet
 import org.apache.streams.activitypub.api.WebfingerApi
 import org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryRequest
 import org.apache.streams.activitypub.api.pojo.webfinger.WebfingerQueryResponse
+import org.apache.streams.activitypub.graph.impl.BaseGraphImpl
 import org.apache.streams.activitypub.graph.impl.WebfingerGraphImpl
 import org.apache.streams.activitypub.remote.WebfingerRest
 
diff --git a/streams-activitypub-utils/pom.xml 
b/streams-activitypub-utils/pom.xml
index 73be40e..850803f 100755
--- a/streams-activitypub-utils/pom.xml
+++ b/streams-activitypub-utils/pom.xml
@@ -57,6 +57,24 @@
             <artifactId>juneau-marshall</artifactId>
             <version>${juneau.version}</version>
         </dependency>
+        <dependency>
+            <groupId>jakarta.json</groupId>
+            <artifactId>jakarta.json-api</artifactId>
+            <version>${jakarta-json.version}</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>jakarta.json</artifactId>
+            <version>${jakarta-json.version}</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>com.apicatalog</groupId>
+            <artifactId>titanium-json-ld</artifactId>
+            <version>${titanium.version}</version>
+            <type>jar</type>
+        </dependency>
         <!-- streams-activitypub-utils only test dependencies -->
     </dependencies>
     <build>
diff --git 
a/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/AcctPrefixResourceToResourceURISwap.scala
 
b/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/AcctPrefixResourceToResourceURISwap.scala
index 28c9413..60396e6 100644
--- 
a/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/AcctPrefixResourceToResourceURISwap.scala
+++ 
b/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/AcctPrefixResourceToResourceURISwap.scala
@@ -7,19 +7,27 @@ import org.apache.juneau.swap.StringSwap
 
 import java.net.URI
 
+/**
+ * Swaps between a URI and a string with the format acct:preferredName@domain
+ * Used by the Webfinger Query endpoint
+ */
 object AcctPrefixResourceToResourceURISwap {
+
+  val unswapExtractFields = "acct:(.*)@(.*)".r
+
   def doSwap(uri: URI): String = {
     val domain = uri.getHost
     val preferredName = uri.getPath.split("/").last
     s"acct:${preferredName}@${domain}"
   }
   def doUnswap(string: String): URI = {
-    val preferredName = string.split("@")(0)
-    val domain = string.split("@")(1)
-    val uriBuilder = new URIBuilder()
-      .setScheme("https")
-      .setHost(domain)
-      .setPath(s"/users/${preferredName}")
+    val uriBuilder = new URIBuilder().setScheme("https")
+    val (preferredName, domain) = string match {
+      case unswapExtractFields(preferredName, domain) => (preferredName, 
domain)
+      case _ => throw new IllegalArgumentException(s"Could not parse $string")
+    }
+    uriBuilder.setHost(domain)
+    uriBuilder.setPath(s"/users/${preferredName}")
     uriBuilder.build()
   }
 }
diff --git 
a/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/JsonLdHelper.scala
 
b/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/JsonLdHelper.scala
new file mode 100644
index 0000000..0363f3f
--- /dev/null
+++ 
b/streams-activitypub-utils/src/main/scala/org/apache/streams/activitypub/utils/JsonLdHelper.scala
@@ -0,0 +1,64 @@
+package org.apache.streams.activitypub.utils
+
+import com.apicatalog.jsonld.JsonLd
+import com.apicatalog.jsonld.JsonLdEmbed
+import com.apicatalog.jsonld.api.CompactionApi
+import com.apicatalog.jsonld.api.FramingApi
+import com.apicatalog.jsonld.document.JsonDocument
+import jakarta.json.Json
+import org.apache.juneau.collections.JsonMap
+import org.apache.juneau.json.JsonParser
+import org.apache.juneau.json.JsonSerializer
+
+import java.io.StringReader
+import java.io.StringWriter
+
+/**
+ * Helper for converting the potentially messy json-ld output of
+ * an activitystreams base jena model to a clean json representation
+ *
+ * Ideally this class would not be used often or necessary, but need to figure
+ * out how to constrain (frame?) the json-ld serializer to output in compact 
form
+ * b/c no one wants unnecessary @id and @lang in the response
+ */
+object JsonLdHelper {
+
+  val BASEURI = "https://www.w3.org/ns/activitystreams#";
+
+  private val jsonParser = JsonParser.DEFAULT
+  private val jsonSerializer = JsonSerializer.DEFAULT
+
+  /**
+   * Convert a JsonLd11Writer output to a json schema compatible representation
+   * via framing.
+   */
+  def clean(model: JsonMap, template: JsonMap): JsonMap = {
+    val modelReader = new StringReader(jsonSerializer.serialize(model))
+    val templateReader = new StringReader(jsonSerializer.serialize(template))
+    val modelDocument = JsonDocument.of(modelReader)
+    val templateDocument = JsonDocument.of(templateReader)
+    var framing: FramingApi = JsonLd.frame(modelDocument, templateDocument)
+    framing = framing.base(BASEURI)
+    framing = framing.context(BASEURI)
+    framing = framing.embed(JsonLdEmbed.ONCE)
+    framing = framing.explicit(true)
+    framing = framing.omitDefault(true)
+    framing = framing.omitGraph(true)
+    val framedWriter = new StringWriter()
+    val framedJsonWriter = Json.createWriter(framedWriter)
+    framedJsonWriter.write(framing.get())
+    val framedReader = new StringReader(framedWriter.toString)
+    val framedDocument = JsonDocument.of(framedReader)
+    var compact: CompactionApi = JsonLd.compact(framedDocument, BASEURI)
+    compact = compact.compactToRelative(true)
+    compact = compact.ordered(true)
+    val compactWriter = new StringWriter()
+    val compactJsonWriter = Json.createWriter(compactWriter)
+    compactJsonWriter.write(compact.get())
+    val compactJson = compactWriter.toString
+    val compactMap = jsonParser.parse(compactJson, classOf[JsonMap])
+    compactMap.putAt("@context", BASEURI)
+    compactMap
+  }
+
+}
diff --git 
a/streams-activitypub-utils/src/test/resources/JsonLdHelperTest/testProfileCleanupInput.jsonld
 
b/streams-activitypub-utils/src/test/resources/JsonLdHelperTest/testProfileCleanupInput.jsonld
new file mode 100644
index 0000000..4f27ff7
--- /dev/null
+++ 
b/streams-activitypub-utils/src/test/resources/JsonLdHelperTest/testProfileCleanupInput.jsonld
@@ -0,0 +1,47 @@
+{
+    "@id": "https://mastodon.social/users/steveblackmon";,
+    "as:url": {
+        "@id": "https://people.apache.org/~sblackmon";
+    },
+    "as:summary": {
+        "@value": "Techie, Dad, Salesforce, PMC Chair of 
http://streams.apache.org";
+    },
+    "as:preferredUsername": "steveblackmon",
+    "as:outbox": {
+        "@id": "https://mastodon.social/users/steveblackmon/outbox";
+    },
+    "as:name": "Steve Blackmon",
+    "as:inbox": {
+        "@id": "https://mastodon.social/users/steveblackmon/inbox";
+    },
+    "as:id": [
+        {
+            "@id": "https://mastodon.social/users/steveblackmon";
+        },
+        "https://mastodon.social/@steveblackmon";
+    ],
+    "as:following": {
+        "@id": "https://mastodon.social/users/steveblackmon/following";
+    },
+    "as:followers": {
+        "@id": "https://mastodon.social/users/steveblackmon/followers";
+    },
+    "vcard:given-name": "Steve",
+    "vcard:family-name": "Blackmon",
+    "vcard:email": "sblack...@apache.org",
+    "@type": "as:Person",
+    "@context": {
+        "schema": "https://schema.org/";,
+        "owl": "http://www.w3.org/2002/07/owl#";,
+        "asfext": "http://projects.apache.org/ns/asfext#";,
+        "xsd": "http://www.w3.org/2001/XMLSchema#";,
+        "rdfs": "http://www.w3.org/2000/01/rdf-schema#";,
+        "vcard": "http://www.w3.org/2006/vcard/ns#";,
+        "sec": "https://w3id.org/security/v1";,
+        "as": "https://www.w3.org/ns/activitystreams#";,
+        "doap": "http://usefulinc.com/ns/doap#";,
+        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#";,
+        "ldp": "http://www.w3.org/ns/ldp#";,
+        "foaf": "http://xmlns.com/foaf/0.1/";
+    }
+}
diff --git 
a/streams-activitypub-utils/src/test/resources/JsonLdHelperTest/testProfileCleanupTemplate.jsonld
 
b/streams-activitypub-utils/src/test/resources/JsonLdHelperTest/testProfileCleanupTemplate.jsonld
new file mode 100644
index 0000000..0f41981
--- /dev/null
+++ 
b/streams-activitypub-utils/src/test/resources/JsonLdHelperTest/testProfileCleanupTemplate.jsonld
@@ -0,0 +1,15 @@
+{
+  "@context": {
+      "@vocab": "https://www.w3.org/ns/activitystreams#";,
+      "id": "@id",
+      "type": "@type"
+  },
+  "name": {},
+  "url": {},
+  "summary": {},
+  "preferredUsername": {},
+  "outbox": {},
+  "inbox": {},
+  "following": {},
+  "followers": {}
+}
diff --git 
a/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/ActivityPubUtilsTestSuite.scala
 
b/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/ActivityPubUtilsTestSuite.scala
new file mode 100755
index 0000000..5c6f193
--- /dev/null
+++ 
b/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/ActivityPubUtilsTestSuite.scala
@@ -0,0 +1,26 @@
+package org.apache.streams.activitypub.utils.test
+
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.junit.platform.suite.api.SelectClasses
+import org.junit.platform.suite.api.Suite
+import org.junit.platform.suite.api.SuiteDisplayName;
+
+/**
+ * ActivityPubServletsTestSuite
+ */
+
+object ActivityPubUtilsTestSuite {
+
+}
+
+@Suite
+@SuiteDisplayName("ActivityPub Utils Unit Tests")
+@Order(Order.DEFAULT)
+@SelectClasses(Array(
+  
classOf[org.apache.streams.activitypub.utils.test.cases.AcctPrefixResourceURISwapTest],
+  classOf[org.apache.streams.activitypub.utils.test.cases.JsonLdHelperTest]
+))
+class ActivityPubUtilsTestSuite {
+
+}
diff --git 
a/streams-activitypub-utils/src/test/scala/AcctPrefixResourceURISwapTest.scala 
b/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/cases/AcctPrefixResourceURISwapTest.scala
similarity index 100%
rename from 
streams-activitypub-utils/src/test/scala/AcctPrefixResourceURISwapTest.scala
rename to 
streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/cases/AcctPrefixResourceURISwapTest.scala
diff --git 
a/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/cases/JsonLdHelperTest.scala
 
b/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/cases/JsonLdHelperTest.scala
new file mode 100644
index 0000000..0806016
--- /dev/null
+++ 
b/streams-activitypub-utils/src/test/scala/org/apache/streams/activitypub/utils/test/cases/JsonLdHelperTest.scala
@@ -0,0 +1,36 @@
+package org.apache.streams.activitypub.utils.test.cases
+
+import org.apache.juneau.collections.JsonList
+import org.apache.juneau.collections.JsonMap
+import org.apache.juneau.json.JsonParser
+import org.apache.juneau.json.JsonSerializer
+import org.apache.streams.activitypub.utils.JsonLdHelper
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+import scala.io.Source
+
+class JsonLdHelperTest {
+
+  /**
+   * unit test for JsonLdHelper.clean()
+   *
+   * This test is designed to ensure that the JsonLdHelper.clean() method is 
functioning as expected.
+   */
+  @Test
+  @DisplayName("Test Profile Response Cleanup")
+  def testProfileFraming(): Unit = {
+    val testInputSource = 
Source.fromResource("JsonLdHelperTest/testProfileCleanupInput.jsonld")
+    val testInputJsonMap = JsonParser.DEFAULT.parse(testInputSource.reader(), 
classOf[JsonMap])
+    val testTemplateSource = 
Source.fromResource("JsonLdHelperTest/testProfileCleanupTemplate.jsonld")
+    val testTemplateJsonMap = 
JsonParser.DEFAULT.parse(testTemplateSource.reader(), classOf[JsonMap])
+    val outputJsonMap: JsonMap = JsonLdHelper.clean(testInputJsonMap, 
testTemplateJsonMap)
+    
System.out.print(JsonSerializer.DEFAULT_READABLE.serializeToString(outputJsonMap))
+    Assertions.assertFalse(outputJsonMap.containsKey("@graph"))
+    Assertions.assertFalse(outputJsonMap.containsKey("@id"))
+    Assertions.assertTrue(outputJsonMap.containsKey("@context"))
+    Assertions.assertEquals(outputJsonMap.get("@context").getClass, 
classOf[String])
+  }
+
+}
diff --git 
a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala
 
b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala
index 8c0e504..9bc3334 100755
--- 
a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala
+++ 
b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebappServerAvailableTest.scala
@@ -1,12 +1,8 @@
 package org.apache.streams.activitypub.webapp.test.cases
 
-import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuite
 import 
org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuiteExtension
 import org.awaitility.Awaitility.await
-import org.awaitility.Awaitility.waitAtMost
-import org.awaitility.core.ConditionTimeoutException
 import org.awaitility.scala.AwaitilitySupport
-import org.hamcrest.MatcherAssert.assertThat
 import org.junit.jupiter.api.Assertions
 import org.junit.jupiter.api.DisplayName
 import org.junit.jupiter.api.Order
diff --git 
a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala
 
b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala
index ee4f0db..f464264 100755
--- 
a/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala
+++ 
b/streams-activitypub-webapp/src/test/scala/org/apache/streams/activitypub/webapp/test/cases/WebfingerServletTest.scala
@@ -4,13 +4,11 @@ import org.apache.http.HttpStatus
 import org.apache.http.client.utils.URIBuilder
 import org.apache.http.entity.ContentType
 import org.apache.streams.activitypub.servlets.WebfingerServlet
-import org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuite
 import 
org.apache.streams.activitypub.webapp.test.ActivityPubWebappTestSuiteExtension
 import org.junit.jupiter.api.DisplayName
 import org.junit.jupiter.api.Order
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
-import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
 import java.nio.charset.Charset;

Reply via email to