Skip to content

Commit

Permalink
Merge 242bd8a into 3b353af
Browse files Browse the repository at this point in the history
  • Loading branch information
Falx committed Mar 7, 2022
2 parents 3b353af + 242bd8a commit 80da53a
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 2 deletions.
63 changes: 62 additions & 1 deletion src/http/representation/RepresentationMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { DataFactory, Store } from 'n3';
import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js';
import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../../util/errors/InternalServerError';
import type { ContentType } from '../../util/HeaderUtil';
import { parseContentTypeWithParameters, parseContentType } from '../../util/HeaderUtil';
import { toNamedTerm, toObjectTerm, toCachedNamedNode, isTerm, toLiteral } from '../../util/TermUtil';
import { CONTENT_TYPE, CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD } from '../../util/Vocabularies';
import { CONTENT_TYPE, CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
import type { ResourceIdentifier } from './ResourceIdentifier';
import { isResourceIdentifier } from './ResourceIdentifier';

Expand Down Expand Up @@ -304,6 +306,52 @@ export class RepresentationMetadata {
return this;
}

private setContentTypeParams(input: ContentType | string | undefined): void {
// Make sure complete Content-Type RDF structure is gone
this.removeContentTypeParameters();

if (!input) {
return;
}

if (typeof input === 'string') {
input = parseContentTypeWithParameters(input);
}

Object.entries(input.parameters ?? []).forEach(([ paramKey, paramValue ], idx): void => {
const paramNode = DataFactory.blankNode(`parameter${idx + 1}`);
this.addQuad(this.id, SOLID_META.terms.ContentTypeParameter, paramNode);
this.addQuad(paramNode, RDFS.terms.label, paramKey);
this.addQuad(paramNode, SOLID_META.terms.value, paramValue);
});
}

/**
* Parse the internal RDF structure to retrieve the Record with ContentType Parameters.
* @returns An Record<string,string> object with Content-Type parameters and their values.
*/
private getContentTypeParams(): Record<string, string> | undefined {
const params = this.getAll(SOLID_META.terms.ContentTypeParameter);
return params.length > 0 ?
params.reduce((acc, term): Record<string, string> => {
const key = this.store.getObjects(term, RDFS.terms.label, null)[0].value;
const { value } = this.store.getObjects(term, SOLID_META.terms.value, null)[0];
return { ...acc, [key]: value };
}, {}) :
undefined;
}

private removeContentTypeParameters(): void {
const params = this.store.getQuads(this.id, SOLID_META.terms.ContentTypeParameter, null, null);
params.forEach((quad): void => {
const labels = this.store.getQuads(quad.object, RDFS.terms.label, null, null);
const values = this.store.getQuads(quad.object, SOLID_META.terms.value, null, null);
this.store.removeQuads(labels);
this.store.removeQuads(values);
});
this.store.removeQuads(params);
}

// Syntactic sugar for common predicates

/**
Expand All @@ -315,6 +363,7 @@ export class RepresentationMetadata {

public set contentType(input) {
this.set(CONTENT_TYPE_TERM, input);
this.setContentTypeParams(input);
}

/**
Expand All @@ -330,4 +379,16 @@ export class RepresentationMetadata {
this.set(CONTENT_LENGTH_TERM, toLiteral(input, XSD.terms.integer));
}
}

/**
* Shorthand for the ContentType as an object (with parameters)
*/
public get contentTypeObject(): ContentType | undefined {
return this.contentType ?
{
value: parseContentType(this.contentType).type,
parameters: this.getContentTypeParams(),
} :
undefined;
}
}
34 changes: 34 additions & 0 deletions src/util/HeaderUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ export interface AcceptLanguage extends AcceptHeader { }
*/
export interface AcceptDatetime extends AcceptHeader { }

/**
* Contents of a HTTP Content-Type Header.
* Optional parameters Record is included.
*/
export interface ContentType {
value: string;
parameters?: Record<string, string>;
}

// REUSED REGEXES
const token = /^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$/u;

Expand Down Expand Up @@ -427,6 +436,31 @@ export function parseContentType(contentType: string): { type: string } {
return { type: contentTypeValue };
}

/**
* Parses the Content-Type header and also parses any parameters in the header.
*
* @param input - The Content-Type header string.
*
* @throws {@link BadRequestHttpError}
* Thrown on invalid header syntax.
*
* @returns A {@link ContentType} object containing the value and optional parameters.
*/
export function parseContentTypeWithParameters(input: string): ContentType {
// Quoted strings could prevent split from having correct results
const { result, replacements } = transformQuotedStrings(input);
const [ value, ...params ] = result.split(';').map((str): string => str.trim());
return params.length > 0 ?
parseParameters(params, replacements).reduce<ContentType>((prev, cur): ContentType => {
if (!prev.parameters) {
prev.parameters = {};
}
prev.parameters[cur.name] = cur.value;
return prev;
}, { value }) :
{ value };
}

/**
* The Forwarded header from RFC7239
*/
Expand Down
7 changes: 7 additions & 0 deletions src/util/Vocabularies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
'type',
);

export const RDFS = createUriAndTermNamespace('http://www.w3.org/2000/01/rdf-schema#',
'label',
);

export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
'deletes',
'inserts',
Expand All @@ -148,6 +152,9 @@ export const SOLID_META = createUriAndTermNamespace('urn:npm:solid:community-ser
'ResponseMetadata',
// This is used to identify templates that can be used for the representation of a resource
'template',
// This is used to store Content-Type Parameters
'ContentTypeParameter',
'value',
);

export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
Expand Down
41 changes: 40 additions & 1 deletion test/unit/http/representation/RepresentationMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'jest-rdf';
import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
import { CONTENT_TYPE, SOLID_META, RDFS } from '../../../../src/util/Vocabularies';
const { defaultGraph, literal, namedNode, quad } = DataFactory;

// Helper functions to filter quads
Expand Down Expand Up @@ -296,5 +296,44 @@ describe('A RepresentationMetadata', (): void => {
metadata.add(CONTENT_TYPE, 'c/d');
expect((): any => metadata.contentType).toThrow();
});

it('has a shorthand for Content-Type with parameters support.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
expect(metadata.contentTypeObject).toEqual({
value: 'text/plain',
parameters: {
charset: 'utf-8',
test: 'value1',
},
});
});

it('can properly clear the Content-Type parameters explicitly.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
metadata.contentType = undefined;
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
expect(metadata.quads(null, SOLID_META.terms.ContentTypeParameter, null, null)).toHaveLength(0);
expect(metadata.quads(null, SOLID_META.terms.value, null, null)).toHaveLength(0);
expect(metadata.quads(null, RDFS.terms.label, null, null)).toHaveLength(0);
});

it('can properly clear the Content-Type parameters implicitly.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
metadata.contentType = 'text/turtle';
expect(metadata.contentType).toBe('text/turtle');
expect(metadata.contentTypeObject).toEqual({
value: 'text/turtle',
});
expect(metadata.quads(null, SOLID_META.terms.ContentTypeParameter, null, null)).toHaveLength(0);
expect(metadata.quads(null, SOLID_META.terms.value, null, null)).toHaveLength(0);
expect(metadata.quads(null, RDFS.terms.label, null, null)).toHaveLength(0);
});
});
});
24 changes: 24 additions & 0 deletions test/unit/util/HeaderUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
parseAcceptEncoding,
parseAcceptLanguage,
parseContentType,
parseContentTypeWithParameters,
parseForwarded,
} from '../../../src/util/HeaderUtil';

Expand Down Expand Up @@ -200,6 +201,29 @@ describe('HeaderUtil', (): void => {
expect(parseContentType('text/turtle; charset=UTF-8').type).toEqual(contentTypeTurtle);
});
});

describe('#parseContentTypeWithParameters', (): void => {
const contentTypePlain: any = {
value: 'text/plain',
parameters: {
charset: 'utf-8',
},
};
it('handles single content-type parameter (with leading and trailing whitespaces).', (): void => {
expect(parseContentTypeWithParameters('text/plain; charset=utf-8')).toEqual(contentTypePlain);
expect(parseContentTypeWithParameters(' text/plain; charset=utf-8')).toEqual(contentTypePlain);
expect(parseContentTypeWithParameters('text/plain ; charset=utf-8')).toEqual(contentTypePlain);
expect(parseContentTypeWithParameters(' text/plain ; charset=utf-8')).toEqual(contentTypePlain);
expect(parseContentTypeWithParameters(' text/plain ; charset="utf-8"')).toEqual(contentTypePlain);
expect(parseContentTypeWithParameters(' text/plain ; charset = "utf-8"')).toEqual(contentTypePlain);
});

it('handles multiple content-type parameters.', (): void => {
contentTypePlain.parameters.test = 'value1';
expect(parseContentTypeWithParameters('text/plain; charset=utf-8;test="value1"')).toEqual(contentTypePlain);
});
});

describe('#parseForwarded', (): void => {
it('handles an empty set of headers.', (): void => {
expect(parseForwarded({})).toEqual({});
Expand Down

0 comments on commit 80da53a

Please sign in to comment.