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;