Skip to content

Commit 0f451a6

Browse files
committed
Handle annotations as RDF-star statements
1 parent 0f54ecd commit 0f451a6

File tree

11 files changed

+823
-22
lines changed

11 files changed

+823
-22
lines changed

lib/JsonLdParser.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {EntryHandlerKeywordValue} from "./entryhandler/keyword/EntryHandlerKeywo
2020
import {ParsingContext} from "./ParsingContext";
2121
import {Util} from "./Util";
2222
import {parse as parseLinkHeader} from "http-link-header";
23+
import { EntryHandlerKeywordAnnotation } from './entryhandler/keyword/EntryHandlerKeywordAnnotation';
2324

2425
/**
2526
* A stream transformer that parses JSON-LD (text) streams to an {@link RDF.Stream}.
@@ -36,6 +37,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
3637
new EntryHandlerKeywordNest(),
3738
new EntryHandlerKeywordType(),
3839
new EntryHandlerKeywordValue(),
40+
new EntryHandlerKeywordAnnotation(),
3941
new EntryHandlerContainer(),
4042
new EntryHandlerKeywordUnknownFallback(),
4143
new EntryHandlerPredicate(),
@@ -251,7 +253,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
251253
}
252254

253255
// Skip further processing if this node is part of a literal
254-
if (this.util.isLiteral(depth)) {
256+
if (await this.util.isLiteral(keys, depth)) {
255257
handleKey = false;
256258
}
257259

@@ -304,6 +306,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
304306
this.parsingContext.jsonLiteralStack.splice(depth, 1);
305307
this.parsingContext.validationStack.splice(depth - 1, 2);
306308
this.parsingContext.literalStack.splice(depth, this.parsingContext.literalStack.length - depth);
309+
this.parsingContext.annotationsBuffer.splice(depth, 1);
307310
// TODO: just like the literal stack, splice all other stack until the end as well?
308311
}
309312

@@ -386,6 +389,23 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
386389
}
387390
this.parsingContext.unidentifiedGraphsBuffer.splice(depth, 1);
388391
}
392+
393+
// Push unhandled annotations up the stack as nested annotations
394+
const annotationsBuffer = this.parsingContext.annotationsBuffer[depth];
395+
if (annotationsBuffer) {
396+
// Throw an error if we reach the top, and still have annotations
397+
if (annotationsBuffer.length > 0 && depth === 1) {
398+
this.parsingContext.emitError(new ErrorCoded(`Annotations can not be made on top-level nodes`,
399+
ERROR_CODES.INVALID_ANNOTATION));
400+
}
401+
402+
// Pass the annotations buffer up one level in the stack
403+
const annotationsBufferParent = this.parsingContext.getAnnotationsBufferSafe(depth - 1);
404+
for (const annotation of annotationsBuffer) {
405+
annotationsBufferParent.push(annotation);
406+
}
407+
delete this.parsingContext.annotationsBuffer[depth];
408+
}
389409
}
390410

391411
/**

lib/ParsingContext.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as RDF from "@rdfjs/types";
55
import {ContextTree} from "./ContextTree";
66
import {IJsonLdParserOptions, JsonLdParser} from "./JsonLdParser";
77

8+
export type AnnotationsBufferEntry = { predicate: RDF.Term, object: RDF.Term, reverse: boolean, nestedAnnotations: AnnotationsBufferEntry[], depth: number };
9+
810
/**
911
* Data holder for parsing information.
1012
*/
@@ -69,6 +71,8 @@ export class ParsingContext {
6971
// Quads that don't know their graph @id yet.
7072
// L0: stack depth; L1: values
7173
public readonly unidentifiedGraphsBuffer: { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term, isEmbedded: boolean }[][];
74+
// Stack of annotation objects on incomplete nodes.
75+
public readonly annotationsBuffer: AnnotationsBufferEntry[][];
7276

7377
// Depths that should be still flushed
7478
public pendingContainerFlushBuffers: { depth: number, keys: any[] }[];
@@ -115,6 +119,7 @@ export class ParsingContext {
115119
this.jsonLiteralStack = [];
116120
this.unidentifiedValuesBuffer = [];
117121
this.unidentifiedGraphsBuffer = [];
122+
this.annotationsBuffer = [];
118123

119124
this.pendingContainerFlushBuffers = [];
120125

@@ -370,6 +375,20 @@ export class ParsingContext {
370375
return buffer;
371376
}
372377

378+
/**
379+
* Safely get or create the depth value of {@link ParsingContext.annotationsBuffer}.
380+
* @param {number} depth A depth.
381+
* @return {} An element of {@link ParsingContext.annotationsBuffer}.
382+
*/
383+
public getAnnotationsBufferSafe(depth: number): AnnotationsBufferEntry[] {
384+
let buffer = this.annotationsBuffer[depth];
385+
if (!buffer) {
386+
buffer = [];
387+
this.annotationsBuffer[depth] = buffer;
388+
}
389+
return buffer;
390+
}
391+
373392
/**
374393
* @return IExpandOptions The expand options for the active processing mode.
375394
*/
@@ -410,6 +429,16 @@ export class ParsingContext {
410429
this.unidentifiedValuesBuffer[depth] = this.unidentifiedValuesBuffer[depth + depthOffset];
411430
delete this.unidentifiedValuesBuffer[depth + depthOffset];
412431
}
432+
if (this.annotationsBuffer[depth + depthOffset - 1]) {
433+
if (!this.annotationsBuffer[depth - 1]) {
434+
this.annotationsBuffer[depth - 1] = [];
435+
}
436+
this.annotationsBuffer[depth - 1] = [
437+
...this.annotationsBuffer[depth - 1],
438+
...this.annotationsBuffer[depth + depthOffset - 1],
439+
];
440+
delete this.annotationsBuffer[depth + depthOffset - 1];
441+
}
413442

414443
// TODO: also do the same for other stacks
415444
}

lib/Util.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ContextParser, ERROR_CODES, ErrorCoded, JsonLdContextNormalized,
33
import * as RDF from "@rdfjs/types";
44
import {DataFactory} from "rdf-data-factory";
55
import {EntryHandlerContainer} from "./entryhandler/EntryHandlerContainer";
6-
import {ParsingContext} from "./ParsingContext";
6+
import { AnnotationsBufferEntry, ParsingContext } from "./ParsingContext";
77

88
// tslint:disable-next-line:no-var-requires
99
const canonicalizeJson = require('canonicalize');
@@ -144,14 +144,22 @@ export class Util {
144144

145145
/**
146146
* Check if the given key exists inside an embedded node as direct child.
147-
* @param {JsonLdContextNormalized} context A JSON-LD context.
148147
* @param {string} parentKey The parent key.
149148
* @return {boolean} If the property is embedded.
150149
*/
151150
public static isPropertyInEmbeddedNode(parentKey: string): boolean {
152151
return parentKey === '@id';
153152
}
154153

154+
/**
155+
* Check if the given key exists inside an annotation object as direct child.
156+
* @param {string} parentKey The parent key.
157+
* @return {boolean} If the property is an annotation.
158+
*/
159+
public static isPropertyInAnnotationObject(parentKey: string): boolean {
160+
return parentKey === '@annotation';
161+
}
162+
155163
/**
156164
* Check if the given IRI is valid.
157165
* @param {string} iri A potential IRI.
@@ -277,6 +285,9 @@ export class Util {
277285
case '@index':
278286
valueIndex = subValue;
279287
break;
288+
case '@annotation':
289+
// This keyword is allowed, but is processed like normal nodes
290+
break;
280291
default:
281292
throw new ErrorCoded(`Unknown value entry '${key}' in @value: ${JSON.stringify(value)}`,
282293
ERROR_CODES.INVALID_VALUE_OBJECT);
@@ -764,11 +775,16 @@ export class Util {
764775
* This will also check higher levels,
765776
* because if a parent is a literal,
766777
* then the deeper levels are definitely a literal as well.
778+
* @param {any[]} keys The keys.
767779
* @param {number} depth The depth.
768780
* @return {boolean} If we are processing a literal.
769781
*/
770-
public isLiteral(depth: number): boolean {
782+
public async isLiteral(keys: any[], depth: number): Promise<boolean> {
771783
for (let i = depth; i >= 0; i--) {
784+
if (await this.unaliasKeyword(keys[i], keys, i) === '@annotation') {
785+
// Literals may have annotations, which require processing of inner nodes.
786+
return false;
787+
}
772788
if (this.parsingContext.literalStack[i] || this.parsingContext.jsonLiteralStack[i]) {
773789
return true;
774790
}
@@ -958,6 +974,35 @@ export class Util {
958974
} else {
959975
this.parsingContext.emitQuad(depth, quad);
960976
}
977+
978+
// Flush annotations
979+
const annotationsBuffer = this.parsingContext.annotationsBuffer[depth];
980+
if (annotationsBuffer) {
981+
for (const annotation of annotationsBuffer) {
982+
this.emitAnnotation(depth, quad, annotation);
983+
}
984+
delete this.parsingContext.annotationsBuffer[depth];
985+
}
986+
}
987+
988+
// This is a separate function to enable recursion
989+
protected emitAnnotation(depth: number, quad: RDF.BaseQuad, annotation: AnnotationsBufferEntry) {
990+
// Construct annotation quad
991+
let annotationQuad;
992+
if (annotation.reverse) {
993+
this.validateReverseSubject(annotation.object);
994+
annotationQuad = this.dataFactory.quad(annotation.object, annotation.predicate, quad);
995+
} else {
996+
annotationQuad = this.dataFactory.quad(quad, annotation.predicate, annotation.object);
997+
}
998+
999+
// Emit annotated quad
1000+
this.parsingContext.emitQuad(depth, annotationQuad);
1001+
1002+
// Also emit nested annotations
1003+
for (const nestedAnnotation of annotation.nestedAnnotations) {
1004+
this.emitAnnotation(depth, annotationQuad, nestedAnnotation);
1005+
}
9611006
}
9621007

9631008
}

lib/containerhandler/ContainerHandlerIndex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class ContainerHandlerIndex implements IContainerHandler {
6969
// Otherwise, attach the index to the node identifier
7070
for (const indexValue of indexValues) {
7171
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth + 1,
72-
indexProperty, indexValue, false, false);
72+
indexProperty, indexValue, false, false, false);
7373
}
7474
}
7575
}

lib/containerhandler/ContainerHandlerType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class ContainerHandlerType implements IContainerHandler {
5959
if (type) {
6060
// Push the type to the stack using the rdf:type predicate
6161
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth + 1,
62-
util.rdfType, type, false, false);
62+
util.rdfType, type, false, false, false);
6363
}
6464

6565
// Flush any pending flush buffers

lib/entryhandler/EntryHandlerArrayValue.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as RDF from "@rdfjs/types";
22
import {ParsingContext} from "../ParsingContext";
33
import {Util} from "../Util";
44
import {IEntryHandler} from "./IEntryHandler";
5+
import { ERROR_CODES, ErrorCoded } from '../../../jsonld-context-flatten';
56

67
/**
78
* Handles values that are part of an array.
@@ -141,6 +142,12 @@ export class EntryHandlerArrayValue implements IEntryHandler<boolean> {
141142
}
142143

143144
parsingContext.listPointerStack[depth] = listPointer;
145+
146+
// Error if an annotation was defined
147+
if (parsingContext.rdfstar && parsingContext.annotationsBuffer[depth]) {
148+
parsingContext.emitError(new ErrorCoded(`Found an illegal annotation inside a list`,
149+
ERROR_CODES.INVALID_ANNOTATION));
150+
}
144151
}
145152

146153
}

lib/entryhandler/EntryHandlerPredicate.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ERROR_CODES, ErrorCoded} from "jsonld-context-parser";
22
import * as RDF from "@rdfjs/types";
3-
import {ParsingContext} from "../ParsingContext";
3+
import { AnnotationsBufferEntry, ParsingContext } from "../ParsingContext";
44
import {Util} from "../Util";
55
import {IEntryHandler} from "./IEntryHandler";
66

@@ -21,17 +21,18 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
2121
* @param {Term} object The object.
2222
* @param {boolean} reverse If the property is reversed.
2323
* @param {boolean} isEmbedded If the property exists in an embedded node as direct child.
24+
* @param {boolean} isAnnotation If the property exists in an annotation object.
2425
* @return {Promise<void>} A promise resolving when handling is done.
2526
*/
2627
public static async handlePredicateObject(parsingContext: ParsingContext, util: Util, keys: any[], depth: number,
2728
predicate: RDF.Term, object: RDF.Term,
28-
reverse: boolean, isEmbedded: boolean) {
29+
reverse: boolean, isEmbedded: boolean, isAnnotation: boolean) {
2930
const depthProperties: number = await util.getPropertiesDepth(keys, depth);
3031
const depthOffsetGraph = await util.getDepthOffsetGraph(depth, keys);
3132
const depthPropertiesGraph: number = depth - depthOffsetGraph;
3233

3334
const subjects = parsingContext.idStack[depthProperties];
34-
if (subjects) {
35+
if (subjects && !isAnnotation) {
3536
// Emit directly if the @id was already defined
3637
for (const subject of subjects) {
3738
// Check if we're in a @graph context
@@ -65,7 +66,44 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
6566
if (reverse) {
6667
util.validateReverseSubject(object);
6768
}
68-
parsingContext.getUnidentifiedValueBufferSafe(depthProperties).push({predicate, object, reverse, isEmbedded});
69+
70+
// Either push to the annotations or the actual value buffer
71+
if (isAnnotation) {
72+
// Only add to buffer if rdfstar is enabled
73+
if (parsingContext.rdfstar) {
74+
// Error if an @id was defined
75+
if (parsingContext.idStack[depth]) {
76+
parsingContext.emitError(new ErrorCoded(`Found an illegal @id inside an annotation: ${parsingContext.idStack[depth][0].value}`,
77+
ERROR_CODES.INVALID_ANNOTATION));
78+
}
79+
80+
// Error if we're in an embedded node
81+
for (let i = 0; i < depth; i++) {
82+
if (await util.unaliasKeyword(keys[i], keys, i) === '@id') {
83+
parsingContext.emitError(new ErrorCoded(`Found an illegal annotation inside an embedded node`,
84+
ERROR_CODES.INVALID_ANNOTATION));
85+
}
86+
}
87+
88+
// Store new annotation in the buffer
89+
const annotationsBuffer = parsingContext.getAnnotationsBufferSafe(depthProperties);
90+
const newAnnotation: AnnotationsBufferEntry = { predicate, object, reverse, nestedAnnotations: [], depth: depthProperties };
91+
annotationsBuffer.push(newAnnotation);
92+
93+
// Check in the buffer if any annotations were defined at a deeper depth,
94+
// if so, they are considered nested annotations.
95+
for (let i = annotationsBuffer.length - 2; i >= 0; i--) {
96+
// We iterate in reverse order, to enable easy item removal from the back.
97+
const existingAnnotation = annotationsBuffer[i];
98+
if (existingAnnotation.depth > depthProperties) {
99+
newAnnotation.nestedAnnotations.push(existingAnnotation);
100+
annotationsBuffer.splice(i, 1);
101+
}
102+
}
103+
}
104+
} else {
105+
parsingContext.getUnidentifiedValueBufferSafe(depthProperties).push({ predicate, object, reverse, isEmbedded });
106+
}
69107
}
70108
}
71109

@@ -108,15 +146,22 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
108146
const objects = await util.valueToTerm(context, key, value, depth, keys);
109147
if (objects.length) {
110148
for (let object of objects) {
149+
// Based on parent key, check if reverse, embedded, and annotation.
111150
let parentKey = await util.unaliasKeywordParent(keys, depth);
112151
const reverse = Util.isPropertyReverse(context, keyOriginal, parentKey);
113-
if (parentKey === '@reverse') {
114-
// Check parent of parent when checking if we're in an embedded node if in @reverse
115-
depth--;
116-
parentKey = await util.unaliasKeywordParent(keys, depth);
152+
let parentDepthOffset = 0;
153+
while (parentKey === '@reverse' || typeof parentKey === 'number') {
154+
// Check parent of parent when checking while we're in an array or in @reverse
155+
if (typeof parentKey === 'number') {
156+
parentDepthOffset++;
157+
} else {
158+
depth--;
159+
}
160+
parentKey = await util.unaliasKeywordParent(keys, depth - parentDepthOffset);
117161
}
118162
const isEmbedded = Util.isPropertyInEmbeddedNode(parentKey);
119163
util.validateReverseInEmbeddedNode(key, reverse, isEmbedded);
164+
const isAnnotation = Util.isPropertyInAnnotationObject(parentKey);
120165

121166
if (value) {
122167
// Special case if our term was defined as an @list, but does not occur in an array,
@@ -143,7 +188,7 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
143188
}
144189

145190
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth,
146-
predicate, object, reverse, isEmbedded);
191+
predicate, object, reverse, isEmbedded, isAnnotation);
147192
}
148193
}
149194
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {ParsingContext} from "../../ParsingContext";
2+
import {Util} from "../../Util";
3+
import {EntryHandlerKeyword} from "./EntryHandlerKeyword";
4+
import { ERROR_CODES, ErrorCoded } from '../../../../jsonld-context-flatten';
5+
6+
/**
7+
* Handles @annotation entries.
8+
*/
9+
export class EntryHandlerKeywordAnnotation extends EntryHandlerKeyword {
10+
11+
constructor() {
12+
super('@annotation');
13+
}
14+
15+
public async handle(parsingContext: ParsingContext, util: Util, key: any, keys: any[], value: any, depth: number)
16+
: Promise<any> {
17+
// Validate value
18+
if (typeof value === 'string' || (typeof value === 'object' && value['@value'])) {
19+
parsingContext.emitError(new ErrorCoded(`Found illegal annotation value: ${JSON.stringify(value)}`,
20+
ERROR_CODES.INVALID_ANNOTATION));
21+
}
22+
23+
// Rest of the processing is done as regular nodes
24+
}
25+
26+
}

0 commit comments

Comments
 (0)