diff --git a/asTypescript/javatots-config.yaml b/asTypescript/javatots-config.yaml new file mode 100644 index 00000000..02822f0a --- /dev/null +++ b/asTypescript/javatots-config.yaml @@ -0,0 +1,44 @@ +inputDirectory: .. +outputDirectory: . +packageTemplate: "// Corresponding shapetrees-java package: %s" +indentation: 2 +unknownImportTemplate: "import { %s } from %s;" +commentThrows: true +unknownAnnotations: comment # comment, ignore, throw (everything else interpreted as throw) + +moduleMaps: + shapetrees-java-client-core: + srcRoot: src/main/java + outputPath: packages/client-core/src + tsModule: '@shapetrees/clientcore/src' + packageMaps: + - pkg: com.janeirodigital.shapetrees.client.core + destPath: + shapetrees-java-client-http: + srcRoot: src/main/java + outputPath: packages/client-http/src + tsModule: '@shapetrees/clienthttp/src' + packageMaps: + - pkg: com.janeirodigital.shapetrees.client.http + destPath: + shapetrees-java-core: + srcRoot: src/main/java + outputPath: packages/core/src + tsModule: '@shapetrees/core/src' + packageMaps: + - pkg: com.janeirodigital.shapetrees.core + destPath: + shapetrees-java-javahttp: + srcRoot: src/main/java + outputPath: packages/javahttp/src + tsModule: '@shapetrees/javahttp/src' + packageMaps: + - pkg: com.janeirodigital.shapetrees.javahttp + destPath: + shapetrees-java-tests: + srcRoot: src/test/java + outputPath: packages/tests/src + tsModule: '@shapetrees/tests' + packageMaps: + - pkg: com.janeirodigital.shapetrees.tests + destPath: diff --git a/asTypescript/packages/client-core/src/ShapeTreeClient.ts b/asTypescript/packages/client-core/src/ShapeTreeClient.ts new file mode 100644 index 00000000..e1d6f39d --- /dev/null +++ b/asTypescript/packages/client-core/src/ShapeTreeClient.ts @@ -0,0 +1,134 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.core +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ShapeTreeContext } from '@shapetrees/core/src/ShapeTreeContext'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; + +/** + * This interface defines a proposed API to be used for any client-side implementations of + * a shape tree client + */ +export interface ShapeTreeClient { + + /** + * Shape Trees, §4.1: This operation is used by a client-side agent to discover any shape trees associated + * with a given resource. If URL is a managed resource, the associated Shape Tree Manager will be returned. + * + * https://shapetrees.org/TR/specification/#discover + * + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The URL of the target resource for shape tree discovery + * @return A ShapeTreeManager associated with targetResource + * @throws ShapeTreeException ShapeTreeException + */ + discoverShapeTree(context: ShapeTreeContext, targetResource: URL): ShapeTreeManager | null /* throws ShapeTreeException */; + + /** + * Shape Trees, §4.2: This operation marks an existing resource as being managed by one or more shape trees, + * by associating a shape tree manager with the resource, and turning it into a managed resource. + * + * If the resource is already managed, the associated shape tree manager will be updated with another + * shape tree assignment for the planted shape tree. + * + * If the resource is a container that already contains existing resources, this operation will + * perform a depth first traversal through the containment hierarchy, validating + * and assigning as it works its way back up to the target resource of this operation. + * + * https://shapetrees.org/TR/specification/#plant-shapetree + * + * Plants one or more shape trees at a given container + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The URL of the resource to plant on + * @param targetShapeTree A URL representing the shape tree to plant for targetResource + * @param focusNode An optional URL representing the target subject within targetResource used for shape validation + * @return DocumentResponse containing status and response headers/attributes + * @throws ShapeTreeException ShapeTreeException + */ + plantShapeTree(context: ShapeTreeContext, targetResource: URL, targetShapeTree: URL, focusNode: URL): DocumentResponse /* throws ShapeTreeException */; + + /** + * Shape Trees, §4.3: This operation unassigns a planted root shape tree from a root shape tree instance. If + * the root shape tree instance is a managed container, it will also unassign contained resources. + * If there are no remaining shape trees managing the resource, it would no longer be considered as managed. + * + * https://shapetrees.org/TR/specification/#unplant-shapetree + * + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource URL of target resource to unplant shape tree from + * @param targetShapeTree URL of shape tree being unplanted + */ + unplantShapeTree(context: ShapeTreeContext, targetResource: URL, targetShapeTree: URL): DocumentResponse /* throws ShapeTreeException */; + + /** + * Creates a resource via HTTP POST that has been validated against the provided shape tree + * @param context ShapeTreeContext that would be used for authentication purposes + * @param parentContainer The container the created resource should be created within + * @param focusNodes One or more nodes/subjects to use as the focus for shape validation + * @param targetShapeTrees One or more target shape trees the resource should be validated by + * @param proposedName Proposed resource name (aka Slug) for the resulting resource + * @param isContainer Specifies whether the newly created resource should be created as a container or not + * @param bodyString String representation of body of the created resource + * @param contentType Content type to parse the bodyString parameter as + * @return DocumentResponse containing status and response headers/attributes + * @throws ShapeTreeException ShapeTreeException + */ + postManagedInstance(context: ShapeTreeContext, parentContainer: URL, focusNodes: Array, targetShapeTrees: Array, proposedName: string, isContainer: boolean, bodyString: string, contentType: string): DocumentResponse /* throws ShapeTreeException */; + + /** + * Creates a resource via HTTP PUT that has been validated against the provided target shape tree + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The target resource to be created or updated + * @param focusNodes One or more nodes/subjects to use as the focus for shape validation + * @param bodyString String representation of the body of the resource to create or update + * @param contentType Content type to parse the bodyString parameter as + * @param targetShapeTrees The shape trees that a proposed resource to be created should be validated against + * @param isContainer Specifies whether a newly created resource should be created as a container or not + * @return DocumentResponse containing status and response header / attributes + * @throws ShapeTreeException + */ + putManagedInstance(context: ShapeTreeContext, targetResource: URL, focusNodes: Array, bodyString: string, contentType: string, targetShapeTrees: Array, isContainer: boolean): DocumentResponse /* throws ShapeTreeException */; + + /** + * Updates a resource via HTTP PUT that has been validated against an associated shape tree + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The target resource to be created or updated + * @param focusNodes One or more nodes/subjects to use as the focus for shape validation + * @param bodyString String representation of the body of the resource to create or update + * @param contentType Content type to parse the bodyString parameter as + * @return DocumentResponse containing status and response header / attributes + * @throws ShapeTreeException + */ + updateManagedInstance(context: ShapeTreeContext, targetResource: URL, focusNodes: Array, bodyString: string, contentType: string): DocumentResponse /* throws ShapeTreeException */; + + /** + * Updates a resource via HTTP PATCH that has been validated against an associated shape tree + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The target resource to be created or updated + * @param focusNodes One or more nodes/subjects to use as the focus for shape validation + * @param patchString SPARQL Update statement to use in patching the resource + * @return DocumentResponse containing status and response header / attributes + * @throws ShapeTreeException + */ + patchManagedInstance(context: ShapeTreeContext, targetResource: URL, focusNodes: Array, patchString: string): DocumentResponse /* throws ShapeTreeException */; + + /** + * Deletes an existing resource. Provided as a convenience - no validation is performed + * @param context ShapeTreeContext that would be used for authentication purposes + * @param resourceUrl The URL of the resource being deleted + * @return DocumentResponse containing status and response headers/attributes + * @throws ShapeTreeException ShapeTreeException + */ + deleteManagedInstance(context: ShapeTreeContext, resourceUrl: URL): DocumentResponse /* throws ShapeTreeException */; + + /** + * Indicates whether validation is currently being applied on the client + * @return boolean of whether client-side validation is being performed + */ + isShapeTreeValidationSkipped(): boolean; + + /** + * Determines whether validation should be performed on the client + * @param skipValidation boolean indicating whether validation should be performed on the client + */ + skipShapeTreeValidation(skipValidation: boolean): void; +} diff --git a/asTypescript/packages/client-http/src/HttpClient.ts b/asTypescript/packages/client-http/src/HttpClient.ts new file mode 100644 index 00000000..c552dbb5 --- /dev/null +++ b/asTypescript/packages/client-http/src/HttpClient.ts @@ -0,0 +1,29 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.http +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { HttpRequest } from './HttpRequest'; + +/** + * abstract base class for ShapeTree library network drivers + */ +export interface HttpClient { + + GET: string = "GET"; + + PUT: string = "PUT"; + + POST: string = "POST"; + + PATCH: string = "PATCH"; + + DELETE: string = "DELETE"; + + /** + * Execute an HTTP request to create a DocumentResponse object + * Implements `HttpClient` interface + * @param request an HTTP request with appropriate headers for ShapeTree interactions + * @return new DocumentResponse with response headers and contents + * @throws ShapeTreeException + */ + fetchShapeTreeResponse(request: HttpRequest): DocumentResponse /* throws ShapeTreeException */; +} diff --git a/asTypescript/packages/client-http/src/HttpClientFactory.ts b/asTypescript/packages/client-http/src/HttpClientFactory.ts new file mode 100644 index 00000000..c2ddcecc --- /dev/null +++ b/asTypescript/packages/client-http/src/HttpClientFactory.ts @@ -0,0 +1,19 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.http +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { HttpClient } from './HttpClient'; + +/** + * Constructs HttpClients based on the passed configuration. + * + *

See the Usage section in README. + */ +export interface HttpClientFactory { + + /** + * Reuses or constructs a new HttpClient tailored to the passed configuration + * @param useShapeTreeValidation whether or not the returned HttpClient must do ShapeTree validation (and Shape validation) on dereferenced resources + * @return an implementation of HttpClient that can be used to map HTTP library (e.g. OkHttp) + * requests and responses to `shapetrees-java-client-http` classes. + */ + get(useShapeTreeValidation: boolean): HttpClient /* throws ShapeTreeException */; +} diff --git a/asTypescript/packages/client-http/src/HttpClientFactoryManager.ts b/asTypescript/packages/client-http/src/HttpClientFactoryManager.ts new file mode 100644 index 00000000..0b583078 --- /dev/null +++ b/asTypescript/packages/client-http/src/HttpClientFactoryManager.ts @@ -0,0 +1,21 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.http +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { HttpClientFactory } from './HttpClientFactory'; + +export abstract class HttpClientFactoryManager { + + @Setter(onMethod_ = { @Synchronized }) + private static factory: HttpClientFactory; + + private constructor() { + throw new IllegalStateException("Utility class"); + } + + // @Synchronized + public static getFactory(): HttpClientFactory /* throws ShapeTreeException */ { + if (factory === null) { + throw new ShapeTreeException(500, "Must provide a valid HTTP client factory"); + } + return HttpClientFactoryManager.factory; + } +} diff --git a/asTypescript/packages/client-http/src/HttpRequest.ts b/asTypescript/packages/client-http/src/HttpRequest.ts new file mode 100644 index 00000000..06ac2e92 --- /dev/null +++ b/asTypescript/packages/client-http/src/HttpRequest.ts @@ -0,0 +1,23 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.http +import { ResourceAttributes } from '@shapetrees/core/src/ResourceAttributes'; + +export class HttpRequest { + + public method: string; + + public resourceURL: URL; + + public headers: ResourceAttributes; + + public body: string; + + public contentType: string; + + public constructor(method: string, resourceURL: URL, headers: ResourceAttributes, body: string, contentType: string) { + this.method = method; + this.resourceURL = resourceURL; + this.headers = headers; + this.body = body; + this.contentType = contentType; + } +} diff --git a/asTypescript/packages/client-http/src/HttpResourceAccessor.ts b/asTypescript/packages/client-http/src/HttpResourceAccessor.ts new file mode 100644 index 00000000..71949b0a --- /dev/null +++ b/asTypescript/packages/client-http/src/HttpResourceAccessor.ts @@ -0,0 +1,552 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.http +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { ShapeTreeContext } from '@shapetrees/core/src/ShapeTreeContext'; +import { ManageableInstance } from '@shapetrees/core/src/ManageableInstance'; +import { ManageableResource } from '@shapetrees/core/src/ManageableResource'; +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ResourceAttributes } from '@shapetrees/core/src/ResourceAttributes'; +import { InstanceResource } from '@shapetrees/core/src/InstanceResource'; +import { ResourceAccessor } from '@shapetrees/core/src/ResourceAccessor'; +import { ManagerResource } from '@shapetrees/core/src/ManagerResource'; +import { MissingManageableResource } from '@shapetrees/core/src/MissingManageableResource'; +import { MissingManagerResource } from '@shapetrees/core/src/MissingManagerResource'; +import { UnmanagedResource } from '@shapetrees/core/src/UnmanagedResource'; +import { ManagedResource } from '@shapetrees/core/src/ManagedResource'; +import { HttpHeaders } from '@shapetrees/core/src/enums/HttpHeaders'; +import { LinkRelations } from '@shapetrees/core/src/enums/LinkRelations'; +import { ShapeTreeResourceType } from '@shapetrees/core/src/enums/ShapeTreeResourceType'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { LdpVocabulary } from '@shapetrees/core/src/vocabularies/LdpVocabulary'; +import * as Graph from 'org/apache/jena/graph'; +import * as Node from 'org/apache/jena/graph'; +import * as NodeFactory from 'org/apache/jena/graph'; +import * as Triple from 'org/apache/jena/graph'; +import * as MalformedURLException from 'java/net'; +import * as Set from 'java/util'; +import * as Collections from 'java/util'; +import { readStringIntoGraph } from '@shapetrees/core/src/helpers/GraphHelper/readStringIntoGraph'; +import { urlToUri } from '@shapetrees/core/src/helpers/GraphHelper/urlToUri'; +import { HttpRequest } from './HttpRequest'; +import { HttpClient } from './HttpClient'; + +/** + * Allows the {@link com.janeirodigital.shapetrees.core shapetrees-core} to access + * {@link ManageableInstance}s and {@link InstanceResource}s over the network via HTTP. This is + * particularly effective when employing client-side shape-tree validation in a + * proxy scenario. + * + *

Given the fact that resources are accessed via HTTP, some inferences must be made on + * resource state based on responses to HTTP requests.

+ */ +export class HttpResourceAccessor implements ResourceAccessor { + + private static readonly supportedRDFContentTypes: Set = Set.of("text/turtle", "application/rdf+xml", "application/n-triples", "application/ld+json"); + + /** + * Return a {@link ManageableInstance} constructed based on the provided resourceUrl, + * which could target either a {@link ManageableResource} or a {@link ManagerResource}. + * Both are retrieved via HTTP and loaded as specifically + * typed sub-classes that indicate whether they exist, or (in the case of manageable resource) + * whether they are managed. + * + * @param context {@link ShapeTreeContext} + * @param resourceUrl URL of the target resource + * @return {@link ManageableInstance} including {@link ManageableResource} and {@link ManagerResource} + */ + override public getInstance(context: ShapeTreeContext, resourceUrl: URL): ManageableInstance /* throws ShapeTreeException */ { + let resource: InstanceResource = this.getResource(context, resourceUrl); + if (resource instanceof MissingManageableResource) { + // Get is for a manageable resource that doesn't exist + return getInstanceFromMissingManageableResource(context, (MissingManageableResource) resource); + } else if (resource instanceof MissingManagerResource) { + // Get is for a manager resource that doesn't exist + return getInstanceFromMissingManagerResource(context, (MissingManagerResource) resource); + } else if (resource instanceof ManageableResource) { + // Get is for an existing manageable resource + return getInstanceFromManageableResource(context, (ManageableResource) resource); + } else if (resource instanceof ManagerResource) { + // Get is for an existing manager resource + return getInstanceFromManagerResource(context, (ManagerResource) resource); + } + throw new ShapeTreeException(500, "Can get instance from resource of unsupported type: " + resource.getUrl()); + } + + /** + * Gets a {@link ManageableInstance} given a {@link MissingManageableResource}, which means that + * a corresponding {@link ManagerResource} cannot exist, so a {@link MissingManagerResource} is + * constructed and included as part of instance construction. + * @param context {@link ShapeTreeContext} + * @param missing {@link MissingManageableResource} + * @return {@link ManageableInstance} including {@link MissingManageableResource} and {@link MissingManagerResource} + */ + private getInstanceFromMissingManageableResource(context: ShapeTreeContext, missing: MissingManageableResource): ManageableInstance { + let missingManager: MissingManagerResource = new MissingManagerResource(missing.getUrl(), missing); + return new ManageableInstance(context, this, false, missing, missingManager); + } + + /** + * Gets a {@link ManageableInstance} given a {@link MissingManagerResource}, which means that + * a {@link ManagerResource} doesn't exist, but an {@link UnmanagedResource} that would be associated + * with it may, so it is looked up over HTTP and populated with the appropriate resulting type + * based on its existence. + * @param context {@link ShapeTreeContext} + * @param missing {@link MissingManagerResource} + * @return {@link ManageableInstance} including {@link UnmanagedResource}|{@link MissingManageableResource} and {@link MissingManagerResource} + * @throws ShapeTreeException + */ + private getInstanceFromMissingManagerResource(context: ShapeTreeContext, missing: MissingManagerResource): ManageableInstance /* throws ShapeTreeException */ { + let manageable: InstanceResource = this.getResource(context, calculateManagedUrl(missing.getUrl(), missing.getAttributes())); + if (manageable.isExists()) { + let unmanaged: UnmanagedResource = new UnmanagedResource((ManageableResource) manageable, Optional.of(missing.getUrl())); + return new ManageableInstance(context, this, true, unmanaged, missing); + } else { + throw new ShapeTreeException(500, "Cannot have a shape tree manager " + missing.getUrl() + " for a missing manageable resource " + manageable.getUrl()); + } + } + + /** + * Gets a {@link ManageableInstance} given a {@link ManageableResource}, which could be a + * {@link ManagedResource} or an {@link UnmanagedResource}. Which type is determined by + * the presence of the {@link ManagerResource}, which is looked up and the instance is + * populated with the appropriate resulting types.* + * @param context {@link ShapeTreeContext} + * @param manageable {@link ManagedResource} or {@link UnmanagedResource} + * @return {@link ManageableInstance} including {@link UnmanagedResource}|{@link ManagedResource} and {@link ManagerResource}|{@link MissingManagerResource} + * @throws ShapeTreeException + */ + private getInstanceFromManageableResource(context: ShapeTreeContext, manageable: ManageableResource): ManageableInstance /* throws ShapeTreeException */ { + let managerResourceUrl: URL = manageable.getManagerResourceUrl().orElseThrow(() -> new ShapeTreeException(500, "Cannot discover shape tree manager for " + manageable.getUrl())); + let manager: InstanceResource = this.getResource(context, managerResourceUrl); + if (manager instanceof MissingManagerResource) { + // If the manager does exist it is unmanaged - Get and store both in instance + let unmanaged: UnmanagedResource = new UnmanagedResource(manageable, Optional.of(manager.getUrl())); + return new ManageableInstance(context, this, false, unmanaged, (ManagerResource) manager); + } else if (manager instanceof ManagerResource) { + // If the manager exists then it is managed - get and store manager and managed resource in instance + let managed: ManagedResource = new ManagedResource(manageable, Optional.of(manager.getUrl())); + return new ManageableInstance(context, this, false, managed, (ManagerResource) manager); + } else { + throw new ShapeTreeException(500, "Error looking up corresponding shape tree manager for " + manageable.getUrl()); + } + } + + /** + * Gets a {@link ManageableInstance} given a {@link ManagerResource}. The corresponding + * {@link ManagedResource} is looked up and the instance is populated with it. + * @param context {@link ShapeTreeContext} + * @param manager Existing {@link ManagerResource} + * @return {@link ManageableInstance} including {@link ManagerResource} and {@link ManagedResource} + * @throws ShapeTreeException + */ + private getInstanceFromManagerResource(context: ShapeTreeContext, manager: ManagerResource): ManageableInstance /* throws ShapeTreeException */ { + let manageable: InstanceResource = this.getResource(context, manager.getManagedResourceUrl()); + if (manageable instanceof MissingManageableResource) { + throw new ShapeTreeException(500, "Cannot have a shape tree manager at " + manager.getUrl() + " without a corresponding managed resource"); + } + let managed: ManagedResource = new ManagedResource((ManageableResource) manageable, Optional.of(manager.getUrl())); + return new ManageableInstance(context, this, true, managed, manager); + } + + /** + * Gets a {@link ManageableInstance} by first creating the provided resourceUrl, which could + * mean creating either a {@link ManageableResource} or a {@link ManagerResource}. The newly created resource + * is loaded into the instance, and the corresponding {@link ManageableResource} or {@link ManagerResource} is + * looked up and loaded into the instance alongside it. They are loaded as specifically + * typed sub-classes that indicate whether they exist, or (in the case of a {@link ManageableResource}), + * whether they are managed. + * @param context {@link ShapeTreeContext} + * @param method HTTP method used for creation + * @param resourceUrl URL of the resource to create + * @param headers HTTP headers used for creation + * @param body Body of the created resource + * @param contentType Content-type of the created resource + * @return {@link ManageableInstance} with {@link ManageableResource} and {@link ManagerResource} + * @throws ShapeTreeException + */ + override public createInstance(context: ShapeTreeContext, method: string, resourceUrl: URL, headers: ResourceAttributes, body: string, contentType: string): ManageableInstance /* throws ShapeTreeException */ { + let resource: InstanceResource = this.createResource(context, method, resourceUrl, headers, body, contentType); + if (resource instanceof ManageableResource) { + // Managed or unmanaged resource was created + return createInstanceFromManageableResource(context, (ManageableResource) resource); + } else if (resource instanceof ManagerResource) { + // Manager resource was created + return createInstanceFromManagerResource(context, (ManagerResource) resource); + } + throw new ShapeTreeException(500, "Invalid resource type returned from resource creation"); + } + + /** + * Gets a {@link ManageableInstance} given a newly created {@link ManageableResource}. A corresponding + * {@link ManagerResource} is looked up. If it exists, a {@link ManagedResource} is initialized and loaded + * into the instance. If it doesn't, an {@link UnmanagedResource} is initialized and loaded instead. + * @param context {@link ShapeTreeContext} + * @param manageable Newly created {@link ManageableResource} + * @return {@link ManageableInstance} including {@link ManagedResource}|{@link UnmanagedResource} and {@link ManagerResource}|{@link MissingManagerResource} + * @throws ShapeTreeException + */ + private createInstanceFromManageableResource(context: ShapeTreeContext, manageable: ManageableResource): ManageableInstance /* throws ShapeTreeException */ { + // Lookup the corresponding ManagerResource for the ManageableResource + let managerResourceUrl: URL = manageable.getManagerResourceUrl().orElseThrow(() -> new ShapeTreeException(500, "Cannot discover shape tree manager for " + manageable.getUrl())); + let manager: InstanceResource = this.getResource(context, managerResourceUrl); + if (manager instanceof MissingManagerResource) { + // Create and store an UnmanagedResource in instance - if the create was a resource in an unmanaged container + let unmanaged: UnmanagedResource = new UnmanagedResource(manageable, Optional.of(manager.getUrl())); + return new ManageableInstance(context, this, false, unmanaged, (ManagerResource) manager); + } else if (manager instanceof ManagerResource) { + // Create and store a ManagedResource in instance - if the create was a resource in a managed container + let managed: ManagedResource = new ManagedResource(manageable, Optional.of(manager.getUrl())); + return new ManageableInstance(context, this, false, managed, (ManagerResource) manager); + } + throw new ShapeTreeException(500, "Error lookup up corresponding shape tree manager for " + manageable.getUrl()); + } + + /** + * Gets a {@link ManageableInstance} given a newly created {@link ManagerResource}. A corresponding + * {@link ManagedResource} is looked up (and which must exist and be associated with this + * manager). + * @param context {@link ShapeTreeContext} + * @param manager Newly created {@link ManagerResource} + * @return {@link ManageableInstance} including {@link ManagerResource} and {@link ManagedResource} + * @throws ShapeTreeException + */ + private createInstanceFromManagerResource(context: ShapeTreeContext, manager: ManagerResource): ManageableInstance /* throws ShapeTreeException */ { + // Lookup the corresponding ManagedResource for the ManagerResource + let resource: InstanceResource = this.getResource(context, manager.getManagedResourceUrl()); + if (resource instanceof MissingManageableResource) { + throw new ShapeTreeException(500, "Cannot have an existing manager resource " + manager.getUrl() + " with a non-existing managed resource " + resource.getUrl()); + } else if (resource instanceof ManagerResource) { + throw new ShapeTreeException(500, "Invalid manager resource " + resource.getUrl() + " seems to be associated with another manager resource " + manager.getUrl()); + } + let managed: ManagedResource = new ManagedResource((ManageableResource) resource, Optional.of(manager.getUrl())); + return new ManageableInstance(context, this, true, managed, manager); + } + + /** + * Get a {@link InstanceResource} at the provided url, which may or may not exist. + * Most of the work happens in {@link #generateResource(URL, DocumentResponse)}, which + * processes the response and returns the corresponding typed resource. + * @param context {@link ShapeTreeContext} + * @param url Url of the resource to get + * @return {@link InstanceResource} + * @throws ShapeTreeException + */ + override public getResource(context: ShapeTreeContext, url: URL): InstanceResource /* throws ShapeTreeException */ { + log.debug("HttpResourceAccessor#getResource({})", url); + let headers: ResourceAttributes = new ResourceAttributes(); + headers.maybeSet(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(false); + let req: HttpRequest = new HttpRequest("GET", url, headers, null, null); + let response: DocumentResponse = fetcher.fetchShapeTreeResponse(req); + return generateResource(url, response); + } + + /** + * Create a {@link InstanceResource} at the provided url via the provided HTTP + * method. Most of the work happens in {@link #generateResource(URL, DocumentResponse)}, + * which processes the response and returns the corresponding typed resource. + * @param context {@link ShapeTreeContext} + * @param method HTTP method to use for resource creation + * @param url Url of the resource to create + * @param headers HTTP headers to use for resource creation + * @param body Body of resource to create + * @param contentType HTTP content-type + * @return {@link InstanceResource} + * @throws ShapeTreeException + */ + override public createResource(context: ShapeTreeContext, method: string, url: URL, headers: ResourceAttributes, body: string, contentType: string): InstanceResource /* throws ShapeTreeException */ { + log.debug("createResource via {}: URL [{}], headers [{}]", method, url, headers.toString()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(false); + let allHeaders: ResourceAttributes = headers.maybePlus(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); + let response: DocumentResponse = fetcher.fetchShapeTreeResponse(new HttpRequest(method, url, allHeaders, body, contentType)); + if (!response.isExists()) { + throw new ShapeTreeException(500, "Unable to create resource <" + url + ">"); + } + return generateResource(url, response); + } + + /** + * Generates a typed {@link InstanceResource} based on the response from {@link #getResource(ShapeTreeContext, URL)} or + * {@link #createResource(ShapeTreeContext, String, URL, ResourceAttributes, String, String)}. + * Determines whether the resource is an existing {@link ManageableResource} or {@link ManagerResource}. + * @param url Url of the resource to generate + * @param response Response from a create or update of url + * @return Generated {@link InstanceResource}, either {@link ManageableResource} or {@link ManagerResource} + * @throws ShapeTreeException + */ + private generateResource(url: URL, response: DocumentResponse): InstanceResource /* throws ShapeTreeException */ { + // If a resource was created, ensure the URL returned in the Location header is valid + let location: string | null = response.getResourceAttributes() === null ? Optional.empty() : response.getResourceAttributes().firstValue(HttpHeaders.LOCATION.getValue()); + if (location.isPresent()) { + try { + url = new URL(location.get()); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Retrieving <" + url + "> yielded a Location header \"" + location.get() + "\" which doesn't parse as a URL: " + e.getMessage()); + } +} + } + // Determine whether the resource exists based on the response. Even if the resource + // doesn't exist, additional context and processing is done to provide the appropriate + // typed resource with adequate context to the caller + const exists: boolean = response.isExists(); + const container: boolean = isContainerFromHeaders(response.getResourceAttributes(), url); + // TODO: could be null + const attributes: ResourceAttributes = response.getResourceAttributes(); + const resourceType: ShapeTreeResourceType = getResourceTypeFromHeaders(response.getResourceAttributes()); + const name: string = calculateName(url); + // TODO: could be null. readStringIntoModel not set up for `rawContent=null` + const body: string = response.getBody(); + if (response.getBody() === null) { + log.error("Could not retrieve the body string from response for " + url); + // TODO: remove when TODO above is resolved. + throw new IllegalStateException("Could not retrieve the body string from response for <" + url + ">"); + } + // Parse Link headers from response and populate ResourceAttributes + const linkHeaders: Array = attributes.allValues(HttpHeaders.LINK.getValue()); + let parsedLinkHeaders: ResourceAttributes = // !! + linkHeaders.isEmpty() ? new ResourceAttributes() : ResourceAttributes.parseLinkHeaders(linkHeaders); + // Determine if the resource is a shape tree manager based on the response + const isManager: boolean = calculateIsManager(url, exists, parsedLinkHeaders); + if (Boolean.TRUE === isManager) { + const managedResourceUrl: URL = calculateManagedUrl(url, parsedLinkHeaders); + if (exists) { + return new ManagerResource(url, resourceType, attributes, body, name, true, managedResourceUrl); + } else { + return new MissingManagerResource(managedResourceUrl, url, resourceType, attributes, body, name); + } + } else { + // Look for presence of st:managedBy in link headers from response and get the target manager URL + const managerUrl: URL | null = calculateManagerUrl(url, parsedLinkHeaders); + if (exists) { + return new ManageableResource(url, resourceType, attributes, body, name, true, managerUrl, container); + } else { + return new MissingManageableResource(url, resourceType, attributes, body, name, managerUrl, container); + } + } + } + + /** + * Gets a List of contained {@link ManageableInstance}s from a given container specified by containerUrl + * @param context {@link ShapeTreeContext} + * @param containerUrl URL of target container resource + * @return List of {@link ManageableInstance}s from the target container + * @throws ShapeTreeException + */ + override public getContainedInstances(context: ShapeTreeContext, containerUrl: URL): Array /* throws ShapeTreeException */ { + try { + let resource: InstanceResource = this.getResource(context, containerUrl); + if (!(resource instanceof ManageableResource)) { + throw new ShapeTreeException(500, "Cannot get contained resources for a manager resource <" + containerUrl + ">"); + } + let containerResource: ManageableResource = (ManageableResource) resource; + if (Boolean.FALSE === containerResource.isContainer()) { + throw new ShapeTreeException(500, "Cannot get contained resources for a resource that is not a Container <" + containerUrl + ">"); + } + let containerGraph: Graph = readStringIntoGraph(urlToUri(containerUrl), containerResource.getBody(), containerResource.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null)); + if (containerGraph.isEmpty()) { + return Collections.emptyList(); + } + let containerTriples: Array = containerGraph.find(NodeFactory.createURI(containerUrl.toString()), NodeFactory.createURI(LdpVocabulary.CONTAINS), Node.ANY).toList(); + if (containerTriples.isEmpty()) { + return Collections.emptyList(); + } + let containedInstances: Array = new Array<>(); + for (const containerTriple of containerTriples) { + let containedInstance: ManageableInstance = this.getInstance(context, new URL(containerTriple.getObject().getURI())); + containedInstances.add(containedInstance); + } + return containedInstances; + } catch (ex) { + if (ex instanceof Exception) { + throw new ShapeTreeException(500, ex.getMessage()); + } +} + } + + /** + * Updates the provided {@link InstanceResource} updateResource with body via the supplied + * method + * @param context Shape tree context + * @param method HTTP method to use for update + * @param updateResource {@link InstanceResource} to update + * @param body Body to use for update + * @return {@link DocumentResponse} of the result + * @throws ShapeTreeException + */ + override public updateResource(context: ShapeTreeContext, method: string, updateResource: InstanceResource, body: string): DocumentResponse /* throws ShapeTreeException */ { + log.debug("updateResource: URL [{}]", updateResource.getUrl()); + let contentType: string = updateResource.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null); + // [careful] updateResource attributes may contain illegal client headers (connection, content-length, date, expect, from, host, upgrade, via, warning) + let allHeaders: ResourceAttributes = updateResource.getAttributes().maybePlus(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(false); + return fetcher.fetchShapeTreeResponse(new HttpRequest(method, updateResource.getUrl(), allHeaders, body, contentType)); + } + + /** + * Deletes the provided {@link InstanceResource }deleteResource + * @param context {@link ShapeTreeContext} + * @param deleteResource {@link InstanceResource} to delete + * @return {@link DocumentResponse} of the result + * @throws ShapeTreeException + */ + override public deleteResource(context: ShapeTreeContext, deleteResource: ManagerResource): DocumentResponse /* throws ShapeTreeException */ { + log.debug("deleteResource: URL [{}]", deleteResource.getUrl()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(false); + let allHeaders: ResourceAttributes = deleteResource.getAttributes().maybePlus(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); + let response: DocumentResponse = fetcher.fetchShapeTreeResponse(new HttpRequest("DELETE", deleteResource.getUrl(), allHeaders, null, null)); + let respCode: number = response.getStatusCode(); + if (respCode < 200 || respCode >= 400) { + log.error("Error deleting resource {}, Status {}", deleteResource.getUrl(), respCode); + } + return response; + } + + /** + * Look for a Link rel=type of ldp:Container or ldp:BasicContainer + * @param headers to parse + * @return True if headers indicating a container are found + */ + private isContainerFromHeaders(headers: ResourceAttributes, url: URL): boolean { + let linkHeaders: Array = headers === null ? Collections.emptyList() : headers.allValues(HttpHeaders.LINK.getValue()); + if (linkHeaders.isEmpty()) { + return url.getPath().endsWith("/"); + } + let parsedLinkHeaders: ResourceAttributes = ResourceAttributes.parseLinkHeaders(linkHeaders); + let typeLinks: Array = parsedLinkHeaders.allValues(LinkRelations.TYPE.getValue()); + if (!typeLinks.isEmpty()) { + return typeLinks.contains(LdpVocabulary.CONTAINER) || typeLinks.contains(LdpVocabulary.BASIC_CONTAINER); + } + return false; + } + + /** + * Determine a resource type by parsing Link rel=type headers + * @param headers to parse + * @return Type of resource + */ + private getResourceTypeFromHeaders(headers: ResourceAttributes): ShapeTreeResourceType { + let linkHeaders: Array = headers === null ? null : headers.allValues(HttpHeaders.LINK.getValue()); + if (linkHeaders === null) { + return null; + } + let parsedLinkHeaders: ResourceAttributes = ResourceAttributes.parseLinkHeaders(linkHeaders); + let typeLinks: Array = parsedLinkHeaders.allValues(LinkRelations.TYPE.getValue()); + if (typeLinks != null && (typeLinks.contains(LdpVocabulary.CONTAINER) || typeLinks.contains(LdpVocabulary.BASIC_CONTAINER))) { + return ShapeTreeResourceType.CONTAINER; + } + if (supportedRDFContentTypes.contains(headers.firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(""))) { + // orElse("") because contains(null) throw NPE + return ShapeTreeResourceType.RESOURCE; + } + return ShapeTreeResourceType.NON_RDF; + } + + /** + * Looks for the presence of the http://www.w3.org/ns/shapetrees#managedBy HTTP Link Relation in the + * provided parsedLinkHeaders, with a valid target URL of a {@link ShapeTreeManager} associated + * with the provided url. + * @param url URL of the (potentially) managed resource + * @param parsedLinkHeaders Parsed HTTP Link headers to evaluate + * @return + * @throws ShapeTreeException + */ + private calculateManagerUrl(url: URL, parsedLinkHeaders: ResourceAttributes): URL | null /* throws ShapeTreeException */ { + const optManagerString: string | null = parsedLinkHeaders.firstValue(LinkRelations.MANAGED_BY.getValue()); + if (optManagerString.isEmpty()) { + log.info("The resource {} does not contain a link header of {}", url, LinkRelations.MANAGED_BY.getValue()); + return Optional.empty(); + } + let managerUrlString: string = optManagerString.get(); + try { + return Optional.of(new URL(url, managerUrlString)); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Malformed relative URL <" + managerUrlString + "> (resolved from <" + url + ">)"); + } +} + } + + /** + * Looks for the presence of the http://www.w3.org/ns/shapetrees#manages HTTP Link Relation in the + * provided parsedLinkHeaders, with a valid target URL of a {@link ManagedResource}. Falls + * back to a relatively crude inference when the more reliable header isn't available + * @param managerUrl URL of the {@link ShapeTreeManager} + * @param parsedLinkHeaders Parsed link headers from {@link ManagerResource} response + * @return URL of {@link ManagedResource} + * @throws ShapeTreeException + */ + private calculateManagedUrl(managerUrl: URL, parsedLinkHeaders: ResourceAttributes): URL /* throws ShapeTreeException */ { + let managedUrlString: string; + let managedResourceUrl: URL; + const optManagedString: string | null = parsedLinkHeaders.firstValue(LinkRelations.MANAGES.getValue()); + if (!optManagedString.isEmpty()) { + managedUrlString = optManagedString.get(); + } else { + // Attempt to (crudely) infer based on path calculation + // If this implementation uses a dot notation for meta, trim it from the path + // Rebuild without the query string in case that was employed + managedUrlString = managerUrl.getPath().replaceAll("\\.shapetree$", ""); + } + try { + managedResourceUrl = new URL(managerUrl, managedUrlString); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Can't calculate managed resource for shape tree manager <" + managerUrl + ">"); + } +} + return managedResourceUrl; + } + + /** + * Calculates the name of the resource itself, removing any leading path and any trailing slash. In + * the event that the resource is '/', then '/' will be returned. + * @param url URL of the resource to evaluate + * @return Name of resource + */ + private calculateName(url: URL): string { + let path: string = url.getPath(); + if (path === "/") + return "/"; + // if this is a container, trim the trailing slash + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + let pathIndex: number = path.lastIndexOf('/'); + // No slashes in the path + if (pathIndex === -1) { + return path; + } + return path.substring(path.lastIndexOf('/') + 1); + } + + /** + * Determine whether url is a {@link ManagerResource}. Since this is a completely HTTP based + * resource processor, this determination can't be made with special server-side knowledge about + * the nature of the resources it serves. Instead, this must be derived based on information + * present in the HTTP response from the server. + * @param url URL of the resource that is being evaluated + * @param exists whether the resource at url exists + * @param parsedLinkHeaders Parsed HTTP Link headers from the response for url + * @return True if {@link ManagerResource} + */ + private calculateIsManager(url: URL, exists: boolean, parsedLinkHeaders: ResourceAttributes): boolean { + // If the resource has an HTTP Link header of type of https://www.w3.org/ns/shapetrees#managedBy + // with a manager target, it is not a manager resource (because it is managed by one) + if (Boolean.TRUE === exists && parsedLinkHeaders.firstValue(LinkRelations.MANAGED_BY.getValue()).isPresent()) { + return false; + } + // If the resource has an HTTP Link header of type of https://www.w3.org/ns/shapetrees#manages + // it is a manager resource (because it manages another one). + if (Boolean.TRUE === exists && parsedLinkHeaders.firstValue(LinkRelations.MANAGES.getValue()).isPresent()) { + return true; + } + // If the resource doesn't exist, attempt to infer based on the URL + if (url.getPath() != null && url.getPath().matches(".*\\.shapetree$")) { + return true; + } + return url.getQuery() != null && url.getQuery().matches(".*ext\\=shapetree$"); + } + + public constructor() { + } +} diff --git a/asTypescript/packages/client-http/src/HttpShapeTreeClient.ts b/asTypescript/packages/client-http/src/HttpShapeTreeClient.ts new file mode 100644 index 00000000..7b262149 --- /dev/null +++ b/asTypescript/packages/client-http/src/HttpShapeTreeClient.ts @@ -0,0 +1,259 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.client.http +import { ShapeTreeClient } from '@shapetrees/clientcore/src/ShapeTreeClient'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { ShapeTreeContext } from '@shapetrees/core/src/ShapeTreeContext'; +import { ManageableInstance } from '@shapetrees/core/src/ManageableInstance'; +import { ManageableResource } from '@shapetrees/core/src/ManageableResource'; +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ShapeTree } from '@shapetrees/core/src/ShapeTree'; +import { ShapeTreeFactory } from '@shapetrees/core/src/ShapeTreeFactory'; +import { ShapeTreeAssignment } from '@shapetrees/core/src/ShapeTreeAssignment'; +import { ResourceAttributes } from '@shapetrees/core/src/ResourceAttributes'; +import { HttpHeaders } from '@shapetrees/core/src/enums/HttpHeaders'; +import { LinkRelations } from '@shapetrees/core/src/enums/LinkRelations'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import * as Lang from 'org/apache/jena/riot'; +import * as RDFDataMgr from 'org/apache/jena/riot'; +import * as RDFWriter from 'org/apache/jena/riot'; +import * as RIOT from 'org/apache/jena/riot'; +import { Writable } from 'stream'; +import * as StandardCharsets from 'java/nio/charset'; +import { HttpRequest } from './HttpRequest'; +import { HttpResourceAccessor } from './HttpResourceAccessor'; +import { HttpClient } from './HttpClient'; + +export class HttpShapeTreeClient implements ShapeTreeClient { + + private useClientShapeTreeValidation: boolean = true; + + override public isShapeTreeValidationSkipped(): boolean { + return !this.useClientShapeTreeValidation; + } + + override public skipShapeTreeValidation(skipValidation: boolean): void { + this.useClientShapeTreeValidation = !skipValidation; + } + + /** + * Discover the ShapeTreeManager associated with a given target resource. + * Implements {@link ShapeTreeClient#discoverShapeTree} + * + * Shape Trees, §4.1: This operation is used by a client-side agent to discover any shape trees associated + * with a given resource. If URL is a managed resource, the associated Shape Tree Manager will be returned. + * https://shapetrees.org/TR/specification/#discover + * + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The URL of the target resource for shape tree discovery + * @return + * @throws ShapeTreeException + */ + override public discoverShapeTree(context: ShapeTreeContext, targetResource: URL): ShapeTreeManager | null /* throws ShapeTreeException */ { + if (targetResource === null) { + throw new ShapeTreeException(500, "Must provide a value target resource for discovery"); + } + log.debug("Discovering shape tree manager managing {}", targetResource); + // Lookup the target resource for pointer to associated shape tree manager + const resourceAccessor: HttpResourceAccessor = new HttpResourceAccessor(); + let instance: ManageableInstance = resourceAccessor.getInstance(context, targetResource); + let manageableResource: ManageableResource = instance.getManageableResource(); + if (!manageableResource.isExists()) { + log.debug("Target resource for discovery {} does not exist", targetResource); + return Optional.empty(); + } + if (instance.wasRequestForManager()) { + throw new ShapeTreeException(500, "Discovery target must not be a shape tree manager resource"); + } + if (instance.isUnmanaged()) { + return Optional.empty(); + } + return Optional.of(instance.getManagerResource().getManager()); + } + + /** + * Shape Trees, §4.2: This operation marks an existing resource as being managed by one or more shape trees, + * by associating a shape tree manager with the resource, and turning it into a managed resource. + * + * If the resource is already managed, the associated shape tree manager will be updated with another + * shape tree assignment for the planted shape tree. + * + * If the resource is a container that already contains existing resources, and a recursive plant is requested, + * this operation will perform a depth first traversal through the containment hierarchy, validating + * and assigning as it works its way back up to the target root resource of this operation. + * + * https://shapetrees.org/TR/specification/#plant-shapetree + * + * @param context ShapeTreeContext that would be used for authentication purposes + * @param targetResource The URL of the resource to plant on + * @param targetShapeTree A URL representing the shape tree to plant for targetResource + * @param focusNode An optional URL representing the target subject within targetResource used for shape validation + * @return The URL of the Shape Tree Manager that was planted for targetResource + * @throws ShapeTreeException + */ + override public plantShapeTree(context: ShapeTreeContext, targetResource: URL, targetShapeTree: URL, focusNode: URL): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || targetResource === null || targetShapeTree === null) { + throw new ShapeTreeException(500, "Must provide a valid context, target resource, and target shape tree to the plant shape tree"); + } + log.debug("Planting shape tree {} on {}: ", targetShapeTree, targetResource); + log.debug("Focus node: {}", focusNode === null ? "None provided" : focusNode); + const resourceAccessor: HttpResourceAccessor = new HttpResourceAccessor(); + // Lookup the shape tree + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(targetShapeTree); + // Lookup the target resource + let instance: ManageableInstance = resourceAccessor.getInstance(context, targetResource); + let manageableResource: ManageableResource = instance.getManageableResource(); + if (!manageableResource.isExists()) { + return new DocumentResponse(new ResourceAttributes(), "Cannot find target resource to plant: " + targetResource, 404); + } + let manager: ShapeTreeManager; + let managerResourceUrl: URL = instance.getManagerResource().getUrl(); + if (instance.isManaged()) { + // TODO: could be null + manager = instance.getManagerResource().getManager(); + } else { + manager = new ShapeTreeManager(managerResourceUrl); + } + // Initialize a shape tree assignment based on the supplied parameters + let assignmentUrl: URL = manager.mintAssignmentUrl(); + let assignment: ShapeTreeAssignment = new ShapeTreeAssignment(targetShapeTree, targetResource, assignmentUrl, focusNode, shapeTree.getShape(), assignmentUrl); + // Add the assignment to the manager + manager.addAssignment(assignment); + // Get an RDF version of the manager stored in a turtle string + let asBytes: ByteArrayOutputStream = new ByteArrayOutputStream(); + RDFWriter.create().base(targetShapeTree.toString()).set(RIOT.symTurtleOmitBase, false).set(RIOT.symTurtleDirectiveStyle, "rdf11").lang(Lang.TTL).source(manager.getGraph()).output(asBytes); + let asString: string = new string(asBytes.toByteArray(), StandardCharsets.UTF_8); + // Build an HTTP PUT request with the manager graph in turtle as the content body + link header + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + let headers: ResourceAttributes = new ResourceAttributes(); + headers.maybeSet(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); + return fetcher.fetchShapeTreeResponse(new HttpRequest("PUT", managerResourceUrl, headers, asString, "text/turtle")); + } + + override public postManagedInstance(context: ShapeTreeContext, parentContainer: URL, focusNodes: Array, targetShapeTrees: Array, proposedResourceName: string, isContainer: boolean, bodyString: string, contentType: string): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || parentContainer === null) { + throw new ShapeTreeException(500, "Must provide a valid context and parent container to post shape tree instance"); + } + log.debug("POST-ing shape tree instance to {}", parentContainer); + log.debug("Proposed name: {}", proposedResourceName === null ? "None provided" : proposedResourceName); + log.debug("Target Shape Tree: {}", targetShapeTrees === null || targetShapeTrees.isEmpty() ? "None provided" : targetShapeTrees.toString()); + log.debug("Focus Node: {}", focusNodes === null || focusNodes.isEmpty() ? "None provided" : focusNodes.toString()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + let headers: ResourceAttributes = getCommonHeaders(context, focusNodes, targetShapeTrees, isContainer, proposedResourceName, contentType); + return fetcher.fetchShapeTreeResponse(new HttpRequest("POST", parentContainer, headers, bodyString, contentType)); + } + + // Create via HTTP PUT + override public putManagedInstance(context: ShapeTreeContext, resourceUrl: URL, focusNodes: Array, bodyString: string, contentType: string, targetShapeTrees: Array, isContainer: boolean): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || resourceUrl === null) { + throw new ShapeTreeException(500, "Must provide a valid context and target resource to create shape tree instance via PUT"); + } + log.debug("Creating shape tree instance via PUT at {}", resourceUrl); + log.debug("Target Shape Tree: {}", targetShapeTrees === null || targetShapeTrees.isEmpty() ? "None provided" : targetShapeTrees.toString()); + log.debug("Focus Node: {}", focusNodes === null || focusNodes.isEmpty() ? "None provided" : focusNodes.toString()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + let headers: ResourceAttributes = getCommonHeaders(context, focusNodes, targetShapeTrees, isContainer, null, contentType); + return fetcher.fetchShapeTreeResponse(new HttpRequest("PUT", resourceUrl, headers, bodyString, contentType)); + } + + // Update via HTTP PUT + override public updateManagedInstance(context: ShapeTreeContext, resourceUrl: URL, focusNodes: Array, bodyString: string, contentType: string): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || resourceUrl === null) { + throw new ShapeTreeException(500, "Must provide a valid context and target resource to update shape tree instance via PUT"); + } + log.debug("Updating shape tree instance via PUT at {}", resourceUrl); + log.debug("Focus Node: {}", focusNodes === null || focusNodes.isEmpty() ? "None provided" : focusNodes.toString()); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + let headers: ResourceAttributes = getCommonHeaders(context, focusNodes, null, null, null, contentType); + return fetcher.fetchShapeTreeResponse(new HttpRequest("PUT", resourceUrl, headers, bodyString, contentType)); + } + + override public patchManagedInstance(context: ShapeTreeContext, resourceUrl: URL, focusNodes: Array, patchString: string): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || resourceUrl === null || patchString === null) { + throw new ShapeTreeException(500, "Must provide a valid context, target resource, and PATCH expression to PATCH shape tree instance"); + } + log.debug("PATCH-ing shape tree instance at {}", resourceUrl); + log.debug("PATCH String: {}", patchString); + log.debug("Focus Node: {}", focusNodes === null || focusNodes.isEmpty() ? "None provided" : focusNodes.toString()); + let contentType: string = "application/sparql-update"; + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + let headers: ResourceAttributes = getCommonHeaders(context, focusNodes, null, null, null, contentType); + return fetcher.fetchShapeTreeResponse(new HttpRequest("PATCH", resourceUrl, headers, patchString, contentType)); + } + + override public deleteManagedInstance(context: ShapeTreeContext, resourceUrl: URL): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || resourceUrl === null) { + throw new ShapeTreeException(500, "Must provide a valid context and target resource to DELETE shape tree instance"); + } + log.debug("DELETE-ing shape tree instance at {}", resourceUrl); + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + let headers: ResourceAttributes = getCommonHeaders(context, null, null, null, null, null); + return fetcher.fetchShapeTreeResponse(new HttpRequest("DELETE", resourceUrl, headers, null, null)); + } + + override public unplantShapeTree(context: ShapeTreeContext, targetResource: URL, targetShapeTree: URL): DocumentResponse /* throws ShapeTreeException */ { + if (context === null || targetResource === null || targetShapeTree === null) { + throw new ShapeTreeException(500, "Must provide a valid context, target resource, and target shape tree to unplant"); + } + log.debug("Unplanting shape tree {} managing {}: ", targetShapeTree, targetResource); + // Lookup the target resource + const resourceAccessor: HttpResourceAccessor = new HttpResourceAccessor(); + let instance: ManageableInstance = resourceAccessor.getInstance(context, targetResource); + let manageableResource: ManageableResource = instance.getManageableResource(); + if (!manageableResource.isExists()) { + return new DocumentResponse(null, "Cannot find target resource to unplant: " + targetResource, 404); + } + if (instance.isUnmanaged()) { + return new DocumentResponse(null, "Cannot unplant target resource that is not managed by a shapetree: " + targetResource, 500); + } + // Remove assignment from manager that corresponds with the provided shape tree + // TODO: could be null + let manager: ShapeTreeManager = instance.getManagerResource().getManager(); + manager.removeAssignmentForShapeTree(targetShapeTree); + let method: string; + let body: string; + let contentType: string; + if (manager.getAssignments().isEmpty()) { + method = "DELETE"; + body = null; + contentType = null; + } else { + // Build an HTTP PUT request with the manager graph in turtle as the content body + link header + method = "PUT"; + // Get a RDF version of the manager stored in a turtle string + let sw: Writable = new Writable(); + RDFDataMgr.write(sw, manager.getGraph(), Lang.TURTLE); + body = sw.toString(); + contentType = "text/turtle"; + } + let fetcher: HttpClient = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); + return fetcher.fetchShapeTreeResponse(new HttpRequest(method, manager.getId(), // why no getCommonHeaders(context, null, null, null, null, null) + null, body, contentType)); + } + + private getCommonHeaders(context: ShapeTreeContext, focusNodes: Array, targetShapeTrees: Array, isContainer: boolean, proposedResourceName: string, contentType: string): ResourceAttributes { + let ret: ResourceAttributes = new ResourceAttributes(); + if (context.getAuthorizationHeaderValue() != null) { + ret.maybeSet(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); + } + if (isContainer != null) { + let resourceTypeUrl: string = Boolean.TRUE === isContainer ? "http://www.w3.org/ns/ldp#Container" : "http://www.w3.org/ns/ldp#Resource"; + ret.maybeSet(HttpHeaders.LINK.getValue(), "<" + resourceTypeUrl + ">; rel=\"type\""); + } + if (focusNodes != null && !focusNodes.isEmpty()) { + for (const focusNode of focusNodes) { + ret.maybeSet(HttpHeaders.LINK.getValue(), "<" + focusNode + ">; rel=\"" + LinkRelations.FOCUS_NODE.getValue() + "\""); + } + } + if (targetShapeTrees != null && !targetShapeTrees.isEmpty()) { + for (const targetShapeTree of targetShapeTrees) { + ret.maybeSet(HttpHeaders.LINK.getValue(), "<" + targetShapeTree + ">; rel=\"" + LinkRelations.TARGET_SHAPETREE.getValue() + "\""); + } + } + if (proposedResourceName != null) { + ret.maybeSet(HttpHeaders.SLUG.getValue(), proposedResourceName); + } + if (contentType != null) { + ret.maybeSet(HttpHeaders.CONTENT_TYPE.getValue(), contentType); + } + return ret; + } +} diff --git a/asTypescript/packages/core/src/DocumentResponse.ts b/asTypescript/packages/core/src/DocumentResponse.ts new file mode 100644 index 00000000..2b88777d --- /dev/null +++ b/asTypescript/packages/core/src/DocumentResponse.ts @@ -0,0 +1,39 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { HttpHeaders } from './enums/HttpHeaders'; +import { ResourceAttributes } from './ResourceAttributes'; + +export class DocumentResponse { + + private readonly resourceAttributes: ResourceAttributes; + + private readonly body: string; + + private readonly statusCode: number; + + public getContentType(): string | null { + return this.resourceAttributes === null ? Optional.empty() : this.resourceAttributes.firstValue(HttpHeaders.CONTENT_TYPE.getValue()); + } + + // TODO: lots of choices re non-404, not >= 4xx, not 3xx. not 201 (meaning there's no body) + public isExists(): boolean { + return this.statusCode / 100 === 2; + } + + public constructor(resourceAttributes: ResourceAttributes, body: string, statusCode: number) { + this.resourceAttributes = resourceAttributes; + this.body = body; + this.statusCode = statusCode; + } + + public getResourceAttributes(): ResourceAttributes { + return this.resourceAttributes; + } + + public getBody(): string { + return this.body; + } + + public getStatusCode(): number { + return this.statusCode; + } +} diff --git a/asTypescript/packages/core/src/InstanceResource.ts b/asTypescript/packages/core/src/InstanceResource.ts new file mode 100644 index 00000000..483ab386 --- /dev/null +++ b/asTypescript/packages/core/src/InstanceResource.ts @@ -0,0 +1,93 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { HttpHeaders } from './enums/HttpHeaders'; +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { GraphHelper } from './helpers/GraphHelper'; +import * as Graph from 'org/apache/jena/graph'; +import * as URI from 'java/net'; +import { urlToUri } from './helpers/GraphHelper/urlToUri'; +import { ResourceAttributes } from './ResourceAttributes'; + +/** + * InstanceResource is a base class which may represent either a ManageableResource + * (a resource that can be managed by a shape tree), or a ManagerResource (a resource + * which assigns shape trees to a ManageableResource). This class is only meant to be + * extended by ManageableResource and ManagerResource, or used to indicate either + * of the two. + */ +export class InstanceResource { + + private readonly url: URL; + + private readonly resourceType: ShapeTreeResourceType; + + private readonly attributes: ResourceAttributes; + + private readonly body: string; + + private readonly name: string; + + private readonly exists: boolean; + + /** + * Construct an InstanceResource by providing essential attributes. This constructor is meant + * to be called by sub-class constructors. + * @param url URL of the instance resource + * @param resourceType Identified shape tree resource type + * @param attributes Associated resource attributes + * @param body Body of the resource + * @param name Name of the resource + * @param exists Whether the resource exists + */ + constructor(url: URL, resourceType: ShapeTreeResourceType, attributes: ResourceAttributes, body: string, name: string, exists: boolean) { + this.url = url; + this.resourceType = resourceType; + this.attributes = attributes; + this.body = body; + this.name = name; + this.exists = exists; + } + + /** + * Get an RDF graph of the body of the InstanceResource. If baseUrl is not + * provided, the URL of the InstanceResource will be used. An exception is thrown if + * the body cannot be processed (e.g. if it isn't a valid RDF resource). + * @param baseUrl Base URL to use for the graph + * @return RDF graph of the InstanceResource body + * @throws ShapeTreeException + */ + public getGraph(baseUrl: URL): Graph /* throws ShapeTreeException */ { + if (!this.isExists()) { + return null; + } + if (baseUrl === null) { + baseUrl = this.url; + } + const baseUri: URI = urlToUri(baseUrl); + return GraphHelper.readStringIntoGraph(baseUri, this.getBody(), this.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null)); + } + + public getUrl(): URL { + return this.url; + } + + public getResourceType(): ShapeTreeResourceType { + return this.resourceType; + } + + public getAttributes(): ResourceAttributes { + return this.attributes; + } + + public getBody(): string { + return this.body; + } + + public getName(): string { + return this.name; + } + + public getExists(): boolean { + return this.exists; + } +} diff --git a/asTypescript/packages/core/src/ManageableInstance.ts b/asTypescript/packages/core/src/ManageableInstance.ts new file mode 100644 index 00000000..60b6fd29 --- /dev/null +++ b/asTypescript/packages/core/src/ManageableInstance.ts @@ -0,0 +1,113 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import * as Objects from 'java/util'; +import { ResourceAccessor } from './ResourceAccessor'; +import { ShapeTreeContext } from './ShapeTreeContext'; +import { ManageableResource } from './ManageableResource'; +import { ManagerResource } from './ManagerResource'; +import { MissingManagerResource } from './MissingManagerResource'; + +/** + * A ManageableInstance represents a pairing of a shape tree ManagerResource + * and a ManageableResource. + * + * The ManageableInstance may represent a managed + * state, where the ManageableResource is a ManagedResource that is + * managed by one or more shape trees assigned by the ShapeTreeManager + * in the ManagedResource.Conversely, it could represent an unmanaged + * state, where the ManageableResource is an UnmanagedResource and the + * ManagedResource is a MissingManagedResource. Lastly, it may + * represent other state combinations where one or both of the + * ManageableResource or ManagedResource are missing. + * + * Both ManageableResource and ManagedResource are looked up and loaded + * upon construction of the ManageableInstance, which should be done + * through a ResourceAccessor. Once constructed, the ManageableInstance + * is immutable. + */ +export class ManageableInstance { + + public static readonly TEXT_TURTLE: string = "text/turtle"; + + private readonly resourceAccessor: ResourceAccessor; + + private readonly shapeTreeContext: ShapeTreeContext; + + private readonly wasRequestForManager: boolean; + + private readonly manageableResource: ManageableResource; + + private readonly managerResource: ManagerResource; + + /** + * Indicates whether the HTTP request that triggered the initialization of the + * ManageableInstance was targeted towards the ManagerResource or the + * ManageableResource. + * @return True when the request targeted the ManagerResource + */ + public wasRequestForManager(): boolean { + return isWasRequestForManager(); + } + + /** + * Indicates whether the ManageableInstance represents an unmanaged state, with a + * UnmanagedResource and a MissingManagerResource + * @return True when the instance is in an unmanaged state + */ + public isUnmanaged(): boolean { + return managerResource instanceof MissingManagerResource; + } + + /** + * Indicates whether the ManageableInstance represents a managed state, with a + * ManagedResource assigned one or more shape trees by a ShapeTreeManager in + * a ManagerResource + * @return True when the instance is in an managed state + */ + public isManaged(): boolean { + return !isUnmanaged(); + } + + /** + * Constructor for a ManageableInstance. Since a ManageableInstance is immutable, all + * elements must be provided, and cannot be null. ManageableInstances should be + * constructed through a ResourceAccessor: + * {@link ResourceAccessor#createInstance(ShapeTreeContext, String, URL, ResourceAttributes, String, String)} + * {@link ResourceAccessor#getInstance(ShapeTreeContext, URL)} + * @param context Shape tree context + * @param resourceAccessor Resource accessor in use + * @param wasRequestForManager True if the manager resource was the target of the associated request + * @param manageableResource Initialized manageable resource, which may be a typed sub-class + * @param managerResource Initialized manager resource, which may be a typed sub-class + */ + public constructor(context: ShapeTreeContext, resourceAccessor: ResourceAccessor, wasRequestForManager: boolean, manageableResource: ManageableResource, managerResource: ManagerResource) { + this.shapeTreeContext = Objects.requireNonNull(context, "Must provide a shape tree context"); + this.resourceAccessor = Objects.requireNonNull(resourceAccessor, "Must provide a resource accessor"); + this.wasRequestForManager = wasRequestForManager; + this.manageableResource = Objects.requireNonNull(manageableResource, "Must provide a manageable resource"); + this.managerResource = Objects.requireNonNull(managerResource, "Must provide a manager resource"); + } + + public getTEXT_TURTLE(): string { + return this.TEXT_TURTLE; + } + + public getResourceAccessor(): ResourceAccessor { + return this.resourceAccessor; + } + + public getShapeTreeContext(): ShapeTreeContext { + return this.shapeTreeContext; + } + + public getWasRequestForManager(): boolean { + return this.wasRequestForManager; + } + + public getManageableResource(): ManageableResource { + return this.manageableResource; + } + + public getManagerResource(): ManagerResource { + return this.managerResource; + } +} diff --git a/asTypescript/packages/core/src/ManageableResource.ts b/asTypescript/packages/core/src/ManageableResource.ts new file mode 100644 index 00000000..64b9ca36 --- /dev/null +++ b/asTypescript/packages/core/src/ManageableResource.ts @@ -0,0 +1,61 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import * as MalformedURLException from 'java/net'; +import { InstanceResource } from './InstanceResource'; +import { ResourceAttributes } from './ResourceAttributes'; + +/** + * A ManageableResource represents a regular resource that could be managed by + * one or more shape trees. Each possible state is represented by typed + * subclasses; ManagedResource, UnmanagedResource, and + * MissingManageableResource. When the state is known, the appropriate + * typed subclass should be used. + */ +export class ManageableResource extends InstanceResource { + + private readonly managerResourceUrl: URL | null; + + private readonly isContainer: boolean; + + /** + * Construct a manageable resource. + * @param url URL of the resource + * @param resourceType Identified shape tree resource type + * @param attributes Associated resource attributes + * @param body Body of the resource + * @param name Name of the resource + * @param exists Whether the resource exists + * @param managerResourceUrl URL of the shape tree manager resource + * @param isContainer Whether the resource is a container + */ + public constructor(url: URL, resourceType: ShapeTreeResourceType, attributes: ResourceAttributes, body: string, name: string, exists: boolean, managerResourceUrl: URL | null, isContainer: boolean) { + super(url, resourceType, attributes, body, name, exists); + this.managerResourceUrl = managerResourceUrl; + this.isContainer = isContainer; + } + + /** + * Get the URL of the resource's parent container + * @return URL of the parent container + * @throws ShapeTreeException + */ + public getParentContainerUrl(): URL /* throws ShapeTreeException */ { + const rel: string = this.isContainer() ? ".." : "."; + try { + return new URL(this.getUrl(), rel); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Malformed focus node when resolving <" + rel + "> against <" + this.getUrl() + ">"); + } +} + } + + public getManagerResourceUrl(): URL | null { + return this.managerResourceUrl; + } + + public getIsContainer(): boolean { + return this.isContainer; + } +} diff --git a/asTypescript/packages/core/src/ManagedResource.ts b/asTypescript/packages/core/src/ManagedResource.ts new file mode 100644 index 00000000..b2ecea0c --- /dev/null +++ b/asTypescript/packages/core/src/ManagedResource.ts @@ -0,0 +1,20 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ManageableResource } from './ManageableResource'; + +/** + * A ManagedResource indicates that a given ManageableResource + * is managed by a shape tree. This means that is has an associated + * ManagerResource that exists and contains a valid ShapeTreeManager. + */ +export class ManagedResource extends ManageableResource { + + /** + * Construct a ManagedResource based on a provided ManageableResource + * manageable and managerUrl + * @param manageable ManageableResource to construct the ManagedResource from + * @param managerUrl URL of the associated shape tree manager resource + */ + public constructor(manageable: ManageableResource, managerUrl: URL | null) { + super(manageable.getUrl(), manageable.getResourceType(), manageable.getAttributes(), manageable.getBody(), manageable.getName(), manageable.isExists(), managerUrl, manageable.isContainer()); + } +} diff --git a/asTypescript/packages/core/src/ManagerResource.ts b/asTypescript/packages/core/src/ManagerResource.ts new file mode 100644 index 00000000..0e1bfe90 --- /dev/null +++ b/asTypescript/packages/core/src/ManagerResource.ts @@ -0,0 +1,55 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import * as Graph from 'org/apache/jena/graph'; +import { InstanceResource } from './InstanceResource'; +import { ShapeTreeManager } from './ShapeTreeManager'; +import { ResourceAttributes } from './ResourceAttributes'; + +/** + * A ManagerResource represents a resource that is associated with + * a regular MangeableResource, and contains metadata in the form + * of a ShapeTreeManager that assigns one or more shape trees to the + * associated ManageableResource. When it exists, the associated + * resource is considered to be managed. When it doesn't, the associated + * resource is considered to be unmanaged. + */ +export class ManagerResource extends InstanceResource { + + private readonly managedResourceUrl: URL; + + /** + * Construct a manager resource + * @param url URL of the resource + * @param resourceType Identified shape tree resource type + * @param attributes Associated resource attributes + * @param body Body of the resource + * @param name Name of the resource + * @param exists Whether the resource exists + * @param managedResourceUrl URL of the associated managed resource + */ + public constructor(url: URL, resourceType: ShapeTreeResourceType, attributes: ResourceAttributes, body: string, name: string, exists: boolean, managedResourceUrl: URL) { + super(url, resourceType, attributes, body, name, exists); + this.managedResourceUrl = managedResourceUrl; + } + + /** + * Get a ShapeTreeManager from the body of the ManagerResource + * @return Shape tree manager + * @throws ShapeTreeException + */ + public getManager(): ShapeTreeManager /* throws ShapeTreeException */ { + if (!this.isExists()) { + return null; + } + let managerGraph: Graph = this.getGraph(this.getUrl()); + if (managerGraph === null) { + return null; + } + return ShapeTreeManager.getFromGraph(this.getUrl(), managerGraph); + } + + public getManagedResourceUrl(): URL { + return this.managedResourceUrl; + } +} diff --git a/asTypescript/packages/core/src/MissingManageableResource.ts b/asTypescript/packages/core/src/MissingManageableResource.ts new file mode 100644 index 00000000..721a5a5f --- /dev/null +++ b/asTypescript/packages/core/src/MissingManageableResource.ts @@ -0,0 +1,25 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { ManageableResource } from './ManageableResource'; +import { ResourceAttributes } from './ResourceAttributes'; + +/** + * A MissingManageableResource represents a state where a given + * ManageableResource at a URL does not exist. + */ +export class MissingManageableResource extends ManageableResource { + + /** + * Construct a missing manageable resource. + * @param url URL of the resource + * @param resourceType Identified shape tree resource type + * @param attributes Associated resource attributes + * @param body Body of the resource + * @param name Name of the resource + * @param managerResourceUrl URL of the shape tree manager resource + * @param isContainer Whether the resource is a container + */ + public constructor(url: URL, resourceType: ShapeTreeResourceType, attributes: ResourceAttributes, body: string, name: string, managerResourceUrl: URL | null, isContainer: boolean) { + super(url, resourceType, attributes, body, name, false, managerResourceUrl, isContainer); + } +} diff --git a/asTypescript/packages/core/src/MissingManagerResource.ts b/asTypescript/packages/core/src/MissingManagerResource.ts new file mode 100644 index 00000000..9a75f7aa --- /dev/null +++ b/asTypescript/packages/core/src/MissingManagerResource.ts @@ -0,0 +1,34 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { MissingManageableResource } from './MissingManageableResource'; +import { ManagerResource } from './ManagerResource'; +import { ResourceAttributes } from './ResourceAttributes'; + +/** + * A MissingManagerResource represents a state where a given + * ManagerResource at a URL does not exist. + */ +export class MissingManagerResource extends ManagerResource { + + /** + * Construct a missing manager resource based on a MissingManageableResource + * @param managedResourceUrl Corresponding URL of the resource that would be managed + * @param manageable Missing manageable resource + */ + public constructor(managedResourceUrl: URL, manageable: MissingManageableResource) { + super(manageable.getUrl(), manageable.getResourceType(), manageable.getAttributes(), manageable.getBody(), manageable.getName(), manageable.isExists(), managedResourceUrl); + } + + /** + * Construct a missing manager resource. + * @param managedResourceUrl URL of the resource that would be managed + * @param url URL of the resource + * @param resourceType Identified shape tree resource type + * @param attributes Associated resource attributes + * @param body Body of the resource + * @param name Name of the resource + */ + public constructor(managedResourceUrl: URL, url: URL, resourceType: ShapeTreeResourceType, attributes: ResourceAttributes, body: string, name: string) { + super(url, resourceType, attributes, body, name, false, managedResourceUrl); + } +} diff --git a/asTypescript/packages/core/src/ResourceAccessor.ts b/asTypescript/packages/core/src/ResourceAccessor.ts new file mode 100644 index 00000000..38977f0e --- /dev/null +++ b/asTypescript/packages/core/src/ResourceAccessor.ts @@ -0,0 +1,112 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { ManageableInstance } from './ManageableInstance'; +import { InstanceResource } from './InstanceResource'; +import { ShapeTreeContext } from './ShapeTreeContext'; +import { DocumentResponse } from './DocumentResponse'; +import { ManagerResource } from './ManagerResource'; +import { ResourceAttributes } from './ResourceAttributes'; + +/** + * Interface used by the shape trees core for accessing {@link ManageableInstance}s + * and individual {@link InstanceResource}s. + * + *

Depending upon the context, this could be implemented by a ResourceAccessor implementation + * accessing a database or filesystem (typical of server-side processing), or by a ResourceAccessor + * implementation that is working with remote resources over http (typical of client-side processing).

+ * + *

Note that create and update methods make the assumption that requests to do so are + * originating from HTTP requests regardless of context (hence the inclusion of method, + * headers, and contentType).

+ * + *

Deletion and Update of {@link ManageableInstance}s aren't supported, as both should be targeted + * specifically to either a {@link ManageableResource} or {@link ManagerResource} with + * {@link #deleteResource(ShapeTreeContext, ManagerResource) deleteResource} or + * {@link #updateResource(ShapeTreeContext, String, InstanceResource, String) updateResource}.

+ */ +export interface ResourceAccessor { + + /** + * Return a {@link ManageableInstance} constructed starting with the resource identified by the provided + * resourceUrl. The resourceUrl may target either a {@link ManageableResource}, + * or a {@link ManagerResource}. + * + *

Both the {@link ManageableResource} and {@link ManagerResource} are retrieved and loaded as specifically + * typed sub-classes that indicate whether they exist, or (in the case of {@link ManageableResource}) + * whether they are managed.

+ * @param context {@link ShapeTreeContext} + * @param resourceUrl URL of the resource to get + * @return {@link ManageableInstance} including {@link ManageableResource} and {@link ManagerResource} + * @throws ShapeTreeException + */ + getInstance(context: ShapeTreeContext, resourceUrl: URL): ManageableInstance /* throws ShapeTreeException */; + + /** + * Gets a {@link ManageableInstance} by first creating the resource identified by the provided + * resourceUrl, which could mean creating either a {@link ManageableResource} or a {@link ManagerResource}. + * The newly created resource is loaded into the instance, and the corresponding {@link ManageableResource} or + * {@link ManagerResource} is looked up and loaded into the instance alongside it. They are loaded as specifically + * typed sub-classes that indicate whether they exist, or (in the case of {@link ManageableResource}), + * whether they are managed. + * @param context {@link ShapeTreeContext} + * @param method Incoming HTTP method triggering resource creation + * @param resourceUrl URL of the resource to create + * @param headers Incoming HTTP headers + * @param body Body of the resource to create + * @param contentType Content-type of the resource to create + * @return {@link ManageableInstance} including {@link ManageableResource} and {@link ManagerResource} + * @throws ShapeTreeException + */ + createInstance(context: ShapeTreeContext, method: string, resourceUrl: URL, headers: ResourceAttributes, body: string, contentType: string): ManageableInstance /* throws ShapeTreeException */; + + /** + * Gets a list of {@link ManageableInstance}s contained in the container at the containerResourceUrl. + * @param context {@link ShapeTreeContext} + * @param containerResourceUrl URL of the target container + * @return List of contained {@link ManageableInstance}s + * @throws ShapeTreeException + */ + getContainedInstances(context: ShapeTreeContext, containerResourceUrl: URL): Array /* throws ShapeTreeException */; + + /** + * Gets a specific {@link InstanceResource} identified by the provided resourceUrl. + * @param context {@link ShapeTreeContext} + * @param resourceUrl URL of the target resource to get + * @return {@link InstanceResource} + * @throws ShapeTreeException + */ + getResource(context: ShapeTreeContext, resourceUrl: URL): InstanceResource /* throws ShapeTreeException */; + + /** + * Creates a specific {@link InstanceResource} identified by the provided resourceUrl. + * @param context {@link ShapeTreeContext} + * @param method Incoming HTTP method triggering resource creation + * @param resourceUrl URL of the resource to create + * @param headers Incoming HTTP headers + * @param body Body of the resource to create + * @param contentType Content-type of the resource to create + * @return {@link InstanceResource} + * @throws ShapeTreeException + */ + createResource(context: ShapeTreeContext, method: string, resourceUrl: URL, headers: ResourceAttributes, body: string, contentType: string): InstanceResource /* throws ShapeTreeException */; + + /** + * Updates a specific {@link InstanceResource} identified by the provided updatedResource + * @param context {@link ShapeTreeContext} + * @param method Incoming HTTP method triggering resource update + * @param updatedResource {@link InstanceResource} to update + * @param body Updated body of the {@link InstanceResource} + * @return Updated {@link InstanceResource} + * @throws ShapeTreeException + */ + updateResource(context: ShapeTreeContext, method: string, updatedResource: InstanceResource, body: string): DocumentResponse /* throws ShapeTreeException */; + + /** + * Deletes a specific {@link InstanceResource} identified by the provided updatedResource + * @param context {@link ShapeTreeContext} + * @param deleteResource {@link InstanceResource} to delete + * @return Resultant {@link DocumentResponse} + * @throws ShapeTreeException + */ + deleteResource(context: ShapeTreeContext, deleteResource: ManagerResource): DocumentResponse /* throws ShapeTreeException */; +} diff --git a/asTypescript/packages/core/src/ResourceAttributes.ts b/asTypescript/packages/core/src/ResourceAttributes.ts new file mode 100644 index 00000000..c771d23b --- /dev/null +++ b/asTypescript/packages/core/src/ResourceAttributes.ts @@ -0,0 +1,195 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import * as TreeMap from 'java/util'; +import * as Arrays from 'java/util'; +import * as Matcher from 'java/util/regex'; +import * as Pattern from 'java/util/regex'; +import * as requireNonNull from 'java/util/Objects'; + +/** + * The HttpClientHeaders object is a multi-map with some constructors and put-ers tailored to the + * shapetrees-java libraries. The only behavior that's at all HTTP-specific is the + * parseLinkHeaders factory which includes logic for HTTP Link headers. + */ +export class ResourceAttributes { + + myMapOfLists: Map>; + + /** + * construct a case-insensitive ResourceAttributes container + */ + public constructor() { + this.myMapOfLists = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + + /** + * construct a case-insensitive ResourceAttributes container and set attr to value if both are not null. + * @param attr attribute (header) name to set + * @param value String value to assign to attr + */ + public constructor(attr: string, value: string) /* throws ShapeTreeException */ { + this.myMapOfLists = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + this.maybeSet(attr, value); + } + + /** + * Construct ResourceAttributes with passed map, which may be case-sensitive. + * @param newMap replacement for myMapOfLists + */ + public constructor(newMap: Map>) { + this.myMapOfLists = newMap; + } + + // copy constructor + private copy(): ResourceAttributes { + let ret: ResourceAttributes = new ResourceAttributes(); + for (const entry of this.myMapOfLists.entrySet()) { + ret.myMapOfLists.put(entry.getKey(), new Array<>(entry.getValue())); + } + return ret; + } + + /** + * Re-use HttpClientHeaders to capture link headers as a mapping from link relation to list of values + * This is really a constructor but a named static function clarifies its intention. + * @param headerValues Header values for Link headers + * @return subset of this matching the pattern + */ + public static parseLinkHeaders(headerValues: Array): ResourceAttributes { + let linkHeaderMap: ResourceAttributes = new ResourceAttributes(); + for (const headerValue of headerValues) { + let matcher: Matcher = LINK_HEADER_PATTERN.matcher(headerValue); + // if (matcher.matches() && matcher.groupCount() >= 2) { + if (matcher.matches()) { + let uri: string = matcher.group(1); + let rel: string = matcher.group(2); + linkHeaderMap.myMapOfLists.computeIfAbsent(rel, k -> new Array<>()); + linkHeaderMap.myMapOfLists.get(rel).add(uri); + } else { + log.warn("Unable to parse link header: [{}]", headerValue); + } + } + return linkHeaderMap; + } + + /** + * make a new HttpClientHeaders with the additional attr/value set. + * @param attr attribute (header) name to set + * @param value String value to assign to attr + * @return original HttpClientHeaders if no change is made; otherwise a new copy. + */ + public maybePlus(attr: string, value: string): ResourceAttributes { + if (attr === null || value === null) { + return this; + } + let ret: ResourceAttributes = copy(); + ret.maybeSet(attr, value); + return ret; + } + + /** + * set attr to value if both are not null. + * @param attr attribute (header) name to set + * @param value String value to assign to attr + */ + /*@SneakyThrows*/ + public maybeSet(attr: string, value: string): void { + if (attr === null || value === null) { + return; + } + if (this.myMapOfLists.containsKey(attr)) { + let existingValues: Array = this.myMapOfLists.get(attr); + let alreadySet: boolean = existingValues.stream().anyMatch(s -> s === value); + if (!alreadySet) { + existingValues.add(value); + } + /* else { + throw new Exception(attr + ": " + value + " already set."); + }*/ + } else { + let list: Array = new Array(); + list.add(value); + this.myMapOfLists.put(attr, list); + } + } + + /** + * replaces the list of attrs (without regard to nulls) + * @param attr attribute (header) name to set + * @param values String values to assign to attr + */ + public setAll(attr: string, values: Array): void { + this.myMapOfLists.put(attr, values); + } + + /** + * Returns a map of attributes to lists of values + */ + public toMultimap(): Map> { + return this.myMapOfLists; + } + + /** + * Returns an array with alternating attributes and values. + * @param exclusions set of headers to exclude from returned array. + * (This is useful for HttpRequest.Builder().) + */ + public toList(...exclusions: string): string[] { + let ret: Array = new Array<>(); + for (const entry of this.myMapOfLists.entrySet()) { + let attr: string = entry.getKey(); + if (!Arrays.stream(exclusions).anyMatch(s -> s === attr)) { + for (const value of entry.getValue()) { + ret.add(attr); + ret.add(value); + } + } + } + return ret.stream().toArray(string[]::new); + } + + /** + * Returns an {@link Optional} containing the first header string value of + * the given named (and possibly multi-valued) header. If the header is not + * present, then the returned {@code Optional} is empty. + * + * @param name the header name + * @return an {@code Optional} containing the first named header + * string value, if present + */ + public firstValue(name: string): string | null { + return allValues(name).stream().findFirst(); + } + + /** + * Returns an unmodifiable List of all of the header string values of the + * given named header. Always returns a List, which may be empty if the + * header is not present. + * + * @param name the header name + * @return a List of headers string values + */ + public allValues(name: string): Array { + requireNonNull(name); + let values: Array = toMultimap().get(name); + // Making unmodifiable list out of empty in order to make a list which + // throws UOE unconditionally + // TODO: some callers, e.g. HttpResourceAccessor.getResourceTypeFromHeaders, expect null + return values != null ? values : List.of(); + } + + public toString(): string { + let sb: StringBuilder = new StringBuilder(); + for (const entry of this.myMapOfLists.entrySet()) { + for (const value of entry.getValue()) { + if (sb.length() != 0) { + sb.append(","); + } + sb.append(entry.getKey()).append("=").append(value); + } + } + return sb.toString(); + } + + private static readonly LINK_HEADER_PATTERN: Pattern = Pattern.compile("^<(.*?)>\\s*;\\s*rel\\s*=\"(.*?)\"\\s*"); +} diff --git a/asTypescript/packages/core/src/SchemaCache.ts b/asTypescript/packages/core/src/SchemaCache.ts new file mode 100644 index 00000000..6b985c57 --- /dev/null +++ b/asTypescript/packages/core/src/SchemaCache.ts @@ -0,0 +1,68 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import * as ShexSchema from 'fr/inria/lille/shexjava/schema'; + +/** + * Optional, static cache for pre-compiled ShEx schemas + */ +export class SchemaCache { + + private constructor() { + } + + public static readonly CACHE_IS_NOT_INITIALIZED: string = "Cache is not initialized"; + + private static cache: Map = null; + + public static initializeCache(): void { + cache = new Map<>(); + } + + public static initializeCache(existingCache: Map): void { + cache = existingCache; + } + + public static isInitialized(): boolean { + let initialized: boolean = cache != null; + log.debug("Cache initialized set to {}", initialized); + return initialized; + } + + public static containsSchema(schemaUrl: URL): boolean /* throws ShapeTreeException */ { + log.debug("Determining if cache contains schema {}", schemaUrl); + if (cache === null) { + throw new ShapeTreeException(500, CACHE_IS_NOT_INITIALIZED); + } + return cache.containsKey(schemaUrl); + } + + public static getSchema(schemaUrl: URL): ShexSchema /* throws ShapeTreeException */ { + log.debug("Getting schema {}", schemaUrl); + if (cache === null) { + throw new ShapeTreeException(500, CACHE_IS_NOT_INITIALIZED); + } + return cache.get(schemaUrl); + } + + public static putSchema(schemaUrl: URL, schema: ShexSchema): void /* throws ShapeTreeException */ { + log.debug("Caching schema {}", schemaUrl.toString()); + if (cache === null) { + throw new ShapeTreeException(500, CACHE_IS_NOT_INITIALIZED); + } + cache.put(schemaUrl, schema); + } + + public static clearCache(): void /* throws ShapeTreeException */ { + if (cache === null) { + throw new ShapeTreeException(500, CACHE_IS_NOT_INITIALIZED); + } + cache.clear(); + } + + public static unInitializeCache(): void /* throws ShapeTreeException */ { + if (cache != null) { + cache.clear(); + } + cache = null; + } +} diff --git a/asTypescript/packages/core/src/ShapeTree.ts b/asTypescript/packages/core/src/ShapeTree.ts new file mode 100644 index 00000000..00e8178e --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTree.ts @@ -0,0 +1,299 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeContainsPriority } from './comparators/ShapeTreeContainsPriority'; +import { DocumentLoaderManager } from './contentloaders/DocumentLoaderManager'; +import { HttpHeaders } from './enums/HttpHeaders'; +import { RecursionMethods } from './enums/RecursionMethods'; +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { GraphHelper } from './helpers/GraphHelper'; +import * as GlobalFactory from 'fr/inria/lille/shexjava'; +import * as Label from 'fr/inria/lille/shexjava/schema'; +import * as ShexSchema from 'fr/inria/lille/shexjava/schema'; +import * as ShExCParser from 'fr/inria/lille/shexjava/schema/parsing'; +import * as RecursiveValidation from 'fr/inria/lille/shexjava/validation'; +import * as ValidationAlgorithm from 'fr/inria/lille/shexjava/validation'; +import * as IRI from 'org/apache/commons/rdf/api'; +import * as JenaRDF from 'org/apache/commons/rdf/jena'; +import * as Graph from 'org/apache/jena/graph'; +import * as GraphUtil from 'org/apache/jena/graph'; +import * as Node from 'org/apache/jena/graph'; +import * as NotNull from 'org/jetbrains/annotations'; +import * as MalformedURLException from 'java/net'; +import * as StandardCharsets from 'java/nio/charset'; +import * as Iterator from 'java/util'; +import * as Collections from 'java/util'; +import * as Queue from 'java/util'; +import * as LinkedList from 'java/util'; +import { urlToUri } from './helpers/GraphHelper/urlToUri'; +import { ShapeTreeReference } from './ShapeTreeReference'; +import { DocumentResponse } from './DocumentResponse'; +import { ManageableResource } from './ManageableResource'; +import { ValidationResult } from './ValidationResult'; + +export class ShapeTree { + + @NotNull + private readonly id: URL; + + @NotNull + private readonly expectedResourceType: URL; + + private readonly shape: URL; + + private readonly label: string; + + @NotNull + private readonly contains: Array; + + @NotNull + private readonly references: Array; + + public constructor(@NotNull id: URL, @NotNull expectedResourceType: URL, label: string, shape: URL, @NotNull references: Array, @NotNull contains: Array) { + this.id = id; + this.expectedResourceType = expectedResourceType; + this.label = label; + this.shape = shape; + this.references = references; + this.contains = contains; + } + + public validateResource(targetResource: ManageableResource): ValidationResult /* throws ShapeTreeException */ { + return validateResource(targetResource, null); + } + + public validateResource(targetResource: ManageableResource, focusNodeUrls: Array): ValidationResult /* throws ShapeTreeException */ { + let bodyGraph: Graph = null; + if (targetResource.getResourceType() != ShapeTreeResourceType.NON_RDF) { + bodyGraph = GraphHelper.readStringIntoGraph(urlToUri(targetResource.getUrl()), targetResource.getBody(), targetResource.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null)); + } + return validateResource(targetResource.getName(), focusNodeUrls, targetResource.getResourceType(), bodyGraph); + } + + public validateResource(requestedName: string, focusNodeUrls: Array, resourceType: ShapeTreeResourceType, bodyGraph: Graph): ValidationResult /* throws ShapeTreeException */ { + // Check whether the proposed resource is the same type as what is expected by the shape tree + if (!this.expectedResourceType.toString() === resourceType.getValue()) { + return new ValidationResult(false, this, "Resource type " + resourceType + " is invalid. Expected " + this.expectedResourceType); + } + // If a label is specified, check if the proposed name is the same + if (this.label != null && !this.label === requestedName) { + return new ValidationResult(false, this, "Proposed resource name " + requestedName + " is invalid. Expected " + this.label); + } + // If the shape tree specifies a shape to validate, perform shape validation + if (this.shape != null) { + if (focusNodeUrls === null) { + focusNodeUrls = Collections.emptyList(); + } + return this.validateGraph(bodyGraph, focusNodeUrls); + } + // Allow if we fall through to here. Focus node is set to null because we only get here if no shape validation was performed + return new ValidationResult(true, this, this, null); + } + + public validateGraph(graph: Graph, focusNodeUrls: Array): ValidationResult /* throws ShapeTreeException */ { + // if (true) return new ValidationResult(true, this, this, focusNodeUrl); // [debug] ShExC parser brings debugger to its knees + if (this.shape === null) { + throw new ShapeTreeException(400, "Attempting to validate a shape for ShapeTree " + this.id + "but it doesn't specify one"); + } + let schema: ShexSchema; + if (SchemaCache.isInitialized() && SchemaCache.containsSchema(this.shape)) { + log.debug("Found cached schema {}", this.shape); + schema = SchemaCache.getSchema(this.shape); + } else { + log.debug("Did not find schema in cache {} will retrieve and parse", this.shape); + let shexShapeContents: DocumentResponse = DocumentLoaderManager.getLoader().loadExternalDocument(this.shape); + if (shexShapeContents === null || shexShapeContents.getBody() === null || shexShapeContents.getBody().isEmpty()) { + throw new ShapeTreeException(400, "Attempting to validate a ShapeTree (" + this.id + ") - Shape at (" + this.shape + ") is not found or is empty"); + } + let shapeBody: string = shexShapeContents.getBody(); + let stream: InputStream = new ByteArrayInputStream(shapeBody.getBytes(StandardCharsets.UTF_8)); + // TODO: set base URL to this.shape + let shexCParser: ShExCParser = new ShExCParser(); + try { + schema = new ShexSchema(GlobalFactory.RDFFactory, shexCParser.getRules(stream), shexCParser.getStart()); + if (SchemaCache.isInitialized()) { + SchemaCache.putSchema(this.shape, schema); + } + } catch (ex) { + if (ex instanceof Exception) { + throw new ShapeTreeException(500, "Error parsing ShEx schema - " + ex.getMessage()); + } +} + } + // Tell ShExJava we want to use Jena as our graph library + let jenaRDF: JenaRDF = new org.apache.commons.rdf.jena.JenaRDF(); + GlobalFactory.RDFFactory = jenaRDF; + let validator: ValidationAlgorithm = new RecursiveValidation(schema, jenaRDF.asGraph(graph)); + let shapeLabel: Label = new Label(GlobalFactory.RDFFactory.createIRI(this.shape.toString())); + if (!focusNodeUrls.isEmpty()) { + // One or more focus nodes were provided for validator + for (const focusNodeUrl of focusNodeUrls) { + // Evaluate each provided focus node + let focusNode: IRI = GlobalFactory.RDFFactory.createIRI(focusNodeUrl.toString()); + log.debug("Validating Shape Label = {}, Focus Node = {}", shapeLabel.toPrettyString(), focusNode.getIRIString()); + validator.validate(focusNode, shapeLabel); + let valid: boolean = validator.getTyping().isConformant(focusNode, shapeLabel); + if (valid) { + return new ValidationResult(valid, this, this, focusNodeUrl); + } + } + // None of the provided focus nodes were valid - this will return the last failure + return new ValidationResult(false, this, "Failed to validate: " + shapeLabel.toPrettyString()); + } else { + // No focus nodes were provided for validator, so all subject nodes will be evaluated + let evaluateNodes: Array = GraphUtil.listSubjects(graph, Node.ANY, Node.ANY).toList(); + for (const evaluateNode of evaluateNodes) { + const focusUriString: string = evaluateNode.getURI(); + let node: IRI = GlobalFactory.RDFFactory.createIRI(focusUriString); + validator.validate(node, shapeLabel); + let valid: boolean = validator.getTyping().isConformant(node, shapeLabel); + if (valid) { + try { + return new ValidationResult(valid, this, this, new URL(focusUriString)); + } catch (ex) { + if (ex instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Error reporting validator success on malformed URL <" + focusUriString + ">: " + ex.getMessage()); + } +} + } + } + return new ValidationResult(false, this, "Failed to validate: " + shapeLabel.toPrettyString()); + } + } + + public validateContainedResource(containedResource: ManageableResource): ValidationResult /* throws ShapeTreeException */ { + // TODO: this same test gets performed in the call to the 2nd valdateContainedResource (after potentially parsing the graph) + if (this.contains === null || this.contains.isEmpty()) { + // TODO: say it can't be null? + // The contained resource is permitted because this shape tree has no restrictions on what it contains + return new ValidationResult(true, this, this, null); + } + let containedResourceGraph: Graph = null; + let resourceType: ShapeTreeResourceType = containedResource.getResourceType(); + if (resourceType != ShapeTreeResourceType.NON_RDF) { + containedResourceGraph = GraphHelper.readStringIntoGraph(urlToUri(containedResource.getUrl()), containedResource.getBody(), containedResource.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null)); + } + let targetShapeTreeUrls: Array = Collections.emptyList(); + let focusNodeUrls: Array = Collections.emptyList(); + let requestedName: string = containedResource.getName(); + return validateContainedResource(requestedName, resourceType, targetShapeTreeUrls, containedResourceGraph, focusNodeUrls); + } + + public validateContainedResource(requestedName: string, resourceType: ShapeTreeResourceType, targetShapeTreeUrls: Array, bodyGraph: Graph, focusNodeUrls: Array): ValidationResult /* throws ShapeTreeException */ { + if (this.contains === null || this.contains.isEmpty()) { + // The contained resource is permitted because this shape tree has no restrictions on what it contains + return new ValidationResult(true, this, this, null); + } + // If one or more target shape trees have been supplied + if (!targetShapeTreeUrls.isEmpty()) { + // Test each supplied target shape tree + for (const targetShapeTreeUrl of targetShapeTreeUrls) { + // Check if it exists in st:contains + if (this.contains.contains(targetShapeTreeUrl)) { + let targetShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(targetShapeTreeUrl); + // Evaluate the shape tree against the attributes of the proposed resources + let result: ValidationResult = targetShapeTree.validateResource(requestedName, focusNodeUrls, resourceType, bodyGraph); + if (Boolean.TRUE === result.getValid()) { + // Return a successful validation result, including the matching shape tree + return new ValidationResult(true, this, targetShapeTree, result.getMatchingFocusNode()); + } + } + } + // None of the provided target shape trees matched + return new ValidationResult(false, null, "Failed to validate " + targetShapeTreeUrls); + } else { + // For each shape tree in st:contains + for (const containsShapeTreeUrl of getPrioritizedContains()) { + let containsShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(containsShapeTreeUrl); + // Continue if the shape tree isn't gettable + if (containsShapeTree === null) { + continue; + } + // Evaluate the shape tree against the attributes of the proposed resources + let result: ValidationResult = containsShapeTree.validateResource(requestedName, focusNodeUrls, resourceType, bodyGraph); + // Continue if the proposed attributes were not a match + if (Boolean.FALSE === result.getValid()) { + continue; + } + // Return the successful validation result + return new ValidationResult(true, this, containsShapeTree, result.getMatchingFocusNode()); + } + } + return new ValidationResult(false, null, "Failed to validate shape tree: " + this.id); + } + + // Return the list of shape tree contains by priority from most to least strict + public getPrioritizedContains(): Array { + let prioritized: Array = new Array<>(this.contains); + Collections.sort(prioritized, new ShapeTreeContainsPriority()); + return prioritized; + } + + public getReferencedShapeTrees(): Iterator /* throws ShapeTreeException */ { + return getReferencedShapeTrees(RecursionMethods.DEPTH_FIRST); + } + + public getReferencedShapeTrees(recursionMethods: RecursionMethods): Iterator /* throws ShapeTreeException */ { + return getReferencedShapeTreesList(recursionMethods).iterator(); + } + + private getReferencedShapeTreesList(recursionMethods: RecursionMethods): Array /* throws ShapeTreeException */ { + if (recursionMethods === RecursionMethods.BREADTH_FIRST) { + return getReferencedShapeTreesListBreadthFirst(); + } else { + let referencedShapeTrees: Array = new Array<>(); + return getReferencedShapeTreesListDepthFirst(this.getReferences(), referencedShapeTrees); + } + } + + private getReferencedShapeTreesListBreadthFirst(): Array /* throws ShapeTreeException */ { + let referencedShapeTrees: Array = new Array<>(); + let queue: Queue = new LinkedList<>(this.getReferences()); + while (!queue.isEmpty()) { + let currentShapeTree: ShapeTreeReference = queue.poll(); + referencedShapeTrees.add(currentShapeTree); + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(currentShapeTree.getReferenceUrl()); + if (shapeTree != null) { + let currentReferencedShapeTrees: Array = shapeTree.getReferences(); + if (currentReferencedShapeTrees != null) { + queue.addAll(currentReferencedShapeTrees); + } + } + } + return referencedShapeTrees; + } + + private getReferencedShapeTreesListDepthFirst(currentReferencedShapeTrees: Array, referencedShapeTrees: Array): Array /* throws ShapeTreeException */ { + for (const currentShapeTreeReference of currentReferencedShapeTrees) { + referencedShapeTrees.add(currentShapeTreeReference); + let currentReferencedShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(currentShapeTreeReference.getReferenceUrl()); + if (currentReferencedShapeTree != null) { + referencedShapeTrees = getReferencedShapeTreesListDepthFirst(currentReferencedShapeTree.getReferences(), referencedShapeTrees); + } + } + return referencedShapeTrees; + } + + public getId(): URL { + return this.id; + } + + public getExpectedResourceType(): URL { + return this.expectedResourceType; + } + + public getShape(): URL { + return this.shape; + } + + public getLabel(): string { + return this.label; + } + + public getContains(): Array { + return this.contains; + } + + public getReferences(): Array { + return this.references; + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeAssignment.ts b/asTypescript/packages/core/src/ShapeTreeAssignment.ts new file mode 100644 index 00000000..6faa8330 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeAssignment.ts @@ -0,0 +1,132 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { RdfVocabulary } from './vocabularies/RdfVocabulary'; +import { ShapeTreeVocabulary } from './vocabularies/ShapeTreeVocabulary'; +import * as Graph from 'org/apache/jena/graph'; +import * as Node from 'org/apache/jena/graph'; +import * as NodeFactory from 'org/apache/jena/graph'; +import * as Triple from 'org/apache/jena/graph'; +import * as MalformedURLException from 'java/net'; +import * as Objects from 'java/util'; + +/** + * ShapeTreeAssignment + * + * Shape Trees, §3: Each shape tree assignment identifies a shape tree associated with the managed resource, + * the focus node for shape validation, and the information needed to navigate the physical hierarchy in + * which that managed resource resides. + * https://shapetrees.org/TR/specification/#manager + */ +@EqualsAndHashCode +export class ShapeTreeAssignment { + + // Identifies the shape tree to be associated with the managed resource + private readonly shapeTree: URL; + + // Identifies the resource managed by the shape tree assignment + private readonly managedResource: URL; + + // Identifies the root shape tree assignment + private readonly rootAssignment: URL; + + // Identifies the focus node for shape validation in the managed resource + private readonly focusNode: URL; + + // Identifies the shape to which focusNode must conform + private readonly shape: URL; + + private readonly url: URL; + + public constructor(shapeTree: URL, managedResource: URL, rootAssignment: URL, focusNode: URL, shape: URL, url: URL) /* throws ShapeTreeException */ { + try { + this.shapeTree = Objects.requireNonNull(shapeTree, "Must provide an assigned shape tree"); + this.managedResource = Objects.requireNonNull(managedResource, "Must provide a shape tree context"); + this.rootAssignment = Objects.requireNonNull(rootAssignment, "Must provide a root shape tree assignment"); + this.url = Objects.requireNonNull(url, "Must provide a url for shape tree assignment"); + if (shape != null) { + this.shape = shape; + this.focusNode = Objects.requireNonNull(focusNode, "Must provide a focus node for shape validation"); + } else { + this.shape = null; + if (focusNode != null) { + throw new IllegalStateException("Cannot provide a focus node when no shape has been provided"); + } + this.focusNode = null; + } + } catch (ex) { + if (ex instanceof NullPointerException || ex instanceof IllegalStateException) { + throw new ShapeTreeException(500, "Failed to initialize shape tree assignment: " + ex.getMessage()); + } +} + } + + public static getFromGraph(url: URL, managerGraph: Graph): ShapeTreeAssignment /* throws MalformedURLException, ShapeTreeException */ { + let shapeTree: URL = null; + let managedResource: URL = null; + let rootAssignment: URL = null; + let focusNode: URL = null; + let shape: URL = null; + // Look up the ShapeTreeAssignment in the ManagerResource Graph via its URL + let assignmentTriples: Array = managerGraph.find(NodeFactory.createURI(url.toString()), Node.ANY, Node.ANY).toList(); + // A valid assignment must have at least a shape tree, managed resource, and root assignment urls + if (assignmentTriples.size() < 3) { + throw new IllegalStateException("Incomplete shape tree assignment, Only " + assignmentTriples.size() + " attributes found"); + } + // Lookup and assign each triple in the nested ShapeTreeAssignment + for (const assignmentTriple of assignmentTriples) { + switch(assignmentTriple.getPredicate().getURI()) { + case RdfVocabulary.TYPE: + if (!assignmentTriple.getObject().isURI() || !assignmentTriple.getObject().getURI() === ShapeTreeVocabulary.SHAPETREE_ASSIGNMENT) { + throw new IllegalStateException("Unexpected value: " + assignmentTriple.getPredicate().getURI()); + } + break; + case ShapeTreeVocabulary.ASSIGNS_SHAPE_TREE: + shapeTree = new URL(assignmentTriple.getObject().getURI()); + break; + case ShapeTreeVocabulary.HAS_ROOT_ASSIGNMENT: + rootAssignment = new URL(assignmentTriple.getObject().getURI()); + break; + case ShapeTreeVocabulary.MANAGES_RESOURCE: + managedResource = new URL(assignmentTriple.getObject().getURI()); + break; + case ShapeTreeVocabulary.SHAPE: + shape = new URL(assignmentTriple.getObject().getURI()); + break; + case ShapeTreeVocabulary.FOCUS_NODE: + focusNode = new URL(assignmentTriple.getObject().getURI()); + break; + default: + throw new IllegalStateException("Unexpected value: " + assignmentTriple.getPredicate().getURI()); + } + } + return new ShapeTreeAssignment(shapeTree, managedResource, rootAssignment, focusNode, shape, url); + } + + public isRootAssignment(): boolean { + return this.getUrl() === this.getRootAssignment(); + } + + public getShapeTree(): URL { + return this.shapeTree; + } + + public getManagedResource(): URL { + return this.managedResource; + } + + public getRootAssignment(): URL { + return this.rootAssignment; + } + + public getFocusNode(): URL { + return this.focusNode; + } + + public getShape(): URL { + return this.shape; + } + + public getUrl(): URL { + return this.url; + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeContext.ts b/asTypescript/packages/core/src/ShapeTreeContext.ts new file mode 100644 index 00000000..a588d764 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeContext.ts @@ -0,0 +1,13 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +export class ShapeTreeContext { + + private authorizationHeaderValue: string; + + public constructor(authorizationHeaderValue: string) { + this.authorizationHeaderValue = authorizationHeaderValue; + } + + public getAuthorizationHeaderValue(): string { + return this.authorizationHeaderValue; + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeFactory.ts b/asTypescript/packages/core/src/ShapeTreeFactory.ts new file mode 100644 index 00000000..e8c048c7 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeFactory.ts @@ -0,0 +1,212 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { ShapeTreeVocabulary } from './vocabularies/ShapeTreeVocabulary'; +import * as Node from 'org/apache/jena/graph'; +import * as Node_URI from 'org/apache/jena/graph'; +import * as Model from 'org/apache/jena/rdf/model'; +import * as Resource from 'org/apache/jena/rdf/model'; +import * as Statement from 'org/apache/jena/rdf/model'; +import * as Property from 'org/apache/jena/rdf/model'; +import * as RDFNode from 'org/apache/jena/rdf/model'; +import * as MalformedURLException from 'java/net'; +import * as URI from 'java/net'; +import { urlToUri } from './helpers/GraphHelper/urlToUri'; +import { ShapeTreeReference } from './ShapeTreeReference'; +import { ShapeTree } from './ShapeTree'; +import { ShapeTreeResource } from './ShapeTreeResource'; + +/** + * Provides a factory to look up and initialize ShapeTrees. + * Includes a simple in-memory local cache to avoid repeated fetching of + * remote shape tree resources. + */ +export class ShapeTreeFactory { + + private constructor() { + } + + private static readonly RDFS_LABEL: string = "http://www.w3.org/2000/01/rdf-schema#label"; + + @Getter + private static readonly localShapeTreeCache: Map = new Map<>(); + + /** + * Looks up and parses the shape tree at shapeTreeUrl. + * Shape trees linked via st:contains and st:references are parsed + * recursively. Maintains a cache to avoid parsing the same shape tree + * more than once. + * @param shapeTreeUrl URL of the shape tree to get + * @return Parsed and initialized shape tree + * @throws ShapeTreeException + */ + public static getShapeTree(shapeTreeUrl: URL): ShapeTree /* throws ShapeTreeException */ { + log.debug("Parsing shape tree: {}", shapeTreeUrl); + if (localShapeTreeCache.containsKey(urlToUri(shapeTreeUrl))) { + log.debug("[{}] previously cached -- returning", shapeTreeUrl.toString()); + return localShapeTreeCache.get(urlToUri(shapeTreeUrl)); + } + // Load the entire shape tree resource (which may contain multiple shape trees) + let shapeTreeResource: ShapeTreeResource = ShapeTreeResource.getShapeTreeResource(shapeTreeUrl); + let resourceModel: Model = shapeTreeResource.getModel(); + let shapeTreeNode: Resource = resourceModel.getResource(shapeTreeUrl.toString()); + // Load and set the expected resource type + const expectsType: URL = getUrlValue(resourceModel, shapeTreeNode, ShapeTreeVocabulary.EXPECTS_TYPE, shapeTreeUrl); + if (expectsType === null) + throw new ShapeTreeException(500, "Shape Tree :expectsType not found"); + // Load and set the Shape URL + const shape: URL = getUrlValue(resourceModel, shapeTreeNode, ShapeTreeVocabulary.SHAPE, shapeTreeUrl); + // Load and set Label + const label: string = getStringValue(resourceModel, shapeTreeNode, RDFS_LABEL); + // Load and set contains list + const contains: Array = getContains(resourceModel, shapeTreeNode, shapeTreeUrl); + // Load and set references list + const references: Array = getReferences(resourceModel, shapeTreeNode, shapeTreeUrl); + if (!contains.isEmpty() && !expectsType.toString() === ShapeTreeVocabulary.CONTAINER) { + throw new ShapeTreeException(400, "Only a container can be expected to have st:contains"); + } + let shapeTree: ShapeTree = new ShapeTree(shapeTreeUrl, expectsType, label, shape, references, contains); + localShapeTreeCache.put(urlToUri(shapeTreeUrl), shapeTree); + // Recursively parse contained shape trees + for (const containedUrl of contains) { + getShapeTree(containedUrl); + } + // Recursively parse referenced shape trees + for (const reference of references) { + getShapeTree(reference.getReferenceUrl()); + } + return shapeTree; + } + + /** + * Get the list of URLs linked via st:contains by the shape tree being parsed. + * @param resourceModel RDF Model representing the shape tree resource + * @param shapeTreeNode RDF Node of the shape tree + * @param shapeTreeUrl URL of the shape tree + * @return List of URLs linked via st:contains + * @throws ShapeTreeException + */ + private static getContains(resourceModel: Model, shapeTreeNode: Resource, shapeTreeUrl: URL): Array /* throws ShapeTreeException */ { + try { + return getURLListValue(resourceModel, shapeTreeNode, ShapeTreeVocabulary.CONTAINS); + } catch (ex) { + if (ex instanceof MalformedURLException || ex instanceof ShapeTreeException) { + throw new ShapeTreeException(500, "List <" + shapeTreeUrl + "> contains malformed URL: " + ex.getMessage()); + } +} + } + + /** + * Get the list of ShapeTreeReferences linked via st:references by the shape tree being parsed. + * @param resourceModel RDF Model representing the shape tree resource + * @param shapeTreeNode RDF Node of the shape tree + * @param shapeTreeUrl URL of the shape tree + * @return List of ShapeTreeReferences linked via st:references + * @throws ShapeTreeException + */ + private static getReferences(resourceModel: Model, shapeTreeNode: Resource, shapeTreeUrl: URL): Array /* throws ShapeTreeException */ { + let references: Array = new Array<>(); + let referencesProperty: Property = resourceModel.createProperty(ShapeTreeVocabulary.REFERENCES); + if (shapeTreeNode.hasProperty(referencesProperty)) { + // TODO: arbitrarily pics from n objects where 1 expected + // TODO: test coverage (never hit) + let referenceStatements: Array = shapeTreeNode.listProperties(referencesProperty).toList(); + for (const referenceStatement of referenceStatements) { + let referenceResource: Resource = referenceStatement.getObject().asResource(); + const referencedShapeTreeUrl: URL = getUrlValue(resourceModel, referenceResource, ShapeTreeVocabulary.REFERENCES_SHAPE_TREE, shapeTreeUrl); + if (referencedShapeTreeUrl === null) { + throw new ShapeTreeException(400, "expected <" + shapeTreeUrl + "> reference " + referenceResource.toString() + " to have one <" + ShapeTreeVocabulary.REFERENCES_SHAPE_TREE + "> property"); + } + let viaShapePath: string = getStringValue(resourceModel, referenceResource, ShapeTreeVocabulary.VIA_SHAPE_PATH); + let viaPredicate: URL = getUrlValue(resourceModel, referenceResource, ShapeTreeVocabulary.VIA_PREDICATE, shapeTreeUrl); + references.add(new ShapeTreeReference(referencedShapeTreeUrl, viaShapePath, viaPredicate)); + } + } + return references; + } + + /** + * Validate and get a single URL value linked to a shape tree by the supplied predicate. + * @param model RDF Model representing the shape tree resource + * @param resource RDF Node of the shape tree + * @param predicate Predicate to match + * @param shapeTreeUrl URL of the shape tree + * @return URL value linked via predicate + * @throws ShapeTreeException + */ + private static getUrlValue(model: Model, resource: Resource, predicate: string, shapeTreeUrl: URL): URL /* throws ShapeTreeException */ { + let property: Property = model.createProperty(predicate); + if (resource.hasProperty(property)) { + let statement: Statement = resource.getProperty(property); + const object: RDFNode = statement.getObject(); + if (object.isURIResource()) { + try { + return new URL(object.asResource().getURI()); + } catch (ex) { + if (ex instanceof MalformedURLException) { + throw new IllegalStateException("Malformed ShapeTree <" + shapeTreeUrl + ">: Jena URIResource <" + object + "> didn't parse as URL - " + ex.getMessage()); + } +} + } else { + throw new ShapeTreeException(500, "Malformed ShapeTree <" + shapeTreeUrl + ">: expected " + object + " to be a URL"); + } + } + return null; + } + + /** + * Validate and get a single String value linked to a shape tree by the supplied predicate. + * @param model RDF Model representing the shape tree resource + * @param resource RDF Node of the shape tree + * @param predicate Predicate to match + * @return String value linked via predicate + * @throws ShapeTreeException + */ + private static getStringValue(model: Model, resource: Resource, predicate: string): string /* throws ShapeTreeException */ { + let property: Property = model.createProperty(predicate); + if (resource.hasProperty(property)) { + let statement: Statement = resource.getProperty(property); + if (statement.getObject().isLiteral()) { + return statement.getObject().asLiteral().getString(); + } else if (statement.getObject().isURIResource()) { + return statement.getObject().asResource().getURI(); + } else { + throw new ShapeTreeException(500, "Cannot determine object type when converting from string for: " + predicate); + } + } + return null; + } + + /** + * Validate and get list of URLs linked to a shape tree by the supplied predicate. + * @param model RDF Model representing the shape tree resource + * @param resource RDF Node of the shape tree + * @param predicate Predicate to match + * @return List of URLs linked via predicate + * @throws MalformedURLException + * @throws ShapeTreeException + */ + private static getURLListValue(model: Model, resource: Resource, predicate: string): Array /* throws MalformedURLException, ShapeTreeException */ { + let urls: Array = new Array<>(); + let property: Property = model.createProperty(predicate); + if (resource.hasProperty(property)) { + let propertyStatements: Array = resource.listProperties(property).toList(); + for (const propertyStatement of propertyStatements) { + let propertyNode: Node = propertyStatement.getObject().asNode(); + if (propertyNode instanceof Node_URI) { + let contentUrl: URL = new URL(propertyNode.getURI()); + urls.add(contentUrl); + } else { + throw new ShapeTreeException(500, "Must provide a valid URI in URI listing"); + } + } + } + return urls; + } + + /** + * Clears the local shape tree cache + */ + public static clearCache(): void { + localShapeTreeCache.clear(); + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeManager.ts b/asTypescript/packages/core/src/ShapeTreeManager.ts new file mode 100644 index 00000000..e16c3904 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeManager.ts @@ -0,0 +1,229 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { GraphHelper } from './helpers/GraphHelper'; +import { RdfVocabulary } from './vocabularies/RdfVocabulary'; +import { ShapeTreeVocabulary } from './vocabularies/ShapeTreeVocabulary'; +import * as RandomStringUtils from 'org/apache/commons/lang3'; +import * as Graph from 'org/apache/jena/graph'; +import * as Node from 'org/apache/jena/graph'; +import * as NodeFactory from 'org/apache/jena/graph'; +import * as Triple from 'org/apache/jena/graph'; +import * as RDF from 'org/apache/jena/vocabulary'; +import * as MalformedURLException from 'java/net'; +import * as URI from 'java/net'; +import { urlToUri } from './helpers/GraphHelper/urlToUri'; +import { ShapeTreeAssignment } from './ShapeTreeAssignment'; +import { ShapeTree } from './ShapeTree'; + +/** + * ShapeTreeManager + * + * Shape Trees, §3: A shape tree manager associates a managed resource with one or more shape trees. No more + * than one shape tree manager may be associated with a managed resource. A shape tree manager includes + * one or more shape tree assignments via st:hasAssignment. + * https://shapetrees.org/TR/specification/#manager + */ +export class ShapeTreeManager { + + private readonly id: URL; + + // Each ShapeTreeManager has one or more ShapeTreeAssignments + // TODO: try Map, makes getContainingAssignments() redundant against getAssignments() + private readonly assignments: Array = new Array<>(); + + /** + * Constructor for a new ShapeTreeManager + * @param id URL of the ShapeTreeManager resource + */ + public constructor(id: URL) { + this.id = id; + } + + /** + * Get the URL (identifier) of the ShapeTreeManager + * @return URL identifier of the ShapeTreeManager + */ + protected getUrl(): URL { + return this.id; + } + + /** + * Get the ShapeTreeManager as an RDF Graph + * @return Graph of the ShapeTreeManager + * @throws ShapeTreeException + */ + public getGraph(): Graph /* throws ShapeTreeException */ { + let managerGraph: Graph = GraphHelper.getEmptyGraph(); + let managerSubject: string = this.getUrl().toString(); + // <> a st:Manager + managerGraph.add(GraphHelper.newTriple(managerSubject, RDF.type.toString(), GraphHelper.knownUrl(ShapeTreeVocabulary.SHAPETREE_MANAGER))); + // For each assignment create a blank node and populate + for (const assignment of this.assignments) { + // <> st:hasAssignment , + managerGraph.add(GraphHelper.newTriple(managerSubject, ShapeTreeVocabulary.HAS_ASSIGNMENT, assignment.getUrl())); + const subject: URI = urlToUri(assignment.getUrl()); + managerGraph.add(GraphHelper.newTriple(subject, URI.create(ShapeTreeVocabulary.ASSIGNS_SHAPE_TREE), assignment.getShapeTree())); + managerGraph.add(GraphHelper.newTriple(subject, URI.create(ShapeTreeVocabulary.MANAGES_RESOURCE), assignment.getManagedResource())); + managerGraph.add(GraphHelper.newTriple(subject, URI.create(ShapeTreeVocabulary.HAS_ROOT_ASSIGNMENT), assignment.getRootAssignment())); + if (assignment.getShape() != null) { + managerGraph.add(GraphHelper.newTriple(subject, URI.create(ShapeTreeVocabulary.SHAPE), assignment.getShape())); + } + if (assignment.getFocusNode() != null) { + managerGraph.add(GraphHelper.newTriple(subject, URI.create(ShapeTreeVocabulary.FOCUS_NODE), assignment.getFocusNode())); + } + } + return managerGraph; + } + + /** + * Add a {@link com.janeirodigital.shapetrees.core.ShapeTreeAssignment} to the ShapeTreeManager. + * @param assignment Shape tree assignment to add + * @throws ShapeTreeException + */ + public addAssignment(assignment: ShapeTreeAssignment): void /* throws ShapeTreeException */ { + if (assignment === null) { + throw new ShapeTreeException(500, "Must provide a non-null assignment to an initialized List of assignments"); + } + if (!this.assignments.isEmpty()) { + for (const existingAssignment of this.assignments) { + if (existingAssignment === assignment) { + throw new ShapeTreeException(422, "Identical shape tree assignment cannot be added to Shape Tree Manager: " + this.id); + } + } + } + this.assignments.add(assignment); + } + + /** + * Generates or "mints" a URL for a new ShapeTreeAssignment + * @return URL minted for a new shape tree assignment + */ + public mintAssignmentUrl(): URL { + let fragment: string = RandomStringUtils.random(8, true, true); + let assignmentString: string = this.getUrl().toString() + "#" + fragment; + const assignmentUrl: URL; + try { + assignmentUrl = new URL(assignmentString); + } catch (ex) { + if (ex instanceof MalformedURLException) { + throw new IllegalStateException("Minted illegal URL <" + assignmentString + "> - " + ex.getMessage()); + } +} + return assignmentUrl; + } + + /** + * Ensure a proposed URL for a new ShapeTreeAssigment doesn't conflict with + * other assignment URLs already allocated for the ShapeTreeManager + * @param proposedAssignmentUrl URL of the proposed shape tree assignment + * @return Minted URL for a new shape tree assignment + */ + public mintAssignmentUrl(proposedAssignmentUrl: URL): URL { + for (const assignment of this.assignments) { + if (assignment.getUrl() === proposedAssignmentUrl) { + // If we somehow managed to randomly generate a location URL that already exists, generate another + return mintAssignmentUrl(); + } + } + return proposedAssignmentUrl; + } + + public getContainingAssignments(): Array /* throws ShapeTreeException */ { + let containingAssignments: Array = new Array<>(); + for (const assignment of this.assignments) { + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(assignment.getShapeTree()); + if (!shapeTree.getContains().isEmpty()) { + containingAssignments.add(assignment); + } + } + return containingAssignments; + } + + public static getFromGraph(id: URL, managerGraph: Graph): ShapeTreeManager /* throws ShapeTreeException */ { + let manager: ShapeTreeManager = new ShapeTreeManager(id); + // Look up the ShapeTreeManager in the ManagerResource Graph via (any subject node, rdf:type, st:ShapeTreeManager) + let managerTriples: Array = managerGraph.find(Node.ANY, NodeFactory.createURI(RdfVocabulary.TYPE), NodeFactory.createURI(ShapeTreeVocabulary.SHAPETREE_MANAGER)).toList(); + // Shape Trees, §3: No more than one shape tree manager may be associated with a managed resource. + // https://shapetrees.org/TR/specification/#manager + if (managerTriples.size() > 1) { + throw new IllegalStateException("Multiple ShapeTreeManager instances found: " + managerTriples.size()); + } else if (managerTriples.isEmpty()) { + // Given the fact that a manager resource exists, there should never be a case where the manager resource + // exists but no manager is found inside of it. + // TODO: isn't that always 0? + throw new IllegalStateException("No ShapeTreeManager instances found: " + managerTriples.size()); + } + // Get the URL of the ShapeTreeManager subject node + let managerUrl: string = managerTriples.get(0).getSubject().getURI(); + // Look up ShapeTreeAssignment nodes (manager subject node, st:hasAssignment, any st:hasAssignment nodes). + // There should be one result per nested ShapeTreeAssignment, each identified by a unique url. + // Shape Trees, §3: A shape tree manager includes one or more shape tree assignments via st:hasAssignment + // https://shapetrees.org/TR/specification/#manager + const s: Node = NodeFactory.createURI(managerUrl); + const stAssignment: Node = NodeFactory.createURI(ShapeTreeVocabulary.HAS_ASSIGNMENT); + let assignmentNodes: Array = managerGraph.find(s, stAssignment, Node.ANY).toList(); + // For each st:hasAssignment node, extract a new ShapeTreeAssignment + for (const assignmentNode of assignmentNodes) { + let assignment: ShapeTreeAssignment = null; + try { + assignment = ShapeTreeAssignment.getFromGraph(new URL(assignmentNode.getObject().getURI()), managerGraph); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Object of { " + s + " " + stAssignment + " " + assignmentNode.getObject() + " } must be a URL."); + } +} + manager.assignments.add(assignment); + } + return manager; + } + + public getAssignmentForShapeTree(shapeTreeUrl: URL): ShapeTreeAssignment { + // TODO: return list of assignments with same ST but different roots + if (this.assignments.isEmpty()) { + return null; + } + for (const assignment of this.assignments) { + if (assignment.getShapeTree() === shapeTreeUrl) { + return assignment; + } + } + return null; + } + + // Given a root assignment, lookup the corresponding assignment in a shape tree manager that has the same root assignment + public getAssignmentForRoot(rootAssignment: ShapeTreeAssignment): ShapeTreeAssignment { + if (this.getAssignments() === null || this.getAssignments().isEmpty()) { + return null; + } + for (const assignment of this.getAssignments()) { + if (rootAssignment.getUrl() === assignment.getRootAssignment()) { + return assignment; + } + } + return null; + } + + public removeAssignment(assignment: ShapeTreeAssignment): void { + if (assignment === null) { + throw new IllegalStateException("Cannot remove a null assignment"); + } + if (this.assignments.isEmpty()) { + throw new IllegalStateException("Cannot remove assignments from empty set"); + } + if (!this.assignments.remove(assignment)) { + throw new IllegalStateException("Cannot remove assignment that does not exist in set"); + } + } + + public removeAssignmentForShapeTree(shapeTreeUrl: URL): void { + removeAssignment(getAssignmentForShapeTree(shapeTreeUrl)); + } + + public getId(): URL { + return this.id; + } + + public getAssignments(): Array { + return this.assignments; + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeManagerDelta.ts b/asTypescript/packages/core/src/ShapeTreeManagerDelta.ts new file mode 100644 index 00000000..02206bea --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeManagerDelta.ts @@ -0,0 +1,121 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import * as URI from 'java/net'; +import * as URISyntaxException from 'java/net'; +import { ShapeTreeAssignment } from './ShapeTreeAssignment'; +import { ShapeTreeManager } from './ShapeTreeManager'; + +export class ShapeTreeManagerDelta { + + existingManager: ShapeTreeManager; + + updatedManager: ShapeTreeManager; + + updatedAssignments: Array; + + removedAssignments: Array; + + /** + * Compares an updated ShapeTreeManager (updatedManager) with an existing one (existingManager). Neither may + * be null, managers with no assignments are acceptable for the purposes of comparison. + * @param existingManager + * @param updatedManager + * @return ShapeTreeManagerDelta + */ + public static evaluate(existingManager: ShapeTreeManager, updatedManager: ShapeTreeManager): ShapeTreeManagerDelta /* throws ShapeTreeException */ { + if (existingManager === null && updatedManager === null) { + throw new ShapeTreeException(422, "Cannot compare two null managers"); + } + let delta: ShapeTreeManagerDelta = new ShapeTreeManagerDelta(); + delta.existingManager = existingManager; + delta.updatedManager = updatedManager; + delta.updatedAssignments = new Array<>(); + delta.removedAssignments = new Array<>(); + if (updatedManager === null || updatedManager.getAssignments().isEmpty()) { + // All assignments have been removed in the updated manager, so any existing assignments should + // similarly be removed. No need for further comparison. + delta.removedAssignments = existingManager.getAssignments(); + return delta; + } + if (existingManager === null || existingManager.getAssignments().isEmpty()) { + // This existing manager doesn't have any assignments (which means it shouldn't exist) + // Anything in the updated manager is being added as new. No need for further comparison. + delta.updatedAssignments = updatedManager.getAssignments(); + return delta; + } + for (const existingAssignment of existingManager.getAssignments()) { + // Assignments match, and are unchanged, so continue + if (updatedManager.getAssignments().contains(existingAssignment)) { + continue; + } + // Assignments have the same URL but are different, so update + let updatedAssignment: ShapeTreeAssignment = containsSameUrl(existingAssignment, updatedManager.getAssignments()); + if (updatedAssignment != null) { + delta.updatedAssignments.add(updatedAssignment); + continue; + } + // existing assignment isn't in the updated assignment, so remove + delta.removedAssignments.add(existingAssignment); + } + for (const updatedAssignment of updatedManager.getAssignments()) { + // Assignments match, and are unchanged, so continue + if (existingManager.getAssignments().contains(updatedAssignment)) { + continue; + } + // If this was already processed and marked as updated continue + if (delta.updatedAssignments.contains(updatedAssignment)) { + continue; + } + // updated assignment isn't in the existing assignments, so it is new, add it + delta.updatedAssignments.add(updatedAssignment); + } + return delta; + } + + public static containsSameUrl(assignment: ShapeTreeAssignment, targetAssignments: Array): ShapeTreeAssignment /* throws ShapeTreeException */ { + for (const targetAssignment of targetAssignments) { + let assignmentUri: URI; + let targetAssignmentUri: URI; + try { + assignmentUri = assignment.getUrl().toURI(); + targetAssignmentUri = targetAssignment.getUrl().toURI(); + } catch (ex) { + if (ex instanceof URISyntaxException) { + throw new ShapeTreeException(500, "Unable to convert assignment URLs for comparison: " + ex.getMessage()); + } +} + if (assignmentUri === targetAssignmentUri) { + return targetAssignment; + } + } + return null; + } + + public allRemoved(): boolean { + return (!this.isUpdated() && this.removedAssignments != null && this.removedAssignments.size() === this.existingManager.getAssignments().size()); + } + + public isUpdated(): boolean { + return !this.updatedAssignments.isEmpty(); + } + + public wasReduced(): boolean { + return !this.removedAssignments.isEmpty(); + } + + public getExistingManager(): ShapeTreeManager { + return this.existingManager; + } + + public getUpdatedManager(): ShapeTreeManager { + return this.updatedManager; + } + + public getUpdatedAssignments(): Array { + return this.updatedAssignments; + } + + public getRemovedAssignments(): Array { + return this.removedAssignments; + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeReference.ts b/asTypescript/packages/core/src/ShapeTreeReference.ts new file mode 100644 index 00000000..2cb562c4 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeReference.ts @@ -0,0 +1,47 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import * as Objects from 'java/util'; + +export class ShapeTreeReference { + + readonly referenceUrl: URL; + + readonly shapePath: string; + + readonly predicate: URL; + + public constructor(referenceUrl: URL, shapePath: string, predicate: URL) /* throws ShapeTreeException */ { + this.referenceUrl = Objects.requireNonNull(referenceUrl); + if (shapePath === null && predicate === null) { + throw new ShapeTreeException(500, "Shape tree reference must have either a shape path or a predicate"); + } else if (shapePath != null && predicate != null) { + throw new ShapeTreeException(500, "Shape tree reference cannot have a shape path and a predicate"); + } else if (shapePath != null) { + this.shapePath = shapePath; + this.predicate = null; + } else { + this.predicate = predicate; + this.shapePath = null; + } + } + + public viaShapePath(): boolean { + return shapePath != null; + } + + public viaPredicate(): boolean { + return predicate != null; + } + + public getReferenceUrl(): URL { + return this.referenceUrl; + } + + public getShapePath(): string { + return this.shapePath; + } + + public getPredicate(): URL { + return this.predicate; + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeRequest.ts b/asTypescript/packages/core/src/ShapeTreeRequest.ts new file mode 100644 index 00000000..5475ae30 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeRequest.ts @@ -0,0 +1,26 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeResourceType } from './enums/ShapeTreeResourceType'; +import { ResourceAttributes } from './ResourceAttributes'; + +export interface ShapeTreeRequest { + + getMethod(): string; + + getUrl(): URL; + + getHeaders(): ResourceAttributes; + + getLinkHeaders(): ResourceAttributes; + + getHeaderValues(header: string): Array; + + getHeaderValue(header: string): string; + + getBody(): string; + + getContentType(): string; + + getResourceType(): ShapeTreeResourceType; + + setResourceType(resourceType: ShapeTreeResourceType): void; +} diff --git a/asTypescript/packages/core/src/ShapeTreeRequestHandler.ts b/asTypescript/packages/core/src/ShapeTreeRequestHandler.ts new file mode 100644 index 00000000..f4a0460b --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeRequestHandler.ts @@ -0,0 +1,443 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ResourceTypeAssignmentPriority } from './comparators/ResourceTypeAssignmentPriority'; +import { HttpHeaders } from './enums/HttpHeaders'; +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { RequestHelper } from './helpers/RequestHelper'; +import * as Graph from 'org/apache/jena/graph'; +import * as Collections from 'java/util'; +import * as Collection from 'java/util'; +import * as Arrays from 'java/util'; +import { TEXT_TURTLE } from './ManageableInstance/TEXT_TURTLE'; +import { ResourceAccessor } from './ResourceAccessor'; +import { ShapeTreeAssignment } from './ShapeTreeAssignment'; +import { InstanceResource } from './InstanceResource'; +import { ShapeTree } from './ShapeTree'; +import { ValidationResult } from './ValidationResult'; +import { ShapeTreeRequest } from './ShapeTreeRequest'; +import { ResourceAttributes } from './ResourceAttributes'; +import { ManageableInstance } from './ManageableInstance'; +import { ShapeTreeManagerDelta } from './ShapeTreeManagerDelta'; +import { DocumentResponse } from './DocumentResponse'; +import { ShapeTreeContext } from './ShapeTreeContext'; +import { ManageableResource } from './ManageableResource'; +import { ShapeTreeManager } from './ShapeTreeManager'; +import { ManagerResource } from './ManagerResource'; + +export class ShapeTreeRequestHandler { + + private static readonly DELETE: string = "DELETE"; + + resourceAccessor: ResourceAccessor; + + public constructor(resourceAccessor: ResourceAccessor) { + this.resourceAccessor = resourceAccessor; + } + + public manageShapeTree(manageableInstance: ManageableInstance, shapeTreeRequest: ShapeTreeRequest): DocumentResponse /* throws ShapeTreeException */ { + let validationResponse: DocumentResponse | null; + // TODO: could be null + let updatedRootManager: ShapeTreeManager = RequestHelper.getIncomingShapeTreeManager(shapeTreeRequest, manageableInstance.getManagerResource()); + // TODO: could be null + let existingRootManager: ShapeTreeManager = manageableInstance.getManagerResource().getManager(); + // Determine assignments that have been removed, added, and/or updated + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingRootManager, updatedRootManager); + // It is invalid for a manager resource to be left with no assignments. + // Shape Trees, §3: A shape tree manager includes one or more shape tree assignments via st:hasAssignment. + if (delta.allRemoved()) { + ensureAllRemovedFromManagerByDelete(shapeTreeRequest); + } + if (delta.wasReduced()) { + // An existing assignment has been removed from the manager for the managed resource. + validationResponse = unplantShapeTree(manageableInstance, manageableInstance.getShapeTreeContext(), delta); + if (validationResponse.isPresent()) { + return validationResponse.get(); + } + } + if (delta.isUpdated()) { + // An existing assignment has been updated, or new assignments have been added + validationResponse = plantShapeTree(manageableInstance, manageableInstance.getShapeTreeContext(), updatedRootManager, delta); + if (validationResponse.isPresent()) { + return validationResponse.get(); + } + } + // TODO: Test: Need a test with reduce and updated delta to make sure we never return success from plant or unplant. + return successfulValidation(); + } + + /** + * Plants a shape tree on an existing resource + * @param manageableInstance + * @param shapeTreeContext + * @param updatedRootManager + * @param delta + * @return DocumentResponse + * @throws ShapeTreeException + */ + public plantShapeTree(manageableInstance: ManageableInstance, shapeTreeContext: ShapeTreeContext, updatedRootManager: ShapeTreeManager, delta: ShapeTreeManagerDelta): DocumentResponse | null /* throws ShapeTreeException */ { + // Cannot directly update assignments that are not root locations + ensureUpdatedAssignmentIsRoot(delta); + // Run recursive assignment for each updated assignment in the root manager + for (const rootAssignment of delta.getUpdatedAssignments()) { + let validationResponse: DocumentResponse | null = assignShapeTreeToResource(manageableInstance, shapeTreeContext, updatedRootManager, rootAssignment, rootAssignment, null); + if (validationResponse.isPresent()) { + return validationResponse; + } + } + return Optional.empty(); + } + + public unplantShapeTree(manageableInstance: ManageableInstance, shapeTreeContext: ShapeTreeContext, delta: ShapeTreeManagerDelta): DocumentResponse | null /* throws ShapeTreeException */ { + // Cannot unplant a non-root location + ensureRemovedAssignmentsAreRoot(delta); + // Run recursive unassignment for each removed assignment in the updated root manager + for (const rootAssignment of delta.getRemovedAssignments()) { + let validationResponse: DocumentResponse | null = unassignShapeTreeFromResource(manageableInstance, shapeTreeContext, rootAssignment); + if (validationResponse.isPresent()) { + return validationResponse; + } + } + return Optional.empty(); + } + + // TODO: #87: do sanity checks on meta of meta, c.f. @see https://github.com/xformativ/shapetrees-java/issues/87 + public createShapeTreeInstance(manageableInstance: ManageableInstance, containerResource: ManageableInstance, shapeTreeRequest: ShapeTreeRequest, proposedName: string): DocumentResponse | null /* throws ShapeTreeException */ { + // Sanity check user-owned resource @@ delete 'cause type checks + ensureInstanceResourceExists(containerResource.getManageableResource(), "Target container for resource creation not found"); + ensureRequestResourceIsContainer(containerResource.getManageableResource(), "Cannot create a shape tree instance in a non-container resource"); + // Prepare the target resource for validation and creation + let targetResourceUrl: URL = RequestHelper.normalizeSolidResourceUrl(containerResource.getManageableResource().getUrl(), proposedName, shapeTreeRequest.getResourceType()); + ensureTargetResourceDoesNotExist(manageableInstance.getShapeTreeContext(), targetResourceUrl, "Cannot create target resource at " + targetResourceUrl + " because it already exists"); + ensureInstanceResourceExists(containerResource.getManagerResource(), "Should not be creating a shape tree instance on an unmanaged target container"); + let containerManager: ShapeTreeManager = containerResource.getManagerResource().getManager(); + ensureShapeTreeManagerExists(containerManager, "Cannot have a shape tree manager resource without a shape tree manager containing at least one shape tree assignment"); + // Get the shape tree associated that specifies what resources can be contained by the target container (st:contains) + let containingAssignments: Array = containerManager.getContainingAssignments(); + // If there are no containing shape trees for the target container, request is valid and can be passed through + if (containingAssignments.isEmpty()) { + return Optional.empty(); + } + let targetShapeTrees: Array = RequestHelper.getIncomingTargetShapeTrees(shapeTreeRequest, targetResourceUrl); + let incomingFocusNodes: Array = RequestHelper.getIncomingFocusNodes(shapeTreeRequest, targetResourceUrl); + let incomingBodyGraph: Graph = RequestHelper.getIncomingBodyGraph(shapeTreeRequest, targetResourceUrl, null); + let validationResults: Map = new Map<>(); + for (const containingAssignment of containingAssignments) { + let containerShapeTreeUrl: URL = containingAssignment.getShapeTree(); + let containerShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(containerShapeTreeUrl); + let validationResult: ValidationResult = containerShapeTree.validateContainedResource(proposedName, shapeTreeRequest.getResourceType(), targetShapeTrees, incomingBodyGraph, incomingFocusNodes); + if (Boolean.FALSE === validationResult.isValid()) { + return failValidation(validationResult); + } + validationResults.put(containingAssignment, validationResult); + } + // if any of the provided focus nodes weren't matched validation must fail + let unmatchedNodes: Array = getUnmatchedFocusNodes(validationResults.values(), incomingFocusNodes); + if (!unmatchedNodes.isEmpty()) { + return failValidation(new ValidationResult(false, null, "Failed to match target focus nodes: " + unmatchedNodes)); + } + log.debug("Creating shape tree instance at {}", targetResourceUrl); + let createdInstance: ManageableInstance = this.resourceAccessor.createInstance(manageableInstance.getShapeTreeContext(), shapeTreeRequest.getMethod(), targetResourceUrl, shapeTreeRequest.getHeaders(), shapeTreeRequest.getBody(), shapeTreeRequest.getContentType()); + for (const containingAssignment of containingAssignments) { + let rootShapeTreeAssignment: ShapeTreeAssignment = getRootAssignment(manageableInstance.getShapeTreeContext(), containingAssignment); + ensureAssignmentExists(rootShapeTreeAssignment, "Unable to find root shape tree assignment at " + containingAssignment.getRootAssignment()); + log.debug("Assigning shape tree to created resource: {}", createdInstance.getManagerResource().getUrl()); + // Note: By providing the positive advance validationResult, we let the assignment operation know that validation + // has already been performed with a positive result, and avoid having it perform the validation a second time + let assignResult: DocumentResponse | null = assignShapeTreeToResource(createdInstance, manageableInstance.getShapeTreeContext(), null, rootShapeTreeAssignment, containingAssignment, validationResults.get(containingAssignment)); + if (assignResult.isPresent()) { + return assignResult; + } + } + return Optional.of(successfulValidation()); + } + + public updateShapeTreeInstance(targetResource: ManageableInstance, shapeTreeContext: ShapeTreeContext, shapeTreeRequest: ShapeTreeRequest): DocumentResponse | null /* throws ShapeTreeException */ { + ensureInstanceResourceExists(targetResource.getManageableResource(), "Target resource to update not found"); + ensureInstanceResourceExists(targetResource.getManagerResource(), "Should not be updating an unmanaged resource as a shape tree instance"); + let manager: ShapeTreeManager = targetResource.getManagerResource().getManager(); + ensureShapeTreeManagerExists(manager, "Cannot have a shape tree manager resource without a shape tree manager with at least one shape tree assignment"); + for (const assignment of manager.getAssignments()) { + // Evaluate the update against each ShapeTreeAssignment managing the resource. + // All must pass for the update to validate + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(assignment.getShapeTree()); + let managedResourceUrl: URL = targetResource.getManageableResource().getUrl(); + // TODO: could be null + let bodyGraph: Graph = RequestHelper.getIncomingBodyGraph(shapeTreeRequest, managedResourceUrl, targetResource.getManageableResource()); + let validationResult: ValidationResult = shapeTree.validateResource(null, RequestHelper.getIncomingFocusNodes(shapeTreeRequest, managedResourceUrl), shapeTreeRequest.getResourceType(), bodyGraph); + if (Boolean.FALSE === validationResult.isValid()) { + return failValidation(validationResult); + } + } + // No issues with validation, so the request is passed along + return Optional.empty(); + } + + public deleteShapeTreeInstance(): DocumentResponse | null { + // Nothing to validate in a delete request, so the request is passed along + return Optional.empty(); + } + + protected assignShapeTreeToResource(manageableInstance: ManageableInstance, shapeTreeContext: ShapeTreeContext, rootManager: ShapeTreeManager, rootAssignment: ShapeTreeAssignment, parentAssignment: ShapeTreeAssignment, advanceValidationResult: ValidationResult): DocumentResponse | null /* throws ShapeTreeException */ { + let managingShapeTree: ShapeTree = null; + let shapeTreeManager: ShapeTreeManager = null; + let matchingFocusNode: URL = null; + let managingAssignment: ShapeTreeAssignment = null; + let validationResponse: DocumentResponse | null; + ensureValidationResultIsUsableForAssignment(advanceValidationResult, "Invalid advance validation result provided for resource assignment"); + if (advanceValidationResult != null) { + managingShapeTree = advanceValidationResult.getMatchingShapeTree(); + } + if (advanceValidationResult != null) { + matchingFocusNode = advanceValidationResult.getMatchingFocusNode(); + } + if (atRootOfPlantHierarchy(rootAssignment, manageableInstance.getManageableResource())) { + // If we are at the root of the plant hierarchy we don't need to validate the managed resource against + // a shape tree managing a parent container. We only need to validate the managed resource against + // the shape tree that is being planted at the root to ensure it conforms. + managingShapeTree = ShapeTreeFactory.getShapeTree(rootAssignment.getShapeTree()); + if (advanceValidationResult === null) { + // If this validation wasn't performed in advance + let validationResult: ValidationResult = managingShapeTree.validateResource(manageableInstance.getManageableResource()); + if (Boolean.FALSE === validationResult.isValid()) { + return failValidation(validationResult); + } + matchingFocusNode = validationResult.getMatchingFocusNode(); + } + } else { + // Not at the root of the plant hierarchy. Validate proposed resource against the shape tree + // managing the parent container, then extract the matching shape tree and focus node on success + if (advanceValidationResult === null) { + // If this validation wasn't performed in advance + let parentShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(parentAssignment.getShapeTree()); + let validationResult: ValidationResult = parentShapeTree.validateContainedResource(manageableInstance.getManageableResource()); + if (Boolean.FALSE === validationResult.isValid()) { + return failValidation(validationResult); + } + managingShapeTree = validationResult.getMatchingShapeTree(); + matchingFocusNode = validationResult.getMatchingFocusNode(); + } + } + shapeTreeManager = getManagerForAssignment(manageableInstance, rootManager, rootAssignment); + managingAssignment = getAssignment(manageableInstance.getManageableResource(), shapeTreeManager, rootAssignment, managingShapeTree, matchingFocusNode); + // If the primary resource is a container, and its shape tree specifies its contents with st:contains + // Recursively traverse the hierarchy and perform shape tree assignment + if (manageableInstance.getManageableResource().isContainer() && !managingShapeTree.getContains().isEmpty()) { + // If the container is not empty, perform a recursive, depth first validation and assignment for each + // contained resource by recursively calling this method (assignShapeTreeToResource) + // TODO - Provide a configurable maximum limit on contained resources for a recursive plant, generate ShapeTreeException + let containedResources: Array = this.resourceAccessor.getContainedInstances(shapeTreeContext, manageableInstance.getManageableResource().getUrl()); + if (!containedResources.isEmpty()) { + // Evaluate containers, then resources + Collections.sort(containedResources, new ResourceTypeAssignmentPriority()); + for (const containedResource of containedResources) { + validationResponse = assignShapeTreeToResource(containedResource, shapeTreeContext, null, rootAssignment, managingAssignment, null); + if (validationResponse.isPresent()) { + return validationResponse; + } + } + } + } + if (manageableInstance.getManagerResource().isExists()) { + // update manager resource + this.resourceAccessor.updateResource(shapeTreeContext, "PUT", manageableInstance.getManagerResource(), shapeTreeManager.getGraph().toString()); + } else { + // create manager resource + let headers: ResourceAttributes = new ResourceAttributes(); + headers.setAll(HttpHeaders.CONTENT_TYPE.getValue(), Collections.singletonList(TEXT_TURTLE)); + this.resourceAccessor.createResource(shapeTreeContext, "POST", manageableInstance.getManagerResource().getUrl(), headers, shapeTreeManager.getGraph().toString(), TEXT_TURTLE); + } + return Optional.empty(); + } + + protected unassignShapeTreeFromResource(manageableInstance: ManageableInstance, shapeTreeContext: ShapeTreeContext, rootAssignment: ShapeTreeAssignment): DocumentResponse | null /* throws ShapeTreeException */ { + ensureInstanceResourceExists(manageableInstance.getManageableResource(), "Cannot remove assignment from non-existent managed resource"); + ensureInstanceResourceExists(manageableInstance.getManagerResource(), "Cannot remove assignment from non-existent manager resource"); + let shapeTreeManager: ShapeTreeManager = manageableInstance.getManagerResource().getManager(); + let assignmentToRemove: ShapeTreeAssignment = shapeTreeManager.getAssignmentForRoot(rootAssignment); + let assignedShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(assignmentToRemove.getShapeTree()); + let validationResponse: DocumentResponse | null; + // If the managed resource is a container, and its shape tree specifies its contents with st:contains + // Recursively traverse the hierarchy and perform shape tree unassignment + if (manageableInstance.getManageableResource().isContainer() && !assignedShapeTree.getContains().isEmpty()) { + // TODO - Should there also be a configurable maximum limit on unplanting? + let containedResources: Array = this.resourceAccessor.getContainedInstances(shapeTreeContext, manageableInstance.getManageableResource().getUrl()); + // If the container is not empty + if (!containedResources.isEmpty()) { + // Sort contained resources so that containers are evaluated first, then resources + Collections.sort(containedResources, new ResourceTypeAssignmentPriority()); + // Perform a depth first unassignment for each contained resource + for (const containedResource of containedResources) { + // Recursively call this function on the contained resource + validationResponse = unassignShapeTreeFromResource(containedResource, shapeTreeContext, rootAssignment); + if (validationResponse.isPresent()) { + return validationResponse; + } + } + } + } + shapeTreeManager.removeAssignment(assignmentToRemove); + deleteOrUpdateManagerResource(shapeTreeContext, manageableInstance.getManagerResource(), shapeTreeManager); + return Optional.empty(); + } + + private deleteOrUpdateManagerResource(shapeTreeContext: ShapeTreeContext, managerResource: ManagerResource, shapeTreeManager: ShapeTreeManager): void /* throws ShapeTreeException */ { + if (shapeTreeManager.getAssignments().isEmpty()) { + let response: DocumentResponse = this.resourceAccessor.deleteResource(shapeTreeContext, managerResource); + ensureDeleteIsSuccessful(response); + } else { + // Update the existing manager resource for the managed resource + this.resourceAccessor.updateResource(shapeTreeContext, "PUT", managerResource, shapeTreeManager.getGraph().toString()); + } + } + + private getManagerForAssignment(manageableInstance: ManageableInstance, rootManager: ShapeTreeManager, rootAssignment: ShapeTreeAssignment): ShapeTreeManager /* throws ShapeTreeException */ { + let shapeTreeManager: ShapeTreeManager = null; + let managerResourceUrl: URL = manageableInstance.getManagerResource().getUrl(); + // When at the top of the plant hierarchy, use the root manager from the initial plant request body + // TODO: rootManager can be null so method could return null (does not in any test) + if (atRootOfPlantHierarchy(rootAssignment, manageableInstance.getManageableResource())) { + return rootManager; + } + if (!manageableInstance.getManagerResource().isExists()) { + // If the existing manager resource doesn't exist make a new shape tree manager + shapeTreeManager = new ShapeTreeManager(managerResourceUrl); + } else { + // Get the existing shape tree manager from the manager resource graph + // TODO - this was seemingly incorrect before it was adjusted. Needs to be debugged and confirmed as working properly now + let managerGraph: Graph = manageableInstance.getManagerResource().getGraph(managerResourceUrl); + shapeTreeManager = ShapeTreeManager.getFromGraph(managerResourceUrl, managerGraph); + } + return shapeTreeManager; + } + + private getAssignment(manageableResource: ManageableResource, shapeTreeManager: ShapeTreeManager, rootAssignment: ShapeTreeAssignment, managingShapeTree: ShapeTree, matchingFocusNode: URL): ShapeTreeAssignment /* throws ShapeTreeException */ { + if (atRootOfPlantHierarchy(rootAssignment, manageableResource)) { + return rootAssignment; + } + // Mint a new assignment URL, since it wouldn't have been passed in the initial request body + let assignmentUrl: URL = shapeTreeManager.mintAssignmentUrl(); + // Build the managed resource assignment + let matchingNode: URL = matchingFocusNode === null ? null : matchingFocusNode; + let managedResourceAssignment: ShapeTreeAssignment = new ShapeTreeAssignment(managingShapeTree.getId(), manageableResource.getUrl(), rootAssignment.getUrl(), matchingNode, managingShapeTree.getShape(), assignmentUrl); + // Add the shape tree assignment to the shape tree managed for the managed resource + shapeTreeManager.addAssignment(managedResourceAssignment); + return managedResourceAssignment; + } + + private atRootOfPlantHierarchy(rootAssignment: ShapeTreeAssignment, manageableResource: ManageableResource): boolean { + return rootAssignment.getManagedResource() === manageableResource.getUrl(); + } + + // Return a root shape tree manager associated with a given shape tree assignment + private getRootManager(shapeTreeContext: ShapeTreeContext, assignment: ShapeTreeAssignment): ShapeTreeManager /* throws ShapeTreeException */ { + let rootAssignmentUrl: URL = assignment.getRootAssignment(); + let instance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, rootAssignmentUrl); + return instance.getManagerResource().getManager(); + } + + // Return a root shape tree manager associated with a given shape tree assignment + private getRootAssignment(shapeTreeContext: ShapeTreeContext, assignment: ShapeTreeAssignment): ShapeTreeAssignment /* throws ShapeTreeException */ { + // TODO: could be null + let rootManager: ShapeTreeManager = getRootManager(shapeTreeContext, assignment); + for (const rootAssignment of rootManager.getAssignments()) { + if (rootAssignment.getUrl() != null && rootAssignment.getUrl() === assignment.getRootAssignment()) { + return rootAssignment; + } + } + return null; + } + + private getUnmatchedFocusNodes(validationResults: Collection, focusNodes: Array): Array { + let unmatchedNodes: Array = new Array<>(); + for (const focusNode of focusNodes) { + // Determine if each target focus node was matched + let matched: boolean = false; + for (const validationResult of validationResults) { + if (validationResult.getMatchingShapeTree().getShape() != null) { + if (validationResult.getMatchingFocusNode() === focusNode) { + matched = true; + } + } + } + if (!matched) { + unmatchedNodes.add(focusNode); + } + } + return unmatchedNodes; + } + + private ensureValidationResultIsUsableForAssignment(validationResult: ValidationResult, message: string): void /* throws ShapeTreeException */ { + // Null is a usable state of the validation result in the context of assignment + if (validationResult != null && (validationResult.getValid() === null || validationResult.getMatchingShapeTree() === null || validationResult.getValidatingShapeTree() === null)) { + throw new ShapeTreeException(400, message); + } + } + + private ensureInstanceResourceExists(instanceResource: InstanceResource, message: string): void /* throws ShapeTreeException */ { + if (instanceResource === null || !instanceResource.isExists()) { + throw new ShapeTreeException(404, message); + } + } + + private ensureRequestResourceIsContainer(shapeTreeResource: ManageableResource, message: string): void /* throws ShapeTreeException */ { + if (!shapeTreeResource.isContainer()) { + throw new ShapeTreeException(400, message); + } + } + + private ensureTargetResourceDoesNotExist(shapeTreeContext: ShapeTreeContext, targetResourceUrl: URL, message: string): void /* throws ShapeTreeException */ { + let targetInstance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, targetResourceUrl); + if (targetInstance.wasRequestForManager() || targetInstance.getManageableResource().isExists()) { + throw new ShapeTreeException(409, message); + } + } + + private ensureShapeTreeManagerExists(manager: ShapeTreeManager, message: string): void /* throws ShapeTreeException */ { + if (manager === null || manager.getAssignments() === null || manager.getAssignments().isEmpty()) { + throw new ShapeTreeException(400, message); + } + } + + private ensureAssignmentExists(assignment: ShapeTreeAssignment, message: string): void /* throws ShapeTreeException */ { + if (assignment === null) { + throw new ShapeTreeException(400, message); + } + } + + private ensureAllRemovedFromManagerByDelete(shapeTreeRequest: ShapeTreeRequest): void /* throws ShapeTreeException */ { + if (!shapeTreeRequest.getMethod() === DELETE) { + throw new ShapeTreeException(500, "Removal of all ShapeTreeAssignments from a ShapeTreeManager MUST use HTTP DELETE"); + } + } + + private ensureRemovedAssignmentsAreRoot(delta: ShapeTreeManagerDelta): void /* throws ShapeTreeException */ { + for (const assignment of delta.getRemovedAssignments()) { + if (!assignment.isRootAssignment()) { + throw new ShapeTreeException(500, "Cannot remove non-root assignment: " + assignment.getUrl().toString() + ". Must unplant root assignment at: " + assignment.getRootAssignment().toString()); + } + } + } + + private ensureUpdatedAssignmentIsRoot(delta: ShapeTreeManagerDelta): void /* throws ShapeTreeException */ { + for (const updatedAssignment of delta.getUpdatedAssignments()) { + if (!updatedAssignment.isRootAssignment()) { + throw new ShapeTreeException(500, "Cannot update non-root assignment: " + updatedAssignment.getUrl().toString() + ". Must update root assignment at: " + updatedAssignment.getRootAssignment().toString()); + } + } + } + + private ensureDeleteIsSuccessful(response: DocumentResponse): void /* throws ShapeTreeException */ { + let successCodes: Array = Arrays.asList(202, 204, 200); + if (!successCodes.contains(response.getStatusCode())) { + throw new ShapeTreeException(500, "Failed to delete manager resource. Received " + response.getStatusCode() + ": " + response.getBody()); + } + } + + private successfulValidation(): DocumentResponse { + return new DocumentResponse(new ResourceAttributes(), "OK", 201); + } + + private failValidation(validationResult: ValidationResult): DocumentResponse | null { + let message: string = validationResult.getMessage() != null ? validationResult.getMessage() : "Unspecified validation failure"; + return Optional.of(new DocumentResponse(new ResourceAttributes(), message, 422)); + } +} diff --git a/asTypescript/packages/core/src/ShapeTreeResource.ts b/asTypescript/packages/core/src/ShapeTreeResource.ts new file mode 100644 index 00000000..fe346bb7 --- /dev/null +++ b/asTypescript/packages/core/src/ShapeTreeResource.ts @@ -0,0 +1,86 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { DocumentLoaderManager } from './contentloaders/DocumentLoaderManager'; +import { ShapeTreeException } from './exceptions/ShapeTreeException'; +import { GraphHelper } from './helpers/GraphHelper'; +import * as Model from 'org/apache/jena/rdf/model'; +import * as URI from 'java/net'; +import { removeUrlFragment } from './helpers/GraphHelper/removeUrlFragment'; +import { urlToUri } from './helpers/GraphHelper/urlToUri'; +import { DocumentResponse } from './DocumentResponse'; + +/** + * Represents a resource that contains one or more shape tree definitions. Provides + * a factory to lookup, initialize, and cache them. + */ +export class ShapeTreeResource { + + readonly url: URL; + + readonly body: string; + + readonly contentType: string; + + readonly model: Model; + + @Getter + private static readonly localResourceCache: Map = new Map<>(); + + /** + * Looks up and caches the shape tree resource at resourceUrl. Will used cached + * verion if it exists. Throws exceptions if the resource doesn't exist, or isn't a valid + * RDF document. + * @param resourceUrl URL of shape tree resource + * @return Shape tree resource at provided resourceUrl + * @throws ShapeTreeException + */ + public static getShapeTreeResource(resourceUrl: URL): ShapeTreeResource /* throws ShapeTreeException */ { + resourceUrl = removeUrlFragment(resourceUrl); + if (localResourceCache.containsKey(urlToUri(resourceUrl))) { + log.debug("[{}] previously cached -- returning", resourceUrl); + return localResourceCache.get(urlToUri(resourceUrl)); + } + let externalDocument: DocumentResponse = DocumentLoaderManager.getLoader().loadExternalDocument(resourceUrl); + if (!externalDocument.isExists()) { + throw new ShapeTreeException(500, "Cannot load shape shape tree resource at " + resourceUrl); + } + let model: Model = GraphHelper.readStringIntoModel(urlToUri(resourceUrl), externalDocument.getBody(), externalDocument.getContentType().orElse("text/turtle")); + let resource: ShapeTreeResource = new ShapeTreeResource(resourceUrl, externalDocument.getBody(), externalDocument.getContentType().orElse("text/turtle"), model); + localResourceCache.put(urlToUri(resourceUrl), resource); + return resource; + } + + /** + * Clears the local shape tree resource cache + */ + public static clearCache(): void { + localResourceCache.clear(); + } + + public constructor(url: URL, body: string, contentType: string, model: Model, localResourceCache: Map) { + this.url = url; + this.body = body; + this.contentType = contentType; + this.model = model; + this.localResourceCache = localResourceCache; + } + + public getUrl(): URL { + return this.url; + } + + public getBody(): string { + return this.body; + } + + public getContentType(): string { + return this.contentType; + } + + public getModel(): Model { + return this.model; + } + + public getLocalResourceCache(): Map { + return this.localResourceCache; + } +} diff --git a/asTypescript/packages/core/src/UnmanagedResource.ts b/asTypescript/packages/core/src/UnmanagedResource.ts new file mode 100644 index 00000000..8cab63e1 --- /dev/null +++ b/asTypescript/packages/core/src/UnmanagedResource.ts @@ -0,0 +1,20 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ManageableResource } from './ManageableResource'; + +/** + * An UnmanagedResource indicates that a given ManageableResource + * is not managed by a shape tree. This means that there is not an + * associated ManagerResource that exists. + */ +export class UnmanagedResource extends ManageableResource { + + /** + * Construct an UnmanagedResource based on a provided ManageableResource + * manageable and managerUrl + * @param manageable ManageableResource to construct the UnmanagedResource from + * @param managerUrl URL of the associated shape tree manager resource + */ + public constructor(manageable: ManageableResource, managerUrl: URL | null) { + super(manageable.getUrl(), manageable.getResourceType(), manageable.getAttributes(), manageable.getBody(), manageable.getName(), manageable.isExists(), managerUrl, manageable.isContainer()); + } +} diff --git a/asTypescript/packages/core/src/ValidationResult.ts b/asTypescript/packages/core/src/ValidationResult.ts new file mode 100644 index 00000000..a4fd5a7f --- /dev/null +++ b/asTypescript/packages/core/src/ValidationResult.ts @@ -0,0 +1,73 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core +import { ShapeTreeAssignment } from './ShapeTreeAssignment'; +import { ShapeTree } from './ShapeTree'; + +export class ValidationResult { + + private valid: boolean; + + private validatingShapeTree: ShapeTree; + + private matchingShapeTree: ShapeTree; + + private managingAssignment: ShapeTreeAssignment; + + private matchingFocusNode: URL; + + private message: string; + + public isValid(): boolean { + return (this.valid != null && this.valid); + } + + public constructor(valid: boolean, validatingShapeTree: ShapeTree, message: string) { + this.valid = valid; + this.message = message; + this.validatingShapeTree = validatingShapeTree; + this.matchingShapeTree = null; + this.managingAssignment = null; + this.matchingFocusNode = null; + } + + public constructor(valid: boolean, validatingShapeTree: ShapeTree, matchingShapeTree: ShapeTree, matchingFocusNode: URL) { + this.valid = valid; + this.message = null; + this.validatingShapeTree = validatingShapeTree; + this.matchingShapeTree = matchingShapeTree; + this.managingAssignment = null; + this.matchingFocusNode = matchingFocusNode; + } + + public constructor(valid: boolean, validatingShapeTree: ShapeTree, matchingShapeTree: ShapeTree, managingAssignment: ShapeTreeAssignment, matchingFocusNode: URL, message: string) { + this.valid = valid; + this.validatingShapeTree = validatingShapeTree; + this.matchingShapeTree = matchingShapeTree; + this.managingAssignment = managingAssignment; + this.matchingFocusNode = matchingFocusNode; + this.message = message; + } + + public getValid(): boolean { + return this.valid; + } + + public getValidatingShapeTree(): ShapeTree { + return this.validatingShapeTree; + } + + public getMatchingShapeTree(): ShapeTree { + return this.matchingShapeTree; + } + + public getManagingAssignment(): ShapeTreeAssignment { + return this.managingAssignment; + } + + public getMatchingFocusNode(): URL { + return this.matchingFocusNode; + } + + public getMessage(): string { + return this.message; + } +} diff --git a/asTypescript/packages/core/src/comparators/ResourceTypeAssignmentPriority.ts b/asTypescript/packages/core/src/comparators/ResourceTypeAssignmentPriority.ts new file mode 100644 index 00000000..f8bd1adc --- /dev/null +++ b/asTypescript/packages/core/src/comparators/ResourceTypeAssignmentPriority.ts @@ -0,0 +1,15 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.comparators +import { ManageableInstance } from '../ManageableInstance'; +import * as Comparator from 'java/util'; + +export class ResourceTypeAssignmentPriority implements Comparator, Serializable { + + // Used for sorting by shape tree resource type with the following order + // 1. Containers + // 2. Resources + // 3. Non-RDF Resources + // @SneakyThrows + public compare(a: ManageableInstance, b: ManageableInstance): number { + return a.getManageableResource().getResourceType().compareTo(b.getManageableResource().getResourceType()); + } +} diff --git a/asTypescript/packages/core/src/comparators/ShapeTreeContainsPriority.ts b/asTypescript/packages/core/src/comparators/ShapeTreeContainsPriority.ts new file mode 100644 index 00000000..d0c93cb9 --- /dev/null +++ b/asTypescript/packages/core/src/comparators/ShapeTreeContainsPriority.ts @@ -0,0 +1,31 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.comparators +import { ShapeTree } from '../ShapeTree'; +import { ShapeTreeFactory } from '../ShapeTreeFactory'; +import * as Comparator from 'java/util'; + +export class ShapeTreeContainsPriority implements Comparator, Serializable { + + // Used for sorting shape trees in st:contains by most to least strict + // @SneakyThrows + override public compare(stUrl1: URL, stUrl2: URL): number { + let st1: ShapeTree = ShapeTreeFactory.getShapeTree(stUrl1); + let st2: ShapeTree = ShapeTreeFactory.getShapeTree(stUrl2); + let st1Priority: number = 0; + let st2Priority: number = 0; + if (st1.getShape() != null) { + st1Priority += 2; + } + if (st1.getLabel() != null) { + st1Priority++; + } + // st:expectsType is required so it doesn't affect score priority + if (st2.getShape() != null) { + st2Priority += 2; + } + if (st2.getLabel() != null) { + st2Priority++; + } + // Reversed to ensure ordering goes from most strict to least + return Integer.compare(st2Priority, st1Priority); + } +} diff --git a/asTypescript/packages/core/src/contentloaders/DocumentLoaderManager.ts b/asTypescript/packages/core/src/contentloaders/DocumentLoaderManager.ts new file mode 100644 index 00000000..e8ca18be --- /dev/null +++ b/asTypescript/packages/core/src/contentloaders/DocumentLoaderManager.ts @@ -0,0 +1,31 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.contentloaders +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { ExternalDocumentLoader } from './ExternalDocumentLoader'; + +/** + * Utility class which allows an external document loader to be set by calling code, and then + * utilized by other code that doesn't require special knowledge of specific document + * loader implementations. + */ +export abstract class DocumentLoaderManager { + + @Setter(onMethod_ = { @Synchronized }) + private static loader: ExternalDocumentLoader; + + // Private constructor to offset an implicit public constructor on a utility class + private constructor() { + } + + /** + * Return an ExternalDocumentLoader that was previously set and stored statically + * @return A valid ExternalDocumentLoader that was previously set + * @throws ShapeTreeException + */ + // @Synchronized + public static getLoader(): ExternalDocumentLoader /* throws ShapeTreeException */ { + if (loader === null) { + throw new ShapeTreeException(500, "Must provide a valid ExternalDocumentLoader"); + } + return DocumentLoaderManager.loader; + } +} diff --git a/asTypescript/packages/core/src/contentloaders/ExternalDocumentLoader.ts b/asTypescript/packages/core/src/contentloaders/ExternalDocumentLoader.ts new file mode 100644 index 00000000..2a977120 --- /dev/null +++ b/asTypescript/packages/core/src/contentloaders/ExternalDocumentLoader.ts @@ -0,0 +1,19 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.contentloaders +import { DocumentResponse } from '../DocumentResponse'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; + +/** + * Interface defining how a remote document can be loaded and its contents extracted. + * Implementations can add capabilities like caching, retrieving resources from alternate + * locations, etc. + */ +export interface ExternalDocumentLoader { + + /** + * Describes the retrieval of a remote document + * @param resourceUrl URL of resource to be retrieved + * @return DocumentResponse representation which contains body and content type + * @throws ShapeTreeException ShapeTreeException + */ + loadExternalDocument(resourceUrl: URL): DocumentResponse /* throws ShapeTreeException */; +} diff --git a/asTypescript/packages/core/src/contentloaders/HttpExternalDocumentLoader.ts b/asTypescript/packages/core/src/contentloaders/HttpExternalDocumentLoader.ts new file mode 100644 index 00000000..6e6638bd --- /dev/null +++ b/asTypescript/packages/core/src/contentloaders/HttpExternalDocumentLoader.ts @@ -0,0 +1,39 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.contentloaders +import { DocumentResponse } from '../DocumentResponse'; +import { ResourceAttributes } from '../ResourceAttributes'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import * as URISyntaxException from 'java/net'; +import * as HttpClient from 'java/net/http'; +import * as HttpRequest from 'java/net/http'; +import * as HttpResponse from 'java/net/http'; +import { ExternalDocumentLoader } from './ExternalDocumentLoader'; + +/** + * Simple HTTP implementation of ExternalDocumentLoader provided as an example + * as well as for its utility in unit tests. + */ +export class HttpExternalDocumentLoader implements ExternalDocumentLoader { + + private readonly httpClient: HttpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build(); + + override public loadExternalDocument(resourceUrl: URL): DocumentResponse /* throws ShapeTreeException */ { + try { + let request: HttpRequest = HttpRequest.newBuilder().GET().uri(resourceUrl.toURI()).build(); + let response: HttpResponse = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("Failed to load contents of document: " + resourceUrl); + } + let attributes: ResourceAttributes = new ResourceAttributes(response.headers().map()); + return new DocumentResponse(attributes, response.body(), response.statusCode()); + } catch (ex) { + if (ex instanceof IOException) { + throw new ShapeTreeException(500, "Error retrieving <" + resourceUrl + ">: " + ex.getMessage()); + } else if (ex instanceof InterruptedException) { + Thread.currentThread().interrupt(); + throw new ShapeTreeException(500, "Error retrieving <" + resourceUrl + ">: " + ex.getMessage()); + } else if (ex instanceof URISyntaxException) { + throw new ShapeTreeException(500, "Malformed URL <" + resourceUrl + ">: " + ex.getMessage()); + } +} + } +} diff --git a/asTypescript/packages/core/src/enums/HttpHeaders.ts b/asTypescript/packages/core/src/enums/HttpHeaders.ts new file mode 100644 index 00000000..48020a1a --- /dev/null +++ b/asTypescript/packages/core/src/enums/HttpHeaders.ts @@ -0,0 +1,22 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.enums +public enum HttpHeaders { + + ACCEPT("Accept"), + AUTHORIZATION("Authorization"), + CONTENT_TYPE("Content-Type"), + LINK("Link"), + LOCATION("Location"), + SLUG("Slug"), + INTEROP_ORIGINATOR("InteropOrigin"), + INTEROP_WEBID("InteropWebID"); + + public getValue(): string { + return this.value; + } + + private readonly value: string; + + constructor(value: string) { + this.value = value; + } +} diff --git a/asTypescript/packages/core/src/enums/LinkRelations.ts b/asTypescript/packages/core/src/enums/LinkRelations.ts new file mode 100644 index 00000000..b5f6f06f --- /dev/null +++ b/asTypescript/packages/core/src/enums/LinkRelations.ts @@ -0,0 +1,15 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.enums +public enum LinkRelations { + + DESCRIBED_BY("describedby"), FOCUS_NODE("http://www.w3.org/ns/shapetrees#FocusNode"), MANAGED_BY("http://www.w3.org/ns/shapetrees#managedBy"), MANAGES("http://www.w3.org/ns/shapetrees#manages"), TARGET_SHAPETREE("http://www.w3.org/ns/shapetrees#TargetShapeTree"), TYPE("type"), ACL("acl"); + + private readonly value: string; + + public getValue(): string { + return this.value; + } + + constructor(value: string) { + this.value = value; + } +} diff --git a/asTypescript/packages/core/src/enums/RecursionMethods.ts b/asTypescript/packages/core/src/enums/RecursionMethods.ts new file mode 100644 index 00000000..5106a741 --- /dev/null +++ b/asTypescript/packages/core/src/enums/RecursionMethods.ts @@ -0,0 +1,5 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.enums +public enum RecursionMethods { + + DEPTH_FIRST, BREADTH_FIRST +} diff --git a/asTypescript/packages/core/src/enums/ShapeTreeResourceType.ts b/asTypescript/packages/core/src/enums/ShapeTreeResourceType.ts new file mode 100644 index 00000000..7a31c03d --- /dev/null +++ b/asTypescript/packages/core/src/enums/ShapeTreeResourceType.ts @@ -0,0 +1,17 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.enums +import { ShapeTreeVocabulary } from '../vocabularies/ShapeTreeVocabulary'; + +public enum ShapeTreeResourceType { + + CONTAINER(ShapeTreeVocabulary.CONTAINER), RESOURCE(ShapeTreeVocabulary.RESOURCE), NON_RDF(ShapeTreeVocabulary.NON_RDF_RESOURCE); + + private readonly value: string; + + public getValue(): string { + return this.value; + } + + constructor(value: string) { + this.value = value; + } +} diff --git a/asTypescript/packages/core/src/exceptions/ShapeTreeException.ts b/asTypescript/packages/core/src/exceptions/ShapeTreeException.ts new file mode 100644 index 00000000..46195435 --- /dev/null +++ b/asTypescript/packages/core/src/exceptions/ShapeTreeException.ts @@ -0,0 +1,21 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.exceptions +export class ShapeTreeException extends Exception { + + private readonly statusCode: number; + + private readonly message: string; + + public constructor(statusCode: number, message: string) { + super; + this.statusCode = statusCode; + this.message = message; + } + + public getStatusCode(): number { + return this.statusCode; + } + + public getMessage(): string { + return this.message; + } +} diff --git a/asTypescript/packages/core/src/helpers/GraphHelper.ts b/asTypescript/packages/core/src/helpers/GraphHelper.ts new file mode 100644 index 00000000..bb455a49 --- /dev/null +++ b/asTypescript/packages/core/src/helpers/GraphHelper.ts @@ -0,0 +1,201 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.helpers +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import * as XSDDatatype from 'org/apache/jena/datatypes/xsd'; +import * as Graph from 'org/apache/jena/graph'; +import * as Triple from 'org/apache/jena/graph'; +import * as NodeFactory from 'org/apache/jena/graph'; +import * as Node from 'org/apache/jena/graph'; +import * as Node_Blank from 'org/apache/jena/graph'; +import * as Model from 'org/apache/jena/rdf/model'; +import * as ModelFactory from 'org/apache/jena/rdf/model'; +import * as Lang from 'org/apache/jena/riot'; +import * as RDFDataMgr from 'org/apache/jena/riot'; +import * as RiotException from 'org/apache/jena/riot'; +import { Writable } from 'stream'; +import * as MalformedURLException from 'java/net'; +import * as URI from 'java/net'; +import * as URISyntaxException from 'java/net'; +import * as OffsetDateTime from 'java/time'; + +/** + * Assorted helper methods related to RDF Graphs + */ +export class GraphHelper { + + private constructor() { + } + + /** + * Determine the Jena language (graph serialization type) based on a content type string + * @param contentType Content type string + * @return Serialization language + */ + public static getLangForContentType(contentType: string): Lang { + // !! Optional + if (contentType === null) { + return Lang.TURTLE; + } + switch(contentType) { + case "application/ld+json": + return Lang.JSONLD; + case "application/rdf+xml": + return Lang.RDFXML; + case "application/n-triples": + return Lang.NTRIPLES; + default: + return Lang.TURTLE; + } + } + + /** + * Writes a graph into a turtle serialization + * @param graph Graph to serialize + * @return String in TTL serialization + */ + public static writeGraphToTurtleString(graph: Graph): string { + if (graph === null) + return null; + if (graph.isClosed()) + return null; + let sw: Writable = new Writable(); + RDFDataMgr.write(sw, graph, Lang.TURTLE); + graph.close(); + return sw.toString(); + } + + /** + * Deserializes a string into a Model + * @param baseURI Base URI to use for statements + * @param rawContent String of RDF + * @param contentType Content type of content + * @return Deserialized model + * @throws ShapeTreeException ShapeTreeException + */ + public static readStringIntoModel(baseURI: URI, rawContent: string, contentType: string): Model /* throws ShapeTreeException */ { + try { + let model: Model = ModelFactory.createDefaultModel(); + let reader: StringReader = new StringReader(rawContent); + RDFDataMgr.read(model.getGraph(), reader, baseURI.toString(), GraphHelper.getLangForContentType(contentType)); + return model; + } catch (rex) { + if (rex instanceof RiotException) { + throw new ShapeTreeException(422, "Error processing input - " + rex.getMessage()); + } +} + } + + /** + * Deserializes a string into a Graph + * @param baseURI Base URI to use for statements + * @param rawContent String of RDF + * @param contentType Content type of content + * @return Deserialized graph + * @throws ShapeTreeException ShapeTreeException + */ + public static readStringIntoGraph(baseURI: URI, rawContent: string, contentType: string): Graph /* throws ShapeTreeException */ { + return readStringIntoModel(baseURI, rawContent, contentType).getGraph(); + } + + /** + * Creates an empty Graph with initialized prefixes + * @return Graph Empty Graph + */ + public static getEmptyGraph(): Graph { + let model: Model = ModelFactory.createDefaultModel(); + model.setNsPrefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#"); + model.setNsPrefix("xsd", "http://www.w3.org/2001/XMLSchema#"); + model.setNsPrefix("st", "http://www.w3.org/ns/shapetrees#"); + return model.getGraph(); + } + + /** + * Create a new triple statement with URIs + * @param subject Subject to include + * @param predicate Predicate to include + * @param object Object to include + * @return + */ + public static newTriple(subject: URI, predicate: URI, object: Object): Triple /* throws ShapeTreeException */ { + if (subject === null || predicate === null || object === null) { + throw new ShapeTreeException(500, "Cannot provide null values as input to triple construction"); + } + return newTriple(subject.toString(), predicate.toString(), object); + } + + /** + * Create a new triple statement with strings + * @param subject Subject to include + * @param predicate Predicate to include + * @param object Object to include + * @return + */ + public static newTriple(subject: string, predicate: string, object: Object): Triple /* throws ShapeTreeException */ { + if (subject === null || predicate === null || object === null) { + throw new ShapeTreeException(500, "Cannot provide null values as input to triple construction"); + } + let objectNode: Node = null; + if (object.getClass() === URI.class) { + // TODO: needed? + objectNode = NodeFactory.createURI(object.toString()); + } else if (object.getClass() === URL.class) { + objectNode = NodeFactory.createURI(object.toString()); + } else if (object.getClass() === String.class) { + objectNode = NodeFactory.createLiteral(object.toString()); + } else if (object.getClass() === OffsetDateTime.class) { + objectNode = NodeFactory.createLiteralByValue(object, XSDDatatype.XSDdateTime); + } else if (object.getClass() === Boolean.class) { + objectNode = NodeFactory.createLiteralByValue(object, XSDDatatype.XSDboolean); + } else if (object.getClass() === Node_Blank.class) { + objectNode = (Node) object; + } + if (objectNode === null) { + throw new ShapeTreeException(500, "Unsupported object value in triple construction: " + object.getClass()); + } + return new Triple(NodeFactory.createURI(subject), NodeFactory.createURI(predicate), objectNode); + } + + /** + * Wrap conversion from URL to URI which should never fail on a well-formed URL. + * @param url covert this URL to a URI + * @return IRI java native object for a URI (useful for Jena graph operations) + */ + public static urlToUri(url: URL): URI { + try { + return url.toURI(); + } catch (ex) { + if (ex instanceof URISyntaxException) { + throw new IllegalStateException("can't convert URL <" + url + "> to IRI: " + ex); + } +} + } + + /** + * Remove a fragment from a URL. Returns the same URL if there is no fragment + * @param url to remove fragment from + * @return URL without fragment + */ + public static removeUrlFragment(url: URL): URL { + let uri: URI = urlToUri(url); + if (uri.getFragment() === null) { + return url; + } + try { + let noFragment: URI = new URI(uri.getScheme(), uri.getSchemeSpecificPart(), null); + return noFragment.toURL(); + } catch (ex) { + if (ex instanceof MalformedURLException || ex instanceof URISyntaxException) { + throw new IllegalStateException("Unable to remove fragment from URL: " + ex.getMessage()); + } +} + } + + public static knownUrl(urlString: string): URL { + try { + return new URL(urlString); + } catch (ex) { + if (ex instanceof MalformedURLException) { + throw new IllegalStateException("Expected known URL <" + urlString + "> to parse as valid URL - " + ex.toString()); + } +} + } +} diff --git a/asTypescript/packages/core/src/helpers/RequestHelper.ts b/asTypescript/packages/core/src/helpers/RequestHelper.ts new file mode 100644 index 00000000..581c8ced --- /dev/null +++ b/asTypescript/packages/core/src/helpers/RequestHelper.ts @@ -0,0 +1,229 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.helpers +import { ShapeTreeManager } from '../ShapeTreeManager'; +import { ShapeTreeContext } from '../ShapeTreeContext'; +import { ManageableInstance } from '../ManageableInstance'; +import { InstanceResource } from '../InstanceResource'; +import { ManagerResource } from '../ManagerResource'; +import { ShapeTreeRequest } from '../ShapeTreeRequest'; +import { HttpHeaders } from '../enums/HttpHeaders'; +import { LinkRelations } from '../enums/LinkRelations'; +import { ShapeTreeResourceType } from '../enums/ShapeTreeResourceType'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { LdpVocabulary } from '../vocabularies/LdpVocabulary'; +import * as Graph from 'org/apache/jena/graph'; +import * as ModelFactory from 'org/apache/jena/rdf/model'; +import * as UpdateAction from 'org/apache/jena/update'; +import * as UpdateFactory from 'org/apache/jena/update'; +import * as UpdateRequest from 'org/apache/jena/update'; +import * as MalformedURLException from 'java/net'; +import * as Set from 'java/util'; +import { urlToUri } from './GraphHelper/urlToUri'; + +export class RequestHelper { + + private static readonly PUT: string = "PUT"; + + private static readonly PATCH: string = "PATCH"; + + private static readonly DELETE: string = "DELETE"; + + private static readonly supportedRDFContentTypes: Set = Set.of("text/turtle", "application/rdf+xml", "application/n-triples", "application/ld+json"); + + private static readonly supportedSPARQLContentTypes: Set = Set.of("application/sparql-update"); + + private constructor() { + } + + /** + * Builds a ShapeTreeContext from the incoming request. Specifically it retrieves + * the incoming Authorization header and stashes that value for use on any additional requests made during + * validation. + * @param shapeTreeRequest Incoming request + * @return ShapeTreeContext object populated with authentication details, if present + */ + public static buildContextFromRequest(shapeTreeRequest: ShapeTreeRequest): ShapeTreeContext { + return new ShapeTreeContext(shapeTreeRequest.getHeaderValue(HttpHeaders.AUTHORIZATION.getValue())); + } + + /** + * This determines the type of resource being processed. + * + * Initial test is based on the incoming request headers, specifically the Content-Type header. + * If the content type is not one of the accepted RDF types, it will be treated as a NON-RDF source. + * + * Then the determination becomes whether or not the resource is a container. + * + * If it is a PATCH or PUT and the URL provided already exists, then the existing resource's Link header(s) + * are used to determine if it is a container or not. + * + * If it is a POST or if the resource does not already exist, the incoming request Link header(s) are relied + * upon. + * + * @param shapeTreeRequest The current incoming request + * @param existingResource The resource located at the incoming request's URL + * @return ShapeTreeResourceType aligning to current request + * @throws ShapeTreeException ShapeTreeException throw, specifically if Content-Type is not included on request + */ + public static determineResourceType(shapeTreeRequest: ShapeTreeRequest, existingResource: ManageableInstance): ShapeTreeResourceType /* throws ShapeTreeException */ { + let isNonRdf: boolean; + if (!shapeTreeRequest.getMethod() === DELETE) { + let incomingRequestContentType: string = shapeTreeRequest.getContentType(); + // Ensure a content-type is present + if (incomingRequestContentType === null) { + throw new ShapeTreeException(400, "Content-Type is required"); + } + isNonRdf = determineIsNonRdfSource(incomingRequestContentType); + } else { + isNonRdf = false; + } + if (isNonRdf) { + return ShapeTreeResourceType.NON_RDF; + } + let isContainer: boolean = false; + let resourceAlreadyExists: boolean = existingResource.getManageableResource().isExists(); + if ((shapeTreeRequest.getMethod() === PUT || shapeTreeRequest.getMethod() === PATCH) && resourceAlreadyExists) { + isContainer = existingResource.getManageableResource().isContainer(); + } else if (shapeTreeRequest.getLinkHeaders() != null) { + isContainer = getIsContainerFromRequest(shapeTreeRequest); + } + return isContainer ? ShapeTreeResourceType.CONTAINER : ShapeTreeResourceType.RESOURCE; + } + + public static getIncomingFocusNodes(shapeTreeRequest: ShapeTreeRequest, baseUrl: URL): Array /* throws ShapeTreeException */ { + const focusNodeStrings: Array = shapeTreeRequest.getLinkHeaders().allValues(LinkRelations.FOCUS_NODE.getValue()); + const focusNodeUrls: Array = new Array<>(); + if (!focusNodeStrings.isEmpty()) { + for (const focusNodeUrlString of focusNodeStrings) { + try { + const focusNodeUrl: URL = new URL(baseUrl, focusNodeUrlString); + focusNodeUrls.add(focusNodeUrl); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Malformed focus node when resolving <" + focusNodeUrlString + "> against <" + baseUrl + ">"); + } +} + } + } + return focusNodeUrls; + } + + /** + * Gets target shape tree / hint from request header + * @param shapeTreeRequest Request + * @return URL value of target shape tree + * @throws ShapeTreeException ShapeTreeException + */ + public static getIncomingTargetShapeTrees(shapeTreeRequest: ShapeTreeRequest, baseUrl: URL): Array /* throws ShapeTreeException */ { + const targetShapeTreeStrings: Array = shapeTreeRequest.getLinkHeaders().allValues(LinkRelations.TARGET_SHAPETREE.getValue()); + const targetShapeTreeUrls: Array = new Array<>(); + if (!targetShapeTreeStrings.isEmpty()) { + for (const targetShapeTreeUrlString of targetShapeTreeStrings) { + try { + const targetShapeTreeUrl: URL = new URL(targetShapeTreeUrlString); + targetShapeTreeUrls.add(targetShapeTreeUrl); + } catch (e) { + if (e instanceof MalformedURLException) { + throw new ShapeTreeException(500, "Malformed focus node when resolving <" + targetShapeTreeUrlString + "> against <" + baseUrl + ">"); + } +} + } + } + return targetShapeTreeUrls; + } + + public static getIncomingShapeTreeManager(shapeTreeRequest: ShapeTreeRequest, managerResource: ManagerResource): ShapeTreeManager /* throws ShapeTreeException */ { + let incomingBodyGraph: Graph = RequestHelper.getIncomingBodyGraph(shapeTreeRequest, RequestHelper.normalizeSolidResourceUrl(shapeTreeRequest.getUrl(), null, ShapeTreeResourceType.RESOURCE), managerResource); + if (incomingBodyGraph === null) { + return null; + } + return ShapeTreeManager.getFromGraph(shapeTreeRequest.getUrl(), incomingBodyGraph); + } + + /** + * Normalizes the BaseURL to use for a request based on the incoming request. + * @param url URL of request + * @param requestedName Requested name of resource (provided on created resources via POST) + * @param resourceType Description of resource (Container, NonRDF, Resource) + * @return BaseURL to use for RDF Graphs + * @throws ShapeTreeException ShapeTreeException + */ + public static normalizeSolidResourceUrl(url: URL, requestedName: string, resourceType: ShapeTreeResourceType): URL /* throws ShapeTreeException */ { + let urlString: string = url.toString(); + if (requestedName != null) { + urlString += requestedName; + } + if (resourceType === ShapeTreeResourceType.CONTAINER && !urlString.endsWith("/")) { + urlString += "/"; + } + try { + return new URL(urlString); + } catch (ex) { + if (ex instanceof MalformedURLException) { + throw new ShapeTreeException(500, "normalized to malformed URL <" + urlString + "> - " + ex.getMessage()); + } +} + } + + /** + * Loads body of request into graph + * @param shapeTreeRequest Request + * @param baseUrl BaseURL to use for graph + * @param targetResource + * @return Graph representation of request body + * @throws ShapeTreeException ShapeTreeException + */ + public static getIncomingBodyGraph(shapeTreeRequest: ShapeTreeRequest, baseUrl: URL, targetResource: InstanceResource): Graph /* throws ShapeTreeException */ { + log.debug("Reading request body into graph with baseUrl {}", baseUrl); + if ((shapeTreeRequest.getResourceType() === ShapeTreeResourceType.NON_RDF && !shapeTreeRequest.getContentType().equalsIgnoreCase("application/sparql-update")) || shapeTreeRequest.getBody() === null || shapeTreeRequest.getBody().length() === 0) { + return null; + } + let targetResourceGraph: Graph = null; + if (shapeTreeRequest.getMethod() === PATCH) { + // In the event of a SPARQL PATCH, we get the SPARQL query and evaluate it, passing the + // resultant graph back to the caller + if (targetResource != null) { + targetResourceGraph = targetResource.getGraph(baseUrl); + } + if (targetResourceGraph === null) { + // if the target resource doesn't exist or has no content + log.debug("Existing target resource graph to patch does not exist. Creating an empty graph."); + targetResourceGraph = ModelFactory.createDefaultModel().getGraph(); + } + // Perform a SPARQL update locally to ensure that resulting graph validates against ShapeTree + let updateRequest: UpdateRequest = UpdateFactory.create(shapeTreeRequest.getBody(), baseUrl.toString()); + UpdateAction.execute(updateRequest, targetResourceGraph); + if (targetResourceGraph === null) { + throw new ShapeTreeException(400, "No graph after update"); + } + } else { + targetResourceGraph = GraphHelper.readStringIntoGraph(urlToUri(baseUrl), shapeTreeRequest.getBody(), shapeTreeRequest.getContentType()); + } + return targetResourceGraph; + } + + /** + * Determines whether a content type is a supported RDF type + * @param incomingRequestContentType Content type to test + * @return Boolean indicating whether it is RDF or not + */ + private static determineIsNonRdfSource(incomingRequestContentType: string): boolean { + return (!supportedRDFContentTypes.contains(incomingRequestContentType.toLowerCase()) && !supportedSPARQLContentTypes.contains(incomingRequestContentType.toLowerCase())); + } + + /** + * Determines if a resource should be treated as a container based on its request Link headers + * @param shapeTreeRequest Request + * @return Is the resource a container? + */ + private static getIsContainerFromRequest(shapeTreeRequest: ShapeTreeRequest): boolean { + // First try to determine based on link headers + if (shapeTreeRequest.getLinkHeaders() != null) { + const typeLinks: Array = shapeTreeRequest.getLinkHeaders().allValues(LinkRelations.TYPE.getValue()); + if (!typeLinks.isEmpty()) { + return (typeLinks.contains(LdpVocabulary.CONTAINER) || typeLinks.contains(LdpVocabulary.BASIC_CONTAINER)); + } + } + // As a secondary attempt, use slash path semantics + return shapeTreeRequest.getUrl().getPath().endsWith("/"); + } +} diff --git a/asTypescript/packages/core/src/methodhandlers/AbstractValidatingMethodHandler.ts b/asTypescript/packages/core/src/methodhandlers/AbstractValidatingMethodHandler.ts new file mode 100644 index 00000000..b4964c51 --- /dev/null +++ b/asTypescript/packages/core/src/methodhandlers/AbstractValidatingMethodHandler.ts @@ -0,0 +1,20 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.methodhandlers +import { ResourceAccessor } from '../ResourceAccessor'; +import { ShapeTreeRequestHandler } from '../ShapeTreeRequestHandler'; + +/** + * Abstract class providing reusable functionality to different method handlers + */ +export abstract class AbstractValidatingMethodHandler { + + private static readonly DELETE: string = "DELETE"; + + protected readonly resourceAccessor: ResourceAccessor; + + protected readonly requestHandler: ShapeTreeRequestHandler; + + protected constructor(resourceAccessor: ResourceAccessor) { + this.resourceAccessor = resourceAccessor; + this.requestHandler = new ShapeTreeRequestHandler(resourceAccessor); + } +} diff --git a/asTypescript/packages/core/src/methodhandlers/ValidatingDeleteMethodHandler.ts b/asTypescript/packages/core/src/methodhandlers/ValidatingDeleteMethodHandler.ts new file mode 100644 index 00000000..47e154b5 --- /dev/null +++ b/asTypescript/packages/core/src/methodhandlers/ValidatingDeleteMethodHandler.ts @@ -0,0 +1,30 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.methodhandlers +import { DocumentResponse } from '../DocumentResponse'; +import { ResourceAccessor } from '../ResourceAccessor'; +import { ManageableInstance } from '../ManageableInstance'; +import { ShapeTreeRequest } from '../ShapeTreeRequest'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { RequestHelper } from '../helpers/RequestHelper'; +import { ShapeTreeContext } from '../ShapeTreeContext'; +import { AbstractValidatingMethodHandler } from './AbstractValidatingMethodHandler'; +import { ValidatingMethodHandler } from './ValidatingMethodHandler'; + +export class ValidatingDeleteMethodHandler extends AbstractValidatingMethodHandler implements ValidatingMethodHandler { + + public constructor(resourceAccessor: ResourceAccessor) { + super(resourceAccessor); + } + + override public validateRequest(shapeTreeRequest: ShapeTreeRequest): DocumentResponse | null /* throws ShapeTreeException */ { + let shapeTreeContext: ShapeTreeContext = RequestHelper.buildContextFromRequest(shapeTreeRequest); + let targetInstance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, shapeTreeRequest.getUrl()); + if (targetInstance.wasRequestForManager() && targetInstance.getManagerResource().isExists()) { + // If the DELETE request is for an existing shapetree manager resource, + // it must be evaluated to determine if unplanting is necessary + return Optional.of(this.requestHandler.manageShapeTree(targetInstance, shapeTreeRequest)); + } + // Reaching this point means validation was not necessary + // Pass the request along with no validation + return Optional.empty(); + } +} diff --git a/asTypescript/packages/core/src/methodhandlers/ValidatingMethodHandler.ts b/asTypescript/packages/core/src/methodhandlers/ValidatingMethodHandler.ts new file mode 100644 index 00000000..ae0dbdaf --- /dev/null +++ b/asTypescript/packages/core/src/methodhandlers/ValidatingMethodHandler.ts @@ -0,0 +1,9 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.methodhandlers +import { DocumentResponse } from '../DocumentResponse'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { ShapeTreeRequest } from '../ShapeTreeRequest'; + +export interface ValidatingMethodHandler { + + validateRequest(shapeTreeRequest: ShapeTreeRequest): DocumentResponse | null /* throws ShapeTreeException */; +} diff --git a/asTypescript/packages/core/src/methodhandlers/ValidatingPatchMethodHandler.ts b/asTypescript/packages/core/src/methodhandlers/ValidatingPatchMethodHandler.ts new file mode 100644 index 00000000..7a0725db --- /dev/null +++ b/asTypescript/packages/core/src/methodhandlers/ValidatingPatchMethodHandler.ts @@ -0,0 +1,51 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.methodhandlers +import { ShapeTreeRequest } from '../ShapeTreeRequest'; +import { ShapeTreeContext } from '../ShapeTreeContext'; +import { ManageableInstance } from '../ManageableInstance'; +import { DocumentResponse } from '../DocumentResponse'; +import { ResourceAccessor } from '../ResourceAccessor'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { RequestHelper } from '../helpers/RequestHelper'; +import { ManageableResource } from '../ManageableResource'; +import { AbstractValidatingMethodHandler } from './AbstractValidatingMethodHandler'; +import { ValidatingMethodHandler } from './ValidatingMethodHandler'; + +export class ValidatingPatchMethodHandler extends AbstractValidatingMethodHandler implements ValidatingMethodHandler { + + public constructor(resourceAccessor: ResourceAccessor) { + super(resourceAccessor); + } + + override public validateRequest(shapeTreeRequest: ShapeTreeRequest): DocumentResponse | null /* throws ShapeTreeException */ { + if (shapeTreeRequest.getContentType() === null || !shapeTreeRequest.getContentType().equalsIgnoreCase("application/sparql-update")) { + log.error("Received a patch without a content type of application/sparql-update"); + throw new ShapeTreeException(415, "PATCH verb expects a content type of application/sparql-update"); + } + let shapeTreeContext: ShapeTreeContext = RequestHelper.buildContextFromRequest(shapeTreeRequest); + let targetInstance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, shapeTreeRequest.getUrl()); + if (targetInstance.wasRequestForManager()) { + // Target resource is for shape tree manager, manage shape trees to plant and/or unplant + return Optional.of(this.requestHandler.manageShapeTree(targetInstance, shapeTreeRequest)); + } else { + let targetResource: ManageableResource = targetInstance.getManageableResource(); + shapeTreeRequest.setResourceType(RequestHelper.determineResourceType(shapeTreeRequest, targetInstance)); + if (targetResource.isExists()) { + // The target resource already exists + if (targetInstance.isManaged()) { + // If it is managed by a shape tree the update must be validated + return this.requestHandler.updateShapeTreeInstance(targetInstance, shapeTreeContext, shapeTreeRequest); + } + } else { + // The target resource doesn't exist + let parentInstance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, targetResource.getParentContainerUrl()); + if (parentInstance.isManaged()) { + // If the parent container is managed by a shape tree, the resource to create must be validated + return this.requestHandler.createShapeTreeInstance(targetInstance, parentInstance, shapeTreeRequest, targetResource.getName()); + } + } + } + // Reaching this point means validation was not necessary + // Pass the request along with no validation + return Optional.empty(); + } +} diff --git a/asTypescript/packages/core/src/methodhandlers/ValidatingPostMethodHandler.ts b/asTypescript/packages/core/src/methodhandlers/ValidatingPostMethodHandler.ts new file mode 100644 index 00000000..2ca8a1f6 --- /dev/null +++ b/asTypescript/packages/core/src/methodhandlers/ValidatingPostMethodHandler.ts @@ -0,0 +1,36 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.methodhandlers +import { ShapeTreeRequest } from '../ShapeTreeRequest'; +import { ShapeTreeContext } from '../ShapeTreeContext'; +import { ManageableInstance } from '../ManageableInstance'; +import { DocumentResponse } from '../DocumentResponse'; +import { ResourceAccessor } from '../ResourceAccessor'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { RequestHelper } from '../helpers/RequestHelper'; +import { HttpHeaders } from '../enums/HttpHeaders'; +import * as UUID from 'java/util'; +import { AbstractValidatingMethodHandler } from './AbstractValidatingMethodHandler'; +import { ValidatingMethodHandler } from './ValidatingMethodHandler'; + +export class ValidatingPostMethodHandler extends AbstractValidatingMethodHandler implements ValidatingMethodHandler { + + public constructor(resourceAccessor: ResourceAccessor) { + super(resourceAccessor); + } + + override public validateRequest(shapeTreeRequest: ShapeTreeRequest): DocumentResponse | null /* throws ShapeTreeException */ { + let shapeTreeContext: ShapeTreeContext = RequestHelper.buildContextFromRequest(shapeTreeRequest); + // Look up the target container for the POST. Error if it doesn't exist, or is a manager resource + let targetContainer: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, shapeTreeRequest.getUrl()); + // Get resource name from the slug or default to UUID + let proposedName: string = shapeTreeRequest.getHeaders().firstValue(HttpHeaders.SLUG.getValue()).orElse(UUID.randomUUID().toString()); + // If the parent container is managed by a shape tree, the proposed resource being posted must be + // validated against the parent tree. + if (targetContainer.isManaged()) { + shapeTreeRequest.setResourceType(RequestHelper.determineResourceType(shapeTreeRequest, targetContainer)); + return this.requestHandler.createShapeTreeInstance(targetContainer, targetContainer, shapeTreeRequest, proposedName); + } + // Reaching this point means validation was not necessary + // Pass the request along with no validation + return Optional.empty(); + } +} diff --git a/asTypescript/packages/core/src/methodhandlers/ValidatingPutMethodHandler.ts b/asTypescript/packages/core/src/methodhandlers/ValidatingPutMethodHandler.ts new file mode 100644 index 00000000..f96627ac --- /dev/null +++ b/asTypescript/packages/core/src/methodhandlers/ValidatingPutMethodHandler.ts @@ -0,0 +1,47 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.methodhandlers +import { ShapeTreeRequest } from '../ShapeTreeRequest'; +import { ShapeTreeContext } from '../ShapeTreeContext'; +import { ManageableInstance } from '../ManageableInstance'; +import { ManageableResource } from '../ManageableResource'; +import { DocumentResponse } from '../DocumentResponse'; +import { ResourceAccessor } from '../ResourceAccessor'; +import { ShapeTreeException } from '../exceptions/ShapeTreeException'; +import { RequestHelper } from '../helpers/RequestHelper'; +import { AbstractValidatingMethodHandler } from './AbstractValidatingMethodHandler'; +import { ValidatingMethodHandler } from './ValidatingMethodHandler'; + +export class ValidatingPutMethodHandler extends AbstractValidatingMethodHandler implements ValidatingMethodHandler { + + public constructor(resourceAccessor: ResourceAccessor) { + super(resourceAccessor); + } + + override public validateRequest(shapeTreeRequest: ShapeTreeRequest): DocumentResponse | null /* throws ShapeTreeException */ { + let shapeTreeContext: ShapeTreeContext = RequestHelper.buildContextFromRequest(shapeTreeRequest); + let targetInstance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, shapeTreeRequest.getUrl()); + if (targetInstance.wasRequestForManager()) { + // Target resource is for shape tree manager, manage shape trees to plant and/or unplant + return Optional.of(this.requestHandler.manageShapeTree(targetInstance, shapeTreeRequest)); + } else { + let targetResource: ManageableResource = targetInstance.getManageableResource(); + shapeTreeRequest.setResourceType(RequestHelper.determineResourceType(shapeTreeRequest, targetInstance)); + if (targetResource.isExists()) { + // The target resource already exists + if (targetInstance.isManaged()) { + // If it is managed by a shape tree the update must be validated + return this.requestHandler.updateShapeTreeInstance(targetInstance, shapeTreeContext, shapeTreeRequest); + } + } else { + // The target resource doesn't exist + let parentInstance: ManageableInstance = this.resourceAccessor.getInstance(shapeTreeContext, targetResource.getParentContainerUrl()); + if (parentInstance.isManaged()) { + // If the parent container is managed by a shape tree, the resource to create must be validated + return this.requestHandler.createShapeTreeInstance(targetInstance, parentInstance, shapeTreeRequest, targetResource.getName()); + } + } + } + // Reaching this point means validation was not necessary + // Pass the request along with no validation + return Optional.empty(); + } +} diff --git a/asTypescript/packages/core/src/vocabularies/LdpVocabulary.ts b/asTypescript/packages/core/src/vocabularies/LdpVocabulary.ts new file mode 100644 index 00000000..6401c005 --- /dev/null +++ b/asTypescript/packages/core/src/vocabularies/LdpVocabulary.ts @@ -0,0 +1,12 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.vocabularies +export const class LdpVocabulary { + + private constructor() { + } + + public static readonly CONTAINER: string = Namespaces.LDP + "Container"; + + public static readonly BASIC_CONTAINER: string = Namespaces.LDP + "BasicContainer"; + + public static readonly CONTAINS: string = Namespaces.LDP + "contains"; +} diff --git a/asTypescript/packages/core/src/vocabularies/Namespaces.ts b/asTypescript/packages/core/src/vocabularies/Namespaces.ts new file mode 100644 index 00000000..5d847435 --- /dev/null +++ b/asTypescript/packages/core/src/vocabularies/Namespaces.ts @@ -0,0 +1,12 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.vocabularies +export const class Namespaces { + + private constructor() { + } + + public static readonly SHAPETREE: string = "http://www.w3.org/ns/shapetrees#"; + + public static readonly LDP: string = "http://www.w3.org/ns/ldp#"; + + public static readonly RDF: string = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; +} diff --git a/asTypescript/packages/core/src/vocabularies/RdfVocabulary.ts b/asTypescript/packages/core/src/vocabularies/RdfVocabulary.ts new file mode 100644 index 00000000..d5f41453 --- /dev/null +++ b/asTypescript/packages/core/src/vocabularies/RdfVocabulary.ts @@ -0,0 +1,9 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.vocabularies +export const class RdfVocabulary { + + private constructor() { + } + + // rdf:type + public static readonly TYPE: string = Namespaces.RDF + "type"; +} diff --git a/asTypescript/packages/core/src/vocabularies/ShapeTreeVocabulary.ts b/asTypescript/packages/core/src/vocabularies/ShapeTreeVocabulary.ts new file mode 100644 index 00000000..7d26a7c8 --- /dev/null +++ b/asTypescript/packages/core/src/vocabularies/ShapeTreeVocabulary.ts @@ -0,0 +1,40 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.core.vocabularies +export const class ShapeTreeVocabulary { + + private constructor() { + } + + public static readonly HAS_ROOT_ASSIGNMENT: string = Namespaces.SHAPETREE + "hasRootAssignment"; + + public static readonly MANAGES_RESOURCE: string = Namespaces.SHAPETREE + "manages"; + + public static readonly ASSIGNS_SHAPE_TREE: string = Namespaces.SHAPETREE + "assigns"; + + public static readonly REFERENCES_SHAPE_TREE: string = Namespaces.SHAPETREE + "referencesShapeTree"; + + public static readonly SHAPETREE_MANAGER: string = Namespaces.SHAPETREE + "Manager"; + + public static readonly SHAPETREE_ASSIGNMENT: string = Namespaces.SHAPETREE + "Assignment"; + + public static readonly EXPECTS_TYPE: string = Namespaces.SHAPETREE + "expectsType"; + + public static readonly REFERENCES: string = Namespaces.SHAPETREE + "references"; + + public static readonly VIA_SHAPE_PATH: string = Namespaces.SHAPETREE + "viaShapePath"; + + public static readonly VIA_PREDICATE: string = Namespaces.SHAPETREE + "viaPredicate"; + + public static readonly CONTAINS: string = Namespaces.SHAPETREE + "contains"; + + public static readonly HAS_ASSIGNMENT: string = Namespaces.SHAPETREE + "hasAssignment"; + + public static readonly SHAPE: string = Namespaces.SHAPETREE + "shape"; + + public static readonly FOCUS_NODE: string = Namespaces.SHAPETREE + "focusNode"; + + public static readonly CONTAINER: string = Namespaces.SHAPETREE + "Container"; + + public static readonly RESOURCE: string = Namespaces.SHAPETREE + "Resource"; + + public static readonly NON_RDF_RESOURCE: string = Namespaces.SHAPETREE + "NonRDFResource"; +} diff --git a/asTypescript/packages/javahttp/src/JavaHttpClient.ts b/asTypescript/packages/javahttp/src/JavaHttpClient.ts new file mode 100644 index 00000000..f7f27a44 --- /dev/null +++ b/asTypescript/packages/javahttp/src/JavaHttpClient.ts @@ -0,0 +1,158 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.javahttp +import { HttpClient } from '@shapetrees/clienthttp/src/HttpClient'; +import { HttpRequest } from '@shapetrees/clienthttp/src/HttpRequest'; +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ResourceAttributes } from '@shapetrees/core/src/ResourceAttributes'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import * as TrustManager from 'javax/net/ssl'; +import * as X509TrustManager from 'javax/net/ssl'; +import * as SSLContext from 'javax/net/ssl'; +import * as HttpsURLConnection from 'javax/net/ssl'; +import * as HostnameVerifier from 'javax/net/ssl'; +import * as SSLSession from 'javax/net/ssl'; +import * as URISyntaxException from 'java/net'; +import * as KeyManagementException from 'java/security'; +import * as NoSuchAlgorithmException from 'java/security'; +import * as CertificateException from 'java/security/cert'; +import * as X509Certificate from 'java/security/cert'; +import * as Objects from 'java/util'; +import { JavaHttpValidatingShapeTreeInterceptor } from './JavaHttpValidatingShapeTreeInterceptor'; + +/** + * java.net.http implementation of HttpClient + */ +export class JavaHttpClient implements HttpClient { + + private readonly httpClient: java.net.http.HttpClient; + + private validatingWrapper: JavaHttpValidatingShapeTreeInterceptor; + + /** + * Execute an HTTP request to create a DocumentResponse object + * Implements `HttpClient` interface + * @param request an HTTP request with appropriate headers for ShapeTree interactions + * @return new DocumentResponse with response headers and contents + * @throws ShapeTreeException + */ + override public fetchShapeTreeResponse(request: HttpRequest): DocumentResponse /* throws ShapeTreeException */ { + let response: java.net.http.HttpResponse = fetch(request); + let body: string = null; + try { + body = Objects.requireNonNull(response.body()).toString(); + } catch (ex) { + if (ex instanceof NullPointerException) { + log.error("Exception retrieving body string"); + } +} + return new DocumentResponse(new ResourceAttributes(response.headers().map()), body, response.statusCode()); + } + + /** + * Construct an JavaHttpClient with switches to enable or disable SSL and ShapeTree validation + * @param useSslValidation + * @param useShapeTreeValidation + * @throws NoSuchAlgorithmException potentially thrown while disabling SSL validation + * @throws KeyManagementException potentially thrown while disabling SSL validation + */ + protected constructor(useSslValidation: boolean, useShapeTreeValidation: boolean) /* throws NoSuchAlgorithmException, KeyManagementException */ { + let clientBuilder: java.net.http.HttpClient.Builder = java.net.http.HttpClient.newBuilder(); + this.validatingWrapper = null; + if (Boolean.TRUE === useShapeTreeValidation) { + this.validatingWrapper = new JavaHttpValidatingShapeTreeInterceptor(); + } + if (Boolean.FALSE === useSslValidation) { + let trustAllCerts: TrustManager[] = new TrustManager[] { new X509TrustManager() { + + public getAcceptedIssuers(): java.security.cert.X509Certificate[] { + return null; + } + + override public checkClientTrusted(arg0: X509Certificate[], arg1: string): void /* throws CertificateException */ { + } + + override public checkServerTrusted(arg0: X509Certificate[], arg1: string): void /* throws CertificateException */ { + } + } }; + let sc: SSLContext = null; + try { + sc = SSLContext.getInstance("TLSv1.2"); + } catch (e) { + if (e instanceof NoSuchAlgorithmException) { + e.printStackTrace(); + } +} + try { + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + } catch (e) { + if (e instanceof KeyManagementException) { + e.printStackTrace(); + } +} + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + // Create all-trusting host name verifier + let validHosts: HostnameVerifier = new HostnameVerifier() { + + override public verify(arg0: string, arg1: SSLSession): boolean { + return true; + } + }; + // All hosts will be valid + HttpsURLConnection.setDefaultHostnameVerifier(validHosts); + } + this.httpClient = clientBuilder.build(); + } + + /** + * Internal function to execute HTTP request and return java.net.http response + * @param request + * @return + * @throws ShapeTreeException + */ + private fetch(request: HttpRequest): java.net.http.HttpResponse /* throws ShapeTreeException */ { + if (request.body === null) + request.body = ""; + try { + let requestBuilder: java.net.http.HttpRequest.Builder = java.net.http.HttpRequest.newBuilder(); + requestBuilder.uri(request.resourceURL.toURI()); + if (request.headers != null) { + let headerList: string[] = request.headers.toList("connection", "content-length", "date", "expect", "from", "host", "upgrade", "via", "warning"); + if (headerList.length > 0) { + requestBuilder.headers(headerList); + } + } + switch(request.method) { + case HttpClient.GET: + case HttpClient.DELETE: + requestBuilder.method(request.method, java.net.http.HttpRequest.BodyPublishers.noBody()); + break; + case HttpClient.PUT: + case HttpClient.POST: + case HttpClient.PATCH: + requestBuilder.method(request.method, java.net.http.HttpRequest.BodyPublishers.ofString(request.body)); + requestBuilder.header("Content-Type", request.contentType); + break; + default: + throw new ShapeTreeException(500, "Unsupported HTTP method for resource creation"); + } + let nativeRequest: java.net.http.HttpRequest = requestBuilder.build(); + if (this.validatingWrapper === null) { + return JavaHttpClient.check(this.httpClient.send(nativeRequest, java.net.http.HttpResponse.BodyHandlers.ofString())); + } else { + return this.validatingWrapper.validatingWrap(nativeRequest, this.httpClient, request.body, request.contentType); + } + } catch (ex) { + if (ex instanceof IOException || ex instanceof InterruptedException) { + throw new ShapeTreeException(500, ex.getMessage()); + } else if (ex instanceof URISyntaxException) { + throw new ShapeTreeException(500, "Malformed URL <" + request.resourceURL + ">: " + ex.getMessage()); + } +} + } + + protected static check(resp: java.net.http.HttpResponse): java.net.http.HttpResponse { + if (resp.statusCode() > 599) { + throw new Error("invalid HTTP response: " + resp + (resp.body() === null ? "" : "\n" + resp.body())); + } + return resp; + } +} diff --git a/asTypescript/packages/javahttp/src/JavaHttpClientFactory.ts b/asTypescript/packages/javahttp/src/JavaHttpClientFactory.ts new file mode 100644 index 00000000..0733cf78 --- /dev/null +++ b/asTypescript/packages/javahttp/src/JavaHttpClientFactory.ts @@ -0,0 +1,62 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.javahttp +import { HttpClientFactory } from '@shapetrees/clienthttp/src/HttpClientFactory'; +import { HttpRequest } from '@shapetrees/clienthttp/src/HttpRequest'; +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/ExternalDocumentLoader'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { JavaHttpClient } from './JavaHttpClient'; + +/** + * The ShapeTree library uses a generic interface (`HttpClient`) to execute HTTP queries on the POD and for external documents. + * The JavaHttpClient uses the java.net.http library to implement `HttpClient`. + * This factory generates variations of java.net.http those clients depending on the need for SSL validation and ShapeTree validation. + */ +export class JavaHttpClientFactory implements HttpClientFactory, ExternalDocumentLoader { + + useSslValidation: boolean; + + /** + * Construct a factory for JavaHttpClients + * + * @param useSslValidation + */ + constructor(useSslValidation: boolean) { + this.useSslValidation = useSslValidation; + } + + /** + * Create a new java.net.http HttpClient. + * This fulfils the HttpClientFactory interface, so this factory can be use in + * HttpClientFactoryManager.setFactory(new JavaHttpClientFactory(...)); + * + * @param useShapeTreeValidation + * @return a new or existing java.net.http HttpClient + * @throws ShapeTreeException if the JavaHttpClient constructor threw one + */ + public get(useShapeTreeValidation: boolean): JavaHttpClient /* throws ShapeTreeException */ { + try { + return new JavaHttpClient(this.useSslValidation, useShapeTreeValidation); + } catch (ex) { + if (ex instanceof Exception) { + throw new ShapeTreeException(500, ex.getMessage()); + } +} + } + + /** + * Load a non-POD document + * This fulfils the ExternalDocumentLoader interface, so this factory can be use in + * DocumentLoaderManager.setLoader(new JavaHttpClientFactory(...)); + * + * @param resourceUrl URL of resource to be retrieved + * @return a DocumentResponse with the results of a successful GET + * @throws ShapeTreeException if the GET was not successful + */ + override public loadExternalDocument(resourceUrl: URL): DocumentResponse /* throws ShapeTreeException */ { + let response: DocumentResponse = this.get(false).fetchShapeTreeResponse(new HttpRequest("GET", resourceUrl, null, null, null)); + if (response.getStatusCode() != 200) { + throw new ShapeTreeException(500, "Failed to load contents of document: " + resourceUrl); + } + return response; + } +} diff --git a/asTypescript/packages/javahttp/src/JavaHttpValidatingShapeTreeInterceptor.ts b/asTypescript/packages/javahttp/src/JavaHttpValidatingShapeTreeInterceptor.ts new file mode 100644 index 00000000..57cf0778 --- /dev/null +++ b/asTypescript/packages/javahttp/src/JavaHttpValidatingShapeTreeInterceptor.ts @@ -0,0 +1,210 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.javahttp +import { HttpResourceAccessor } from '@shapetrees/clienthttp/src/HttpResourceAccessor'; +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ResourceAttributes } from '@shapetrees/core/src/ResourceAttributes'; +import { ResourceAccessor } from '@shapetrees/core/src/ResourceAccessor'; +import { ShapeTreeRequest } from '@shapetrees/core/src/ShapeTreeRequest'; +import { HttpHeaders } from '@shapetrees/core/src/enums/HttpHeaders'; +import { ShapeTreeResourceType } from '@shapetrees/core/src/enums/ShapeTreeResourceType'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { ValidatingMethodHandler } from '@shapetrees/core/src/methodhandlers/ValidatingMethodHandler'; +import { ValidatingDeleteMethodHandler } from '@shapetrees/core/src/methodhandlers/ValidatingDeleteMethodHandler'; +import { ValidatingPutMethodHandler } from '@shapetrees/core/src/methodhandlers/ValidatingPutMethodHandler'; +import { ValidatingPatchMethodHandler } from '@shapetrees/core/src/methodhandlers/ValidatingPatchMethodHandler'; +import { ValidatingPostMethodHandler } from '@shapetrees/core/src/methodhandlers/ValidatingPostMethodHandler'; +import * as NotNull from 'org/jetbrains/annotations'; +import * as SSLSession from 'javax/net/ssl'; +import * as MalformedURLException from 'java/net'; +import * as URI from 'java/net'; +import * as HttpResponse from 'java/net/http'; +import * as Collections from 'java/util'; +import * as TreeMap from 'java/util'; + +/** + * Wrapper used for client-side validation + */ +export class JavaHttpValidatingShapeTreeInterceptor { + + private static readonly POST: string = "POST"; + + private static readonly PUT: string = "PUT"; + + private static readonly PATCH: string = "PATCH"; + + private static readonly DELETE: string = "DELETE"; + + // @NotNull + public validatingWrap(clientRequest: java.net.http.HttpRequest, httpClient: java.net.http.HttpClient, body: string, contentType: string): java.net.http.HttpResponse /* throws IOException, InterruptedException */ { + let shapeTreeRequest: ShapeTreeRequest = new JavaHttpShapeTreeRequest(clientRequest, body, contentType); + let resourceAccessor: ResourceAccessor = new HttpResourceAccessor(); + // Get the handler + let handler: ValidatingMethodHandler = getHandler(shapeTreeRequest.getMethod(), resourceAccessor); + if (handler != null) { + try { + let shapeTreeResponse: DocumentResponse | null = handler.validateRequest(shapeTreeRequest); + if (!shapeTreeResponse.isPresent()) { + return JavaHttpClient.check(httpClient.send(clientRequest, java.net.http.HttpResponse.BodyHandlers.ofString())); + } else { + return createResponse(clientRequest, shapeTreeResponse.get()); + } + } catch (ex) { + if (ex instanceof ShapeTreeException) { + log.error("Error processing shape tree request: ", ex); + return createErrorResponse(ex, clientRequest); + } else if (ex instanceof Exception) { + log.error("Error processing shape tree request: ", ex); + return createErrorResponse(new ShapeTreeException(500, ex.getMessage()), clientRequest); + } +} + } else { + log.warn("No handler for method [{}] - passing through request", shapeTreeRequest.getMethod()); + return JavaHttpClient.check(httpClient.send(clientRequest, java.net.http.HttpResponse.BodyHandlers.ofString())); + } + } + + private getHandler(requestMethod: string, resourceAccessor: ResourceAccessor): ValidatingMethodHandler { + switch(requestMethod) { + case POST: + return new ValidatingPostMethodHandler(resourceAccessor); + case PUT: + return new ValidatingPutMethodHandler(resourceAccessor); + case PATCH: + return new ValidatingPatchMethodHandler(resourceAccessor); + case DELETE: + return new ValidatingDeleteMethodHandler(resourceAccessor); + default: + return null; + } + } + + private createErrorResponse(exception: ShapeTreeException, nativeRequest: java.net.http.HttpRequest): java.net.http.HttpResponse { + return new MyHttpResponse(exception.getStatusCode(), nativeRequest, java.net.http.HttpHeaders.of(Collections.emptyMap(), (a, v) -> true), exception.getMessage()); + } + + // @SneakyThrows + private createResponse(nativeRequest: java.net.http.HttpRequest, response: DocumentResponse): java.net.http.HttpResponse { + let headers: java.net.http.HttpHeaders = java.net.http.HttpHeaders.of(response.getResourceAttributes().toMultimap(), (a, v) -> true); + return new MyHttpResponse(response.getStatusCode(), nativeRequest, headers, response.getBody()); + } + + private class JavaHttpShapeTreeRequest implements ShapeTreeRequest { + + private readonly request: java.net.http.HttpRequest; + + private resourceType: ShapeTreeResourceType; + + private readonly body: string; + + private readonly contentType: string; + + private readonly headers: ResourceAttributes; + + public constructor(request: java.net.http.HttpRequest, body: string, contentType: string) { + this.request = request; + this.body = body; + this.contentType = contentType; + let tm: TreeMap> = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + let headerMap: Map> = this.request.headers().map(); + for (const entry of headerMap.entrySet()) { + tm.put(entry.getKey(), entry.getValue()); + } + this.headers = new ResourceAttributes(tm); + } + + override public getMethod(): string { + return this.request.method(); + } + + override public getUrl(): URL { + try { + return this.request.uri().toURL(); + } catch (ex) { + if (ex instanceof MalformedURLException) { + throw new IllegalStateException("request has a malformed URL <" + request.uri() + ">: " + ex.getMessage()); + } +} + } + + override public getHeaders(): ResourceAttributes { + return this.headers; + } + + override public getLinkHeaders(): ResourceAttributes { + return ResourceAttributes.parseLinkHeaders(this.getHeaderValues(HttpHeaders.LINK.getValue())); + } + + override public getHeaderValues(header: string): Array { + return this.request.headers().allValues(header); + } + + override public getHeaderValue(header: string): string { + return this.request.headers().firstValue(header).orElse(null); + } + + override public getContentType(): string { + return this.getHeaders().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null); + } + + override public getResourceType(): ShapeTreeResourceType { + return this.resourceType; + } + + override public setResourceType(resourceType: ShapeTreeResourceType): void { + this.resourceType = resourceType; + } + + override public getBody(): string { + return this.body; + } + } + + private class MyHttpResponse implements java.net.http.HttpResponse { + + private statusCode: number; + + private request: java.net.http.HttpRequest; + + private headers: java.net.http.HttpHeaders; + + private body: string; + + override public statusCode(): number { + return this.statusCode; + } + + override public request(): java.net.http.HttpRequest { + return this.request; + } + + override public previousResponse(): HttpResponse | null { + return Optional.empty(); + } + + override public headers(): java.net.http.HttpHeaders { + return this.headers; + } + + override public body(): string { + return this.body; + } + + override public sslSession(): SSLSession | null { + return Optional.empty(); + } + + override public uri(): URI { + return null; + } + + override public version(): java.net.http.HttpClient.Version { + return null; + } + + public constructor(statusCode: number, request: java.net.http.HttpRequest, headers: java.net.http.HttpHeaders, body: string) { + this.statusCode = statusCode; + this.request = request; + this.headers = headers; + this.body = body; + } + } +} diff --git a/asTypescript/packages/tests/src/AbstractResourceAccessorTests.ts b/asTypescript/packages/tests/src/AbstractResourceAccessorTests.ts new file mode 100644 index 00000000..8e1b4688 --- /dev/null +++ b/asTypescript/packages/tests/src/AbstractResourceAccessorTests.ts @@ -0,0 +1,260 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { ShapeTreeContext } from '@shapetrees/core/src/ShapeTreeContext'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as Assertions from 'org/junit/jupiter/api'; +import * as BeforeAll from 'org/junit/jupiter/api'; +import * as DisplayName from 'org/junit/jupiter/api'; +import * as Test from 'org/junit/jupiter/api'; +import * as MalformedURLException from 'java/net'; +import * as assertFalse from 'org/junit/jupiter/api/Assertions'; +import * as assertTrue from 'org/junit/jupiter/api/Assertions'; + +export class AbstractResourceAccessorTests { + + protected resourceAccessor: ResourceAccessor = null; + + protected readonly context: ShapeTreeContext; + + protected static server: MockWebServer = null; + + protected static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + this.context = new ShapeTreeContext(null); + } + + public toUrl(server: MockWebServer, path: string): URL /* throws MalformedURLException */ { + // TODO: duplicates com.janeirodigital.shapetrees.tests.fixtures.MockWebServerHelper.getURL; + return new URL(server.url(path).toString()); + } + + // @BeforeAll + static beforeAll(): void { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("resourceAccessor/resource-no-link-headers"), "GET", "/static/resource/resource-no-link-headers", null), new DispatcherEntry(List.of("resourceAccessor/resource-empty-link-header"), "GET", "/static/resource/resource-empty-link-header", null), new DispatcherEntry(List.of("resourceAccessor/resource-container-link-header"), "GET", "/static/resource/resource-container-link-header", null), new DispatcherEntry(List.of("resourceAccessor/resource-container-link-header"), "GET", "/static/resource/resource-container-link-header/", null), new DispatcherEntry(List.of("resourceAccessor/resource-container-invalid-link-header"), "GET", "/static/resource/resource-container-invalid-link-header/", null), new DispatcherEntry(List.of("resourceAccessor/managed-container-1"), "GET", "/static/resource/managed-container-1/", null), new DispatcherEntry(List.of("resourceAccessor/managed-resource-1-create"), "PUT", "/static/resource/managed-container-1/managed-resource-1/", null), new DispatcherEntry(List.of("resourceAccessor/managed-resource-1-manager"), "GET", "/static/resource/managed-container-1/managed-resource-1/.shapetree", null), new DispatcherEntry(List.of("resourceAccessor/managed-container-1-manager"), "GET", "/static/resource/managed-container-1/.shapetree", null), new DispatcherEntry(List.of("resourceAccessor/unmanaged-container-2"), "GET", "/static/resource/unmanaged-container-2/", null), new DispatcherEntry(List.of("resourceAccessor/managed-container-2"), "GET", "/static/resource/managed-container-2/", null), new DispatcherEntry(List.of("resourceAccessor/unmanaged-resource-1-create"), "PUT", "/static/resource/unmanaged-resource-1", null), new DispatcherEntry(List.of("resourceAccessor/managed-container-2-manager-create"), "PUT", "/static/resource/managed-container-2/.shapetree", null), new DispatcherEntry(List.of("errors/404"), "GET", "/static/resource/missing-resource-1.shapetree", null), new DispatcherEntry(List.of("errors/404"), "GET", "/static/resource/missing-resource-2", null), new DispatcherEntry(List.of("resourceAccessor/missing-resource-2-manager-create"), "PUT", "/static/resource/missing-resource-2.shapetree", null), new DispatcherEntry(List.of("shapetrees/project-shapetree-ttl"), "GET", "/static/shapetrees/project/shapetree", null), new DispatcherEntry(List.of("schemas/project-shex"), "GET", "/static/shex/project/shex", null), new DispatcherEntry(List.of("errors/404"), "GET", "/static/resource/notpresent", null))); + server = new MockWebServer(); + server.setDispatcher(dispatcher); + } + + // Tests to Get ManageableInstances + // @Test, @SneakyThrows, @DisplayName("Get instance from missing resource") + getInstanceFromMissingResource(): void { + let instance: ManageableInstance = this.resourceAccessor.getInstance(context, toUrl(server, "/static/resource/notpresent")); + Assertions.assertTrue(instance.getManageableResource() instanceof MissingManageableResource); + Assertions.assertTrue(instance.getManagerResource() instanceof MissingManagerResource); + Assertions.assertFalse(instance.isManaged()); + Assertions.assertEquals(instance.getManageableResource().getUrl(), toUrl(server, "/static/resource/notpresent")); + Assertions.assertFalse(instance.getManageableResource().isExists()); + Assertions.assertFalse(instance.getManagerResource().isExists()); + } + + // @Test, @SneakyThrows, @DisplayName("Get instance from managed resource") + getInstanceFromManagedResource(): void { + // If the resource is Manageable - determine if it is managed by getting manager + // Get and store a ManagedResource in instance - Manager exists - store manager in instance too + let instance: ManageableInstance = this.resourceAccessor.getInstance(context, toUrl(server, "/static/resource/managed-container-1/")); + Assertions.assertTrue(instance.getManageableResource() instanceof ManagedResource); + Assertions.assertNotNull(instance.getManagerResource()); + Assertions.assertFalse(instance.getManagerResource() instanceof MissingManagerResource); + Assertions.assertTrue(instance.isManaged()); + Assertions.assertFalse(instance.isUnmanaged()); + let managerResource: ManagerResource = instance.getManagerResource(); + let manager: ShapeTreeManager = managerResource.getManager(); + Assertions.assertEquals(1, manager.getAssignments().size()); + } + + // @Test, @SneakyThrows, @DisplayName("Get instance for managed resource from manager request") + getInstanceFromManagedResourceFromManager(): void { + let instance: ManageableInstance = this.resourceAccessor.getInstance(context, toUrl(server, "/static/resource/managed-container-1/.shapetree")); + Assertions.assertNotNull(instance.getManagerResource()); + Assertions.assertFalse(instance.getManagerResource() instanceof MissingManagerResource); + Assertions.assertTrue(instance.getManagerResource().isExists()); + Assertions.assertTrue(instance.getManageableResource() instanceof ManagedResource); + Assertions.assertEquals(instance.getManagerResource().getManagedResourceUrl(), instance.getManageableResource().getUrl()); + } + + // @Test, @SneakyThrows, @DisplayName("Fail to get instance for missing resource from manager request") + failToGetInstanceForMissingManageableResourceFromManager(): void { + // Note that in this request, the manager is also non-existent + Assertions.assertThrows(ShapeTreeException.class, () -> { + let instance: ManageableInstance = this.resourceAccessor.getInstance(context, toUrl(server, "/static/resource/missing-resource-1.shapetree")); + }); + } + + // @Test, @SneakyThrows, @DisplayName("Get instance from unmanaged resource") + getInstanceFromUnmanagedResource(): void { + // If the resource is Manageable - determine if it is managed by getting manager + // Get and store an UnmanagedResource in instance - No manager exists - store the location of the manager url + let instance: ManageableInstance = this.resourceAccessor.getInstance(context, toUrl(server, "/static/resource/unmanaged-container-2/")); + Assertions.assertTrue(instance.getManageableResource() instanceof UnmanagedResource); + Assertions.assertTrue(instance.getManagerResource() instanceof MissingManagerResource); + Assertions.assertTrue(instance.isUnmanaged()); + Assertions.assertFalse(instance.isManaged()); + } + + // @Test, @SneakyThrows, @DisplayName("Get instance from unmanaged resource from manager request") + getInstanceFromUnmanagedResourceFromManager(): void { + // Manager resource doesn't exist. Unmanaged resource associated with it does exist + let instance: ManageableInstance = this.resourceAccessor.getInstance(context, toUrl(server, "/static/resource/unmanaged-container-2/.shapetree")); + Assertions.assertTrue(instance.getManageableResource() instanceof UnmanagedResource); + Assertions.assertTrue(instance.getManagerResource() instanceof MissingManagerResource); + Assertions.assertTrue(instance.wasRequestForManager()); + Assertions.assertTrue(instance.isUnmanaged()); + Assertions.assertFalse(instance.isManaged()); + } + + // Tests to Create ManageableInstances + // @Test, @SneakyThrows, @DisplayName("Create instance from managed resource") + createInstanceFromManagedResource(): void { + let headers: ResourceAttributes = new ResourceAttributes(); + let instance: ManageableInstance = this.resourceAccessor.createInstance(context, "PUT", toUrl(server, "/static/resource/managed-container-1/managed-resource-1/"), headers, getMilestoneThreeBodyGraph(), "text/turtle"); + Assertions.assertTrue(instance.isManaged()); + Assertions.assertTrue(instance.getManageableResource() instanceof ManagedResource); + Assertions.assertFalse(instance.getManageableResource() instanceof MissingManageableResource); + Assertions.assertTrue(instance.getManagerResource().isExists()); + Assertions.assertEquals(instance.getManagerResource().getManagedResourceUrl(), instance.getManageableResource().getUrl()); + let managerResource: ManagerResource = instance.getManagerResource(); + let manager: ShapeTreeManager = managerResource.getManager(); + Assertions.assertEquals(1, manager.getAssignments().size()); + } + + // @Test, @SneakyThrows, @DisplayName("Create instance from unmanaged resource") + createInstanceFromUnmanagedResource(): void { + let headers: ResourceAttributes = new ResourceAttributes(); + let instance: ManageableInstance = this.resourceAccessor.createInstance(context, "PUT", toUrl(server, "/static/resource/unmanaged-resource-1"), headers, "<#a> <#b> <#c>", "text/turtle"); + Assertions.assertTrue(instance.isUnmanaged()); + Assertions.assertTrue(instance.getManageableResource() instanceof UnmanagedResource); + Assertions.assertTrue(instance.getManagerResource() instanceof MissingManagerResource); + } + + // @Test, @SneakyThrows, @DisplayName("Fail to create instance from existing manageable resource") + failToCreateInstanceFromExistingResource(): void { + // Resource exists - ERROR - can't create a manageable resource when one already exists + // May need to populate this + let headers: ResourceAttributes = new ResourceAttributes(); + Assertions.assertThrows(ShapeTreeException.class, () -> { + let instance: ManageableInstance = this.resourceAccessor.createInstance(context, "PUT", toUrl(server, "/static/resource/unmanaged-container-2/"), headers, "<#a> <#b> <#c>", "text/turtle"); + }); + } + + // @Test, @SneakyThrows, @DisplayName("Create instance from manager resource") + createInstanceFromManagerResource(): void { + // Create a new manager and store in instance and load the managed resource and store in instance (possibly just pre-fetch metadata if lazily loading) + let headers: ResourceAttributes = new ResourceAttributes(); + let instance: ManageableInstance = this.resourceAccessor.createInstance(context, "PUT", toUrl(server, "/static/resource/managed-container-2/.shapetree"), headers, getProjectTwoManagerGraph(), "text/turtle"); + Assertions.assertTrue(instance.isManaged()); + Assertions.assertTrue(instance.getManageableResource() instanceof ManagedResource); + Assertions.assertFalse(instance.getManagerResource() instanceof MissingManagerResource); + // Probably need some additional tests + } + + // @Test, @DisplayName("Fail to create instance from isolated manager resource") + failToCreateInstanceFromIsolatedManagerResource(): void { + let headers: ResourceAttributes = new ResourceAttributes(); + Assertions.assertThrows(ShapeTreeException.class, () -> { + let instance: ManageableInstance = this.resourceAccessor.createInstance(context, "PUT", toUrl(server, "/static/resource/missing-resource-2.shapetree"), headers, getProjectTwoManagerGraph(), "text/turtle"); + }); + } + + // TODO - currently missing dedicated tests for create and delete. only one test for update (which is a failure test) + // @Test, @DisplayName("Get a resource without any link headers") + getResourceWithNoLinkHeaders(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(this.context, toUrl(server, "/static/resource/resource-no-link-headers")); + // This is a strange way to check whether something has no link headers + assertTrue(resource.isExists()); + assertTrue(((ManageableResource) resource).getManagerResourceUrl().isEmpty()); + } + + // @Test, @DisplayName("Get a resource with an empty link header") + getResourceWithEmptyLinkHeader(): void /* throws MalformedURLException, ShapeTreeException */ { + // Link header is present but has nothing in it + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/resource-empty-link-header")); + assertTrue(resource.isExists()); + Assertions.assertTrue(((ManageableResource) resource).getManagerResourceUrl().isEmpty()); + } + + // @Test, @DisplayName("Fail to get a resource with an invalid URL string") + failToAccessResourceWithInvalidUrlString(): void /* throws MalformedURLException, ShapeTreeException */ { + // TODO: Test: may as well deleted as it's only testing URL.create() + Assertions.assertThrows(MalformedURLException.class, () -> this.resourceAccessor.getResource(context, new URL(":invalid"))); + // TODO - this should also test create, update, delete, getContained, (also get/create instance) + } + + // @Test, @DisplayName("Get a missing resource with no slash") + getMissingResourceWithNoSlash(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/not-existing-no-slash")); + assertFalse(resource.isExists()); + assertFalse(((ManageableResource) resource).isContainer()); + } + + // @Test, @DisplayName("Get a missing container with slash") + getMissingContainerWithSlash(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/not-existing-slash/")); + assertFalse(resource.isExists()); + assertTrue(((ManageableResource) resource).isContainer()); + } + + // @Test, @DisplayName("Get a missing container with slash and fragment") + getMissingContainerWithSlashAndFragment(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/not-existing-slash/#withfragment")); + assertFalse(resource.isExists()); + assertTrue(((ManageableResource) resource).isContainer()); + } + + // @Test, @DisplayName("Get an existing container with no slash") + getExistingContainerNoSlash(): void /* throws MalformedURLException, ShapeTreeException */ { + // TODO - In Solid at least, the slash must be present, so I question whether setting this as a container helps or hurts + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/resource-container-link-header")); + assertTrue(resource.isExists()); + assertTrue(((ManageableResource) resource).isContainer()); + } + + // @Test, @DisplayName("Get an existing container") + getExistingContainer(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/resource-container-link-header/")); + assertTrue(resource.isExists()); + assertTrue(((ManageableResource) resource).isContainer()); + } + + // @Test, @DisplayName("Fail to lookup invalid resource attributes") + failToLookupInvalidAttributes(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/resource-container-link-header")); + assertTrue(resource.isExists()); + Assertions.assertNull(resource.getAttributes().firstValue("invalid").orElse(null)); + } + + // @Test, @DisplayName("Get a missing resource") + getMissingResource(): void /* throws MalformedURLException, ShapeTreeException */ { + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/notpresent")); + Assertions.assertEquals("", resource.getBody()); + // TODO - what other tests and assertions should be included here? isExists()? + } + + // @Test, @DisplayName("Get a container with an invalid link type header") + getContainerWithInvalidLinkTypeHeader(): void /* throws MalformedURLException, ShapeTreeException */ { + // TODO - at the moment we process this happily. Aside from not marking it as a container, should there be a more severe handling? + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/resource-container-invalid-link-header/")); + assertTrue(resource.isExists()); + assertFalse(((ManageableResource) resource).isContainer()); + } + + // @Test, @DisplayName("Fail to update resource") + failToUpdateResource(): void /* throws MalformedURLException, ShapeTreeException */ { + // Succeed in getting a resource + let resource: InstanceResource = this.resourceAccessor.getResource(context, toUrl(server, "/static/resource/resource-container-link-header/")); + // Fail to update it + let response: DocumentResponse = this.resourceAccessor.updateResource(context, "PUT", resource, "BODY"); + assertFalse(response.isExists()); + } + + private getMilestoneThreeBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#milestone> \n" + " ex:uri ; \n" + " ex:id 12345 ; \n" + " ex:name \"Milestone 3 of Project 1\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:target \"2021-06-05T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:inProject . \n"; + } + + private getProjectTwoManagerGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "<> \n" + " a st:Manager ; \n" + " st:hasAssignment <#ln1> . \n" + "\n" + "<#ln1> \n" + " st:assigns <${SERVER_BASE}/static/shapetrees/project/shapetree#ProjectTree> ; \n" + " st:manages ; \n" + " st:hasRootAssignment <#ln1> ; \n" + " st:focusNode ; \n" + " st:shape <${SERVER_BASE}/static/shex/project/shex#ProjectShape> . \n"; + } +} diff --git a/asTypescript/packages/tests/src/DocumentLoaderTests.ts b/asTypescript/packages/tests/src/DocumentLoaderTests.ts new file mode 100644 index 00000000..65422378 --- /dev/null +++ b/asTypescript/packages/tests/src/DocumentLoaderTests.ts @@ -0,0 +1,34 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { ExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/ExternalDocumentLoader'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DocumentLoaderTests { + + // @BeforeAll, @AfterAll + static clearDocumentManager(): void { + DocumentLoaderManager.setLoader(null); + } + + // @Test, @Order(1), @DisplayName("Fail to get missing document loader"), @SneakyThrows + failToGetMissingDocumentLoader(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> { + DocumentLoaderManager.getLoader(); + }); + } + + // @Test, @Order(2), @DisplayName("Get document loader"), @SneakyThrows + getDocumentLoader(): void { + DocumentLoaderManager.setLoader(new TestDocumentLoader()); + Assertions.assertNotNull(DocumentLoaderManager.getLoader()); + } +} + +class TestDocumentLoader implements ExternalDocumentLoader { + + public loadExternalDocument(resourceURL: URL): DocumentResponse /* throws ShapeTreeException */ { + return new DocumentResponse(null, null, 200); + } +} diff --git a/asTypescript/packages/tests/src/GraphHelperTests.ts b/asTypescript/packages/tests/src/GraphHelperTests.ts new file mode 100644 index 00000000..42e36872 --- /dev/null +++ b/asTypescript/packages/tests/src/GraphHelperTests.ts @@ -0,0 +1,162 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { GraphHelper } from '@shapetrees/core/src/helpers/GraphHelper'; +import * as Graph from 'org/apache/jena/graph'; +import * as NodeFactory from 'org/apache/jena/graph'; +import * as Triple from 'org/apache/jena/graph'; +import * as ModelFactory from 'org/apache/jena/rdf/model'; +import * as Lang from 'org/apache/jena/riot'; +import * as Assertions from 'org/junit/jupiter/api'; +import * as DisplayName from 'org/junit/jupiter/api'; +import * as Test from 'org/junit/jupiter/api'; +import * as ParameterizedTest from 'org/junit/jupiter/params'; +import * as NullAndEmptySource from 'org/junit/jupiter/params/provider'; +import * as ValueSource from 'org/junit/jupiter/params/provider'; +import * as URI from 'java/net'; +import * as OffsetDateTime from 'java/time'; +import { newTriple } from '@shapetrees/core/src/helpers/GraphHelper/newTriple'; + +class GraphHelperTests { + + // @ParameterizedTest, @NullAndEmptySource, @DisplayName("Handle null or empty content types with defaults"), @SneakyThrows + handleNullOrEmptyContentTypes(type: string): void { + let lang: Lang = GraphHelper.getLangForContentType(type); + Assertions.assertEquals(lang, Lang.TURTLE); + } + + // @ParameterizedTest, @ValueSource(strings = { "text/turtle", "something/bogus" }), @DisplayName("Handle turtle content type when specified or as default"), @SneakyThrows + handleTurtleContentType(type: string): void { + let lang: Lang = GraphHelper.getLangForContentType(type); + Assertions.assertEquals(lang, Lang.TURTLE); + } + + // @Test, @DisplayName("JSON LD content type"), @SneakyThrows + handleJsonLD(): void { + let lang: Lang = GraphHelper.getLangForContentType("application/ld+json"); + Assertions.assertEquals(lang, Lang.JSONLD); + } + + // @Test, @DisplayName("N-Triples content type"), @SneakyThrows + hanldeNTriples(): void { + let lang: Lang = GraphHelper.getLangForContentType("application/n-triples"); + Assertions.assertEquals(lang, Lang.NTRIPLES); + } + + // @Test, @DisplayName("rdf+xml content type"), @SneakyThrows + hanldeRDFXMLTriples(): void { + let lang: Lang = GraphHelper.getLangForContentType("application/rdf+xml"); + Assertions.assertEquals(lang, Lang.RDFXML); + } + + // @Test, @DisplayName("Parse invalid TTL"), @SneakyThrows + parseInvalidTTL(): void { + let invalidTtl: string = "<#a> b c"; + Assertions.assertThrows(ShapeTreeException.class, () -> GraphHelper.readStringIntoGraph(URI.create("https://example.com/a"), invalidTtl, "text/turtle")); + } + + // @Test, @DisplayName("Parse valid TTL"), @SneakyThrows + parseValidTTL(): void { + let invalidTtl: string = "<#a> <#b> <#c> ."; + Assertions.assertNotNull(GraphHelper.readStringIntoGraph(URI.create("https://example.com/a"), invalidTtl, "text/turtle")); + } + + // @Test, @DisplayName("Write graph to TTL String"), @SneakyThrows + writeGraphToTTLString(): void { + let graph: Graph = ModelFactory.createDefaultModel().getGraph(); + graph.add(new Triple(NodeFactory.createURI("<#b>"), NodeFactory.createURI("<#c>"), NodeFactory.createURI("<#d>"))); + Assertions.assertNotNull(GraphHelper.writeGraphToTurtleString(graph)); + } + + // @Test, @DisplayName("Write null graph to TTL String"), @SneakyThrows + writeNullGraphToTTLString(): void { + Assertions.assertNull(GraphHelper.writeGraphToTurtleString(null)); + } + + // @Test, @DisplayName("Write closed graph to TTL String"), @SneakyThrows + writeClosedGraphtoTTLString(): void { + let graph: Graph = ModelFactory.createDefaultModel().getGraph(); + graph.add(new Triple(NodeFactory.createURI("<#b>"), NodeFactory.createURI("<#c>"), NodeFactory.createURI("<#d>"))); + graph.close(); + Assertions.assertNull(GraphHelper.writeGraphToTurtleString(graph)); + } + + // @Test, @DisplayName("Fail to store null string objects in new Triple helper"), @SneakyThrows + failToStoreNullTripleStringObjects(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple("<#b>", "<#c>", null); + }); + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple("<#b>", null, "<#d>"); + }); + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple(null, "<#c>", "<#d>"); + }); + } + + // @Test, @DisplayName("Fail to store null URI objects in new Triple helper"), @SneakyThrows + failToStoreNullTripleURIObjects(): void { + let subjectURI: URI = URI.create("https://site.example/#a"); + let predicateURI: URI = URI.create("https://site.example/#b"); + let objectURI: URI = URI.create("https://site.example/#c"); + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple(subjectURI, predicateURI, null); + }); + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple(subjectURI, null, objectURI); + }); + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple(null, predicateURI, objectURI); + }); + } + + // @Test, @DisplayName("Store URI as subject and predicate with new Triple helper"), @SneakyThrows + storeURISubjectAndPredicate(): void { + let uriTriple: Triple = newTriple(URI.create("https://site.example/#a"), URI.create("https://site.example/#b"), URI.create("https://site.example/#a")); + Assertions.assertNotNull(uriTriple); + Assertions.assertTrue(uriTriple.getSubject().isURI()); + Assertions.assertTrue(uriTriple.getPredicate().isURI()); + Assertions.assertTrue(uriTriple.getObject().isURI()); + } + + // @Test, @DisplayName("Store URI object with new Triple helper"), @SneakyThrows + storeURIasTripleObject(): void { + let uriTriple: Triple = newTriple("https://site.example/#a", "https://site.example/#b", URI.create("https://site.example/#c")); + Assertions.assertNotNull(uriTriple); + Assertions.assertTrue(uriTriple.getObject().isURI()); + } + + // @Test, @DisplayName("Store String object with new Triple helper"), @SneakyThrows + storeStringAsTripleObject(): void { + let uriTriple: Triple = newTriple("https://site.example/#a", "https://site.example/#b", "This is a test string"); + Assertions.assertNotNull(uriTriple); + Assertions.assertTrue(uriTriple.getObject().isLiteral()); + } + + // @Test, @DisplayName("Store DateTime object with new Triple helper"), @SneakyThrows + storeDateTimeAsTripleObject(): void { + let uriTriple: Triple = newTriple("https://site.example/#a", "https://site.example/#b", OffsetDateTime.now()); + Assertions.assertNotNull(uriTriple); + Assertions.assertTrue(uriTriple.getObject().isLiteral()); + } + + // @Test, @DisplayName("Store Boolean object with new Triple helper"), @SneakyThrows + storeBooleanAsTripleObject(): void { + let uriTriple: Triple = newTriple("https://site.example/#a", "https://site.example/#b", Boolean.TRUE); + Assertions.assertNotNull(uriTriple); + Assertions.assertTrue(uriTriple.getObject().isLiteral()); + } + + // @Test, @DisplayName("Store Blank Node with new Triple helper"), @SneakyThrows + storeBlankNodeAsTripleObject(): void { + let uriTriple: Triple = newTriple("https://site.example/#a", "https://site.example/#b", NodeFactory.createBlankNode()); + Assertions.assertNotNull(uriTriple); + Assertions.assertTrue(uriTriple.getObject().isBlank()); + } + + // @Test, @DisplayName("Fail to store Unsupported Type with new Triple helper"), @SneakyThrows + failedToStoreUnsupportedTypeAsTripleObject(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> { + newTriple("https://site.example/#a", "https://site.example/#b", (float) 35.6); + }); + } +} diff --git a/asTypescript/packages/tests/src/HttpDocumentLoaderTests.ts b/asTypescript/packages/tests/src/HttpDocumentLoaderTests.ts new file mode 100644 index 00000000..cd422d2c --- /dev/null +++ b/asTypescript/packages/tests/src/HttpDocumentLoaderTests.ts @@ -0,0 +1,66 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { HttpExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/HttpExternalDocumentLoader'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as MalformedURLException from 'java/net'; + +class HttpDocumentLoaderTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + private static httpExternalDocumentLoader: HttpExternalDocumentLoader; + + public constructor() { + httpExternalDocumentLoader = new HttpExternalDocumentLoader(); + DocumentLoaderManager.setLoader(httpExternalDocumentLoader); + } + + protected getURL(server: MockWebServer, path: string): URL /* throws MalformedURLException */ { + return new URL(server.url(path).toString()); + } + + // @BeforeAll + static beforeAll(): void { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("shapetrees/validation-shapetree-ttl"), "GET", "/static/shapetrees/validation/shapetree", null), new DispatcherEntry(List.of("http/404"), "GET", "/static/shex/missing", null))); + } + + // @AfterAll + static clearDocumentManager(): void { + DocumentLoaderManager.setLoader(null); + } + + // @Test, @DisplayName("Fail to load missing document over http") + failToLoadMissingHttpDocument(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + Assertions.assertThrows(ShapeTreeException.class, () -> { + httpExternalDocumentLoader.loadExternalDocument(getURL(server, "/static/shex/missing")); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Successfully load shape tree document over http") + loadHttpDocument(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let shapeTreeDocument: DocumentResponse = httpExternalDocumentLoader.loadExternalDocument(getURL(server, "/static/shapetrees/validation/shapetree")); + Assertions.assertNotNull(shapeTreeDocument); + Assertions.assertEquals(200, shapeTreeDocument.getStatusCode()); + Assertions.assertTrue(shapeTreeDocument.isExists()); + Assertions.assertNotNull(shapeTreeDocument.getBody()); + Assertions.assertNotNull(shapeTreeDocument.getResourceAttributes()); + } + + // @SneakyThrows, @Test, @DisplayName("Successfully handle thread interruption") + handleInterruptedThreadOnLoadHttpDocument(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + Thread.currentThread().interrupt(); + Assertions.assertThrows(ShapeTreeException.class, () -> { + let shapeTreeDocument: DocumentResponse = httpExternalDocumentLoader.loadExternalDocument(getURL(server, "/static/shapetrees/validation/shapetree")); + }); + } +} diff --git a/asTypescript/packages/tests/src/ResourceAttributesTests.ts b/asTypescript/packages/tests/src/ResourceAttributesTests.ts new file mode 100644 index 00000000..36326920 --- /dev/null +++ b/asTypescript/packages/tests/src/ResourceAttributesTests.ts @@ -0,0 +1,83 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ResourceAttributes } from '@shapetrees/core/src/ResourceAttributes'; +import * as Assertions from 'org/junit/jupiter/api'; +import * as DisplayName from 'org/junit/jupiter/api'; +import * as Test from 'org/junit/jupiter/api'; + +class ResourceAttributesTests { + + // @Test, @DisplayName("Initialize empty ResourceAttributes"), @SneakyThrows + initializeEmptyResourceAttributes(): void { + let resourceAttributes: ResourceAttributes = new ResourceAttributes(); + Assertions.assertNotNull(resourceAttributes); + } + + // @Test, @DisplayName("Initialize ResourceAttributes with value pair"), @SneakyThrows + initializeResourceAttributesWithPair(): void { + let attribute: string = "Attribute"; + let value: string = "Value"; + let resourceAttributes: ResourceAttributes = new ResourceAttributes(attribute, value); + Assertions.assertNotNull(resourceAttributes); + Assertions.assertTrue(resourceAttributes.toMultimap().containsKey(attribute)); + } + + // @Test, @DisplayName("Initialize ResourceAttributes with Map"), @SneakyThrows + initializeResourceAttributesWithMap(): void { + let attributeStrings1: List = new Array(); + let attributeStrings2: List = new Array(); + let attributesMap: Map> = new Map>(); + attributeStrings1.add("string1"); + attributeStrings1.add("string2"); + attributeStrings2.add("string3"); + attributeStrings2.add("string4"); + attributesMap.put("attribute1", attributeStrings1); + attributesMap.put("attribute2", attributeStrings2); + let resourceAttributes: ResourceAttributes = new ResourceAttributes(attributesMap); + Assertions.assertNotNull(resourceAttributes); + Assertions.assertEquals(resourceAttributes.toMultimap(), attributesMap); + } + + // @Test, @DisplayName("Add pairs with MaybePlus"), @SneakyThrows + addPairsWithMaybePlus(): void { + let resourceAttributes: ResourceAttributes = new ResourceAttributes(); + Assertions.assertNotNull(resourceAttributes); + let resourceAttributes2: ResourceAttributes = resourceAttributes.maybePlus(null, "value"); + Assertions.assertTrue(resourceAttributes2.toMultimap().isEmpty()); + resourceAttributes2 = resourceAttributes.maybePlus("attribute", null); + Assertions.assertTrue(resourceAttributes2.toMultimap().isEmpty()); + resourceAttributes2 = resourceAttributes.maybePlus(null, null); + Assertions.assertTrue(resourceAttributes2.toMultimap().isEmpty()); + Assertions.assertEquals(resourceAttributes, resourceAttributes2); + resourceAttributes2 = resourceAttributes.maybePlus("Attributes", "First Value"); + Assertions.assertNotEquals(resourceAttributes, resourceAttributes2); + Assertions.assertFalse(resourceAttributes2.toMultimap().isEmpty()); + Assertions.assertTrue(resourceAttributes2.toMultimap().containsKey("Attributes")); + let resourceAttributes3: ResourceAttributes = resourceAttributes2.maybePlus("Attributes2", "Another First Value"); + Assertions.assertNotEquals(resourceAttributes2, resourceAttributes3); + Assertions.assertFalse(resourceAttributes3.toMultimap().isEmpty()); + Assertions.assertTrue(resourceAttributes3.toMultimap().containsKey("Attributes")); + Assertions.assertTrue(resourceAttributes3.toMultimap().containsKey("Attributes2")); + } + + // @Test, @DisplayName("Add pairs with MaybeSet"), @SneakyThrows + addPairsWithMaybeSet(): void { + let resourceAttributes: ResourceAttributes = new ResourceAttributes(); + Assertions.assertNotNull(resourceAttributes); + resourceAttributes.maybeSet(null, "value"); + Assertions.assertTrue(resourceAttributes.toMultimap().isEmpty()); + resourceAttributes.maybeSet("attribute", null); + Assertions.assertTrue(resourceAttributes.toMultimap().isEmpty()); + resourceAttributes.maybeSet(null, null); + Assertions.assertTrue(resourceAttributes.toMultimap().isEmpty()); + resourceAttributes.maybeSet("First Attribute", "First Attribute First Value"); + Assertions.assertEquals(resourceAttributes.firstValue("First Attribute"), Optional.of("First Attribute First Value")); + // Try to reset with the same attribute and value (to no change) + resourceAttributes.maybeSet("First Attribute", "First Attribute First Value"); + Assertions.assertEquals(1, resourceAttributes.toMultimap().size()); + Assertions.assertEquals(Optional.of("First Attribute First Value"), resourceAttributes.firstValue("First Attribute")); + // Add to the same attribute with a different value, growing the list size + resourceAttributes.maybeSet("First Attribute", "First Attribute Second Value"); + Assertions.assertEquals(1, resourceAttributes.toMultimap().size()); + Assertions.assertEquals(2, resourceAttributes.allValues("First Attribute").size()); + } +} diff --git a/asTypescript/packages/tests/src/SchemaCacheTests.ts b/asTypescript/packages/tests/src/SchemaCacheTests.ts new file mode 100644 index 00000000..c2e794ee --- /dev/null +++ b/asTypescript/packages/tests/src/SchemaCacheTests.ts @@ -0,0 +1,121 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { SchemaCache } from '@shapetrees/core/src/SchemaCache'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { HttpExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/HttpExternalDocumentLoader'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as GlobalFactory from 'fr/inria/lille/shexjava'; +import * as ShexSchema from 'fr/inria/lille/shexjava/schema'; +import * as ShExCParser from 'fr/inria/lille/shexjava/schema/parsing'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as MalformedURLException from 'java/net'; +import { toUrl } from './fixtures/MockWebServerHelper/toUrl'; +import * as assertFalse from 'org/junit/jupiter/api/Assertions'; +import * as assertTrue from 'org/junit/jupiter/api/Assertions'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class SchemaCacheTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + private static httpExternalDocumentLoader: HttpExternalDocumentLoader; + + public constructor() { + httpExternalDocumentLoader = new HttpExternalDocumentLoader(); + DocumentLoaderManager.setLoader(httpExternalDocumentLoader); + } + + // @BeforeAll + static beforeAll(): void /* throws ShapeTreeException */ { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("schemas/project-shex"), "GET", "/static/shex/project", null))); + SchemaCache.unInitializeCache(); + } + + // @Test, @Order(1) + testFailToOperateOnUninitializedCache(): void /* throws MalformedURLException, ShapeTreeException */ { + assertFalse(SchemaCache.isInitialized()); + // containsSchema + let containsException: Throwable = Assertions.assertThrows(ShapeTreeException.class, () -> SchemaCache.containsSchema(new URL("http://schema.example"))); + Assertions.assertEquals(SchemaCache.CACHE_IS_NOT_INITIALIZED, containsException.getMessage()); + // getSchema + let getException: Throwable = Assertions.assertThrows(ShapeTreeException.class, () -> SchemaCache.getSchema(new URL("http://schema.example"))); + Assertions.assertEquals(SchemaCache.CACHE_IS_NOT_INITIALIZED, getException.getMessage()); + // putSchema + let putException: Throwable = Assertions.assertThrows(ShapeTreeException.class, () -> SchemaCache.putSchema(new URL("http://schema.example"), null)); + Assertions.assertEquals(SchemaCache.CACHE_IS_NOT_INITIALIZED, putException.getMessage()); + // clearSchema + let clearException: Throwable = Assertions.assertThrows(ShapeTreeException.class, () -> SchemaCache.clearCache()); + Assertions.assertEquals(SchemaCache.CACHE_IS_NOT_INITIALIZED, clearException.getMessage()); + } + + // @Test, @Order(2) + testInitializeCache(): void /* throws MalformedURLException, ShapeTreeException */ { + SchemaCache.initializeCache(); + assertTrue(SchemaCache.isInitialized()); + assertFalse(SchemaCache.containsSchema(new URL("http://schema.example"))); + } + + // @Test, @Order(3) + testPreloadCache(): void /* throws MalformedURLException, ShapeTreeException */ { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let schemas: Map = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); + SchemaCache.initializeCache(schemas); + assertTrue(SchemaCache.containsSchema(toUrl(server, "/static/shex/project"))); + } + + // @Test, @Order(4) + testClearPutGet(): void /* throws MalformedURLException, ShapeTreeException */ { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + SchemaCache.clearCache(); + Assertions.assertNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); + let schemas: Map = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); + let firstEntry: Map.Entry = schemas.entrySet().stream().findFirst().orElse(null); + if (firstEntry === null) + return; + SchemaCache.putSchema(firstEntry.getKey(), firstEntry.getValue()); + Assertions.assertNotNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); + } + + // @Test, @Order(5) + testNullOnCacheContains(): void /* throws MalformedURLException, ShapeTreeException */ { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + SchemaCache.clearCache(); + Assertions.assertNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); + let schemas: Map = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); + let firstEntry: Map.Entry = schemas.entrySet().stream().findFirst().orElse(null); + if (firstEntry === null) + return; + SchemaCache.putSchema(firstEntry.getKey(), firstEntry.getValue()); + Assertions.assertNotNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); + } + + public static buildSchemaCache(schemasToCache: Array): Map /* throws MalformedURLException, ShapeTreeException */ { + let schemaCache: Map = new Map<>(); + log.info("Building schema cache"); + for (const schemaUrl of schemasToCache) { + log.debug("Caching schema {}", schemaUrl); + let shexShapeSchema: DocumentResponse = DocumentLoaderManager.getLoader().loadExternalDocument(new URL(schemaUrl)); + if (Boolean.FALSE === shexShapeSchema.isExists() || shexShapeSchema.getBody() === null) { + log.warn("Schema at {} doesn't exist or is empty", schemaUrl); + continue; + } + let shapeBody: string = shexShapeSchema.getBody(); + try (let stream: InputStream = new ByteArrayInputStream(shapeBody.getBytes())) { + let shexCParser: ShExCParser = new ShExCParser(); + let schema: ShexSchema = new ShexSchema(GlobalFactory.RDFFactory, shexCParser.getRules(stream), shexCParser.getStart()); + schemaCache.put(new URL(schemaUrl), schema); + } catch (ex) { + if (ex instanceof Exception) { + log.error("Error parsing schema {}", schemaUrl); + log.error("Exception:", ex); + } +} + } + return schemaCache; + } +} diff --git a/asTypescript/packages/tests/src/ShapeTreeContainsPriorityTests.ts b/asTypescript/packages/tests/src/ShapeTreeContainsPriorityTests.ts new file mode 100644 index 00000000..99bf67b7 --- /dev/null +++ b/asTypescript/packages/tests/src/ShapeTreeContainsPriorityTests.ts @@ -0,0 +1,67 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ShapeTreeFactory } from '@shapetrees/core/src/ShapeTreeFactory'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { HttpExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/HttpExternalDocumentLoader'; +import { ShapeTree } from '@shapetrees/core/src/ShapeTree'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as Assertions from 'org/junit/jupiter/api'; +import * as BeforeAll from 'org/junit/jupiter/api'; +import * as DisplayName from 'org/junit/jupiter/api'; +import * as Test from 'org/junit/jupiter/api'; +import { toUrl } from './fixtures/MockWebServerHelper/toUrl'; + +class ShapeTreeContainsPriorityTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + private static httpExternalDocumentLoader: HttpExternalDocumentLoader; + + public constructor() { + httpExternalDocumentLoader = new HttpExternalDocumentLoader(); + DocumentLoaderManager.setLoader(httpExternalDocumentLoader); + } + + // @BeforeAll + static beforeAll(): void { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("shapetrees/contains-priority-shapetree-ttl"), "GET", "/static/shapetrees/contains-priority/shapetree", null))); + } + + // @SneakyThrows, @Test, @DisplayName("Validate prioritized retrieval of all shape tree types") + testContainsPriorityOrderOfAllTreeTypes(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let containingShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/contains-priority/shapetree#ContainsAllTypesTree")); + // Ensure the ordered result is correct + let prioritizedContains: Array = containingShapeTree.getPrioritizedContains(); + Assertions.assertEquals(3, prioritizedContains.size()); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#LabelShapeTypeTree"), prioritizedContains.get(0)); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#LabelTypeTree"), prioritizedContains.get(1)); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#TypeOnlyTree"), prioritizedContains.get(2)); + } + + // @SneakyThrows, @Test, @DisplayName("Validate prioritized retrieval of shape trees with shape and resource type validation") + testContainsPriorityOrderOfShapeTypeTrees(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let containingShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/contains-priority/shapetree#ContainsShapeTypeTree")); + // Ensure the ordered result is correct + let prioritizedContains: Array = containingShapeTree.getPrioritizedContains(); + Assertions.assertEquals(2, prioritizedContains.size()); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#ShapeTypeTree"), prioritizedContains.get(0)); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#TypeOnlyTree"), prioritizedContains.get(1)); + } + + // @SneakyThrows, @Test, @DisplayName("Validate prioritized retrieval of shape tree trees with label and resource type validation") + testContainsPriorityOrderOfLabelTypeTrees(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let containingShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/contains-priority/shapetree#ContainsLabelTypeTree")); + // Ensure the ordered result is correct + let prioritizedContains: Array = containingShapeTree.getPrioritizedContains(); + Assertions.assertEquals(2, prioritizedContains.size()); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#LabelTypeTree"), prioritizedContains.get(0)); + Assertions.assertEquals(toUrl(server, "/static/shapetrees/contains-priority/shapetree#TypeOnlyTree"), prioritizedContains.get(1)); + } +} diff --git a/asTypescript/packages/tests/src/ShapeTreeManagerDeltaTests.ts b/asTypescript/packages/tests/src/ShapeTreeManagerDeltaTests.ts new file mode 100644 index 00000000..4e802036 --- /dev/null +++ b/asTypescript/packages/tests/src/ShapeTreeManagerDeltaTests.ts @@ -0,0 +1,215 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { ShapeTreeAssignment } from '@shapetrees/core/src/ShapeTreeAssignment'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { ShapeTreeManagerDelta } from '@shapetrees/core/src/ShapeTreeManagerDelta'; +import * as Label from 'jdk/jfr'; +import * as MalformedURLException from 'java/net'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ShapeTreeManagerDeltaTests { + + private static existingManager: ShapeTreeManager = null; + + private static updatedManager: ShapeTreeManager = null; + + private static assignmentOne: ShapeTreeAssignment = null; + + private static assignmentTwo: ShapeTreeAssignment = null; + + private static assignmentThree: ShapeTreeAssignment = null; + + private static assignmentFour: ShapeTreeAssignment = null; + + private static assignmentFive: ShapeTreeAssignment = null; + + // @BeforeEach + beforeEach(): void /* throws ShapeTreeException, MalformedURLException */ { + existingManager = new ShapeTreeManager(new URL("https://manager.example/#existing")); + updatedManager = new ShapeTreeManager(new URL("https://manager.example/#updated")); + assignmentOne = new ShapeTreeAssignment(// ShapeTree + new URL("http://shapetrees.example/#firstTree"), // ManageableResource + new URL("http://data.example/resourceOne"), // RootAssignment + new URL("http://data.example/resourceOne.shapetree#assignmentOne"), // FocusNode + new URL("http://data.example/resourceOne#focus"), // Shape + new URL("http://shapes.example/#firstShape"), // Uri + new URL("http://data.example/resourceOne.shapetree#assignmentOne")); + assignmentTwo = new ShapeTreeAssignment(// ShapeTree + new URL("http://shapetrees.example/#secondTree"), // ManageableResource + new URL("http://data.example/resourceTwo"), // RootAssignment + new URL("http://data.example/resourceTwo.shapetree#assignmentTwo"), // FocusNode + new URL("http://data.example/resourceTwo#focus"), // Shape + new URL("http://shapes.example/#secondShape"), // Uri + new URL("http://data.example/resourceTwo.shapetree#assignmentTwo")); + assignmentThree = new ShapeTreeAssignment(// ShapeTree + new URL("http://shapetrees.example/#thirdTree"), // ManageableResource + new URL("http://data.example/resourceThree"), // RootAssignment + new URL("http://data.example/resourceThree.shapetree#assignmentThree"), // FocusNode + new URL("http://data.example/resourceThree#focus"), // Shape + new URL("http://shapes.example/#thirdShape"), // Uri + new URL("http://data.example/resourceThree.shapetree#assignmentThree")); + assignmentFour = new ShapeTreeAssignment(// ShapeTree + new URL("http://shapetrees.example/#fourthTree"), // ManageableResource + new URL("http://data.example/resourceFour"), // RootAssignment + new URL("http://data.example/resourceFour.shapetree#assignmentFour"), // FocusNode + new URL("http://data.example/resourceFour#focus"), // Shape + new URL("http://shapes.example/#fourthShape"), // Uri + new URL("http://data.example/resourceFour.shapetree#assignmentFour")); + assignmentFive = new ShapeTreeAssignment(// ShapeTree + new URL("http://shapetrees.example/#fifthTree"), // ManageableResource + new URL("http://data.example/resourceFive"), // RootAssignment + new URL("http://data.example/resourceFive.shapetree#assignmentFive"), // FocusNode + new URL("http://data.example/resourceFive#focus"), // Shape + new URL("http://shapes.example/#fifthShape"), // Uri + new URL("http://data.example/resourceFive.shapetree#assignmentFive")); + } + + // @SneakyThrows, @Test, @Label("Delete all existing assignments") + deleteAllExistingAssignments(): void { + // Compare an existing manager with multiple assignments with an empty updated manager + // This should show that all assignments are removed with none left + existingManager.addAssignment(assignmentOne); + existingManager.addAssignment(assignmentTwo); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.getUpdatedAssignments().isEmpty()); + Assertions.assertEquals(2, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.allRemoved()); + Assertions.assertTrue(delta.getRemovedAssignments().contains(assignmentOne)); + Assertions.assertTrue(delta.getRemovedAssignments().contains(assignmentTwo)); + } + + // @SneakyThrows, @Test, @Label("Delete existing assignments and add new ones") + deleteAllExistingAssignmentsAndAddNew(): void { + existingManager.addAssignment(assignmentOne); + existingManager.addAssignment(assignmentTwo); + updatedManager.addAssignment(assignmentThree); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + Assertions.assertTrue(delta.wasReduced()); + Assertions.assertFalse(delta.allRemoved()); + Assertions.assertEquals(1, delta.getUpdatedAssignments().size()); + Assertions.assertEquals(2, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentThree)); + Assertions.assertTrue(delta.getRemovedAssignments().contains(assignmentOne)); + Assertions.assertTrue(delta.getRemovedAssignments().contains(assignmentTwo)); + } + + // @SneakyThrows, @Test, @Label("Delete an assignment, update another, and add one") + deleteUpdateAndAddAssignments(): void { + // remove assignment one + // update assignment two + // add assignment four + let assignmentThreeUpdated: ShapeTreeAssignment = duplicateAssignment(assignmentThree, new URL("http://shapetrees.pub/appleTree"), null); + existingManager.addAssignment(assignmentOne); + existingManager.addAssignment(assignmentTwo); + existingManager.addAssignment(assignmentThree); + updatedManager.addAssignment(assignmentTwo); + updatedManager.addAssignment(assignmentThreeUpdated); + updatedManager.addAssignment(assignmentFour); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + Assertions.assertTrue(delta.wasReduced()); + Assertions.assertFalse(delta.allRemoved()); + Assertions.assertEquals(2, delta.getUpdatedAssignments().size()); + Assertions.assertEquals(1, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentThreeUpdated)); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentFour)); + Assertions.assertTrue(delta.getRemovedAssignments().contains(assignmentOne)); + } + + // @SneakyThrows, @Test, @Label("Update assignment and add another") + updateAssignmentAndAddAnother(): void { + let assignmentThreeUpdated: ShapeTreeAssignment = duplicateAssignment(assignmentThree, new URL("http://shapetrees.pub/appleTree"), null); + existingManager.addAssignment(assignmentThree); + updatedManager.addAssignment(assignmentThreeUpdated); + updatedManager.addAssignment(assignmentFour); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + Assertions.assertFalse(delta.wasReduced()); + Assertions.assertFalse(delta.allRemoved()); + Assertions.assertEquals(2, delta.getUpdatedAssignments().size()); + Assertions.assertEquals(0, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentThreeUpdated)); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentFour)); + } + + // @SneakyThrows, @Test, @Label("Delete assignment and update another") + DeleteAssignmentAndUpdateAnother(): void { + let assignmentThreeUpdated: ShapeTreeAssignment = duplicateAssignment(assignmentThree, new URL("http://shapetrees.pub/appleTree"), null); + existingManager.addAssignment(assignmentTwo); + existingManager.addAssignment(assignmentThree); + updatedManager.addAssignment(assignmentThreeUpdated); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + Assertions.assertTrue(delta.wasReduced()); + Assertions.assertFalse(delta.allRemoved()); + Assertions.assertEquals(1, delta.getUpdatedAssignments().size()); + Assertions.assertEquals(1, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentThreeUpdated)); + Assertions.assertTrue(delta.getRemovedAssignments().contains(assignmentTwo)); + } + + // @SneakyThrows, @Test, @Label("Add a new assignments to an empty set") + AddNewAssignmentToEmptySet(): void { + updatedManager.addAssignment(assignmentOne); + updatedManager.addAssignment(assignmentTwo); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + Assertions.assertFalse(delta.wasReduced()); + Assertions.assertFalse(delta.allRemoved()); + Assertions.assertEquals(2, delta.getUpdatedAssignments().size()); + Assertions.assertEquals(0, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentOne)); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentTwo)); + } + + // @SneakyThrows, @Test, @Label("Update existing assignments") + UpdateExistingAssignment(): void { + let assignmentOneUpdated: ShapeTreeAssignment = duplicateAssignment(assignmentOne, null, new URL("http://data.example/resourceOne#Otherfocus")); + let assignmentTwoUpdated: ShapeTreeAssignment = duplicateAssignment(assignmentTwo, null, new URL("http://data.example/resourceTwo#Otherfocus")); + existingManager.addAssignment(assignmentOne); + existingManager.addAssignment(assignmentTwo); + updatedManager.addAssignment(assignmentOneUpdated); + updatedManager.addAssignment(assignmentTwoUpdated); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + Assertions.assertFalse(delta.wasReduced()); + Assertions.assertFalse(delta.allRemoved()); + Assertions.assertEquals(2, delta.getUpdatedAssignments().size()); + Assertions.assertEquals(0, delta.getRemovedAssignments().size()); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentOneUpdated)); + Assertions.assertTrue(delta.getUpdatedAssignments().contains(assignmentTwoUpdated)); + } + + // @Test, @Label("Compare two null managers") + compareTwoNullManagers(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeManagerDelta.evaluate(null, null)); + } + + // @SneakyThrows, @Test, @Label("Check null values on updated manager") + checkNullsOnUpdatedManager(): void { + existingManager.addAssignment(assignmentOne); + existingManager.addAssignment(assignmentTwo); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(existingManager, null); + Assertions.assertTrue(delta.allRemoved()); + updatedManager.getAssignments().clear(); + delta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.allRemoved()); + } + + // @SneakyThrows, @Test, @Label("Check null values on existing manager") + checkNullsOnExistingManager(): void { + updatedManager.addAssignment(assignmentOne); + updatedManager.addAssignment(assignmentTwo); + let delta: ShapeTreeManagerDelta = ShapeTreeManagerDelta.evaluate(null, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + existingManager.getAssignments().clear(); + delta = ShapeTreeManagerDelta.evaluate(existingManager, updatedManager); + Assertions.assertTrue(delta.isUpdated()); + } + + private duplicateAssignment(assignment: ShapeTreeAssignment, /*const*/ shapeTree: URL, /*const*/ focusNode: URL): ShapeTreeAssignment /* throws MalformedURLException, ShapeTreeException */ { + let duplicateAssignment: ShapeTreeAssignment = new ShapeTreeAssignment(shapeTree != null ? shapeTree : assignment.getShapeTree(), assignment.getManagedResource(), assignment.getRootAssignment(), focusNode != null ? focusNode : assignment.getFocusNode(), assignment.getShape(), assignment.getUrl()); + return duplicateAssignment; + } +} diff --git a/asTypescript/packages/tests/src/ShapeTreeManagerTests.ts b/asTypescript/packages/tests/src/ShapeTreeManagerTests.ts new file mode 100644 index 00000000..b14387a2 --- /dev/null +++ b/asTypescript/packages/tests/src/ShapeTreeManagerTests.ts @@ -0,0 +1,207 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ShapeTreeAssignment } from '@shapetrees/core/src/ShapeTreeAssignment'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { HttpExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/HttpExternalDocumentLoader'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { GraphHelper } from '@shapetrees/core/src/helpers/GraphHelper'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as Graph from 'org/apache/jena/graph'; +import * as MalformedURLException from 'java/net'; +import * as URI from 'java/net'; +import { toUrl } from './fixtures/MockWebServerHelper/toUrl'; + +class ShapeTreeManagerTests { + + private static managerUrl: URL; + + private static manager: ShapeTreeManager; + + private static server: MockWebServer; + + private static assignment1: ShapeTreeAssignmentprivate static assignment2: ShapeTreeAssignmentprivate static assignment3: ShapeTreeAssignmentprivate static nonContainingAssignment1: ShapeTreeAssignmentprivate static nonContainingAssignment2: ShapeTreeAssignmentprivate static containingAssignment1: ShapeTreeAssignment; + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + private static httpExternalDocumentLoader: HttpExternalDocumentLoader; + + public constructor() { + httpExternalDocumentLoader = new HttpExternalDocumentLoader(); + DocumentLoaderManager.setLoader(httpExternalDocumentLoader); + } + + // @BeforeAll + static beforeAll(): void /* throws MalformedURLException, ShapeTreeException */ { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("shapetrees/manager-shapetree-ttl"), "GET", "/static/shapetrees/managers/shapetree", null))); + server = new MockWebServer(); + server.setDispatcher(dispatcher); + managerUrl = new URL("https://site.example/resource.shapetree"); + assignment1 = new ShapeTreeAssignment(new URL("https://tree.example/tree#TreeOne"), new URL("https://site.example/resource"), new URL("https://site.example/resource.shapetree#ln1"), new URL("https://site.example/resource#node"), new URL("https://shapes.example/schema#ShapeOne"), new URL("https://site.example/resource.shapetree#ln1")); + assignment2 = new ShapeTreeAssignment(new URL("https://tree.example/tree#TreeTwo"), new URL("https://site.example/resource"), new URL("https://site.example/resource.shapetree#ln2"), new URL("https://site.example/resource#node"), new URL("https://shapes.example/schema#ShapeTwo"), new URL("https://site.example/resource.shapetree#ln2")); + assignment3 = new ShapeTreeAssignment(new URL("https://tree.example/tree#TreeThree"), new URL("https://site.example/resource"), new URL("https://site.example/resource.shapetree#ln3"), new URL("https://site.example/resource#node"), new URL("https://shapes.example/schema#ShapeThree"), new URL("https://site.example/resource.shapetree#ln3")); + nonContainingAssignment1 = new ShapeTreeAssignment(toUrl(server, "/static/shapetrees/managers/shapetree#NonContainingTree"), toUrl(server, "/data/container/"), toUrl(server, "/data/container/.shapetree#ln1"), null, null, toUrl(server, "/data/container/.shapetree#ln1")); + containingAssignment1 = new ShapeTreeAssignment(toUrl(server, "/static/shapetrees/managers/shapetree#ContainingTree"), toUrl(server, "/data/container/"), toUrl(server, "/data/container/.shapetree#ln2"), null, null, toUrl(server, "/data/container/.shapetree#ln2")); + nonContainingAssignment2 = new ShapeTreeAssignment(toUrl(server, "/static/shapetrees/managers/shapetree#NonContainingTree2"), toUrl(server, "/data/container/"), toUrl(server, "/data/container/.shapetree#ln3"), null, null, toUrl(server, "/data/container/.shapetree#ln3")); + } + + // @BeforeEach + beforeEach(): void { + manager = new ShapeTreeManager(managerUrl); + } + + // @SneakyThrows, @Test, @DisplayName("Initialize a new manager") + initializeShapeTreeManager(): void { + let newManager: ShapeTreeManager = new ShapeTreeManager(managerUrl); + Assertions.assertNotNull(newManager); + Assertions.assertEquals(newManager.getId(), managerUrl); + } + + // @SneakyThrows, @Test, @DisplayName("Add a new assignment") + addNewShapeTreeAssignmentToManager(): void { + Assertions.assertTrue(manager.getAssignments().isEmpty()); + manager.addAssignment(assignment1); + Assertions.assertFalse(manager.getAssignments().isEmpty()); + Assertions.assertEquals(manager.getAssignments().size(), 1); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to add a null assignment") + failToAddNullAssignmentToManager(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> { + manager.addAssignment(null); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to add a duplicate assignment") + failToAddDuplicateAssignment(): void { + manager.addAssignment(assignment1); + Assertions.assertThrows(ShapeTreeException.class, () -> { + manager.addAssignment(assignment1); + }); + } + + // @Test, @DisplayName("Fail to add assignment with certain null values") + failToAddAssignmentWithBadValues(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> { + new ShapeTreeAssignment(null, new URL("https://site.example/resource"), null, new URL("https://site.example/resource#node"), new URL("https://shapes.example/schema#ShapeThree"), new URL("https://site.example/resource.shapetree#ln3")); + }); + Assertions.assertThrows(ShapeTreeException.class, () -> { + // focus node with no shape + new ShapeTreeAssignment(new URL("https://tree.example/tree#TreeThree"), new URL("https://site.example/resource"), new URL("https://site.example/resource.shapetree#ln3"), new URL("https://site.example/resource#node"), null, new URL("https://site.example/resource.shapetree#ln3")); + }); + Assertions.assertThrows(ShapeTreeException.class, () -> { + // shape with no focus node + new ShapeTreeAssignment(new URL("https://tree.example/tree#TreeThree"), new URL("https://site.example/resource"), new URL("https://site.example/resource.shapetree#ln3"), null, new URL("https://shapes.example/schema#ShapeThree"), new URL("https://site.example/resource.shapetree#ln3")); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to mint the same assignment twice") + failToMintDuplicateAssignment(): void { + manager.addAssignment(assignment1); + let adjustedUrl: URL = manager.mintAssignmentUrl(assignment1.getUrl()); + Assertions.assertNotEquals(assignment1.getUrl(), adjustedUrl); + } + + // @SneakyThrows, @Test, @DisplayName("Get containing shape tree assignment from shape tree manager") + getContainingShapeTreeAssignmentsFromManager(): void { + manager.addAssignment(nonContainingAssignment1); + manager.addAssignment(containingAssignment1); + Assertions.assertEquals(1, manager.getContainingAssignments().size()); + Assertions.assertTrue(manager.getContainingAssignments().contains(containingAssignment1)); + Assertions.assertFalse(manager.getContainingAssignments().contains(nonContainingAssignment1)); + } + + // @SneakyThrows, @Test, @DisplayName("Get no containing shape tree assignment for shape tree manager") + getNoContainingShapeTreeAssignmentFromManager(): void { + manager.addAssignment(nonContainingAssignment1); + manager.addAssignment(nonContainingAssignment2); + Assertions.assertTrue(manager.getContainingAssignments().isEmpty()); + } + + // @SneakyThrows, @Test, @DisplayName("Get no shape tree assignment for shape tree from manager with no assignments") + getNoShapeTreeAssignmentsFromEmptyManager(): void { + Assertions.assertNull(manager.getAssignmentForShapeTree(new URL("https://tree.example/shapetree#ExampleTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Get shape tree assignment from manager for shape tree") + getShapeTreeAssignmentFromManagerForShapeTree(): void { + manager.addAssignment(nonContainingAssignment1); + manager.addAssignment(nonContainingAssignment2); + manager.addAssignment(containingAssignment1); + Assertions.assertEquals(containingAssignment1, manager.getAssignmentForShapeTree(containingAssignment1.getShapeTree())); + } + + // @SneakyThrows, @Test, @DisplayName("Get no shape tree assignment from manager without matching shape tree") + getNoShapeTreeAssignmentForShapeTree(): void { + manager.addAssignment(nonContainingAssignment1); + manager.addAssignment(nonContainingAssignment2); + manager.addAssignment(containingAssignment1); + Assertions.assertNull(manager.getAssignmentForShapeTree(new URL("https://tree.example/shapetree#ExampleTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to remove assignment from empty manager") + failToRemoveAssignmentFromEmptyManager(): void { + Assertions.assertThrows(IllegalStateException.class, () -> { + manager.removeAssignment(containingAssignment1); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to remove assignment from empty manager") + failToRemoveAssignmentMissingFromManager(): void { + manager.addAssignment(nonContainingAssignment1); + manager.addAssignment(nonContainingAssignment2); + Assertions.assertThrows(IllegalStateException.class, () -> { + manager.removeAssignment(containingAssignment1); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Remove assignment from manager") + removeAssignmentFromManager(): void { + manager.addAssignment(nonContainingAssignment1); + manager.addAssignment(nonContainingAssignment2); + manager.addAssignment(containingAssignment1); + Assertions.assertEquals(manager.getAssignmentForShapeTree(containingAssignment1.getShapeTree()), containingAssignment1); + manager.removeAssignment(containingAssignment1); + Assertions.assertNull(manager.getAssignmentForShapeTree(containingAssignment1.getShapeTree())); + } + + // @SneakyThrows, @Test, @DisplayName("Get valid assignment from graph") + getAssignmentFromGraph(): void { + let managerUri: URI = URI.create("https://data.example/container.shapetree"); + let managerGraph: Graph = GraphHelper.readStringIntoGraph(managerUri, getValidManagerString(), "text/turtle"); + let manager: ShapeTreeManager = ShapeTreeManager.getFromGraph(managerUri.toURL(), managerGraph); + Assertions.assertNotNull(manager); + Assertions.assertNotNull(manager.getAssignmentForShapeTree(new URL("https://tree.example/#Tree1"))); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to get assignment from graph due to missing triples") + failToGetAssignmentFromGraphMissingTriples(): void { + let managerUri: URI = URI.create("https://data.example/container.shapetree"); + let managerGraph: Graph = GraphHelper.readStringIntoGraph(managerUri, getInvalidManagerMissingTriplesString(), "text/turtle"); + Assertions.assertThrows(IllegalStateException.class, () -> { + ShapeTreeManager.getFromGraph(managerUrl, managerGraph); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to get assignment from graph due to unexpected values") + failToGetAssignmentFromGraphUnexpectedValues(): void { + let managerUri: URI = URI.create("https://data.example/container.shapetree"); + let managerGraph: Graph = GraphHelper.readStringIntoGraph(managerUri, getInvalidManagerUnexpectedTriplesString(), "text/turtle"); + Assertions.assertThrows(IllegalStateException.class, () -> { + ShapeTreeManager.getFromGraph(managerUrl, managerGraph); + }); + } + + private getValidManagerString(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX st: \n \n" + "PREFIX ex: \n" + "\n" + "\n" + " \n" + " a st:Manager ; \n" + " st:hasAssignment . \n" + "\n" + " \n" + " st:assigns ; \n" + " st:hasRootAssignment ; \n" + " st:manages ; \n" + " st:shape ; \n" + " st:focusNode . \n" + "\n"; + } + + private getInvalidManagerMissingTriplesString(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX st: \n \n" + "PREFIX ex: \n" + "\n" + "\n" + " \n" + " a st:Manager ; \n" + " st:hasAssignment . \n" + "\n" + " \n" + " st:assigns ; \n" + "\n"; + } + + private getInvalidManagerUnexpectedTriplesString(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX st: \n \n" + "PREFIX ex: \n" + "\n" + "\n" + " \n" + " a st:Manager ; \n" + " st:hasAssignment . \n" + "\n" + " \n" + " st:assigns ; \n" + " st:hasRootAssignment ; \n" + " st:manages ; \n" + " st:shape ; \n" + " st:focusNode ; \n" + " st:unexpected \"why am i here\" . \n" + "\n"; + } +} diff --git a/asTypescript/packages/tests/src/ShapeTreeParsingTests.ts b/asTypescript/packages/tests/src/ShapeTreeParsingTests.ts new file mode 100644 index 00000000..08a3181a --- /dev/null +++ b/asTypescript/packages/tests/src/ShapeTreeParsingTests.ts @@ -0,0 +1,230 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { ShapeTreeFactory } from '@shapetrees/core/src/ShapeTreeFactory'; +import { ShapeTreeResource } from '@shapetrees/core/src/ShapeTreeResource'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { HttpExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/HttpExternalDocumentLoader'; +import { RecursionMethods } from '@shapetrees/core/src/enums/RecursionMethods'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { ShapeTree } from '@shapetrees/core/src/ShapeTree'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import { toUrl } from './fixtures/MockWebServerHelper/toUrl'; + +class ShapeTreeParsingTests { + + private static httpExternalDocumentLoader: HttpExternalDocumentLoader; + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + protected static server: MockWebServer = null; + + public constructor() { + httpExternalDocumentLoader = new HttpExternalDocumentLoader(); + DocumentLoaderManager.setLoader(httpExternalDocumentLoader); + } + + // @BeforeEach, @SneakyThrows + beforeEach(): void { + ShapeTreeFactory.clearCache(); + ShapeTreeResource.clearCache(); + } + + // @BeforeAll + static beforeAll(): void { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("shapetrees/project-shapetree-ttl"), "GET", "/static/shapetrees/project/shapetree", null), new DispatcherEntry(List.of("shapetrees/business-shapetree-ttl"), "GET", "/static/shapetrees/business/shapetree", null), new DispatcherEntry(List.of("shapetrees/reserved-type-shapetree-ttl"), "GET", "/static/shapetrees/reserved/shapetree", null), new DispatcherEntry(List.of("shapetrees/project-shapetree-virtual-ttl"), "GET", "/static/shapetrees/project/shapetree-virtual", null), new DispatcherEntry(List.of("shapetrees/project-shapetree-invalid-ttl"), "GET", "/static/shapetrees/project/shapetree-invalid", null), new DispatcherEntry(List.of("shapetrees/project-shapetree-invalid-2-ttl"), "GET", "/static/shapetrees/project/shapetree-invalid2", null), new DispatcherEntry(List.of("shapetrees/content-type-invalid-shapetree-ttl"), "GET", "/static/shapetrees/project/shapetree-bad-content-type", null), new DispatcherEntry(List.of("shapetrees/missing-expects-type-shapetree-ttl"), "GET", "/static/shapetrees/invalid/missing-expects-type", null), new DispatcherEntry(List.of("shapetrees/contains-with-bad-expects-type-shapetree-ttl"), "GET", "/static/shapetrees/invalid/contains-with-bad-expects-type", null), new DispatcherEntry(List.of("shapetrees/bad-object-type-shapetree-ttl"), "GET", "/static/shapetrees/invalid/bad-object-type", null), new DispatcherEntry(List.of("shapetrees/invalid-contains-objects-shapetree-ttl"), "GET", "/static/shapetrees/invalid/shapetree-invalid-contains-objects", null), new DispatcherEntry(List.of("shapetrees/contains-with-nonrdf-expects-type-shapetree-ttl"), "GET", "/static/shapetrees/invalid/contains-with-nonrdf-expects-type", null), new DispatcherEntry(List.of("parsing/contains/contains-1-ttl"), "GET", "/static/shapetrees/parsing/contains-1", null), new DispatcherEntry(List.of("parsing/contains/contains-2-ttl"), "GET", "/static/shapetrees/parsing/contains-2", null), new DispatcherEntry(List.of("parsing/contains/contains-2A-ttl"), "GET", "/static/shapetrees/parsing/contains-2A", null), new DispatcherEntry(List.of("parsing/contains/contains-2B-ttl"), "GET", "/static/shapetrees/parsing/contains-2B", null), new DispatcherEntry(List.of("parsing/contains/contains-2C-ttl"), "GET", "/static/shapetrees/parsing/contains-2C", null), new DispatcherEntry(List.of("parsing/contains/contains-2C2-ttl"), "GET", "/static/shapetrees/parsing/contains-2C2", null), new DispatcherEntry(List.of("parsing/references/references-1-ttl"), "GET", "/static/shapetrees/parsing/references-1", null), new DispatcherEntry(List.of("parsing/references/references-2-ttl"), "GET", "/static/shapetrees/parsing/references-2", null), new DispatcherEntry(List.of("parsing/references/references-2A-ttl"), "GET", "/static/shapetrees/parsing/references-2A", null), new DispatcherEntry(List.of("parsing/references/references-2B-ttl"), "GET", "/static/shapetrees/parsing/references-2B", null), new DispatcherEntry(List.of("parsing/references/references-2C-ttl"), "GET", "/static/shapetrees/parsing/references-2C", null), new DispatcherEntry(List.of("parsing/references/references-2C2-ttl"), "GET", "/static/shapetrees/parsing/references-2C2", null), new DispatcherEntry(List.of("parsing/mixed/mixed-1-ttl"), "GET", "/static/shapetrees/parsing/mixed-1", null), new DispatcherEntry(List.of("parsing/mixed/mixed-2-ttl"), "GET", "/static/shapetrees/parsing/mixed-2", null), new DispatcherEntry(List.of("parsing/mixed/mixed-2A-ttl"), "GET", "/static/shapetrees/parsing/mixed-2A", null), new DispatcherEntry(List.of("parsing/mixed/mixed-2B-ttl"), "GET", "/static/shapetrees/parsing/mixed-2B", null), new DispatcherEntry(List.of("parsing/mixed/mixed-2C-ttl"), "GET", "/static/shapetrees/parsing/mixed-2C", null), new DispatcherEntry(List.of("parsing/mixed/mixed-2C2-ttl"), "GET", "/static/shapetrees/parsing/mixed-2C2", null), new DispatcherEntry(List.of("parsing/mixed/mixed-2D-ttl"), "GET", "/static/shapetrees/parsing/mixed-2D", null), new DispatcherEntry(List.of("parsing/cycle-ttl"), "GET", "/static/shapetrees/parsing/cycle", null), new DispatcherEntry(List.of("http/404"), "GET", "/static/shapetrees/invalid/shapetree-missing", null))); + server = new MockWebServer(); + server.setDispatcher(dispatcher); + } + + // @SneakyThrows, @Test, @DisplayName("Reuse previously cached shapetree") + parseShapeTreeReuse(): void { + let projectShapeTree1: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree#ProjectTree")); + Assertions.assertNotNull(projectShapeTree1); + let projectShapeTree2: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree#ProjectTree")); + Assertions.assertNotNull(projectShapeTree2); + assertEquals(projectShapeTree1.hashCode(), projectShapeTree2.hashCode()); + // The "business" shape tree won't be in the cache, but it cross-contains pm:MilestoneTree, which should be. + let businessShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/business/shapetree#BusinessTree")); + Assertions.assertNotNull(businessShapeTree); + } + + // @SneakyThrows, @Test, @DisplayName("Ensure reuse within recursion") + ensureCacheWithRecursion(): void { + // Retrieve the MilestoneTree shapetree (which is referred to by the ProjectTree shapetree) + let milestoneShapeTree1: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree-virtual#MilestoneTree")); + Assertions.assertNotNull(milestoneShapeTree1); + // Retrieve the ProjectTree shapetree which will recursively cache the MilestoneTree shapetree + let projectShapeTree1: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree-virtual#ProjectTree")); + Assertions.assertNotNull(projectShapeTree1); + // Retrieve the MilestoneTree shapetree again, ensuring the same instance is used + let milestoneShapeTree2: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree-virtual#MilestoneTree")); + Assertions.assertNotNull(milestoneShapeTree2); + assertEquals(milestoneShapeTree1.hashCode(), milestoneShapeTree2.hashCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Parse Tree with references") + parseShapeTreeReferences(): void { + let projectShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree-virtual#ProjectTree")); + Assertions.assertNotNull(projectShapeTree); + assertFalse(projectShapeTree.getReferences().isEmpty()); + } + + // @SneakyThrows, @Test, @DisplayName("Parse Tree with contains") + parseShapeTreeContains(): void { + let projectShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree#ProjectTree")); + Assertions.assertNotNull(projectShapeTree); + assertTrue(projectShapeTree.getContains().contains(toUrl(server, "/static/shapetrees/project/shapetree#MilestoneTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Parse Tree that allows reserved resource types") + parseShapeTreeContainsReservedTypes(): void { + let reservedShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/reserved/shapetree#EverythingTree")); + Assertions.assertNotNull(reservedShapeTree); + assertTrue(reservedShapeTree.getContains().contains(toUrl(server, "http://www.w3.org/ns/shapetrees#ResourceTree"))); + assertTrue(reservedShapeTree.getContains().contains(toUrl(server, "http://www.w3.org/ns/shapetrees#NonRDFResourceTree"))); + assertTrue(reservedShapeTree.getContains().contains(toUrl(server, "http://www.w3.org/ns/shapetrees#ContainerTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Traverse References") + testTraverseReferences(): void { + let projectShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree-virtual#ProjectTree")); + projectShapeTree.getReferencedShapeTrees(); + Assertions.assertTrue(projectShapeTree.getReferencedShapeTrees(RecursionMethods.BREADTH_FIRST).hasNext()); + Assertions.assertTrue(projectShapeTree.getReferencedShapeTrees(RecursionMethods.DEPTH_FIRST).hasNext()); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to parse shape tree with missing expectsType") + failToParseMissingExpectsType(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/invalid/missing-expects-type#DataRepositoryTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to parse shape tree with st:contains but expects a non-container resource") + failToParseBadExpectsTypeOnContains(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/invalid/contains-with-bad-expects-type#DataRepositoryTree"))); + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/invalid/contains-with-nonrdf-expects-type#DataRepositoryTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to parse shape tree with invalid object type") + failToParseBadObjectTypeOnContains(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/invalid/bad-object-type#DataRepositoryTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to parse missing shape tree") + failToParseMissingShapeTree(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/invalid/shapetree-missing#missing"))); + } + + // @Test, @DisplayName("Fail to parse shape tree with invalid content type") + failToParseShapeTreeWithInvalidContentType(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/project/shapetree-bad-content-type#bad"))); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to parse shape tree with invalid contains objects") + failToParseInvalidContainsObjects(): void { + Assertions.assertThrows(ShapeTreeException.class, () -> ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/invalid/shapetree-invalid-contains-objects#DataRepositoryTree"))); + } + + // @SneakyThrows, @Test, @DisplayName("Parse st:contains across multiple documents") + parseContainsAcrossMultipleDocuments(): void { + // Parse for recursive st:contains (use contains across multiple documents) + let containsShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/parsing/contains-1#1ATree")); + // Check the shape tree cache to ensure every contains shape tree was visited, parsed, and cached + Assertions.assertEquals(11, ShapeTreeFactory.getLocalShapeTreeCache().size()); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-1#1ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2#2ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2#2BTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2#2CTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2A#2A1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2A#2A2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2B#2B1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2C#2C1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2C#2C2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2C#2C3Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2C2#2C21Tree")); + // Check the resource cache to ensure every visited resource was cached + Assertions.assertEquals(6, ShapeTreeResource.getLocalResourceCache().size()); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-1")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2A")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2B")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2C2")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/contains-2C")); + } + + // @SneakyThrows, @Test, @DisplayName("Parse st:references across multiple documents") + parseReferencesAcrossMultipleDocuments(): void { + // Parse for recursive st:references (use references across multiple documents) + let referencesShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/parsing/references-1#1ATree")); + // Check the shape tree cache to ensure every referenced shape tree was visited, parsed, and cached + Assertions.assertEquals(11, ShapeTreeFactory.getLocalShapeTreeCache().size()); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-1#1ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2#2ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2#2BTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2#2CTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2A#2A1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2A#2A2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2B#2B1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2C#2C1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2C#2C2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2C#2C3Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2C2#2C21Tree")); + // Check the resource cache to ensure every visited resource was cached + Assertions.assertEquals(6, ShapeTreeResource.getLocalResourceCache().size()); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-1")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2A")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2B")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2C2")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/references-2C")); + } + + // @SneakyThrows, @Test, @DisplayName("Parse st:contains and st:references across multiple documents") + parseContainsAndReferencesAcrossMultipleDocuments(): void { + // Parse for mix of st:contains and references + let referencesShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/parsing/mixed-1#1ATree")); + // Check the shape tree cache to ensure every referenced shape tree was visited, parsed, and cached + Assertions.assertEquals(13, ShapeTreeFactory.getLocalShapeTreeCache().size()); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-1#1ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2BTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2CTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2DTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2A#2A1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2A#2A2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2B#2B1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C#2C1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C#2C2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C#2C3Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C2#2C21Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2D#2D1Tree")); + // Check the resource cache to ensure every visited resource was cached + Assertions.assertEquals(7, ShapeTreeResource.getLocalResourceCache().size()); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-1")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2A")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2B")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C2")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C")); + ShapeTreeResource.getLocalResourceCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2D")); + } + + // @SneakyThrows, @Test, @DisplayName("Parse shape tree hierarchy with circular reference") + parseWithCircularReference(): void { + // Ensure the parser correctly handles circular references + let circularShapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/parsing/cycle#1ATree")); + Assertions.assertEquals(12, ShapeTreeFactory.getLocalShapeTreeCache().size()); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-1#1ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2ATree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2BTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2CTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2#2DTree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2A#2A1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2A#2A2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2B#2B1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C#2C1Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C#2C2Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2C#2C3Tree")); + ShapeTreeFactory.getLocalShapeTreeCache().containsKey(toUrl(server, "/static/shapetrees/parsing/mixed-2D#2D1Tree")); + } +} diff --git a/asTypescript/packages/tests/src/ShapeTreeValidationTests.ts b/asTypescript/packages/tests/src/ShapeTreeValidationTests.ts new file mode 100644 index 00000000..0ea954e3 --- /dev/null +++ b/asTypescript/packages/tests/src/ShapeTreeValidationTests.ts @@ -0,0 +1,202 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests +import { SchemaCache } from '@shapetrees/core/src/SchemaCache'; +import { ShapeTree } from '@shapetrees/core/src/ShapeTree'; +import { ShapeTreeFactory } from '@shapetrees/core/src/ShapeTreeFactory'; +import { ValidationResult } from '@shapetrees/core/src/ValidationResult'; +import { DocumentLoaderManager } from '@shapetrees/core/src/contentloaders/DocumentLoaderManager'; +import { HttpExternalDocumentLoader } from '@shapetrees/core/src/contentloaders/HttpExternalDocumentLoader'; +import { ShapeTreeResourceType } from '@shapetrees/core/src/enums/ShapeTreeResourceType'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { GraphHelper } from '@shapetrees/core/src/helpers/GraphHelper'; +import { DispatcherEntry } from './fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from './fixtures/RequestMatchingFixtureDispatcher'; +import * as ShexSchema from 'fr/inria/lille/shexjava/schema'; +import * as Label from 'jdk/jfr'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as Graph from 'org/apache/jena/graph'; +import * as Model from 'org/apache/jena/rdf/model'; +import * as ModelFactory from 'org/apache/jena/rdf/model'; +import * as Lang from 'org/apache/jena/riot'; +import * as RDFDataMgr from 'org/apache/jena/riot'; +import * as Assertions from 'org/junit/jupiter/api'; +import * as BeforeAll from 'org/junit/jupiter/api'; +import * as Test from 'org/junit/jupiter/api'; +import { toUrl } from './fixtures/MockWebServerHelper/toUrl'; + +class ShapeTreeValidationTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + private static httpExternalDocumentLoader: HttpExternalDocumentLoader; + + public constructor() { + httpExternalDocumentLoader = new HttpExternalDocumentLoader(); + DocumentLoaderManager.setLoader(httpExternalDocumentLoader); + } + + // @BeforeAll + static beforeAll(): void { + dispatcher = new RequestMatchingFixtureDispatcher(List.of(new DispatcherEntry(List.of("shapetrees/validation-shapetree-ttl"), "GET", "/static/shapetrees/validation/shapetree", null), new DispatcherEntry(List.of("shapetrees/containment-shapetree-ttl"), "GET", "/static/shapetrees/containment/shapetree", null), new DispatcherEntry(List.of("validation/validation-container"), "GET", "/validation/", null), new DispatcherEntry(List.of("validation/valid-resource"), "GET", "/validation/valid-resource", null), new DispatcherEntry(List.of("validation/containment/container-1"), "GET", "/validation/container-1/", null), new DispatcherEntry(List.of("validation/containment/container-1-multiplecontains-manager"), "GET", "/validation/container-1/.shapetree", null), new DispatcherEntry(List.of("http/404"), "GET", "/static/shex/missing", null), new DispatcherEntry(List.of("http/404"), "GET", "/static/shapetrees/missing", null), new DispatcherEntry(List.of("schemas/validation-shex"), "GET", "/static/shex/validation", null), new DispatcherEntry(List.of("schemas/containment-shex"), "GET", "/static/shex/containment", null), new DispatcherEntry(List.of("schemas/invalid-shex"), "GET", "/static/shex/invalid", null))); + } + + // @SneakyThrows, @Test, @Label("Validate expectsType of Container") + validateExpectsContainerType(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#ExpectsContainerTree")); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.CONTAINER, null); + Assertions.assertTrue(result.isValid()); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, null); + Assertions.assertFalse(result.isValid()); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.NON_RDF, null); + Assertions.assertFalse(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Validate expectsType of Resource") + validateExpectsResourceType(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#ExpectsResourceTree")); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, null); + Assertions.assertTrue(result.isValid()); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.CONTAINER, null); + Assertions.assertFalse(result.isValid()); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.NON_RDF, null); + Assertions.assertFalse(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Validate expectsType of NonRDFResource") + validateExpectsNonRDFResourceType(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#ExpectsNonRDFResourceTree")); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.NON_RDF, null); + Assertions.assertTrue(result.isValid()); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, null); + Assertions.assertFalse(result.isValid()); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.CONTAINER, null); + Assertions.assertFalse(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Validate label") + validateLabel(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#LabelTree")); + result = shapeTree.validateResource("resource-name", null, ShapeTreeResourceType.RESOURCE, null); + Assertions.assertTrue(result.isValid()); + result = shapeTree.validateResource("invalid-name", null, ShapeTreeResourceType.RESOURCE, null); + Assertions.assertFalse(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Validate shape") + validateShape(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#FooTree")); + // Validate shape with focus node + let focusNodeUrls: Array = List.of(toUrl(server, "/validation/valid-resource#foo")); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); + Assertions.assertTrue(result.isValid()); + // Validate shape without focus node + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); + Assertions.assertTrue(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Fail to validate shape") + failToValidateShape(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#FooTree")); + // Pass in body content that will fail validation of the shape associated with FooTree + let focusNodeUrls: Array = List.of(toUrl(server, "/validation/valid-resource#foo")); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getInvalidFooBodyGraph(toUrl(server, "/validation/valid-resource"))); + Assertions.assertFalse(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Fail to validate shape when the shape resource cannot be found") + failToValidateMissingShape(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#MissingShapeSchemaTree")); + let fooBodyGraph: Graph = getFooBodyGraph(toUrl(server, "/validation/valid-resource")); + // Catch exception thrown when a shape in a shape tree cannot be found + let focusNodeUrls: Array = List.of(toUrl(server, "/validation/valid-resource#foo")); + Assertions.assertThrows(ShapeTreeException.class, () -> shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, fooBodyGraph)); + } + + // @SneakyThrows, @Test, @Label("Fail to validate shape when the shape resource is malformed") + failToValidateMalformedShape(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#InvalidShapeSchemaTree")); + let fooBodyGraph: Graph = getFooBodyGraph(toUrl(server, "/validation/valid-resource")); + // Catch exception thrown when a shape in a shape tree is invalid + let focusNodeUrls: Array = List.of(toUrl(server, "/validation/valid-resource#foo")); + Assertions.assertThrows(ShapeTreeException.class, () -> shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, fooBodyGraph)); + } + + // @SneakyThrows, @Test, @Label("Fail shape validation when shape tree doesn't validate a shape") + failToValidateWhenNoShapeInShapeTree(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Get the NoShapeValidationTree shape tree. This shape tree doesn't enforce shape validation, + // so it should return an error when using to validate + let noShapeValidationTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#NoShapeValidationTree")); + let graphTtl: string = "<#a> <#b> <#c> ."; + let focusNodeUrls: Array = List.of(toUrl(server, "http://a.example/b/c.d#a")); + let sr: StringReader = new StringReader(graphTtl); + let model: Model = ModelFactory.createDefaultModel(); + RDFDataMgr.read(model, sr, "http://example.com/", Lang.TTL); + Assertions.assertThrows(ShapeTreeException.class, () -> noShapeValidationTree.validateGraph(model.getGraph(), focusNodeUrls)); + } + + // @SneakyThrows, @Test, @Label("Validate shape before it is cached in schema cache") + validateShapeBeforeCaching(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + SchemaCache.initializeCache(); + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#FooTree")); + // Validate shape with focus node + let focusNodeUrls: Array = List.of(toUrl(server, "/validation/valid-resource#foo")); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); + Assertions.assertTrue(result.isValid()); + } + + // @SneakyThrows, @Test, @Label("Validate shape after it is cached in schema cache") + validateShapeAfterCaching(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let result: ValidationResult; + let schemas: Map = SchemaCacheTests.buildSchemaCache(List.of(toUrl(server, "/static/shex/validation").toString())); + SchemaCache.initializeCache(schemas); + let shapeTree: ShapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#FooTree")); + // Validate shape with focus node + let focusNodeUrls: Array = List.of(toUrl(server, "/validation/valid-resource#foo")); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); + Assertions.assertTrue(result.isValid()); + } + + private getFooBodyGraph(baseUrl: URL): Graph /* throws ShapeTreeException */ { + let body: string = "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "<#foo> \n" + " ex:id 56789 ; \n" + " ex:name \"Footastic\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n"; + return GraphHelper.readStringIntoGraph(GraphHelper.urlToUri(baseUrl), body, "text/turtle"); + } + + private getInvalidFooBodyGraph(baseUrl: URL): Graph /* throws ShapeTreeException */ { + let body: string = "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "<#foo> \n" + " ex:id 56789 ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n"; + return GraphHelper.readStringIntoGraph(GraphHelper.urlToUri(baseUrl), body, "text/turtle"); + } + + private getAttributeOneBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "<#resource> \n" + " ex:name \"Attribute 1\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n"; + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientDiscoverTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientDiscoverTests.ts new file mode 100644 index 00000000..e8d331e5 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientDiscoverTests.ts @@ -0,0 +1,97 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { ShapeTreeAssignment } from '@shapetrees/core/src/ShapeTreeAssignment'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { DispatcherEntry } from '../fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from '../fixtures/RequestMatchingFixtureDispatcher'; +import * as Label from 'jdk/jfr'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import { AbstractHttpClientTests } from './AbstractHttpClientTests'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class AbstractHttpClientDiscoverTests extends AbstractHttpClientTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + // Call AbstractHttpClient constructor + super(); + } + + // @BeforeAll + static beforeAll(): void { + let dispatcherList: Array = new Array(); + dispatcherList.add(new DispatcherEntry(List.of("discover/unmanaged"), "GET", "/unmanaged", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/unmanaged-manager"), "GET", "/unmanaged.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/managed"), "GET", "/managed", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/managed-manager"), "GET", "/managed.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/managed-invalid-1"), "GET", "/managed-invalid-1", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/managed-invalid-1-manager"), "GET", "/managed-invalid-1.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/managed-invalid-2"), "GET", "/managed-invalid-2", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/managed-invalid-2-manager"), "GET", "/managed-invalid-2.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("discover/no-manager"), "GET", "/no-manager", null)); + dispatcher = new RequestMatchingFixtureDispatcher(dispatcherList); + } + + // @Order(1), @SneakyThrows, @Test, @Label("Discover unmanaged resource") + discoverUnmanagedResource(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/unmanaged"); + // Use the discover operation to see if the root container is managed + let manager: ShapeTreeManager = this.shapeTreeClient.discoverShapeTree(this.context, targetResource).orElse(null); + // The root container isn't managed so check to ensure that a NULL value is returned + Assertions.assertNull(manager); + } + + // @Order(2), @SneakyThrows, @Test, @Label("Discover managed resource") + discoverManagedResource(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/managed"); + // Perform a discover on a resource that has a shape tree manager already planted + let manager: ShapeTreeManager = this.shapeTreeClient.discoverShapeTree(this.context, targetResource).orElse(null); + // Ensure that it was planted successfully + Assertions.assertNotNull(manager); + Assertions.assertEquals(1, manager.getAssignments().size()); + let assignment: ShapeTreeAssignment = manager.getAssignments().get(0); + Assertions.assertEquals(new URL("http://www.example.com/ns/ex#DataTree"), assignment.getShapeTree()); + Assertions.assertEquals(targetResource.toString(), assignment.getManagedResource().toString()); + Assertions.assertEquals(assignment.getUrl(), assignment.getRootAssignment()); + Assertions.assertEquals(toUrl(server, "/managed").toString() + "#set", assignment.getFocusNode().toString()); + Assertions.assertEquals("http://www.example.com/ns/ex#DataSetShape", assignment.getShape().toString()); + } + + // @Order(3), @SneakyThrows, @Test, @Label("Fail to discover managed resource with multiple managers") + failToDiscoverDueToMultipleManagers(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/managed-invalid-1"); + // If a manager resource has multiple shapetree managers it is considered invalid + Assertions.assertThrows(IllegalStateException.class, () -> { + let manager: ShapeTreeManager | null = this.shapeTreeClient.discoverShapeTree(this.context, targetResource); + }); + } + + // @Order(4), @SneakyThrows, @Test, @Label("Fail to discover managed resource with no managers") + failToDiscoverDueToNoManagers(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/managed-invalid-2"); + // If a manager resource exists, but has no managers it is considered invalid + Assertions.assertThrows(IllegalStateException.class, () -> { + let manager: ShapeTreeManager | null = this.shapeTreeClient.discoverShapeTree(this.context, targetResource); + }); + } + + // @Order(5), @SneakyThrows, @Test, @Label("Discover server doesn't support ShapeTrees") + failToDiscoverDueToNoManagerLink(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/no-manager"); + // If a manager resource exists, but has no managers it is considered invalid + Assertions.assertThrows(ShapeTreeException.class, () -> { + let manager: ShapeTreeManager | null = this.shapeTreeClient.discoverShapeTree(this.context, targetResource); + }); + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientInitializeTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientInitializeTests.ts new file mode 100644 index 00000000..579ac59e --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientInitializeTests.ts @@ -0,0 +1,27 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { HttpClient } from '@shapetrees/clienthttp/src/HttpClient'; +import { RequestMatchingFixtureDispatcher } from '../fixtures/RequestMatchingFixtureDispatcher'; +import { AbstractHttpClientTests } from './AbstractHttpClientTests'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class AbstractHttpClientInitializeTests extends AbstractHttpClientTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + // Call AbstractHttpClient constructor + super(); + } + + // @Test, @SneakyThrows + testNonValidatingHandler(): void { + let client: HttpClient = this.factory.get(false); + Assertions.assertNotNull(client); + } + + // @Test, @SneakyThrows + testValidatingHandler(): void { + let client: HttpClient = this.factory.get(true); + Assertions.assertNotNull(client); + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientProjectRecursiveTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientProjectRecursiveTests.ts new file mode 100644 index 00000000..c7bba1c6 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientProjectRecursiveTests.ts @@ -0,0 +1,72 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { DispatcherEntry } from '../fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from '../fixtures/RequestMatchingFixtureDispatcher'; +import * as Label from 'jdk/jfr'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import { AbstractHttpClientTests } from './AbstractHttpClientTests'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class AbstractHttpClientProjectRecursiveTests extends AbstractHttpClientTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + // Call AbstractHttpClient constructor + super(); + } + + // @BeforeAll + static beforeAll(): void { + let dispatcherList: Array = new Array(); + dispatcherList.add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/project-1-container"), "GET", "/data/projects/project-1/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/milestone-3-container"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/task-48-container"), "GET", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/task-6-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/task-6/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/issue-2"), "GET", "/data/projects/project-1/milestone-3/issue-2", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/issue-3"), "GET", "/data/projects/project-1/milestone-3/issue-3", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/attachment-48"), "GET", "/data/projects/project-1/milestone-3/task-48/attachment-48", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/random-png"), "GET", "/data/projects/project-1/milestone-3/task-48/random.png", null)); + dispatcherList.add(new DispatcherEntry(List.of("shapetrees/project-shapetree-ttl"), "GET", "/static/shapetrees/project/shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("schemas/project-shex"), "GET", "/static/shex/project/shex", null)); + dispatcher = new RequestMatchingFixtureDispatcher(dispatcherList); + } + + // @Order(1), @SneakyThrows, @Test, @Label("Recursively Plant Data Set") + plantDataRecursively(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#DataRepositoryTree"); + let focusNode: URL = toUrl(server, "/data/#repository"); + // Plant the data collection recursively on already existing hierarchy + let response: DocumentResponse = this.shapeTreeClient.plantShapeTree(this.context, targetResource, targetShapeTree, focusNode); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @Order(2), @SneakyThrows, @Test, @Label("Recursively Plant Projects Collection") + plantProjectsRecursively(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add planted data set + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager"), "GET", "/data/projects/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/attachment-48.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/random.png.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-6/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/issue-3.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/issue-2.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#ProjectCollectionTree"); + // Plant the projects collection recursively on already existing hierarchy + let response: DocumentResponse = this.shapeTreeClient.plantShapeTree(this.context, targetResource, targetShapeTree, null); + Assertions.assertEquals(201, response.getStatusCode()); + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientProjectTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientProjectTests.ts new file mode 100644 index 00000000..ffa8a398 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientProjectTests.ts @@ -0,0 +1,616 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { ShapeTreeManager } from '@shapetrees/core/src/ShapeTreeManager'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { DispatcherEntry } from '../fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from '../fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as MalformedURLException from 'java/net'; +import * as Arrays from 'java/util'; +import { AbstractHttpClientTests } from './AbstractHttpClientTests'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class AbstractHttpClientProjectTests extends AbstractHttpClientTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + // Call AbstractHttpClient constructor + super(); + } + + // @BeforeEach + initializeDispatcher(): void { + // For this set of tests, we reinitialize the dispatcher set for every test, because almost every test needs a + // slightly different context. Consequently, we could either modify the state from test to test (which felt a + // little dirty as we couldn't run tests standalone, or set the context for each test (which we're doing) + let dispatcherList: Array = new Array(); + dispatcherList.add(new DispatcherEntry(List.of("project/root-container"), "GET", "/", null)); + dispatcherList.add(new DispatcherEntry(List.of("project/root-container-manager"), "GET", "/.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("shapetrees/project-shapetree-ttl"), "GET", "/static/shapetrees/project/shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("shapetrees/information-shapetree-ttl"), "GET", "/static/shapetrees/information/shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("schemas/project-shex"), "GET", "/static/shex/project/shex", null)); + dispatcherList.add(new DispatcherEntry(List.of("schemas/information-shex"), "GET", "/static/shex/information/shex", null)); + dispatcher = new RequestMatchingFixtureDispatcher(dispatcherList); + } + + // @SneakyThrows, @Test, @DisplayName("Discover unmanaged root resource") + discoverUnmanagedRoot(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/"); + // Use the discover operation to see if the root container is managed + let manager: ShapeTreeManager = this.shapeTreeClient.discoverShapeTree(this.context, targetResource).orElse(null); + // The root container isn't managed so check to ensure that a NULL value is returned + Assertions.assertNull(manager); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to plant on a non-existent data container") + failPlantOnMissingDataContainer(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let targetResource: URL = toUrl(server, "/data/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#DataRepositoryTree"); + // Perform plant on /data container that doesn't exist yet (fails) + let response: DocumentResponse = this.shapeTreeClient.plantShapeTree(this.context, targetResource, targetShapeTree, null); + // Look for 404 because /data doesn't exist + Assertions.assertEquals(404, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Plant Data Repository") + plantDataRepository(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Create the data container + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-no-contains"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#DataRepositoryTree"); + let focusNode: URL = toUrl(server, "/data/#repository"); + // Plant the data repository on newly created data container + let response: DocumentResponse = this.shapeTreeClient.plantShapeTree(this.context, targetResource, targetShapeTree, focusNode); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to plant on missing shape tree") + failPlantOnMissingShapeTree(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Create the data container + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-no-contains"), "GET", "/data/", null)); + let targetResource: URL = toUrl(server, "/data/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/missing/shapetree#NonExistentTree"); + let focusNode: URL = toUrl(server, "/data/#repository"); + // Plant will fail and throw an exception when the shape tree to plant cannot be looked up + Assertions.assertThrows(ShapeTreeException.class, () -> { + let response: DocumentResponse = this.shapeTreeClient.plantShapeTree(this.context, targetResource, targetShapeTree, focusNode); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Create Projects Container and Validate DataCollectionTree and InformationSetTree") + createAndValidateProjectsWithMultipleContains(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Setup initial fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-multiplecontains-manager"), "GET", "/data/.shapetree", null)); + // Add fixture for /projects/ to handle the POST response + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-create-response"), "POST", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/.shapetree", null)); + let parentContainer: URL = toUrl(server, "/data/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/#collection")); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#DataCollectionTree"), toUrl(server, "/static/shapetrees/information/shapetree#InformationSetTree")); + // Create the projects container as a managed instance. + // 1. Will be validated by the parent DataRepositoryTree and the InformationSetTree both planted on /data (multiple contains) + // 2. Will have a manager/assignment created for it as an instance of DataCollectionTree and InformationSetTree + let response: DocumentResponse = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + // Another attempt without any target shape trees + response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, null, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + // Another attempt without any target focus nodes + response = shapeTreeClient.postManagedInstance(context, parentContainer, null, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + // Another attempt without any only one of two target shape trees + response = shapeTreeClient.postManagedInstance(context, parentContainer, null, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Projects Container and Validate DataCollectionTree") + createAndValidateProjects(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Setup initial fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixture for /projects/ to handle the POST response + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-create-response"), "POST", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/.shapetree", null)); + let parentContainer: URL = toUrl(server, "/data/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/#collection")); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#DataCollectionTree")); + // Create the projects container as a shape tree instance. + // 1. Will be validated by the parent DataRepositoryTree planted on /data + // 2. Will have a manager/assignment created for it as an instance of DataCollectionTree + let response: DocumentResponse = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Plant ProjectCollectionTree on Projects Container") + plantSecondShapeTreeOnProjects(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-no-contains"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager"), "GET", "/data/projects/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#ProjectCollectionTree"); + // Plant the second shape tree (ProjectCollectionTree) on /data/projects/ + let response: DocumentResponse = this.shapeTreeClient.plantShapeTree(this.context, targetResource, targetShapeTree, null); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Project in the Projects Collection") + createProjectInProjects(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-no-contains"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ to handle the POST response + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-create-response"), "POST", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/.shapetree", null)); + let parentContainer: URL = toUrl(server, "/data/projects/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/#project")); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#ProjectTree")); + // Create the project-1 container as a shape tree instance. + // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ + // 2. Will have a manager/assignment created for it as an instance of ProjectTree + let response: DocumentResponse = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, targetShapeTrees, "project-1", true, getProjectOneBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Update Project in the Projects Collection") + updateProjectInProjects(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for updated project-1 + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains-updated"), "PUT", "/data/projects/project-1/", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/#project")); + // Update the project-1 container as a shape tree instance. + // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ + // 2. Will have a manager/assignment created for it as an instance of ProjectTree + let response: DocumentResponse = shapeTreeClient.updateManagedInstance(context, targetResource, focusNodes, getProjectOneUpdatedBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(200, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to Create a Malformed Project in the Projects Collection") + failToCreateMalformedProject(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/#project")); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#ProjectTree")); + // Create the project-1 container as a shape tree instance via PUT + // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ + let response: DocumentResponse = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, getProjectOneMalformedBodyGraph(), TEXT_TURTLE, targetShapeTrees, true); + // 2. Will fail validation because the body content doesn't validate against the assigned shape + Assertions.assertEquals(422, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to Update a Project to be Malformed in the Projects Collection") + failToUpdateMalformedProject(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // try to update an existing project-1 to be malformed and fail validation + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/#project")); + // Update the project-1 container as a shape tree instance via PUT + // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ + let response: DocumentResponse = shapeTreeClient.updateManagedInstance(context, targetResource, focusNodes, getProjectOneMalformedBodyGraph(), TEXT_TURTLE); + // 2. Will fail validation because the body content doesn't validate against the assigned shape + Assertions.assertEquals(422, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Milestone in Project With Put") + createMilestoneInProjectWithPut(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3 to handle response to create via PUT + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "PUT", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/milestone-3/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/milestone-3/#milestone")); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#MilestoneTree")); + // Create the milestone-3 container in /projects/project-1/ as a shape tree instance using PUT to create + // 1. Will be validated by the parent ProjectTree planted on /data/projects/project-1/ + // 2. Will have a manager/assignment created for it as an instance of MilestoneTree + let response: DocumentResponse = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, getMilestoneThreeBodyGraph(), TEXT_TURTLE, targetShapeTrees, true); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Update Milestone in Project With Patch") + updateMilestoneInProjectWithPatch(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3 to handle response to update via PATCH + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains-updated"), "PATCH", "/data/projects/project-1/milestone-3/", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/milestone-3/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/milestone-3/#milestone")); + // Update the milestone-3 container in /projects/project-1/ using PATCH + // 1. Will be validated by the MilestoneTree planted on /data/projects/project-1/milestone-3/ + let response: DocumentResponse = shapeTreeClient.patchManagedInstance(context, targetResource, focusNodes, getMilestoneThreeSparqlPatch()); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create First Task in Project With Patch") + createFirstTaskInProjectWithPatch(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/task-6/ to handle response to update via PATCH + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-6-container-no-contains-updated"), "PATCH", "/data/projects/project-1/milestone-3/task-6/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-6/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/milestone-3/task-6/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/milestone-3/task-6/#task")); + // Create the task-6 container in /projects/project-1/milestone-3/ using PATCH + // 1. Will be validated by the parent MilestoneTree planted on /data/projects/project-1/milestone-3/ + let response: DocumentResponse = shapeTreeClient.patchManagedInstance(context, targetResource, focusNodes, getTaskSixSparqlPatch()); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Second Task in Project Without Focus Node") + createSecondTaskInProjectWithoutFocusNode(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/task-6/ to handle response to create via POST + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-create-response"), "POST", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + let targetContainer: URL = toUrl(server, "/data/projects/project-1/milestone-3/"); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#TaskTree")); + // create task-48 in milestone-3 - supply a target shape tree, but not a focus node + let response: DocumentResponse = shapeTreeClient.postManagedInstance(context, targetContainer, null, targetShapeTrees, "task-48", true, getTaskFortyEightBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Third Task in Project Without Target Shape Tree or Focus Node") + createThirdTaskInProjectWithoutAnyContext(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/task-6/ to handle response to create via POST + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-create-response"), "POST", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + let targetContainer: URL = toUrl(server, "/data/projects/project-1/milestone-3/"); + // create task-48 in milestone-3 - don't supply a target shape tree or focus node + let response: DocumentResponse = shapeTreeClient.postManagedInstance(context, targetContainer, null, null, "task-48", true, getTaskFortyEightBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Second Task in Project With Focus Node Without Target Shape Tree") + createSecondTaskInProjectWithFocusNodeWithoutTargetShapeTree(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/task-6/ to handle response to create via POST + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-create-response"), "POST", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + let targetContainer: URL = toUrl(server, "/data/projects/project-1/milestone-3/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/projects/project-1/milestone-3/task-48/#task")); + // create task-48 in milestone-3 - supply a focus node but no target shape tree + let response: DocumentResponse = shapeTreeClient.postManagedInstance(context, targetContainer, focusNodes, null, "task-48", true, getTaskFortyEightBodyGraph(), TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Attachment in Task") + createAttachmentInTask(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // create an attachment in task-48 (success) + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/task-6/ to handle response to create via POST + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-container-manager"), "GET", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + // Add fixture to handle PUT response and follow-up request + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/attachment-48"), "PUT", "/data/projects/project-1/milestone-3/task-48/attachment-48", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/attachment-48.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/milestone-3/task-48/attachment-48"); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#AttachmentTree")); + let response: DocumentResponse = shapeTreeClient.putManagedInstance(context, targetResource, null, null, "application/octet-stream", targetShapeTrees, false); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create Second Attachment in Task") + createSecondAttachmentInTask(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // create an attachment in task-48 (success) + // Add fixtures for /data/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-no-contains"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/task-6/ to handle response to create via POST + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-container-manager"), "GET", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + // Add fixture to handle PUT response and follow-up request + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/random-png"), "PUT", "/data/projects/project-1/milestone-3/task-48/random.png", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/projects/project-1/milestone-3/task-48/random.png.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/milestone-3/task-48/random.png"); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#AttachmentTree")); + let response: DocumentResponse = shapeTreeClient.putManagedInstance(context, targetResource, null, null, "application/octet-stream", targetShapeTrees, false); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to Unplant Non-Root Task") + failToUnplantNonRootTask(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixture for /data/projects/project-1/milestone-3/, which is not the root of the project hierarchy according to its manager + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/project-1/milestone-3/"); + let targetShapeTreeOne: URL = toUrl(server, "/static/shapetrees/project/shapetree#MilestoneTree"); + let targetShapeTreeTwo: URL = toUrl(server, "/static/shapetrees/project/shapetree#ProjectsTree"); + // Try first by providing the Milestone Shape Tree as the unplant target + let responseOne: DocumentResponse = shapeTreeClient.unplantShapeTree(context, targetResource, targetShapeTreeOne); + Assertions.assertEquals(500, responseOne.getStatusCode()); + // Try again by providing the (incorrect) Project Shape Tree as the unplant target (which is the shape tree at the root of the hierarchy) - this will be caught by the client immediately + Assertions.assertThrows(IllegalStateException.class, () -> { + let responseTwo: DocumentResponse = shapeTreeClient.unplantShapeTree(context, targetResource, targetShapeTreeTwo); + }); + } + + // @SneakyThrows, @Test, @DisplayName("Unplant Projects") + unplantProjects(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Unplant the project collection, recursing down the tree (success) + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/.shapetree", null)); + // Add fixture for /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container"), "GET", "/data/projects/project-1/milestone-3/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/milestone-3-container-manager"), "GET", "/data/projects/project-1/milestone-3/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/.shapetree", null)); + // Add fixtures for tasks in /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-6-container-no-contains"), "GET", "/data/projects/project-1/milestone-3/task-6/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-6-container-manager"), "GET", "/data/projects/project-1/milestone-3/task-6/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/task-6/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-container"), "GET", "/data/projects/project-1/milestone-3/task-48/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/task-48-container-manager"), "GET", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/task-48/.shapetree", null)); + // Add fixtures for attachments in task-48 + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/random-png"), "GET", "/data/projects/project-1/milestone-3/task-48/random.png", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/random-png-manager"), "GET", "/data/projects/project-1/milestone-3/task-48/random.png.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/task-48/random.png.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/attachment-48"), "GET", "/data/projects/project-1/milestone-3/task-48/attachment-48", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/attachment-48-manager"), "GET", "/data/projects/project-1/milestone-3/task-48/attachment-48.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/task-48/attachment-48.shapetree", null)); + // Add fixtures for issues in /projects/project-1/milestone-3/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/issue-2"), "GET", "/data/projects/project-1/milestone-3/issue-2", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/issue-2-manager"), "GET", "/data/projects/project-1/milestone-3/issue-2.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/issue-2.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/issue-3"), "GET", "/data/projects/project-1/milestone-3/issue-3", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/issue-3-manager"), "GET", "/data/projects/project-1/milestone-3/issue-3.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/projects/project-1/milestone-3/issue-3.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#ProjectCollectionTree"); + let response: DocumentResponse = shapeTreeClient.unplantShapeTree(context, targetResource, targetShapeTree); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Unplant Data Set") + unplantData(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Unplant the data collection, recursing down the tree (success). The root level (pre-loaded) and one level below projects included for completeness + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "DELETE", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager-two-assignments"), "GET", "/data/projects/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/204"), "PUT", "/data/projects/.shapetree", null)); + // Add fixture for /projects/project-1/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container"), "GET", "/data/projects/project-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/project-1-container-manager"), "GET", "/data/projects/project-1/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/"); + let targetShapeTree: URL = toUrl(server, "/static/shapetrees/project/shapetree#DataRepositoryTree"); + // Unplant the data collection, recursing down the tree (only two levels) + // Since the projects collection still manages /data/projects/, it should not delete the manager, only update it + let response: DocumentResponse = shapeTreeClient.unplantShapeTree(context, targetResource, targetShapeTree); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Plant Data Repository with Patch") + plantDataRepositoryWithPatch(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Create the data container + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-no-contains"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/.shapetree"); + // Plant the data repository on newly created data container + let response: DocumentResponse = shapeTreeClient.patchManagedInstance(context, targetResource, null, getPlantDataRepositorySparqlPatch(server)); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Update Project Collection manager with Patch") + updateProjectsManagerWithPatch(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixtures for data repository container and manager + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container"), "GET", "/data/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/data-container-manager"), "GET", "/data/.shapetree", null)); + // Add fixtures for /projects/ + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-no-contains"), "GET", "/data/projects/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("project/projects-container-manager"), "GET", "/data/projects/.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/projects/.shapetree"); + // Update the manager directly for the /data/projects/ with PATCH + let response: DocumentResponse = shapeTreeClient.patchManagedInstance(context, targetResource, null, getUpdateDataRepositorySparqlPatch(server)); + Assertions.assertEquals(201, response.getStatusCode()); + } + + private getProjectsBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#collection> \n" + " ex:uri ; \n" + " ex:id 32 ; \n" + " ex:name \"Projects Data Collection \" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n"; + } + + private getProjectOneBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#project> \n" + " ex:uri ; \n" + " ex:id 6 ; \n" + " ex:name \"Great Validations \" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:hasMilestone . "; + } + + private getProjectOneUpdatedBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#project> \n" + " ex:uri ; \n" + " ex:id 12 ; \n" + " ex:name \"Even Greater Validations For Everyone!\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:hasMilestone . "; + } + + private getProjectOneMalformedBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#project> \n" + " ex:uri ; \n" + " ex:name 5 ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:hasMilestone . "; + } + + private getMilestoneThreeBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#milestone> \n" + " ex:uri ; \n" + " ex:id 12345 ; \n" + " ex:name \"Milestone 3 of Project 1\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:target \"2021-06-05T20:15:47.000Z\"^^xsd:dateTime ; \n" + " ex:inProject . \n"; + } + + private getMilestoneThreeSparqlPatch(): string { + return "PREFIX ex: \n" + "DELETE { ?milestone ex:id 12345 } \n" + "INSERT { ?milestone ex:id 54321 } \n" + "WHERE { ?milestone ex:uri } \n"; + } + + private getTaskSixSparqlPatch(): string { + return "PREFIX ex: \n" + "PREFIX xsd: \n" + "INSERT DATA { \n" + " <#task> ex:uri . \n" + " <#task> ex:id 6 . \n" + " <#task> ex:name \"Somewhat urgent but not critical task\" . \n" + " <#task> ex:description \"Not particularly worried about this but it should get done\" . \n" + " <#task> ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n" + "} \n"; + } + + private getTaskFortyEightBodyGraph(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#task> \n" + " ex:uri ; \n" + " ex:id 2 ; \n" + " ex:name \"Some Development Task\" ; \n" + " ex:description \"Something extremely important that must be done!\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n"; + } + + private getPlantDataRepositorySparqlPatch(server: MockWebServer): string /* throws MalformedURLException */ { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX st: \n" + "PREFIX ex: \n" + "INSERT DATA { \n" + " <> a st:Manager . \n" + " <> st:hasAssignment <#ln1> . \n" + " <#ln1> st:assigns <" + toUrl(server, "/static/shapetrees/project/shapetree#DataRepositoryTree") + "> . \n" + " <#ln1> st:manages . \n" + " <#ln1> st:hasRootAssignment . \n" + " <#ln1> st:focusNode . \n" + " <#ln1> st:shape <" + toUrl(server, "/static/shex/project/shex#DataRepositoryShape") + "> . \n" + "} \n"; + } + + private getUpdateDataRepositorySparqlPatch(server: MockWebServer): string /* throws MalformedURLException */ { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX st: \n" + "PREFIX ex: \n" + "INSERT DATA { \n" + " <> a st:Manager . \n" + " <> st:hasAssignment <#ln2> . \n" + " <#ln2> st:assigns <" + toUrl(server, "/static/shapetrees/project/shapetree#ProjectCollectionTree") + "> . \n" + " <#ln2> st:manages . \n" + " <#ln2> st:hasRootAssignment . \n" + " <#ln2> st:focusNode . \n" + " <#ln2> st:shape <" + toUrl(server, "/static/shex/project/shex#ProjectCollectionShape") + "> . \n" + "} \n"; + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientResourceAccessorTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientResourceAccessorTests.ts new file mode 100644 index 00000000..90fbd265 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientResourceAccessorTests.ts @@ -0,0 +1,33 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { HttpClient } from '@shapetrees/clienthttp/src/HttpClient'; +import { HttpClientFactory } from '@shapetrees/clienthttp/src/HttpClientFactory'; +import { HttpResourceAccessor } from '@shapetrees/clienthttp/src/HttpResourceAccessor'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { AbstractResourceAccessorTests } from '../AbstractResourceAccessorTests'; +import * as MethodOrderer from 'org/junit/jupiter/api'; +import * as TestMethodOrder from 'org/junit/jupiter/api'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class AbstractHttpClientResourceAccessorTests extends AbstractResourceAccessorTests { + + protected httpResourceAccessor: HttpResourceAccessor = null; + + protected fetcher: HttpClient = null; + + protected factory: HttpClientFactory = null; + + public constructor() { + super(); + this.resourceAccessor = new HttpResourceAccessor(); + } + + protected skipShapeTreeValidation(b: boolean): void { + try { + this.fetcher = this.factory.get(!b); + } catch (e) { + if (e instanceof ShapeTreeException) { + throw new Error(e); + } +} + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientTests.ts new file mode 100644 index 00000000..aede4c54 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientTests.ts @@ -0,0 +1,40 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { HttpClient } from '@shapetrees/clienthttp/src/HttpClient'; +import { HttpClientFactory } from '@shapetrees/clienthttp/src/HttpClientFactory'; +import { HttpShapeTreeClient } from '@shapetrees/clienthttp/src/HttpShapeTreeClient'; +import { ShapeTreeException } from '@shapetrees/core/src/exceptions/ShapeTreeException'; +import { ShapeTreeContext } from '@shapetrees/core/src/ShapeTreeContext'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as MalformedURLException from 'java/net'; + +export abstract class AbstractHttpClientTests { + + protected factory: HttpClientFactory = null; + + protected shapeTreeClient: HttpShapeTreeClient = new HttpShapeTreeClient(); + + protected readonly context: ShapeTreeContext; + + protected fetcher: HttpClient; + + protected static TEXT_TURTLE: string = "text/turtle"; + + public constructor() { + this.context = new ShapeTreeContext(null); + } + + public toUrl(server: MockWebServer, path: string): URL /* throws MalformedURLException */ { + // TODO: duplicates com.janeirodigital.shapetrees.tests.fixtures.MockWebServerHelper.getURL; + return new URL(server.url(path).toString()); + } + + protected skipShapeTreeValidation(b: boolean): void { + try { + this.fetcher = this.factory.get(!b); + } catch (e) { + if (e instanceof ShapeTreeException) { + throw new Error(e); + } +} + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientTypeTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientTypeTests.ts new file mode 100644 index 00000000..a534a9b6 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientTypeTests.ts @@ -0,0 +1,127 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { DispatcherEntry } from '../fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from '../fixtures/RequestMatchingFixtureDispatcher'; +import * as Label from 'jdk/jfr'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as Arrays from 'java/util'; +import { AbstractHttpClientTests } from './AbstractHttpClientTests'; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +export class AbstractHttpClientTypeTests extends AbstractHttpClientTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + // Call AbstractHttpClientTests constructor + super(); + } + + // @BeforeEach + initializeDispatcher(): void { + let dispatcherList: Array = new Array(); + dispatcherList.add(new DispatcherEntry(List.of("type/containers-container"), "GET", "/containers/", null)); + dispatcherList.add(new DispatcherEntry(List.of("type/containers-container-manager"), "GET", "/containers/.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("type/resources-container"), "GET", "/resources/", null)); + dispatcherList.add(new DispatcherEntry(List.of("type/resources-container-manager"), "GET", "/resources/.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("type/non-rdf-resources-container"), "GET", "/non-rdf-resources/", null)); + dispatcherList.add(new DispatcherEntry(List.of("type/non-rdf-resources-container-manager"), "GET", "/non-rdf-resources/.shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("shapetrees/type-shapetree-ttl"), "GET", "/static/shapetrees/type/shapetree", null)); + dispatcher = new RequestMatchingFixtureDispatcher(dispatcherList); + } + + // @SneakyThrows, @Test, @Label("Create container when only containers are allowed") + createContainer(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixture to handle successful POST response + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("type/valid-container-create-response"), "POST", "/containers/valid-container/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/containers/valid-container/.shapetree", null)); + let response: DocumentResponse; + // Provide target shape tree + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "valid-container", true, null, TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + // Do not provide target shape tree + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, "valid-container", true, null, TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @Label("Fail to create resource when only containers are allowed") + failToCreateNonContainer(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let response: DocumentResponse; + // Provide target shape tree for a resource when container shape tree is expected + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + // Provide target shape tree for a container even though what's being sent is a resource + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "invalid-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + // Don't provide a target shape tree at all + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, "invalid-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @Label("Create resource when only resources are allowed") + createResource(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixture to handle successful POST response + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("type/valid-resource-create-response"), "POST", "/resources/valid-resource", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/resources/valid-resource.shapetree", null)); + let response: DocumentResponse; + // Provide target shape tree + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "valid-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + // Do not provide target shape tree + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, "valid-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @Label("Fail to create container when only resources are allowed") + failToCreateNonResource(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let response: DocumentResponse; + // Provide target shape tree for a container when resource shape tree is expected + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "invalid-container", true, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + // Provide target shape tree for a resource even though what's being sent is a container + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-container", true, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + // Don't provide a target shape tree at all + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, "invalid-container", true, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @Label("Create non-rdf resource when only non-rdf resources are allowed") + createNonRDFResource(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + // Add fixture to handle successful POST response + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("type/valid-non-rdf-resource-create-response"), "POST", "/non-rdf-resources/valid-non-rdf-resource", null)); + // TODO: Test: should this fail? should it have already failed? + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/non-rdf-resources/valid-non-rdf-resource.shapetree", null)); + let response: DocumentResponse; + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#NonRDFResourceTree")), "valid-non-rdf-resource", false, null, "application/octet-stream"); + Assertions.assertEquals(201, response.getStatusCode()); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, "valid-non-rdf-resource", false, null, "application/octet-stream"); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @Label("Fail to create resource when only non-rdf resources are allowed") + failToCreateNonRDFResource(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + let response: DocumentResponse; + // Provide target shape tree for a resource when non-rdf-resource shape tree is expected + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-non-rdf-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + // Provide target shape tree for a non-rdf-resource even though what's being sent is a resource + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#NonRDFResourceTree")), "invalid-non-rdf-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + // Don't provide a target shape tree at all + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, "invalid-non-rdf-resource", false, null, TEXT_TURTLE); + Assertions.assertEquals(422, response.getStatusCode()); + } +} diff --git a/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientValidationTests.ts b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientValidationTests.ts new file mode 100644 index 00000000..bd8c1994 --- /dev/null +++ b/asTypescript/packages/tests/src/clienthttp/AbstractHttpClientValidationTests.ts @@ -0,0 +1,142 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.clienthttp +import { DocumentResponse } from '@shapetrees/core/src/DocumentResponse'; +import { DispatcherEntry } from '../fixtures/DispatcherEntry'; +import { RequestMatchingFixtureDispatcher } from '../fixtures/RequestMatchingFixtureDispatcher'; +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as Assertions from 'org/junit/jupiter/api'; +import * as BeforeEach from 'org/junit/jupiter/api'; +import * as DisplayName from 'org/junit/jupiter/api'; +import * as Test from 'org/junit/jupiter/api'; +import * as Arrays from 'java/util'; +import { AbstractHttpClientTests } from './AbstractHttpClientTests'; + +export class AbstractHttpClientValidationTests extends AbstractHttpClientTests { + + private static dispatcher: RequestMatchingFixtureDispatcher = null; + + public constructor() { + // Call AbstractHttpClient constructor + super(); + } + + // @BeforeEach + initializeDispatcher(): void { + // For this set of tests, we reinitialize the dispatcher set for every test, because almost every test needs a + // slightly different context. Consequently, we could either modify the state from test to test (which felt a + // little dirty as we couldn't run tests standalone, or set the context for each test (which we're doing) + let dispatcherList: Array = new Array(); + dispatcherList.add(new DispatcherEntry(List.of("validation/container-1"), "GET", "/data/container-1/", null)); + dispatcherList.add(new DispatcherEntry(List.of("shapetrees/containment-shapetree-ttl"), "GET", "/static/shapetrees/validation/shapetree", null)); + dispatcherList.add(new DispatcherEntry(List.of("schemas/containment-shex"), "GET", "/static/shex/validation/shex", null)); + dispatcher = new RequestMatchingFixtureDispatcher(dispatcherList); + } + + // @SneakyThrows, @Test, @DisplayName("Create resource - two containing trees, two shapes, two nodes") + validateTwoContainsTwoShapesTwoNodes(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/container-1-twocontains-manager"), "GET", "/data/container-1/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/resource-1-create-response"), "POST", "/data/container-1/resource-1", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/resource-1.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/container-1/"); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree"), toUrl(server, "/static/shapetrees/validation/shapetree#ElementTree")); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource"), toUrl(server, "/data/container-1/resource-1#element")); + // Plant the data repository on newly created data container + let response: DocumentResponse = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Create resource - two containing trees of same tree") + validateTwoContainsSameContainingTree(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/container-1-samecontains-manager"), "GET", "/data/container-1/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/resource-1-create-response"), "POST", "/data/container-1/resource-1", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/resource-1.shapetree", null)); + // Validate multiple contains, same shape tree, same node + let targetResource: URL = toUrl(server, "/data/container-1/"); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#ChildTree")); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource")); + // Plant the data repository on newly created data container + let response: DocumentResponse = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(201, response.getStatusCode()); + } + + // @SneakyThrows, @Test, @DisplayName("Fail to create - two containing trees and focus node issues") + failToValidateTwoContainsWithBadFocusNodes(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/container-1-twocontains-manager"), "GET", "/data/container-1/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/resource-1-create-response"), "POST", "/data/container-1/resource-1", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/resource-1.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/container-1/"); + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree"), toUrl(server, "/static/shapetrees/validation/shapetree#ElementTree")); + // Only one matching target focus node is provided + let focusNodes: Array = Arrays.asList(toUrl(server, "/super/bad#node")); + let response: DocumentResponse = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + // Multiple non-matching target focus nodes are provided + focusNodes = Arrays.asList(toUrl(server, "/super/bad#node"), toUrl(server, "/data/container-1/resource-1#badnode"), toUrl(server, "/data/container-1/#badnode")); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + // Only one matching target focus node is provided when two are needed + focusNodes = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource")); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + } + + /* TODO - Cannot execute this test predicatably as constituted when passing focus nodes from client. Need to test closer to shape tree validation + @SneakyThrows + @Test + @DisplayName("Fail to validate created resource - two containing trees, target node unused") + void failToValidateTwoContainsTargetNodeUnused() { + MockWebServer server = new MockWebServer(); + server.setDispatcher(dispatcher); + + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/container-1-twocontains-onenode-manager"), "GET", "/data/container-1/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/resource-1-create-response"), "POST", "/data/container-1/resource-1", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/resource-1.shapetree", null)); + + URL targetResource = toUrl(server, "/data/container-1/"); + List targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree"), + toUrl(server, "/static/shapetrees/validation/shapetree#ElementTree")); + // Two target nodes are provided, but one of the nodes is matched twice, and the other isn't matched at all + List focusNodes = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource")); + + DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + + } + */ + // @SneakyThrows, @Test, @DisplayName("Fail to create - two containing trees, bad target shape trees") + failToValidateTwoContainsWithBadTargetShapeTrees(): void { + let server: MockWebServer = new MockWebServer(); + server.setDispatcher(dispatcher); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/container-1-twocontains-manager"), "GET", "/data/container-1/.shapetree", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("validation/resource-1-create-response"), "POST", "/data/container-1/resource-1", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/", null)); + dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/data/container-1/resource-1.shapetree", null)); + let targetResource: URL = toUrl(server, "/data/container-1/"); + let focusNodes: Array = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource"), toUrl(server, "/data/container-1/resource-1#element")); + // Only one matching target shape tree is provided + let targetShapeTrees: Array = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree")); + let response: DocumentResponse = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + // Multiple non-matching target focus nodes are provided + targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#OtherAttributeTree"), toUrl(server, "/static/shapetrees/validation/shapetree#OtherElementTree")); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + // One tree provided that isn't in either st:contains lists + targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree"), toUrl(server, "/static/shapetrees/validation/shapetree#StandaloneTree")); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + Assertions.assertEquals(422, response.getStatusCode()); + } + + private getResource1BodyString(): string { + return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + "PREFIX xsd: \n" + "PREFIX ex: \n" + "\n" + "\n" + "<#resource> \n" + " ex:name \"Some Development Task\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n" + "\n" + "<#element> \n" + " ex:name \"Some element\" ; \n" + " ex:description \"This is a description of an element\" ; \n" + " ex:created_at \"2021-04-04T20:15:47.000Z\"^^xsd:dateTime . \n"; + } +} diff --git a/asTypescript/packages/tests/src/fixtures/DispatcherEntry.ts b/asTypescript/packages/tests/src/fixtures/DispatcherEntry.ts new file mode 100644 index 00000000..056d2066 --- /dev/null +++ b/asTypescript/packages/tests/src/fixtures/DispatcherEntry.ts @@ -0,0 +1,38 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.fixtures +export class DispatcherEntry { + + private fixtureNames: Array; + + private expectedMethod: string; + + private expectedPath: string; + + private expectedHeaders: Map>; + + public toString(): string { + return "DispatcherEntry{" + fixtureNames + ":" + expectedMethod + '\'' + " " + expectedPath + '\'' + " " + expectedHeaders + "}"; + } + + public constructor(fixtureNames: Array, expectedMethod: string, expectedPath: string, expectedHeaders: Map>) { + this.fixtureNames = fixtureNames; + this.expectedMethod = expectedMethod; + this.expectedPath = expectedPath; + this.expectedHeaders = expectedHeaders; + } + + public getFixtureNames(): Array { + return this.fixtureNames; + } + + public getExpectedMethod(): string { + return this.expectedMethod; + } + + public getExpectedPath(): string { + return this.expectedPath; + } + + public getExpectedHeaders(): Map> { + return this.expectedHeaders; + } +} diff --git a/asTypescript/packages/tests/src/fixtures/Fixture.ts b/asTypescript/packages/tests/src/fixtures/Fixture.ts new file mode 100644 index 00000000..cd8a2dbc --- /dev/null +++ b/asTypescript/packages/tests/src/fixtures/Fixture.ts @@ -0,0 +1,102 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.fixtures +import * as MockResponse from 'okhttp3/mockwebserver'; +import * as RecordedRequest from 'okhttp3/mockwebserver'; +import * as StringSubstitutor from 'org/apache/commons/text'; +import * as TimeUnit from 'java/util/concurrent'; +import { YamlParser } from './YamlParser'; +import { Parser } from './Parser'; + +/** + * Originated from: https://github.com/orhanobut/mockwebserverplus (apache license) + * Key changes: + * - Did not support non-JSON body response + * - Added minor token replacement for server base url in fixture contents + * + * A value container that holds all information about the fixture file. + */ +export class Fixture { + + public statusCode: number; + + public body: string; + + public headers: Array; + + public delay: number; + + /** + * Parse the given filename and returns the Fixture object. + * + * @param fileName filename should not contain extension or relative path. ie: login + */ + public static parseFrom(fileName: string, request: RecordedRequest): Fixture { + return parseFrom(fileName, new YamlParser(), request); + } + + /** + * Parse the given filename and returns the Fixture object. + * + * @param fileName filename should not contain extension or relative path. ie: login + * @param parser parser is required for parsing operation, it should not be null + */ + public static parseFrom(fileName: string, parser: Parser, request: RecordedRequest): Fixture { + if (fileName === null) { + throw new NullPointerException("File name should not be null"); + } + let path: string = "fixtures/" + fileName + ".yaml"; + let variables: Map = new Map<>(); + variables.put("SERVER_BASE", getServerBaseFromRequest(request)); + let substitutor: StringSubstitutor = new StringSubstitutor(variables); + try { + return parser.parse(substitutor.replace(readPathIntoString(path))); + } catch (ex) { + if (ex instanceof IOException) { + throw new IllegalStateException("Test Harness: Error reading from " + path + ": " + ex.getStackTrace()); + } +} + } + + private static getServerBaseFromRequest(request: RecordedRequest): string { + return request.getRequestUrl().scheme() + "://" + request.getRequestUrl().host() + ":" + request.getRequestUrl().port(); + } + + private static openPathAsStream(path: string): InputStream { + let loader: ClassLoader = Thread.currentThread().getContextClassLoader(); + let inputStream: InputStream = loader.getResourceAsStream(path); + if (inputStream === null) { + throw new IllegalStateException("Test Harness: Invalid path: " + path); + } + return inputStream; + } + + private static readPathIntoString(path: string): string /* throws IOException */ { + let inputStream: InputStream = openPathAsStream(path); + let reader: BufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + let out: StringBuilder = new StringBuilder(); + let read: number; + while ((read = reader.read()) != -1) { + out.append((char) read); + } + reader.close(); + return out.toString(); + } + + public toMockResponse(): MockResponse { + let mockResponse: MockResponse = new MockResponse(); + if (this.statusCode != 0) { + mockResponse.setResponseCode(this.statusCode); + } + if (this.body != null) { + mockResponse.setBody(this.body); + } + if (this.delay != 0) { + mockResponse.setBodyDelay(this.delay, TimeUnit.MILLISECONDS); + } + if (this.headers != null) { + for (const header of this.headers) { + mockResponse.addHeader(header); + } + } + return mockResponse; + } +} diff --git a/asTypescript/packages/tests/src/fixtures/MockWebServerHelper.ts b/asTypescript/packages/tests/src/fixtures/MockWebServerHelper.ts new file mode 100644 index 00000000..b65bf877 --- /dev/null +++ b/asTypescript/packages/tests/src/fixtures/MockWebServerHelper.ts @@ -0,0 +1,10 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.fixtures +import * as MockWebServer from 'okhttp3/mockwebserver'; +import * as MalformedURLException from 'java/net'; + +export class MockWebServerHelper { + + public static toUrl(server: MockWebServer, path: string): URL /* throws MalformedURLException */ { + return new URL(server.url(path).toString()); + } +} diff --git a/asTypescript/packages/tests/src/fixtures/Parser.ts b/asTypescript/packages/tests/src/fixtures/Parser.ts new file mode 100644 index 00000000..0cb746f9 --- /dev/null +++ b/asTypescript/packages/tests/src/fixtures/Parser.ts @@ -0,0 +1,7 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.fixtures +import { Fixture } from './Fixture'; + +interface Parser { + + parse(string: string): Fixture; +} diff --git a/asTypescript/packages/tests/src/fixtures/RequestMatchingFixtureDispatcher.ts b/asTypescript/packages/tests/src/fixtures/RequestMatchingFixtureDispatcher.ts new file mode 100644 index 00000000..9a1053d6 --- /dev/null +++ b/asTypescript/packages/tests/src/fixtures/RequestMatchingFixtureDispatcher.ts @@ -0,0 +1,127 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.fixtures +import * as Dispatcher from 'okhttp3/mockwebserver'; +import * as MockResponse from 'okhttp3/mockwebserver'; +import * as RecordedRequest from 'okhttp3/mockwebserver'; +import * as NotNull from 'org/jetbrains/annotations'; +import { DispatcherEntry } from './DispatcherEntry'; + +export class RequestMatchingFixtureDispatcher extends Dispatcher { + + configuredFixtures: Array; + + private readonly fixtureHitCounts: Map = new Map<>(); + + public constructor(configuredFixtures: Array) { + this.configuredFixtures = configuredFixtures; + } + + // @NotNull + override public dispatch(@NotNull recordedRequest: RecordedRequest): MockResponse { + for (const entry of configuredFixtures) { + if (matchesRequest(recordedRequest, entry)) { + let fixtureName: string = getFixtureName(entry); + try { + let resp: MockResponse = Fixture.parseFrom(fixtureName, recordedRequest).toMockResponse(); + // status isn't a number, it's e.g. "HTTP/1.1 200 OK" + if (resp.getStatus().contains("200") && recordedRequest.getMethod() === "POST") { + const msg: string = "Mock: response to POST " + recordedRequest + " with " + entry + " returns " + resp.getStatus(); + log.error(msg); + // This will show up in a stack trace, + resp.setStatus("HTTP/1.1 999 " + msg); + // but we can add it as a body as well. + resp.setBody(msg); + } + return resp; + } catch (ex) { + if (ex instanceof Exception) { + let msg: string = ex.getMessage(); + let resp: MockResponse = new MockResponse(); + // log.error(msg); + ex.printStackTrace(); + // This will show up in a stack trace, + resp.setStatus("HTTP/1.1 999 " + msg); + // but we can add it as a body as well. + resp.setBody(msg); + } +} + } + } + log.error("Mock: no response found for {} {}", recordedRequest.getMethod(), recordedRequest.getPath()); + return new MockResponse().setResponseCode(404); + } + + public getFixtureByPath(expectedPath: string): DispatcherEntry { + for (const entry of this.configuredFixtures) { + if (entry.getExpectedPath() === expectedPath) { + return entry; + } + } + return null; + } + + public removeFixtureByPath(expectedPath: string): void { + let fixture: DispatcherEntry = getFixtureByPath(expectedPath); + if (fixture != null) { + this.configuredFixtures.remove(fixture); + } + } + + private getFixtureName(entry: DispatcherEntry): string { + let hits: number; + if (!fixtureHitCounts.containsKey(entry)) { + fixtureHitCounts.put(entry, 1); + hits = 1; + } else { + let existingHits: number = fixtureHitCounts.get(entry); + existingHits++; + fixtureHitCounts.replace(entry, existingHits); + hits = existingHits; + } + if (entry.getFixtureNames().size() === 1) { + return entry.getFixtureNames().get(0); + } else if (entry.getFixtureNames().size() > 1) { + let listIndex: number = hits - 1; + if (listIndex >= entry.getFixtureNames().size()) { + return entry.getFixtureNames().get(entry.getFixtureNames().size() - 1); + } + return entry.getFixtureNames().get(listIndex); + } else if (entry.getFixtureNames().size() < 1) { + return null; + } + return null; + } + + private matchesRequest(recordedRequest: RecordedRequest, configuredFixture: DispatcherEntry): boolean { + if (recordedRequest.getMethod() === null) + return false; + if (recordedRequest.getPath() === null) + return false; + if (!recordedRequest.getMethod() === configuredFixture.getExpectedMethod()) + return false; + if (!recordedRequest.getPath() === configuredFixture.getExpectedPath()) + return false; + if (configuredFixture.getExpectedHeaders() === null) + return true; + let recordedHeaders: Map> = recordedRequest.getHeaders().toMultimap(); + for (const expectedHeader of configuredFixture.getExpectedHeaders().entrySet()) { + let expectedHeaderName: string = expectedHeader.getKey(); + if (!recordedHeaders.containsKey(expectedHeaderName)) + return false; + if (expectedHeader.getValue() === null) + return true; + for (const expectedHeaderValue of expectedHeader.getValue()) { + if (!recordedHeaders.get(expectedHeaderName).contains(expectedHeaderValue)) + return false; + } + } + return true; + } + + public getConfiguredFixtures(): Array { + return this.configuredFixtures; + } + + public getFixtureHitCounts(): Map { + return this.fixtureHitCounts; + } +} diff --git a/asTypescript/packages/tests/src/fixtures/YamlParser.ts b/asTypescript/packages/tests/src/fixtures/YamlParser.ts new file mode 100644 index 00000000..e6e5a4e4 --- /dev/null +++ b/asTypescript/packages/tests/src/fixtures/YamlParser.ts @@ -0,0 +1,11 @@ +// Corresponding shapetrees-java package: com.janeirodigital.shapetrees.tests.fixtures +import * as Yaml from 'org/yaml/snakeyaml'; +import { Fixture } from './Fixture'; +import { Parser } from './Parser'; + +class YamlParser implements Parser { + + override public parse(string: string): Fixture { + return new Yaml().loadAs(string, Fixture.class); + } +} diff --git a/asTypescript/run-javatots.sh b/asTypescript/run-javatots.sh new file mode 100755 index 00000000..e438516f --- /dev/null +++ b/asTypescript/run-javatots.sh @@ -0,0 +1,5 @@ +JAR_REPO=/home/eric/.m2/repository/ + +java -Dfile.encoding=UTF-8 \ + -classpath ../../../ericprud/java-to-typescript/javatots/target/classes:$JAR_REPO/org/yaml/snakeyaml/1.29/snakeyaml-1.29.jar:$JAR_REPO/com/github/javaparser/javaparser-core/3.23.1/javaparser-core-3.23.1.jar \ + org.javatots.main.JavaToTypescript javatots-config.yaml diff --git a/shapetrees-java-client-core/src/main/java/com/janeirodigital/shapetrees/client/core/ShapeTreeClient.java b/shapetrees-java-client-core/src/main/java/com/janeirodigital/shapetrees/client/core/ShapeTreeClient.java index 711cb833..57b6ebd6 100644 --- a/shapetrees-java-client-core/src/main/java/com/janeirodigital/shapetrees/client/core/ShapeTreeClient.java +++ b/shapetrees-java-client-core/src/main/java/com/janeirodigital/shapetrees/client/core/ShapeTreeClient.java @@ -69,29 +69,29 @@ public interface ShapeTreeClient { * @param context ShapeTreeContext that would be used for authentication purposes * @param parentContainer The container the created resource should be created within * @param focusNodes One or more nodes/subjects to use as the focus for shape validation + * @param bodyString String representation of body of the created resource + * @param contentType Content type to parse the bodyString parameter as * @param targetShapeTrees One or more target shape trees the resource should be validated by * @param proposedName Proposed resource name (aka Slug) for the resulting resource * @param isContainer Specifies whether the newly created resource should be created as a container or not - * @param bodyString String representation of body of the created resource - * @param contentType Content type to parse the bodyString parameter as * @return DocumentResponse containing status and response headers/attributes * @throws ShapeTreeException ShapeTreeException */ - DocumentResponse postManagedInstance(ShapeTreeContext context, URL parentContainer, List focusNodes, List targetShapeTrees, String proposedName, Boolean isContainer, String bodyString, String contentType) throws ShapeTreeException; + DocumentResponse postManagedInstance(ShapeTreeContext context, URL parentContainer, List focusNodes, String bodyString, String contentType, List targetShapeTrees, String proposedName, Boolean isContainer) throws ShapeTreeException; /** * Creates a resource via HTTP PUT that has been validated against the provided target shape tree * @param context ShapeTreeContext that would be used for authentication purposes * @param targetResource The target resource to be created or updated * @param focusNodes One or more nodes/subjects to use as the focus for shape validation - * @param targetShapeTrees The shape trees that a proposed resource to be created should be validated against - * @param isContainer Specifies whether a newly created resource should be created as a container or not * @param bodyString String representation of the body of the resource to create or update * @param contentType Content type to parse the bodyString parameter as + * @param targetShapeTrees The shape trees that a proposed resource to be created should be validated against + * @param isContainer Specifies whether a newly created resource should be created as a container or not * @return DocumentResponse containing status and response header / attributes * @throws ShapeTreeException */ - DocumentResponse putManagedInstance(ShapeTreeContext context, URL targetResource, List focusNodes, List targetShapeTrees, Boolean isContainer, String bodyString, String contentType) throws ShapeTreeException; + DocumentResponse putManagedInstance(ShapeTreeContext context, URL targetResource, List focusNodes, String bodyString, String contentType, List targetShapeTrees, Boolean isContainer) throws ShapeTreeException; /** * Updates a resource via HTTP PUT that has been validated against an associated shape tree @@ -103,7 +103,7 @@ public interface ShapeTreeClient { * @return DocumentResponse containing status and response header / attributes * @throws ShapeTreeException */ - DocumentResponse putManagedInstance(ShapeTreeContext context, URL targetResource, List focusNodes, String bodyString, String contentType) throws ShapeTreeException; + DocumentResponse updateManagedInstance(ShapeTreeContext context, URL targetResource, List focusNodes, String bodyString, String contentType) throws ShapeTreeException; /** * Updates a resource via HTTP PATCH that has been validated against an associated shape tree diff --git a/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpResourceAccessor.java b/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpResourceAccessor.java index 7b13b3c2..224a4871 100644 --- a/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpResourceAccessor.java +++ b/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpResourceAccessor.java @@ -1,11 +1,22 @@ package com.janeirodigital.shapetrees.client.http; -import com.janeirodigital.shapetrees.core.*; +import com.janeirodigital.shapetrees.core.ShapeTreeManager; +import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableInstance; +import com.janeirodigital.shapetrees.core.ManageableResource; +import com.janeirodigital.shapetrees.core.DocumentResponse; +import com.janeirodigital.shapetrees.core.ResourceAttributes; +import com.janeirodigital.shapetrees.core.InstanceResource; +import com.janeirodigital.shapetrees.core.ResourceAccessor; +import com.janeirodigital.shapetrees.core.ManagerResource; +import com.janeirodigital.shapetrees.core.MissingManageableResource; +import com.janeirodigital.shapetrees.core.MissingManagerResource; +import com.janeirodigital.shapetrees.core.UnmanagedResource; +import com.janeirodigital.shapetrees.core.ManagedResource; import com.janeirodigital.shapetrees.core.enums.HttpHeaders; import com.janeirodigital.shapetrees.core.enums.LinkRelations; import com.janeirodigital.shapetrees.core.enums.ShapeTreeResourceType; import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; -import com.janeirodigital.shapetrees.core.ShapeTreeContext; import com.janeirodigital.shapetrees.core.vocabularies.LdpVocabulary; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,7 +27,11 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.*; +import java.util.Set; +import java.util.Optional; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; import static com.janeirodigital.shapetrees.core.helpers.GraphHelper.readStringIntoGraph; import static com.janeirodigital.shapetrees.core.helpers.GraphHelper.urlToUri; @@ -82,7 +97,7 @@ public class HttpResourceAccessor implements ResourceAccessor { private ManageableInstance getInstanceFromMissingManageableResource(ShapeTreeContext context, MissingManageableResource missing) { - MissingManagerResource missingManager = new MissingManagerResource(missing, null); + MissingManagerResource missingManager = new MissingManagerResource(missing.getUrl(), missing); return new ManageableInstance(context, this, false, missing, missingManager); } @@ -312,7 +327,9 @@ public class HttpResourceAccessor implements ResourceAccessor { generateResource(URL url, DocumentResponse response) throws ShapeTreeException { // If a resource was created, ensure the URL returned in the Location header is valid - Optional location = response.getResourceAttributes().firstValue(HttpHeaders.LOCATION.getValue()); + Optional location = response.getResourceAttributes() == null + ? Optional.empty() + : response.getResourceAttributes().firstValue(HttpHeaders.LOCATION.getValue()); if (location.isPresent()) { try { url = new URL(location.get()); @@ -326,13 +343,14 @@ public class HttpResourceAccessor implements ResourceAccessor { // typed resource with adequate context to the caller final boolean exists = response.isExists(); final boolean container = isContainerFromHeaders(response.getResourceAttributes(), url); - final ResourceAttributes attributes = response.getResourceAttributes(); + final ResourceAttributes attributes = response.getResourceAttributes(); // TODO: could be null final ShapeTreeResourceType resourceType = getResourceTypeFromHeaders(response.getResourceAttributes()); final String name = calculateName(url); - final String body = response.getBody(); + final String body = response.getBody(); // TODO: could be null. readStringIntoModel not set up for `rawContent=null` if (response.getBody() == null) { log.error("Could not retrieve the body string from response for " + url); + throw new IllegalStateException("Could not retrieve the body string from response for <" + url + ">"); // TODO: remove when TODO above is resolved. } // Parse Link headers from response and populate ResourceAttributes @@ -349,7 +367,7 @@ public class HttpResourceAccessor implements ResourceAccessor { if (exists) { return new ManagerResource(url, resourceType, attributes, body, name, true, managedResourceUrl); } else { - return new MissingManagerResource(url, resourceType, attributes, body, name, managedResourceUrl); + return new MissingManagerResource(managedResourceUrl, url, resourceType, attributes, body, name); } } else { // Look for presence of st:managedBy in link headers from response and get the target manager URL @@ -458,7 +476,9 @@ public class HttpResourceAccessor implements ResourceAccessor { private boolean isContainerFromHeaders(ResourceAttributes headers, URL url) { - List linkHeaders = headers.allValues(HttpHeaders.LINK.getValue()); + List linkHeaders = headers == null + ? Collections.emptyList() + : headers.allValues(HttpHeaders.LINK.getValue()); if (linkHeaders.isEmpty()) { return url.getPath().endsWith("/"); } @@ -480,7 +500,9 @@ public class HttpResourceAccessor implements ResourceAccessor { private ShapeTreeResourceType getResourceTypeFromHeaders(ResourceAttributes headers) { - List linkHeaders = headers.allValues(HttpHeaders.LINK.getValue()); + List linkHeaders = headers == null + ? null + : headers.allValues(HttpHeaders.LINK.getValue()); if (linkHeaders == null) { return null; } diff --git a/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpShapeTreeClient.java b/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpShapeTreeClient.java index f5ffcb5d..242bb04b 100644 --- a/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpShapeTreeClient.java +++ b/shapetrees-java-client-http/src/main/java/com/janeirodigital/shapetrees/client/http/HttpShapeTreeClient.java @@ -1,16 +1,28 @@ package com.janeirodigital.shapetrees.client.http; import com.janeirodigital.shapetrees.client.core.ShapeTreeClient; -import com.janeirodigital.shapetrees.core.*; +import com.janeirodigital.shapetrees.core.ShapeTreeManager; +import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableInstance; +import com.janeirodigital.shapetrees.core.ManageableResource; +import com.janeirodigital.shapetrees.core.DocumentResponse; +import com.janeirodigital.shapetrees.core.ShapeTree; +import com.janeirodigital.shapetrees.core.ShapeTreeFactory; +import com.janeirodigital.shapetrees.core.ShapeTreeAssignment; +import com.janeirodigital.shapetrees.core.ResourceAttributes; import com.janeirodigital.shapetrees.core.enums.HttpHeaders; import com.janeirodigital.shapetrees.core.enums.LinkRelations; import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import lombok.extern.slf4j.Slf4j; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFWriter; +import org.apache.jena.riot.RIOT; +import java.io.ByteArrayOutputStream; import java.io.StringWriter; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -109,13 +121,13 @@ public DocumentResponse plantShapeTree(ShapeTreeContext context, URL targetResou ManageableResource manageableResource = instance.getManageableResource(); if (!manageableResource.isExists()) { - return new DocumentResponse(null, "Cannot find target resource to plant: " + targetResource, 404); + return new DocumentResponse(new ResourceAttributes(), "Cannot find target resource to plant: " + targetResource, 404); } ShapeTreeManager manager; URL managerResourceUrl = instance.getManagerResource().getUrl(); if (instance.isManaged()) { - manager = instance.getManagerResource().getManager(); + manager = instance.getManagerResource().getManager(); // TODO: could be null } else { manager = new ShapeTreeManager(managerResourceUrl); } @@ -133,18 +145,26 @@ public DocumentResponse plantShapeTree(ShapeTreeContext context, URL targetResou manager.addAssignment(assignment); // Get an RDF version of the manager stored in a turtle string - StringWriter sw = new StringWriter(); - RDFDataMgr.write(sw, manager.getGraph(), Lang.TURTLE); + + ByteArrayOutputStream asBytes = new ByteArrayOutputStream(); + RDFWriter.create() + .base(targetShapeTree.toString()) + .set(RIOT.symTurtleOmitBase, false) + .set(RIOT.symTurtleDirectiveStyle, "rdf11") + .lang(Lang.TTL) + .source(manager.getGraph()) + .output(asBytes); + String asString = new String(asBytes.toByteArray(), StandardCharsets.UTF_8); // Build an HTTP PUT request with the manager graph in turtle as the content body + link header HttpClient fetcher = HttpClientFactoryManager.getFactory().get(this.useClientShapeTreeValidation); ResourceAttributes headers = new ResourceAttributes(); headers.maybeSet(HttpHeaders.AUTHORIZATION.getValue(), context.getAuthorizationHeaderValue()); - return fetcher.fetchShapeTreeResponse(new HttpRequest("PUT", managerResourceUrl, headers, sw.toString(), "text/turtle")); + return fetcher.fetchShapeTreeResponse(new HttpRequest("PUT", managerResourceUrl, headers, asString, "text/turtle")); } @Override - public DocumentResponse postManagedInstance(ShapeTreeContext context, URL parentContainer, List focusNodes, List targetShapeTrees, String proposedResourceName, Boolean isContainer, String bodyString, String contentType) throws ShapeTreeException { + public DocumentResponse postManagedInstance(ShapeTreeContext context, URL parentContainer, List focusNodes, String bodyString, String contentType, List targetShapeTrees, String proposedResourceName, Boolean isContainer) throws ShapeTreeException { if (context == null || parentContainer == null) { throw new ShapeTreeException(500, "Must provide a valid context and parent container to post shape tree instance"); @@ -162,7 +182,7 @@ public DocumentResponse postManagedInstance(ShapeTreeContext context, URL parent // Create via HTTP PUT @Override - public DocumentResponse putManagedInstance(ShapeTreeContext context, URL resourceUrl, List focusNodes, List targetShapeTrees, Boolean isContainer, String bodyString, String contentType) throws ShapeTreeException { + public DocumentResponse putManagedInstance(ShapeTreeContext context, URL resourceUrl, List focusNodes, String bodyString, String contentType, List targetShapeTrees, Boolean isContainer) throws ShapeTreeException { if (context == null || resourceUrl == null) { throw new ShapeTreeException(500, "Must provide a valid context and target resource to create shape tree instance via PUT"); @@ -179,7 +199,7 @@ public DocumentResponse putManagedInstance(ShapeTreeContext context, URL resourc // Update via HTTP PUT @Override - public DocumentResponse putManagedInstance(ShapeTreeContext context, URL resourceUrl, List focusNodes, String bodyString, String contentType) throws ShapeTreeException { + public DocumentResponse updateManagedInstance(ShapeTreeContext context, URL resourceUrl, List focusNodes, String bodyString, String contentType) throws ShapeTreeException { if (context == null || resourceUrl == null) { throw new ShapeTreeException(500, "Must provide a valid context and target resource to update shape tree instance via PUT"); @@ -248,7 +268,7 @@ public DocumentResponse unplantShapeTree(ShapeTreeContext context, URL targetRes } // Remove assignment from manager that corresponds with the provided shape tree - ShapeTreeManager manager = instance.getManagerResource().getManager(); + ShapeTreeManager manager = instance.getManagerResource().getManager(); // TODO: could be null manager.removeAssignmentForShapeTree(targetShapeTree); String method; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/DocumentResponse.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/DocumentResponse.java index dfc474bb..3c5c7010 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/DocumentResponse.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/DocumentResponse.java @@ -13,7 +13,9 @@ public class DocumentResponse { private final int statusCode; public Optional getContentType() { - return this.resourceAttributes.firstValue(HttpHeaders.CONTENT_TYPE.getValue()); + return this.resourceAttributes == null + ? Optional.empty() + : this.resourceAttributes.firstValue(HttpHeaders.CONTENT_TYPE.getValue()); } // TODO: lots of choices re non-404, not >= 4xx, not 3xx. not 201 (meaning there's no body) diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ManageableInstance.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ManageableInstance.java index caee906e..1c725729 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ManageableInstance.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ManageableInstance.java @@ -25,7 +25,7 @@ * is immutable. */ @Slf4j @Getter -public class ManageableInstance { +public class ManageableInstance { public static final String TEXT_TURTLE = "text/turtle"; private final ResourceAccessor resourceAccessor; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/MissingManagerResource.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/MissingManagerResource.java index a7bb0f87..3181ec26 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/MissingManagerResource.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/MissingManagerResource.java @@ -12,23 +12,23 @@ public class MissingManagerResource extends ManagerResource { /** * Construct a missing manager resource based on a MissingManageableResource + * @param managedResourceUrl Corresponding URL of the resource that would be managed * @param manageable Missing manageable resource - * @param managedUrl Corresponding URL of the resource that would be managed */ - public MissingManagerResource(MissingManageableResource manageable, URL managedUrl) { - super(manageable.getUrl(), manageable.getResourceType(), manageable.getAttributes(), manageable.getBody(), manageable.getName(), manageable.isExists(), managedUrl); + public MissingManagerResource(URL managedResourceUrl, MissingManageableResource manageable) { + super(manageable.getUrl(), manageable.getResourceType(), manageable.getAttributes(), manageable.getBody(), manageable.getName(), manageable.isExists(), managedResourceUrl); } /** * Construct a missing manager resource. + * @param managedResourceUrl URL of the resource that would be managed * @param url URL of the resource * @param resourceType Identified shape tree resource type * @param attributes Associated resource attributes * @param body Body of the resource * @param name Name of the resource - * @param managedResourceUrl URL of the resource that would be managed */ - public MissingManagerResource(URL url, ShapeTreeResourceType resourceType, ResourceAttributes attributes, String body, String name, URL managedResourceUrl) { + public MissingManagerResource(URL managedResourceUrl, URL url, ShapeTreeResourceType resourceType, ResourceAttributes attributes, String body, String name) { super(url, resourceType, attributes, body, name, false, managedResourceUrl); } diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ResourceAttributes.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ResourceAttributes.java index a208b6dc..f9a3d0d2 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ResourceAttributes.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ResourceAttributes.java @@ -2,7 +2,12 @@ import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import lombok.extern.slf4j.Slf4j; -import java.util.*; +import java.util.TreeMap; +import java.util.Optional; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -174,7 +179,7 @@ public List allValues(String name) { List values = toMultimap().get(name); // Making unmodifiable list out of empty in order to make a list which // throws UOE unconditionally - return values != null ? values : List.of(); + return values != null ? values : List.of(); // TODO: some callers, e.g. HttpResourceAccessor.getResourceTypeFromHeaders, expect null } public String toString() { diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTree.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTree.java index 88f669eb..38170266 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTree.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTree.java @@ -27,7 +27,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Iterator; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Queue; +import java.util.LinkedList; import static com.janeirodigital.shapetrees.core.helpers.GraphHelper.urlToUri; @@ -72,10 +77,10 @@ public ValidationResult validateResource(ManageableResource targetResource, List targetResource.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null)); } - return validateResource(targetResource.getName(), targetResource.getResourceType(), bodyGraph, focusNodeUrls); + return validateResource(targetResource.getName(), focusNodeUrls, targetResource.getResourceType(), bodyGraph); } - public ValidationResult validateResource(String requestedName, ShapeTreeResourceType resourceType, Graph bodyGraph, List focusNodeUrls) throws ShapeTreeException { + public ValidationResult validateResource(String requestedName, List focusNodeUrls, ShapeTreeResourceType resourceType, Graph bodyGraph) throws ShapeTreeException { // Check whether the proposed resource is the same type as what is expected by the shape tree if (!this.expectedResourceType.toString().equals(resourceType.getValue())) { @@ -117,7 +122,7 @@ public ValidationResult validateGraph(Graph graph, List focusNodeUrls) thro String shapeBody = shexShapeContents.getBody(); InputStream stream = new ByteArrayInputStream(shapeBody.getBytes(StandardCharsets.UTF_8)); - ShExCParser shexCParser = new ShExCParser(); + ShExCParser shexCParser = new ShExCParser(); // TODO: set base URL to this.shape try { schema = new ShexSchema(GlobalFactory.RDFFactory,shexCParser.getRules(stream),shexCParser.getStart()); if (SchemaCache.isInitialized()) { @@ -132,24 +137,24 @@ public ValidationResult validateGraph(Graph graph, List focusNodeUrls) thro JenaRDF jenaRDF = new org.apache.commons.rdf.jena.JenaRDF(); GlobalFactory.RDFFactory = jenaRDF; - ValidationAlgorithm validation = new RecursiveValidation(schema, jenaRDF.asGraph(graph)); + ValidationAlgorithm validator = new RecursiveValidation(schema, jenaRDF.asGraph(graph)); Label shapeLabel = new Label(GlobalFactory.RDFFactory.createIRI(this.shape.toString())); - if (!focusNodeUrls.isEmpty()) { // One or more focus nodes were provided for validation + if (!focusNodeUrls.isEmpty()) { // One or more focus nodes were provided for validator for (URL focusNodeUrl : focusNodeUrls) { // Evaluate each provided focus node IRI focusNode = GlobalFactory.RDFFactory.createIRI(focusNodeUrl.toString()); log.debug("Validating Shape Label = {}, Focus Node = {}", shapeLabel.toPrettyString(), focusNode.getIRIString()); - validation.validate(focusNode, shapeLabel); - boolean valid = validation.getTyping().isConformant(focusNode, shapeLabel); + validator.validate(focusNode, shapeLabel); + boolean valid = validator.getTyping().isConformant(focusNode, shapeLabel); if (valid) { return new ValidationResult(valid, this, this, focusNodeUrl); } } // None of the provided focus nodes were valid - this will return the last failure return new ValidationResult(false, this, "Failed to validate: " + shapeLabel.toPrettyString()); - } else { // No focus nodes were provided for validation, so all subject nodes will be evaluated + } else { // No focus nodes were provided for validator, so all subject nodes will be evaluated List evaluateNodes = GraphUtil.listSubjects(graph, Node.ANY, Node.ANY).toList(); @@ -157,17 +162,15 @@ public ValidationResult validateGraph(Graph graph, List focusNodeUrls) thro final String focusUriString = evaluateNode.getURI(); IRI node = GlobalFactory.RDFFactory.createIRI(focusUriString); - validation.validate(node, shapeLabel); - boolean valid = validation.getTyping().isConformant(node, shapeLabel); + validator.validate(node, shapeLabel); + boolean valid = validator.getTyping().isConformant(node, shapeLabel); if (valid) { - final URL matchingFocusNode; try { - matchingFocusNode = new URL(focusUriString); + return new ValidationResult(valid, this, this, new URL(focusUriString)); } catch (MalformedURLException ex) { - throw new ShapeTreeException(500, "Error reporting validation success on malformed URL <" + focusUriString + ">: " + ex.getMessage()); + throw new ShapeTreeException(500, "Error reporting validator success on malformed URL <" + focusUriString + ">: " + ex.getMessage()); } - return new ValidationResult(valid, this, this, matchingFocusNode); } } @@ -178,28 +181,25 @@ public ValidationResult validateGraph(Graph graph, List focusNodeUrls) thro } public ValidationResult validateContainedResource(ManageableResource containedResource) throws ShapeTreeException { - + // TODO: this same test gets performed in the call to the 2nd valdateContainedResource (after potentially parsing the graph) if (this.contains == null || this.contains.isEmpty()) { // TODO: say it can't be null? // The contained resource is permitted because this shape tree has no restrictions on what it contains return new ValidationResult(true, this, this, null); } - return validateContainedResource(containedResource, Collections.emptyList(), Collections.emptyList()); - - } - - public ValidationResult validateContainedResource(ManageableResource containedResource, List targetShapeTreeUrls, List focusNodeUrls) throws ShapeTreeException { - Graph containedResourceGraph = null; - if (containedResource.getResourceType() != ShapeTreeResourceType.NON_RDF) { + ShapeTreeResourceType resourceType = containedResource.getResourceType(); + if (resourceType != ShapeTreeResourceType.NON_RDF) { containedResourceGraph = GraphHelper.readStringIntoGraph(urlToUri(containedResource.getUrl()), containedResource.getBody(), containedResource.getAttributes().firstValue(HttpHeaders.CONTENT_TYPE.getValue()).orElse(null)); } - return validateContainedResource(containedResource.getName(), containedResource.getResourceType(), targetShapeTreeUrls, containedResourceGraph, focusNodeUrls); - + List targetShapeTreeUrls = Collections.emptyList(); + List focusNodeUrls = Collections.emptyList(); + String requestedName = containedResource.getName(); + return validateContainedResource(requestedName, resourceType, targetShapeTreeUrls, containedResourceGraph, focusNodeUrls); } public ValidationResult validateContainedResource(String requestedName, ShapeTreeResourceType resourceType, List targetShapeTreeUrls, Graph bodyGraph, List focusNodeUrls) throws ShapeTreeException { @@ -217,7 +217,7 @@ public ValidationResult validateContainedResource(String requestedName, ShapeTre if (this.contains.contains(targetShapeTreeUrl)) { ShapeTree targetShapeTree = ShapeTreeFactory.getShapeTree(targetShapeTreeUrl); // Evaluate the shape tree against the attributes of the proposed resources - ValidationResult result = targetShapeTree.validateResource(requestedName, resourceType, bodyGraph, focusNodeUrls); + ValidationResult result = targetShapeTree.validateResource(requestedName, focusNodeUrls, resourceType, bodyGraph); if (Boolean.TRUE.equals(result.getValid())) { // Return a successful validation result, including the matching shape tree return new ValidationResult(true, this, targetShapeTree, result.getMatchingFocusNode()); @@ -234,7 +234,7 @@ public ValidationResult validateContainedResource(String requestedName, ShapeTre if (containsShapeTree == null) { continue; } // Continue if the shape tree isn't gettable // Evaluate the shape tree against the attributes of the proposed resources - ValidationResult result = containsShapeTree.validateResource(requestedName, resourceType, bodyGraph, focusNodeUrls); + ValidationResult result = containsShapeTree.validateResource(requestedName, focusNodeUrls, resourceType, bodyGraph); // Continue if the proposed attributes were not a match if (Boolean.FALSE.equals(result.getValid())) { continue; } // Return the successful validation result @@ -246,14 +246,6 @@ public ValidationResult validateContainedResource(String requestedName, ShapeTre } - public Iterator getReferencedShapeTrees() throws ShapeTreeException { - return getReferencedShapeTrees(RecursionMethods.DEPTH_FIRST); - } - - public Iterator getReferencedShapeTrees(RecursionMethods recursionMethods) throws ShapeTreeException { - return getReferencedShapeTreesList(recursionMethods).iterator(); - } - // Return the list of shape tree contains by priority from most to least strict public List getPrioritizedContains() { @@ -263,6 +255,14 @@ public List getPrioritizedContains() { } + public Iterator getReferencedShapeTrees() throws ShapeTreeException { + return getReferencedShapeTrees(RecursionMethods.DEPTH_FIRST); + } + + public Iterator getReferencedShapeTrees(RecursionMethods recursionMethods) throws ShapeTreeException { + return getReferencedShapeTreesList(recursionMethods).iterator(); + } + private List getReferencedShapeTreesList(RecursionMethods recursionMethods) throws ShapeTreeException { if (recursionMethods.equals(RecursionMethods.BREADTH_FIRST)) { return getReferencedShapeTreesListBreadthFirst(); diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeAssignment.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeAssignment.java index f1aec018..b00e9427 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeAssignment.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeAssignment.java @@ -107,4 +107,15 @@ public boolean isRootAssignment() { return this.getUrl().equals(this.getRootAssignment()); } + @Override + public String toString() { + return "ShapeTreeAssignment{" + + "shapeTree=" + shapeTree + + ", managedResource=" + managedResource + + ", rootAssignment=" + rootAssignment + + ", focusNode=" + focusNode + + ", shape=" + shape + + ", url=" + url + + '}'; + } } diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeFactory.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeFactory.java index 0bacc6c0..6f2bbcb0 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeFactory.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeFactory.java @@ -6,7 +6,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.jena.graph.Node; import org.apache.jena.graph.Node_URI; -import org.apache.jena.rdf.model.*; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; import java.net.MalformedURLException; import java.net.URI; @@ -117,27 +121,20 @@ private ShapeTreeFactory() { } ArrayList references = new ArrayList<>(); Property referencesProperty = resourceModel.createProperty(ShapeTreeVocabulary.REFERENCES); - if (shapeTreeNode.hasProperty(referencesProperty)) { + if (shapeTreeNode.hasProperty(referencesProperty)) { // TODO: arbitrarily pics from n objects where 1 expected List referenceStatements = shapeTreeNode.listProperties(referencesProperty).toList(); for (Statement referenceStatement : referenceStatements) { Resource referenceResource = referenceStatement.getObject().asResource(); - final String referencedShapeTreeUrlString = getStringValue(resourceModel, referenceResource, ShapeTreeVocabulary.REFERENCES_SHAPE_TREE); - final URL referencedShapeTreeUrl; - ShapeTreeReference referencedShapeTree; - - try { - referencedShapeTreeUrl = new URL(referencedShapeTreeUrlString); - } catch (MalformedURLException ex) { - throw new ShapeTreeException(500, "ShapeTree <" + shapeTreeUrl + "> references malformed URL <" + referencedShapeTreeUrlString + ">: " + ex.getMessage()); + final URL referencedShapeTreeUrl = getUrlValue(resourceModel, referenceResource, ShapeTreeVocabulary.REFERENCES_SHAPE_TREE, shapeTreeUrl); + if (referencedShapeTreeUrl == null) { + throw new ShapeTreeException(400, "expected <" + shapeTreeUrl + "> reference " + referenceResource.toString() + " to have one <" + ShapeTreeVocabulary.REFERENCES_SHAPE_TREE + "> property"); } String viaShapePath = getStringValue(resourceModel, referenceResource, ShapeTreeVocabulary.VIA_SHAPE_PATH); URL viaPredicate = getUrlValue(resourceModel, referenceResource, ShapeTreeVocabulary.VIA_PREDICATE, shapeTreeUrl); - referencedShapeTree = new ShapeTreeReference(referencedShapeTreeUrl, viaShapePath, viaPredicate); - - references.add(referencedShapeTree); + references.add(new ShapeTreeReference(referencedShapeTreeUrl, viaShapePath, viaPredicate)); } } return references; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManager.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManager.java index 6a8a6760..3cc27420 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManager.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManager.java @@ -34,7 +34,7 @@ public class ShapeTreeManager { private final URL id; // Each ShapeTreeManager has one or more ShapeTreeAssignments - private final List assignments = new ArrayList<>(); + private final List assignments = new ArrayList<>(); // TODO: try Map, makes getContainingAssignments() redundant against getAssignments() /** * Constructor for a new ShapeTreeManager @@ -45,6 +45,14 @@ public class ShapeTreeManager { this.id = id; } + @Override + public String toString() { + return "ShapeTreeManager{" + + "id=" + id + + ", assignments=" + assignments + + '}'; + } + /** * Get the URL (identifier) of the ShapeTreeManager * @return URL identifier of the ShapeTreeManager @@ -105,10 +113,8 @@ public class ShapeTreeManager { } if (!this.assignments.isEmpty()) { - for (ShapeTreeAssignment existingAssignment : this.assignments) { - if (existingAssignment.equals(assignment)) { - throw new ShapeTreeException(422, "Identical shape tree assignment cannot be added to Shape Tree Manager: " + this.id); - } + if (this.assignments.contains(assignment)) { + throw new ShapeTreeException(422, "Identical shape tree assignment cannot be added to Shape Tree Manager: " + this.id); } } @@ -185,7 +191,7 @@ public static ShapeTreeManager getFromGraph(URL id, Graph managerGraph) throws S } else if (managerTriples.isEmpty()) { // Given the fact that a manager resource exists, there should never be a case where the manager resource // exists but no manager is found inside of it. - throw new IllegalStateException("No ShapeTreeManager instances found: " + managerTriples.size()); + throw new IllegalStateException("No ShapeTreeManager instances found: " + managerTriples.size()); // TODO: isn't that always 0? } // Get the URL of the ShapeTreeManager subject node @@ -214,7 +220,7 @@ public static ShapeTreeManager getFromGraph(URL id, Graph managerGraph) throws S } - public ShapeTreeAssignment getAssignmentForShapeTree(URL shapeTreeUrl) { + public ShapeTreeAssignment getAssignmentForShapeTree(URL shapeTreeUrl) { // TODO: return list of assignments with same ST but different roots if (this.assignments.isEmpty()) { return null; } diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManagerDelta.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManagerDelta.java index fed8779f..7102bf7d 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManagerDelta.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeManagerDelta.java @@ -40,7 +40,7 @@ public static ShapeTreeManagerDelta evaluate(ShapeTreeManager existingManager, S if (updatedManager == null || updatedManager.getAssignments().isEmpty()) { // All assignments have been removed in the updated manager, so any existing assignments should // similarly be removed. No need for further comparison. - delta.removedAssignments = existingManager.getAssignments(); + delta.removedAssignments = existingManager.getAssignments(); // known not null from checking both parms at top return delta; } @@ -101,7 +101,7 @@ public static ShapeTreeAssignment containsSameUrl(ShapeTreeAssignment assignment } public boolean allRemoved() { - return (!this.isUpdated() && this.removedAssignments.size() == this.existingManager.getAssignments().size()); + return (!this.isUpdated() && this.removedAssignments != null && this.removedAssignments.size() == this.existingManager.getAssignments().size()); } public boolean isUpdated() { diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeRequestHandler.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeRequestHandler.java index dc1dd9bb..c65a7185 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeRequestHandler.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ShapeTreeRequestHandler.java @@ -8,7 +8,13 @@ import org.apache.jena.graph.Graph; import java.net.URL; -import java.util.*; +import java.util.HashMap; +import java.util.Optional; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Arrays; import static com.janeirodigital.shapetrees.core.ManageableInstance.TEXT_TURTLE; @@ -25,8 +31,8 @@ public ShapeTreeRequestHandler(ResourceAccessor resourceAccessor) { public DocumentResponse manageShapeTree(ManageableInstance manageableInstance, ShapeTreeRequest shapeTreeRequest) throws ShapeTreeException { Optional validationResponse; - ShapeTreeManager updatedRootManager = RequestHelper.getIncomingShapeTreeManager(shapeTreeRequest, manageableInstance.getManagerResource()); - ShapeTreeManager existingRootManager = manageableInstance.getManagerResource().getManager(); + ShapeTreeManager updatedRootManager = RequestHelper.getIncomingShapeTreeManager(shapeTreeRequest, manageableInstance.getManagerResource()); // TODO: could be null + ShapeTreeManager existingRootManager = manageableInstance.getManagerResource().getManager(); // TODO: could be null // Determine assignments that have been removed, added, and/or updated ShapeTreeManagerDelta delta = ShapeTreeManagerDelta.evaluate(existingRootManager, updatedRootManager); @@ -125,7 +131,7 @@ public Optional createShapeTreeInstance(ManageableInstance man // if any of the provided focus nodes weren't matched validation must fail List unmatchedNodes = getUnmatchedFocusNodes(validationResults.values(), incomingFocusNodes); - if (!unmatchedNodes.isEmpty()) { return failValidation(new ValidationResult(false, "Failed to match target focus nodes: " + unmatchedNodes)); } + if (!unmatchedNodes.isEmpty()) { return failValidation(new ValidationResult(false, null,"Failed to match target focus nodes: " + unmatchedNodes)); } log.debug("Creating shape tree instance at {}", targetResourceUrl); @@ -162,7 +168,8 @@ public Optional updateShapeTreeInstance(ManageableInstance tar // All must pass for the update to validate ShapeTree shapeTree = ShapeTreeFactory.getShapeTree(assignment.getShapeTree()); URL managedResourceUrl = targetResource.getManageableResource().getUrl(); - ValidationResult validationResult = shapeTree.validateResource(null, shapeTreeRequest.getResourceType(), RequestHelper.getIncomingBodyGraph(shapeTreeRequest, managedResourceUrl, targetResource.getManageableResource()), RequestHelper.getIncomingFocusNodes(shapeTreeRequest, managedResourceUrl)); + Graph bodyGraph = RequestHelper.getIncomingBodyGraph(shapeTreeRequest, managedResourceUrl, targetResource.getManageableResource()); // TODO: could be null + ValidationResult validationResult = shapeTree.validateResource(null, RequestHelper.getIncomingFocusNodes(shapeTreeRequest, managedResourceUrl), shapeTreeRequest.getResourceType(), bodyGraph); if (Boolean.FALSE.equals(validationResult.isValid())) { return failValidation(validationResult); } } @@ -317,6 +324,7 @@ private ShapeTreeManager getManagerForAssignment(ManageableInstance manageableIn URL managerResourceUrl = manageableInstance.getManagerResource().getUrl(); // When at the top of the plant hierarchy, use the root manager from the initial plant request body + // TODO: rootManager can be null so method could return null (does not in any test) if (atRootOfPlantHierarchy(rootAssignment, manageableInstance.getManageableResource())) { return rootManager; } if (!manageableInstance.getManagerResource().isExists()) { @@ -377,7 +385,7 @@ private ShapeTreeManager getRootManager(ShapeTreeContext shapeTreeContext, Shape // Return a root shape tree manager associated with a given shape tree assignment private ShapeTreeAssignment getRootAssignment(ShapeTreeContext shapeTreeContext, ShapeTreeAssignment assignment) throws ShapeTreeException { - ShapeTreeManager rootManager = getRootManager(shapeTreeContext, assignment); + ShapeTreeManager rootManager = getRootManager(shapeTreeContext, assignment); // TODO: could be null for (ShapeTreeAssignment rootAssignment : rootManager.getAssignments()) { if (rootAssignment.getUrl() != null && rootAssignment.getUrl().equals(assignment.getRootAssignment())) { @@ -479,7 +487,8 @@ private DocumentResponse successfulValidation() { } private Optional failValidation(ValidationResult validationResult) { - return Optional.of(new DocumentResponse(new ResourceAttributes(), validationResult.getMessage(),422)); + String message = validationResult.getMessage() != null ? validationResult.getMessage() : "Unspecified validation failure"; + return Optional.of(new DocumentResponse(new ResourceAttributes(), message,422)); } } diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ValidationResult.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ValidationResult.java index 316bbf1e..4b631ad3 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ValidationResult.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/ValidationResult.java @@ -19,24 +19,6 @@ public Boolean isValid() { return (this.valid != null && this.valid); } - public ValidationResult(Boolean valid, String message) { - this.valid = valid; - this.message = message; - this.validatingShapeTree = null; - this.matchingShapeTree = null; - this.managingAssignment = null; - this.matchingFocusNode = null; - } - - public ValidationResult(Boolean valid, ShapeTree validatingShapeTree) { - this.valid = valid; - this.message = null; - this.validatingShapeTree = validatingShapeTree; - this.matchingShapeTree = null; - this.managingAssignment = null; - this.matchingFocusNode = null; - } - public ValidationResult(Boolean valid, ShapeTree validatingShapeTree, String message) { this.valid = valid; this.message = message; @@ -46,15 +28,6 @@ public ValidationResult(Boolean valid, ShapeTree validatingShapeTree, String mes this.matchingFocusNode = null; } - public ValidationResult(Boolean valid, ShapeTree validatingShapeTree, URL matchingFocusNode) { - this.valid = valid; - this.message = null; - this.validatingShapeTree = validatingShapeTree; - this.matchingShapeTree = null; - this.managingAssignment = null; - this.matchingFocusNode = matchingFocusNode; - } - public ValidationResult(Boolean valid, ShapeTree validatingShapeTree, ShapeTree matchingShapeTree, URL matchingFocusNode) { this.valid = valid; this.message = null; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/GraphHelper.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/GraphHelper.java index 1e721475..ae0d4676 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/GraphHelper.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/GraphHelper.java @@ -3,7 +3,11 @@ import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import lombok.extern.slf4j.Slf4j; import org.apache.jena.datatypes.xsd.XSDDatatype; -import org.apache.jena.graph.*; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Triple; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.Node_Blank; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.Lang; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/RequestHelper.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/RequestHelper.java index 2c2152b6..db0248b1 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/RequestHelper.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/helpers/RequestHelper.java @@ -1,6 +1,11 @@ package com.janeirodigital.shapetrees.core.helpers; -import com.janeirodigital.shapetrees.core.*; +import com.janeirodigital.shapetrees.core.ShapeTreeManager; +import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableInstance; +import com.janeirodigital.shapetrees.core.InstanceResource; +import com.janeirodigital.shapetrees.core.ManagerResource; +import com.janeirodigital.shapetrees.core.ShapeTreeRequest; import com.janeirodigital.shapetrees.core.enums.HttpHeaders; import com.janeirodigital.shapetrees.core.enums.LinkRelations; import com.janeirodigital.shapetrees.core.enums.ShapeTreeResourceType; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPatchMethodHandler.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPatchMethodHandler.java index 86415124..fddb6b10 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPatchMethodHandler.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPatchMethodHandler.java @@ -1,9 +1,13 @@ package com.janeirodigital.shapetrees.core.methodhandlers; -import com.janeirodigital.shapetrees.core.*; +import com.janeirodigital.shapetrees.core.ShapeTreeRequest; +import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableInstance; +import com.janeirodigital.shapetrees.core.DocumentResponse; +import com.janeirodigital.shapetrees.core.ResourceAccessor; import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import com.janeirodigital.shapetrees.core.helpers.RequestHelper; -import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableResource; import lombok.extern.slf4j.Slf4j; import java.util.Optional; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPostMethodHandler.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPostMethodHandler.java index 36680f18..96fff667 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPostMethodHandler.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPostMethodHandler.java @@ -1,10 +1,13 @@ package com.janeirodigital.shapetrees.core.methodhandlers; -import com.janeirodigital.shapetrees.core.*; -import com.janeirodigital.shapetrees.core.enums.HttpHeaders; +import com.janeirodigital.shapetrees.core.ShapeTreeRequest; +import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableInstance; +import com.janeirodigital.shapetrees.core.DocumentResponse; +import com.janeirodigital.shapetrees.core.ResourceAccessor; import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import com.janeirodigital.shapetrees.core.helpers.RequestHelper; -import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.enums.HttpHeaders; import lombok.extern.slf4j.Slf4j; import java.util.Optional; diff --git a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPutMethodHandler.java b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPutMethodHandler.java index 199bb96a..2790b345 100644 --- a/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPutMethodHandler.java +++ b/shapetrees-java-core/src/main/java/com/janeirodigital/shapetrees/core/methodhandlers/ValidatingPutMethodHandler.java @@ -1,9 +1,13 @@ package com.janeirodigital.shapetrees.core.methodhandlers; -import com.janeirodigital.shapetrees.core.*; +import com.janeirodigital.shapetrees.core.ShapeTreeRequest; +import com.janeirodigital.shapetrees.core.ShapeTreeContext; +import com.janeirodigital.shapetrees.core.ManageableInstance; +import com.janeirodigital.shapetrees.core.ManageableResource; +import com.janeirodigital.shapetrees.core.DocumentResponse; +import com.janeirodigital.shapetrees.core.ResourceAccessor; import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import com.janeirodigital.shapetrees.core.helpers.RequestHelper; -import com.janeirodigital.shapetrees.core.ShapeTreeContext; import java.util.Optional; diff --git a/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpClient.java b/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpClient.java index 3008d8cb..74c29978 100644 --- a/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpClient.java +++ b/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpClient.java @@ -7,7 +7,12 @@ import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; import lombok.extern.slf4j.Slf4j; -import javax.net.ssl.*; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; import java.io.IOException; import java.net.URISyntaxException; import java.security.KeyManagementException; diff --git a/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpValidatingShapeTreeInterceptor.java b/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpValidatingShapeTreeInterceptor.java index a807c4dc..ef0da83a 100644 --- a/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpValidatingShapeTreeInterceptor.java +++ b/shapetrees-java-javahttp/src/main/java/com/janeirodigital/shapetrees/javahttp/JavaHttpValidatingShapeTreeInterceptor.java @@ -1,11 +1,18 @@ package com.janeirodigital.shapetrees.javahttp; import com.janeirodigital.shapetrees.client.http.HttpResourceAccessor; -import com.janeirodigital.shapetrees.core.*; +import com.janeirodigital.shapetrees.core.DocumentResponse; +import com.janeirodigital.shapetrees.core.ResourceAttributes; +import com.janeirodigital.shapetrees.core.ResourceAccessor; +import com.janeirodigital.shapetrees.core.ShapeTreeRequest; import com.janeirodigital.shapetrees.core.enums.HttpHeaders; import com.janeirodigital.shapetrees.core.enums.ShapeTreeResourceType; import com.janeirodigital.shapetrees.core.exceptions.ShapeTreeException; -import com.janeirodigital.shapetrees.core.methodhandlers.*; +import com.janeirodigital.shapetrees.core.methodhandlers.ValidatingMethodHandler; +import com.janeirodigital.shapetrees.core.methodhandlers.ValidatingDeleteMethodHandler; +import com.janeirodigital.shapetrees.core.methodhandlers.ValidatingPutMethodHandler; +import com.janeirodigital.shapetrees.core.methodhandlers.ValidatingPatchMethodHandler; +import com.janeirodigital.shapetrees.core.methodhandlers.ValidatingPostMethodHandler; import lombok.AllArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -17,7 +24,11 @@ import java.net.URI; import java.net.URL; import java.net.http.HttpResponse; -import java.util.*; +import java.util.Map; +import java.util.Optional; +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; /** * Wrapper used for client-side validation diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/AbstractResourceAccessorTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/AbstractResourceAccessorTests.java index 6a7d0010..a0c24ac5 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/AbstractResourceAccessorTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/AbstractResourceAccessorTests.java @@ -326,7 +326,7 @@ private String getMilestoneThreeBodyGraph() { " ex:inProject . \n"; } - private String getProjectTwoManagerGraph() { + private String getProjectTwoManagerGraph() { // TODO: the SERVER_BASE is never substituted return "PREFIX rdf: \n" + "PREFIX rdfs: \n" + "PREFIX xml: \n" + diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/GraphHelperTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/GraphHelperTests.java index a1c31304..e1f8402e 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/GraphHelperTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/GraphHelperTests.java @@ -76,8 +76,8 @@ void parseInvalidTTL() { @DisplayName("Parse valid TTL") @SneakyThrows void parseValidTTL() { - String invalidTtl = "<#a> <#b> <#c> ."; - Assertions.assertNotNull(GraphHelper.readStringIntoGraph(URI.create("https://example.com/a"), invalidTtl, "text/turtle")); + String validTtl = "<#a> <#b> <#c> ."; + Assertions.assertNotNull(GraphHelper.readStringIntoGraph(URI.create("https://example.com/a"), validTtl, "text/turtle")); } @@ -86,7 +86,7 @@ void parseValidTTL() { @SneakyThrows void writeGraphToTTLString() { Graph graph = ModelFactory.createDefaultModel().getGraph(); - graph.add(new Triple(NodeFactory.createURI("<#b>"), NodeFactory.createURI("<#c>"), NodeFactory.createURI("<#d>"))); + graph.add(new Triple(NodeFactory.createURI("#b"), NodeFactory.createURI("#c"), NodeFactory.createURI("#d"))); Assertions.assertNotNull(GraphHelper.writeGraphToTurtleString(graph)); } @@ -102,7 +102,7 @@ void writeNullGraphToTTLString() { @SneakyThrows void writeClosedGraphtoTTLString() { Graph graph = ModelFactory.createDefaultModel().getGraph(); - graph.add(new Triple(NodeFactory.createURI("<#b>"), NodeFactory.createURI("<#c>"), NodeFactory.createURI("<#d>"))); + graph.add(new Triple(NodeFactory.createURI("#b"), NodeFactory.createURI("#c"), NodeFactory.createURI("#d"))); graph.close(); Assertions.assertNull(GraphHelper.writeGraphToTurtleString(graph)); } diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/SchemaCacheTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/SchemaCacheTests.java index d02c491a..3b0ed95c 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/SchemaCacheTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/SchemaCacheTests.java @@ -39,15 +39,18 @@ public SchemaCacheTests() { } @BeforeAll - static void beforeAll() throws ShapeTreeException { + static void beforeAll() { dispatcher = new RequestMatchingFixtureDispatcher(List.of( new DispatcherEntry(List.of("schemas/project-shex"), "GET", "/static/shex/project", null) )); + } + + @AfterEach + void afterEach() throws ShapeTreeException { SchemaCache.unInitializeCache(); } @Test - @Order(1) void testFailToOperateOnUninitializedCache() throws MalformedURLException, ShapeTreeException { assertFalse(SchemaCache.isInitialized()); @@ -77,7 +80,6 @@ void testFailToOperateOnUninitializedCache() throws MalformedURLException, Shape } @Test - @Order(2) void testInitializeCache() throws MalformedURLException, ShapeTreeException { SchemaCache.initializeCache(); assertTrue(SchemaCache.isInitialized()); @@ -85,41 +87,37 @@ void testInitializeCache() throws MalformedURLException, ShapeTreeException { } @Test - @Order(3) void testPreloadCache() throws MalformedURLException, ShapeTreeException { MockWebServer server = new MockWebServer(); server.setDispatcher(dispatcher); - Map schemas = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); + final Map schemas = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); SchemaCache.initializeCache(schemas); assertTrue(SchemaCache.containsSchema(toUrl(server, "/static/shex/project"))); } @Test - @Order(4) void testClearPutGet() throws MalformedURLException, ShapeTreeException { MockWebServer server = new MockWebServer(); server.setDispatcher(dispatcher); - SchemaCache.clearCache(); + SchemaCache.initializeCache(); + Assertions.assertNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); - Map schemas = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); - Map.Entry firstEntry = schemas.entrySet().stream().findFirst().orElse(null); - if (firstEntry == null) return; + final Map schemas = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); + final Map.Entry firstEntry = schemas.entrySet().stream().findFirst().orElse(null); SchemaCache.putSchema(firstEntry.getKey(), firstEntry.getValue()); Assertions.assertNotNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); } @Test - @Order(5) void testNullOnCacheContains() throws MalformedURLException, ShapeTreeException { MockWebServer server = new MockWebServer(); server.setDispatcher(dispatcher); - SchemaCache.clearCache(); + SchemaCache.initializeCache(); Assertions.assertNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); - Map schemas = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); - Map.Entry firstEntry = schemas.entrySet().stream().findFirst().orElse(null); - if (firstEntry == null) return; + final Map schemas = buildSchemaCache(List.of(toUrl(server, "/static/shex/project").toString())); + final Map.Entry firstEntry = schemas.entrySet().stream().findFirst().orElse(null); SchemaCache.putSchema(firstEntry.getKey(), firstEntry.getValue()); Assertions.assertNotNull(SchemaCache.getSchema(toUrl(server, "/static/shex/project"))); @@ -132,8 +130,7 @@ public static Map buildSchemaCache(List schemasToCache) log.debug("Caching schema {}", schemaUrl); DocumentResponse shexShapeSchema = DocumentLoaderManager.getLoader().loadExternalDocument(new URL(schemaUrl)); if (Boolean.FALSE.equals(shexShapeSchema.isExists()) || shexShapeSchema.getBody() == null) { - log.warn("Schema at {} doesn't exist or is empty", schemaUrl); - continue; + throw new Error("Schema at <" + schemaUrl + "> doesn't exist or is empty"); } String shapeBody = shexShapeSchema.getBody(); diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeManagerTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeManagerTests.java index 197be233..aa0110e4 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeManagerTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeManagerTests.java @@ -340,7 +340,7 @@ private String getInvalidManagerMissingTriplesString() { "\n" + " \n" + " st:assigns ; \n" + - "\n" ; + "\n." ; } private String getInvalidManagerUnexpectedTriplesString() { diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeValidationTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeValidationTests.java index 7f64cd6b..679dac41 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeValidationTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/ShapeTreeValidationTests.java @@ -67,13 +67,13 @@ void validateExpectsContainerType() { ValidationResult result; ShapeTree shapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#ExpectsContainerTree")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.CONTAINER, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.CONTAINER, null); Assertions.assertTrue(result.isValid()); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, null); Assertions.assertFalse(result.isValid()); - result = shapeTree.validateResource(null, ShapeTreeResourceType.NON_RDF, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.NON_RDF, null); Assertions.assertFalse(result.isValid()); } @@ -86,13 +86,13 @@ void validateExpectsResourceType() { ValidationResult result; ShapeTree shapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#ExpectsResourceTree")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, null); Assertions.assertTrue(result.isValid()); - result = shapeTree.validateResource(null, ShapeTreeResourceType.CONTAINER, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.CONTAINER, null); Assertions.assertFalse(result.isValid()); - result = shapeTree.validateResource(null, ShapeTreeResourceType.NON_RDF, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.NON_RDF, null); Assertions.assertFalse(result.isValid()); } @@ -105,13 +105,13 @@ void validateExpectsNonRDFResourceType() { ValidationResult result; ShapeTree shapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#ExpectsNonRDFResourceTree")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.NON_RDF, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.NON_RDF, null); Assertions.assertTrue(result.isValid()); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, null); Assertions.assertFalse(result.isValid()); - result = shapeTree.validateResource(null, ShapeTreeResourceType.CONTAINER, null, null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.CONTAINER, null); Assertions.assertFalse(result.isValid()); } @@ -124,10 +124,10 @@ void validateLabel() { ValidationResult result; ShapeTree shapeTree = ShapeTreeFactory.getShapeTree(toUrl(server, "/static/shapetrees/validation/shapetree#LabelTree")); - result = shapeTree.validateResource("resource-name", ShapeTreeResourceType.RESOURCE, null, null); + result = shapeTree.validateResource("resource-name", null, ShapeTreeResourceType.RESOURCE, null); Assertions.assertTrue(result.isValid()); - result = shapeTree.validateResource("invalid-name", ShapeTreeResourceType.RESOURCE, null, null); + result = shapeTree.validateResource("invalid-name", null, ShapeTreeResourceType.RESOURCE, null); Assertions.assertFalse(result.isValid()); } @@ -143,11 +143,11 @@ void validateShape() { // Validate shape with focus node List focusNodeUrls = List.of(toUrl(server, "/validation/valid-resource#foo")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource")), focusNodeUrls); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); Assertions.assertTrue(result.isValid()); // Validate shape without focus node - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource")), null); + result = shapeTree.validateResource(null, null, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); Assertions.assertTrue(result.isValid()); } @@ -164,7 +164,7 @@ void failToValidateShape() { // Pass in body content that will fail validation of the shape associated with FooTree List focusNodeUrls = List.of(toUrl(server,"/validation/valid-resource#foo")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, getInvalidFooBodyGraph(toUrl(server, "/validation/valid-resource")), focusNodeUrls); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getInvalidFooBodyGraph(toUrl(server, "/validation/valid-resource"))); Assertions.assertFalse(result.isValid()); } @@ -183,7 +183,7 @@ void failToValidateMissingShape() { // Catch exception thrown when a shape in a shape tree cannot be found List focusNodeUrls = List.of(toUrl(server,"/validation/valid-resource#foo")); - Assertions.assertThrows(ShapeTreeException.class, () -> shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, fooBodyGraph, focusNodeUrls)); + Assertions.assertThrows(ShapeTreeException.class, () -> shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, fooBodyGraph)); } @@ -201,7 +201,7 @@ void failToValidateMalformedShape() { // Catch exception thrown when a shape in a shape tree is invalid List focusNodeUrls = List.of(toUrl(server,"/validation/valid-resource#foo")); - Assertions.assertThrows(ShapeTreeException.class, () -> shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, fooBodyGraph, focusNodeUrls)); + Assertions.assertThrows(ShapeTreeException.class, () -> shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, fooBodyGraph)); } @@ -218,7 +218,7 @@ void failToValidateWhenNoShapeInShapeTree() { String graphTtl = "<#a> <#b> <#c> ."; List focusNodeUrls = List.of(toUrl(server,"http://a.example/b/c.d#a")); StringReader sr = new StringReader(graphTtl); - Model model = ModelFactory.createDefaultModel(); + Model model = ModelFactory.createDefaultModel(); // TODO: how about: GraphHelper.readStringIntoModel(new URL("http://example.com/"), graphTtl, "text/turtle") RDFDataMgr.read(model, sr, "http://example.com/", Lang.TTL); Assertions.assertThrows(ShapeTreeException.class, () -> noShapeValidationTree.validateGraph(model.getGraph(), focusNodeUrls)); @@ -238,7 +238,7 @@ void validateShapeBeforeCaching() { // Validate shape with focus node List focusNodeUrls = List.of(toUrl(server,"/validation/valid-resource#foo")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource")), focusNodeUrls); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); Assertions.assertTrue(result.isValid()); } @@ -258,7 +258,7 @@ void validateShapeAfterCaching() { // Validate shape with focus node List focusNodeUrls = List.of(toUrl(server,"/validation/valid-resource#foo")); - result = shapeTree.validateResource(null, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource")), focusNodeUrls); + result = shapeTree.validateResource(null, focusNodeUrls, ShapeTreeResourceType.RESOURCE, getFooBodyGraph(toUrl(server, "/validation/valid-resource"))); Assertions.assertTrue(result.isValid()); } diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientProjectTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientProjectTests.java index 576265e0..cae6194a 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientProjectTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientProjectTests.java @@ -141,19 +141,19 @@ void createAndValidateProjectsWithMultipleContains() { // Create the projects container as a managed instance. // 1. Will be validated by the parent DataRepositoryTree and the InformationSetTree both planted on /data (multiple contains) // 2. Will have a manager/assignment created for it as an instance of DataCollectionTree and InformationSetTree - DocumentResponse response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, getProjectsBodyGraph(), TEXT_TURTLE, targetShapeTrees, "projects", true); Assertions.assertEquals(201, response.getStatusCode()); // Another attempt without any target shape trees - response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, null, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, getProjectsBodyGraph(), TEXT_TURTLE, null, "projects", true); Assertions.assertEquals(201, response.getStatusCode()); // Another attempt without any target focus nodes - response = shapeTreeClient.postManagedInstance(context, parentContainer, null, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, parentContainer, null, getProjectsBodyGraph(), TEXT_TURTLE, targetShapeTrees, "projects", true); Assertions.assertEquals(201, response.getStatusCode()); // Another attempt without any only one of two target shape trees - response = shapeTreeClient.postManagedInstance(context, parentContainer, null, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, parentContainer, null, getProjectsBodyGraph(), TEXT_TURTLE, targetShapeTrees, "projects", true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -178,7 +178,7 @@ void createAndValidateProjects() { // Create the projects container as a shape tree instance. // 1. Will be validated by the parent DataRepositoryTree planted on /data // 2. Will have a manager/assignment created for it as an instance of DataCollectionTree - DocumentResponse response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, targetShapeTrees, "projects", true, getProjectsBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, getProjectsBodyGraph(), TEXT_TURTLE, targetShapeTrees, "projects", true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -229,7 +229,7 @@ void createProjectInProjects() { // Create the project-1 container as a shape tree instance. // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ // 2. Will have a manager/assignment created for it as an instance of ProjectTree - DocumentResponse response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, targetShapeTrees, "project-1", true, getProjectOneBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.postManagedInstance(context, parentContainer, focusNodes, getProjectOneBodyGraph(), TEXT_TURTLE, targetShapeTrees, "project-1", true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -259,7 +259,7 @@ void updateProjectInProjects() { // Update the project-1 container as a shape tree instance. // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ // 2. Will have a manager/assignment created for it as an instance of ProjectTree - DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, getProjectOneUpdatedBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.updateManagedInstance(context, targetResource, focusNodes, getProjectOneUpdatedBodyGraph(), TEXT_TURTLE); Assertions.assertEquals(200, response.getStatusCode()); } @@ -284,7 +284,7 @@ void failToCreateMalformedProject() { // Create the project-1 container as a shape tree instance via PUT // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ - DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, targetShapeTrees, true, getProjectOneMalformedBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, getProjectOneMalformedBodyGraph(), TEXT_TURTLE, targetShapeTrees, true); // 2. Will fail validation because the body content doesn't validate against the assigned shape Assertions.assertEquals(422, response.getStatusCode()); @@ -313,7 +313,7 @@ void failToUpdateMalformedProject() { // Update the project-1 container as a shape tree instance via PUT // 1. Will be validated by the parent ProjectCollectionTree planted on /data/projects/ - DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, getProjectOneMalformedBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.updateManagedInstance(context, targetResource, focusNodes, getProjectOneMalformedBodyGraph(), TEXT_TURTLE); // 2. Will fail validation because the body content doesn't validate against the assigned shape Assertions.assertEquals(422, response.getStatusCode()); @@ -346,7 +346,7 @@ void createMilestoneInProjectWithPut() { // Create the milestone-3 container in /projects/project-1/ as a shape tree instance using PUT to create // 1. Will be validated by the parent ProjectTree planted on /data/projects/project-1/ // 2. Will have a manager/assignment created for it as an instance of MilestoneTree - DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, targetShapeTrees, true, getMilestoneThreeBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, focusNodes, getMilestoneThreeBodyGraph(), TEXT_TURTLE, targetShapeTrees, true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -442,7 +442,7 @@ void createSecondTaskInProjectWithoutFocusNode() { List targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#TaskTree")); // create task-48 in milestone-3 - supply a target shape tree, but not a focus node - DocumentResponse response = shapeTreeClient.postManagedInstance(context, targetContainer, null, targetShapeTrees, "task-48", true, getTaskFortyEightBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.postManagedInstance(context, targetContainer, null, getTaskFortyEightBodyGraph(), TEXT_TURTLE, targetShapeTrees, "task-48", true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -473,7 +473,7 @@ void createThirdTaskInProjectWithoutAnyContext() { URL targetContainer = toUrl(server, "/data/projects/project-1/milestone-3/"); // create task-48 in milestone-3 - don't supply a target shape tree or focus node - DocumentResponse response = shapeTreeClient.postManagedInstance(context, targetContainer, null, null, "task-48", true, getTaskFortyEightBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.postManagedInstance(context, targetContainer, null, getTaskFortyEightBodyGraph(), TEXT_TURTLE, null, "task-48", true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -505,7 +505,7 @@ void createSecondTaskInProjectWithFocusNodeWithoutTargetShapeTree() { List focusNodes = Arrays.asList(toUrl(server, "/data/projects/project-1/milestone-3/task-48/#task")); // create task-48 in milestone-3 - supply a focus node but no target shape tree - DocumentResponse response = shapeTreeClient.postManagedInstance(context, targetContainer, focusNodes, null, "task-48", true, getTaskFortyEightBodyGraph(), TEXT_TURTLE); + DocumentResponse response = shapeTreeClient.postManagedInstance(context, targetContainer, focusNodes, getTaskFortyEightBodyGraph(), TEXT_TURTLE, null, "task-48", true); Assertions.assertEquals(201, response.getStatusCode()); } @@ -540,7 +540,7 @@ void createAttachmentInTask() { URL targetResource = toUrl(server, "/data/projects/project-1/milestone-3/task-48/attachment-48"); List targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#AttachmentTree")); - DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, null, targetShapeTrees, false, null, "application/octet-stream"); + DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, null, null, "application/octet-stream", targetShapeTrees, false); Assertions.assertEquals(201, response.getStatusCode()); } @@ -575,7 +575,7 @@ void createSecondAttachmentInTask() { URL targetResource = toUrl(server, "/data/projects/project-1/milestone-3/task-48/random.png"); List targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/project/shapetree#AttachmentTree")); - DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, null, targetShapeTrees, false, null, "application/octet-stream"); + DocumentResponse response = shapeTreeClient.putManagedInstance(context, targetResource, null, null, "application/octet-stream", targetShapeTrees, false); Assertions.assertEquals(201, response.getStatusCode()); } diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientTypeTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientTypeTests.java index 0d9c78de..28c42926 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientTypeTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientTypeTests.java @@ -51,11 +51,12 @@ void createContainer() { DocumentResponse response; // Provide target shape tree - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "valid-container", true, null, TEXT_TURTLE); + // TODO: (this and following tests) can we POST a null bodyString? + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "valid-container", true); Assertions.assertEquals(201, response.getStatusCode()); // Do not provide target shape tree - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, "valid-container", true, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, TEXT_TURTLE, null, "valid-container", true); Assertions.assertEquals(201, response.getStatusCode()); @@ -71,15 +72,15 @@ void failToCreateNonContainer() { DocumentResponse response; // Provide target shape tree for a resource when container shape tree is expected - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-resource", false); Assertions.assertEquals(422, response.getStatusCode()); // Provide target shape tree for a container even though what's being sent is a resource - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "invalid-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "invalid-resource", false); Assertions.assertEquals(422, response.getStatusCode()); // Don't provide a target shape tree at all - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, "invalid-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/containers/"), null, null, TEXT_TURTLE, null, "invalid-resource", false); Assertions.assertEquals(422, response.getStatusCode()); } @@ -97,11 +98,11 @@ void createResource() { DocumentResponse response; // Provide target shape tree - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "valid-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "valid-resource", false); Assertions.assertEquals(201, response.getStatusCode()); // Do not provide target shape tree - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, "valid-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, TEXT_TURTLE, null, "valid-resource", false); Assertions.assertEquals(201, response.getStatusCode()); } @@ -116,15 +117,15 @@ void failToCreateNonResource() { DocumentResponse response; // Provide target shape tree for a container when resource shape tree is expected - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "invalid-container", true, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ContainerTree")), "invalid-container", true); Assertions.assertEquals(422, response.getStatusCode()); // Provide target shape tree for a resource even though what's being sent is a container - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-container", true, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-container", true); Assertions.assertEquals(422, response.getStatusCode()); // Don't provide a target shape tree at all - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, "invalid-container", true, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/resources/"), null, null, TEXT_TURTLE, null, "invalid-container", true); Assertions.assertEquals(422, response.getStatusCode()); } @@ -141,10 +142,10 @@ void createNonRDFResource() { dispatcher.getConfiguredFixtures().add(new DispatcherEntry(List.of("http/201"), "POST", "/non-rdf-resources/valid-non-rdf-resource.shapetree", null)); // TODO: Test: should this fail? should it have already failed? DocumentResponse response; - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#NonRDFResourceTree")), "valid-non-rdf-resource", false, null, "application/octet-stream"); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, "application/octet-stream", Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#NonRDFResourceTree")), "valid-non-rdf-resource", false); Assertions.assertEquals(201, response.getStatusCode()); - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, "valid-non-rdf-resource", false, null, "application/octet-stream"); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, "application/octet-stream", null, "valid-non-rdf-resource", false); Assertions.assertEquals(201, response.getStatusCode()); } @@ -158,15 +159,15 @@ void failToCreateNonRDFResource() { DocumentResponse response; // Provide target shape tree for a resource when non-rdf-resource shape tree is expected - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-non-rdf-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#ResourceTree")), "invalid-non-rdf-resource", false); Assertions.assertEquals(422, response.getStatusCode()); // Provide target shape tree for a non-rdf-resource even though what's being sent is a resource - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#NonRDFResourceTree")), "invalid-non-rdf-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, TEXT_TURTLE, Arrays.asList(toUrl(server, "/static/shapetrees/type/shapetree#NonRDFResourceTree")), "invalid-non-rdf-resource", false); Assertions.assertEquals(422, response.getStatusCode()); // Don't provide a target shape tree at all - response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, "invalid-non-rdf-resource", false, null, TEXT_TURTLE); + response = shapeTreeClient.postManagedInstance(context, toUrl(server, "/non-rdf-resources/"), null, null, TEXT_TURTLE, null, "invalid-non-rdf-resource", false); Assertions.assertEquals(422, response.getStatusCode()); } diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientValidationTests.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientValidationTests.java index 0ebe2e99..b87537f7 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientValidationTests.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/clienthttp/AbstractHttpClientValidationTests.java @@ -60,7 +60,7 @@ void validateTwoContainsTwoShapesTwoNodes() { toUrl(server, "/data/container-1/resource-1#element")); // Plant the data repository on newly created data container - DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(201, response.getStatusCode()); } @@ -84,7 +84,7 @@ void validateTwoContainsSameContainingTree() { List focusNodes = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource")); // Plant the data repository on newly created data container - DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(201, response.getStatusCode()); } @@ -107,19 +107,19 @@ void failToValidateTwoContainsWithBadFocusNodes() { // Only one matching target focus node is provided List focusNodes = Arrays.asList(toUrl(server, "/super/bad#node")); - DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(422, response.getStatusCode()); // Multiple non-matching target focus nodes are provided focusNodes = Arrays.asList(toUrl(server, "/super/bad#node"), toUrl(server, "/data/container-1/resource-1#badnode"), toUrl(server, "/data/container-1/#badnode")); - response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(422, response.getStatusCode()); // Only one matching target focus node is provided when two are needed focusNodes = Arrays.asList(toUrl(server, "/data/container-1/resource-1#resource")); - response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(422, response.getStatusCode()); } @@ -166,19 +166,19 @@ void failToValidateTwoContainsWithBadTargetShapeTrees() { // Only one matching target shape tree is provided List targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree")); - DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + DocumentResponse response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(422, response.getStatusCode()); // Multiple non-matching target focus nodes are provided targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#OtherAttributeTree"), toUrl(server, "/static/shapetrees/validation/shapetree#OtherElementTree")); - response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(422, response.getStatusCode()); // One tree provided that isn't in either st:contains lists targetShapeTrees = Arrays.asList(toUrl(server, "/static/shapetrees/validation/shapetree#AttributeTree"), toUrl(server, "/static/shapetrees/validation/shapetree#StandaloneTree")); - response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, targetShapeTrees, "resource-1", false, getResource1BodyString(), "text/turtle"); + response = this.shapeTreeClient.postManagedInstance(context, targetResource, focusNodes, getResource1BodyString(), "text/turtle", targetShapeTrees, "resource-1", false); Assertions.assertEquals(422, response.getStatusCode()); } diff --git a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/fixtures/Fixture.java b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/fixtures/Fixture.java index d23132a0..9d16a03f 100644 --- a/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/fixtures/Fixture.java +++ b/shapetrees-java-tests/src/test/java/com/janeirodigital/shapetrees/tests/fixtures/Fixture.java @@ -34,20 +34,10 @@ public class Fixture { * @param fileName filename should not contain extension or relative path. ie: login */ public static Fixture parseFrom(String fileName, RecordedRequest request) { - return parseFrom(fileName, new YamlParser(), request); - } - - - /** - * Parse the given filename and returns the Fixture object. - * - * @param fileName filename should not contain extension or relative path. ie: login - * @param parser parser is required for parsing operation, it should not be null - */ - public static Fixture parseFrom(String fileName, Parser parser, RecordedRequest request) { if (fileName == null) { throw new NullPointerException("File name should not be null"); } + Parser parser = new YamlParser(); String path = "fixtures/" + fileName + ".yaml"; Map variables = new HashMap<>(); variables.put("SERVER_BASE", getServerBaseFromRequest(request));