Skip to content

Commit

Permalink
Merge d8b6b03 into 14db5fe
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Aug 17, 2020
2 parents 14db5fe + d8b6b03 commit 0e07f31
Show file tree
Hide file tree
Showing 30 changed files with 691 additions and 323 deletions.
21 changes: 13 additions & 8 deletions bin/server.ts
Expand Up @@ -2,14 +2,13 @@ import yargs from 'yargs';
import {
AcceptPreferenceParser,
AuthenticatedLdpHandler,
BodyParser,
CompositeAsyncHandler,
ExpressHttpServer,
HttpRequest,
Operation,
PatchingStore,
QuadToTurtleConverter,
Representation,
ResponseDescription,
RepresentationConvertingStore,
SimpleAuthorizer,
SimpleBodyParser,
SimpleCredentialsExtractor,
Expand All @@ -25,6 +24,7 @@ import {
SimpleSparqlUpdatePatchHandler,
SimpleTargetExtractor,
SingleThreadedResourceLocker,
TurtleToQuadConverter,
} from '..';

const { argv } = yargs
Expand All @@ -37,9 +37,9 @@ const { argv } = yargs
const { port } = argv;

// This is instead of the dependency injection that still needs to be added
const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleBodyParser(),
const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleSparqlUpdateBodyParser(),
new SimpleBodyParser(),
]);
const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(),
Expand All @@ -53,11 +53,16 @@ const authorizer = new SimpleAuthorizer();

// Will have to see how to best handle this
const store = new SimpleResourceStore(`http://localhost:${port}/`);
const converter = new CompositeAsyncHandler([
new TurtleToQuadConverter(),
new QuadToTurtleConverter(),
]);
const convertingStore = new RepresentationConvertingStore(store, converter);
const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
const patchingStore = new PatchingStore(store, patcher);
const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(convertingStore, patcher);

const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
const operationHandler = new CompositeAsyncHandler([
new SimpleDeleteOperationHandler(patchingStore),
new SimpleGetOperationHandler(patchingStore),
new SimplePatchOperationHandler(patchingStore),
Expand Down
8 changes: 7 additions & 1 deletion index.ts
Expand Up @@ -55,6 +55,11 @@ export * from './src/server/HttpHandler';
export * from './src/server/HttpRequest';
export * from './src/server/HttpResponse';

// Storage/Conversion
export * from './src/storage/conversion/QuadToTurtleConverter';
export * from './src/storage/conversion/RepresentationConverter';
export * from './src/storage/conversion/TurtleToQuadConverter';

// Storage/Patch
export * from './src/storage/patch/PatchHandler';
export * from './src/storage/patch/SimpleSparqlUpdatePatchHandler';
Expand All @@ -64,8 +69,9 @@ export * from './src/storage/AtomicResourceStore';
export * from './src/storage/Conditions';
export * from './src/storage/Lock';
export * from './src/storage/LockingResourceStore';
export * from './src/storage/PassthroughStore';
export * from './src/storage/PatchingStore';
export * from './src/storage/RepresentationConverter';
export * from './src/storage/RepresentationConvertingStore';
export * from './src/storage/ResourceLocker';
export * from './src/storage/ResourceMapper';
export * from './src/storage/ResourceStore';
Expand Down
4 changes: 0 additions & 4 deletions src/ldp/http/Patch.ts
Expand Up @@ -4,8 +4,4 @@ import { Representation } from '../representation/Representation';
* Represents the changes needed for a PATCH request.
*/
export interface Patch extends Representation {
/**
* The raw body of the PATCH request.
*/
raw: string;
}
42 changes: 10 additions & 32 deletions src/ldp/http/SimpleBodyParser.ts
@@ -1,37 +1,22 @@
import { BinaryRepresentation } from '../representation/BinaryRepresentation';
import { BodyParser } from './BodyParser';
import { DATA_TYPE_QUAD } from '../../util/ContentTypes';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest';
import { PassThrough } from 'stream';
import { QuadRepresentation } from '../representation/QuadRepresentation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { StreamParser } from 'n3';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';

/**
* Parses the incoming {@link HttpRequest} if there is no body or if it contains turtle (or similar) RDF data.
* Naively parses the content-type header to determine the body type.
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
* Naively parses the mediatype from the content-type header.
* Metadata is not generated (yet).
*/
export class SimpleBodyParser extends BodyParser {
private static readonly contentTypes = [
'application/n-quads',
'application/trig',
'application/n-triples',
'text/turtle',
'text/n3',
];

public async canHandle(input: HttpRequest): Promise<void> {
const contentType = input.headers['content-type'];

if (contentType && !SimpleBodyParser.contentTypes.some((type): boolean => contentType.includes(type))) {
throw new UnsupportedMediaTypeHttpError('This parser only supports RDF data.');
}
public async canHandle(): Promise<void> {
// Default BodyParser supports all content-types
}

// Note that the only reason this is a union is in case the body is empty.
// If this check gets moved away from the BodyParsers this union could be removed
public async handle(input: HttpRequest): Promise<QuadRepresentation | undefined> {
public async handle(input: HttpRequest): Promise<BinaryRepresentation | undefined> {
const contentType = input.headers['content-type'];

if (!contentType) {
Expand All @@ -46,16 +31,9 @@ export class SimpleBodyParser extends BodyParser {
contentType: mediaType,
};

// Catch parsing errors and emit correct error
// Node 10 requires both writableObjectMode and readableObjectMode
const errorStream = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
const data = input.pipe(new StreamParser());
data.pipe(errorStream);
data.on('error', (error): boolean => errorStream.emit('error', new UnsupportedHttpError(error.message)));

return {
dataType: DATA_TYPE_QUAD,
data: errorStream,
dataType: DATA_TYPE_BINARY,
data: input,
metadata,
};
}
Expand Down
19 changes: 12 additions & 7 deletions src/ldp/http/SimpleSparqlUpdateBodyParser.ts
@@ -1,6 +1,7 @@
import { BodyParser } from './BodyParser';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest';
import { Readable } from 'stream';
import { PassThrough } from 'stream';
import { readableToString } from '../../util/Util';
import { SparqlUpdatePatch } from './SparqlUpdatePatch';
import { translate } from 'sparqlalgebrajs';
Expand All @@ -23,17 +24,21 @@ export class SimpleSparqlUpdateBodyParser extends BodyParser {

public async handle(input: HttpRequest): Promise<SparqlUpdatePatch> {
try {
const sparql = await readableToString(input);
// Note that readableObjectMode is only defined starting from Node 12
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
const options = { objectMode: input.readableObjectMode };
const toAlgebraStream = new PassThrough(options);
const dataCopy = new PassThrough(options);
input.pipe(toAlgebraStream);
input.pipe(dataCopy);
const sparql = await readableToString(toAlgebraStream);
const algebra = translate(sparql, { quads: true });

// Prevent body from being requested again
return {
algebra,
dataType: 'sparql-algebra',
raw: sparql,
get data(): Readable {
throw new Error('Body already parsed');
},
dataType: DATA_TYPE_BINARY,
data: dataCopy,
metadata: {
raw: [],
profiles: [],
Expand Down
42 changes: 42 additions & 0 deletions src/storage/PassthroughStore.ts
@@ -0,0 +1,42 @@
import { Conditions } from './Conditions';
import { Patch } from '../ldp/http/Patch';
import { Representation } from '../ldp/representation/Representation';
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ResourceStore } from './ResourceStore';

/**
* Store that calls the corresponding functions of the source Store.
* Can be extended by stores that do not want to override all functions
* by implementing a decorator pattern.
*/
export class PassthroughStore implements ResourceStore {
protected readonly source: ResourceStore;

public constructor(source: ResourceStore) {
this.source = source;
}

public async addResource(container: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<ResourceIdentifier> {
return this.source.addResource(container, representation, conditions);
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
return this.source.deleteResource(identifier, conditions);
}

public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
conditions?: Conditions): Promise<Representation> {
return this.source.getRepresentation(identifier, preferences, conditions);
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
return this.source.modifyResource(identifier, patch, conditions);
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
return this.source.setRepresentation(identifier, representation, conditions);
}
}
27 changes: 3 additions & 24 deletions src/storage/PatchingStore.ts
@@ -1,8 +1,7 @@
import { Conditions } from './Conditions';
import { PassthroughStore } from './PassthroughStore';
import { Patch } from '../ldp/http/Patch';
import { PatchHandler } from './patch/PatchHandler';
import { Representation } from '../ldp/representation/Representation';
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ResourceStore } from './ResourceStore';

Expand All @@ -11,34 +10,14 @@ import { ResourceStore } from './ResourceStore';
* If the original store supports the {@link Patch}, behaviour will be identical,
* otherwise one of the {@link PatchHandler}s supporting the given Patch will be called instead.
*/
export class PatchingStore implements ResourceStore {
private readonly source: ResourceStore;
export class PatchingStore extends PassthroughStore {
private readonly patcher: PatchHandler;

public constructor(source: ResourceStore, patcher: PatchHandler) {
this.source = source;
super(source);
this.patcher = patcher;
}

public async addResource(container: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<ResourceIdentifier> {
return this.source.addResource(container, representation, conditions);
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
return this.source.deleteResource(identifier, conditions);
}

public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
conditions?: Conditions): Promise<Representation> {
return this.source.getRepresentation(identifier, preferences, conditions);
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
return this.source.setRepresentation(identifier, representation, conditions);
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
try {
return await this.source.modifyResource(identifier, patch, conditions);
Expand Down
24 changes: 0 additions & 24 deletions src/storage/RepresentationConverter.ts

This file was deleted.

47 changes: 47 additions & 0 deletions src/storage/RepresentationConvertingStore.ts
@@ -0,0 +1,47 @@
import { Conditions } from './Conditions';
import { matchingMediaType } from '../util/Util';
import { PassthroughStore } from './PassthroughStore';
import { Representation } from '../ldp/representation/Representation';
import { RepresentationConverter } from './conversion/RepresentationConverter';
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ResourceStore } from './ResourceStore';

/**
* Store that overrides the `getRepresentation` function.
* Tries to convert the {@link Representation} it got from the source store
* so it matches one of the given type preferences.
*
* In the future this class should take the preferences of the request into account.
* Even if there is a match with the output from the store,
* if there is a low weight for that type conversions might still be preferred.
*/
export class RepresentationConvertingStore extends PassthroughStore {
private readonly converter: RepresentationConverter;

public constructor(source: ResourceStore, converter: RepresentationConverter) {
super(source);
this.converter = converter;
}

public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
conditions?: Conditions): Promise<Representation> {
const representation = await super.getRepresentation(identifier, preferences, conditions);
if (this.matchesPreferences(representation, preferences)) {
return representation;
}
return this.converter.handleSafe({ identifier, representation, preferences });
}

private matchesPreferences(representation: Representation, preferences: RepresentationPreferences): boolean {
if (!preferences.type) {
return true;
}
return Boolean(
representation.metadata.contentType &&
preferences.type.some((type): boolean =>
type.weight > 0 &&
matchingMediaType(type.value, representation.metadata.contentType!)),
);
}
}

0 comments on commit 0e07f31

Please sign in to comment.