no.hasmac.jsonld.expansion.ObjectExpansion Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hasmac-json-ld Show documentation
Show all versions of hasmac-json-ld Show documentation
A more performant JSON-LD 1.1 Processor & API forked from Titanium JSON-LD.
The newest version!
/*
* Copyright 2020 APICATALOG and HASMAC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package no.hasmac.jsonld.expansion;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import no.hasmac.jsonld.JsonLdError;
import no.hasmac.jsonld.JsonLdErrorCode;
import no.hasmac.jsonld.StringUtils;
import no.hasmac.jsonld.context.ActiveContext;
import no.hasmac.jsonld.context.TermDefinition;
import no.hasmac.jsonld.json.JsonMapBuilder;
import no.hasmac.jsonld.json.JsonProvider;
import no.hasmac.jsonld.json.JsonUtils;
import no.hasmac.jsonld.lang.Keywords;
import no.hasmac.jsonld.lang.NodeObject;
import no.hasmac.jsonld.lang.Utils;
import no.hasmac.jsonld.uri.UriUtils;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
/**
* @see Expansion
* Algorithm
*/
public final class ObjectExpansion {
// mandatory
private ActiveContext activeContext;
private JsonValue propertyContext;
private JsonObject element;
private String activeProperty;
private URI baseUrl;
// optional
private boolean frameExpansion;
private boolean ordered;
private boolean fromMap;
private ObjectExpansion(final ActiveContext activeContext, final JsonValue propertyContext, final JsonObject element,
final String activeProperty, final URI baseUrl) {
this.activeContext = activeContext;
this.propertyContext = propertyContext;
this.element = element;
this.activeProperty = activeProperty;
this.baseUrl = baseUrl;
// default values
this.frameExpansion = false;
this.ordered = false;
this.fromMap = false;
}
public static ObjectExpansion with(final ActiveContext activeContext, final JsonValue propertyContext,
final JsonObject element, final String activeProperty, final URI baseUrl) {
return new ObjectExpansion(activeContext, propertyContext, element, activeProperty, baseUrl);
}
public ObjectExpansion frameExpansion(boolean value) {
this.frameExpansion = value;
return this;
}
public ObjectExpansion ordered(boolean value) {
this.ordered = value;
return this;
}
public ObjectExpansion fromMap(boolean value) {
this.fromMap = value;
return this;
}
public JsonValue expand() throws JsonLdError {
initPreviousContext();
initPropertyContext();
initLocalContext();
// 10.
final ActiveContext typeContext = activeContext;
final String typeKey = processTypeScoped(typeContext);
final String inputType = findInputType(typeKey);
final JsonMapBuilder result = JsonMapBuilder.create();
ObjectExpansion1314
.with(activeContext, element, activeProperty, baseUrl)
.inputType(inputType)
.result(result)
.typeContext(typeContext)
.nest(new LinkedHashMap<>())
.frameExpansion(frameExpansion)
.ordered(ordered)
.expand();
// 15.
if (result.containsKey(Keywords.VALUE)) {
return normalizeValue(result);
// 16.
} else if (result.containsKey(Keywords.TYPE)) {
return normalizeType(result);
// 17.
} else if (result.containsKey(Keywords.LIST) || result.containsKey(Keywords.SET)) {
return normalizeContainer(result);
}
return normalize(result);
}
private void initPropertyContext() throws JsonLdError {
// 8.
if (propertyContext != null) {
activeContext = activeContext
.newContext()
.overrideProtected(true)
.create(
propertyContext,
activeContext
.getTerm(activeProperty)
.map(TermDefinition::getBaseUrl)
.orElse(null)
);
}
}
private void initPreviousContext() throws JsonLdError {
// 7. If active context has a previous context, the active context is not
// propagated.
// If from map is undefined or false, and element does not contain an entry
// expanding to @value,
// and element does not consist of a single entry expanding to @id (where
// entries are IRI expanded),
// set active context to previous context from active context,
// as the scope of a term-scoped context does not apply when processing new node
// objects.
if (activeContext.getPreviousContext() != null && !fromMap) {
boolean revert = true;
for (final String key : Utils.index(element.keySet(), true)) {
final String expandedKey =
activeContext
.uriExpansion()
.vocab(true)
.expand(key);
if (Keywords.VALUE.equals(expandedKey) || (Keywords.ID.equals(expandedKey) && (element.size() == 1))) {
revert = false;
break;
}
}
if (revert) {
activeContext = activeContext.getPreviousContext();
}
}
}
private void initLocalContext() throws JsonLdError {
// 9.
if (element.containsKey(Keywords.CONTEXT)) {
activeContext = activeContext
.newContext()
.create(element.get(Keywords.CONTEXT), baseUrl);
}
}
private String processTypeScoped(final ActiveContext typeContext) throws JsonLdError {
if (element.containsKey(Keywords.TYPE)) {
// TODO: This is safe only if an element can't have two different keys for TYPE.
// 11.2
JsonValue value = element.get(Keywords.TYPE);
List terms = JsonUtils.optimizedGetStrings(value);
for (final String term : terms) {
Optional localContext = typeContext.getTerm(term).map(TermDefinition::getLocalContext);
if (localContext.isPresent()) {
Optional valueDefinition = activeContext.getTerm(term);
activeContext =
activeContext
.newContext()
.propagate(false)
.create(localContext.get(),
valueDefinition
.map(TermDefinition::getBaseUrl)
.orElse(null)
);
}
}
return Keywords.TYPE;
}
String typeKey = null;
// 11.
for (final String key : Utils.index(element.keySet(), true)) {
final String expandedKey =
activeContext
.uriExpansion()
.vocab(true)
.expand(key);
if (!Keywords.TYPE.equals(expandedKey)) {
continue;
} else if (typeKey == null) {
typeKey = key;
}
// 11.2
JsonValue value = element.get(key);
List terms = JsonUtils.optimizedGetStrings(value);
for (final String term : terms) {
Optional localContext = typeContext.getTerm(term).map(TermDefinition::getLocalContext);
if (localContext.isPresent()) {
Optional valueDefinition = activeContext.getTerm(term);
activeContext =
activeContext
.newContext()
.propagate(false)
.create(localContext.get(),
valueDefinition
.map(TermDefinition::getBaseUrl)
.orElse(null)
);
}
}
}
return typeKey;
}
private String findInputType(final String typeKey) throws JsonLdError {
// Initialize input type to expansion of the last value of the first entry in
// element
// expanding to @type (if any), ordering entries lexicographically by key. Both
// the key and
// value of the matched entry are IRI expanded.
if (typeKey != null) {
final JsonValue type = element.get(typeKey);
if (JsonUtils.isArray(type)) {
final String lastValue = type.asJsonArray()
.stream()
.filter(JsonUtils::isString)
.map(JsonString.class::cast)
.map(JsonString::getString)
.sorted()
.reduce((first, second) -> second)
.orElse(null);
if (lastValue != null) {
return activeContext.uriExpansion().vocab(true).expand(lastValue);
}
} else if (JsonUtils.isString(type)) {
return activeContext.uriExpansion().vocab(true).expand(((JsonString) type).getString());
}
}
return null;
}
private JsonValue normalizeValue(final JsonMapBuilder result) throws JsonLdError {
// 15.1.
if (result.isNotValueObject()) {
throw new JsonLdError(JsonLdErrorCode.INVALID_VALUE_OBJECT);
}
if ((result.containsKey(Keywords.DIRECTION) || result.containsKey(Keywords.LANGUAGE))
&& result.containsKey(Keywords.TYPE)) {
throw new JsonLdError(JsonLdErrorCode.INVALID_VALUE_OBJECT);
}
// 15.2.
final Optional type = result.get(Keywords.TYPE);
if (type.map(t -> !JsonUtils.contains(Keywords.JSON, t)).orElse(true)) {
final Optional value = result.get(Keywords.VALUE);
// 15.3.
if (value.map(v -> JsonUtils.isNull(v) || (JsonUtils.isArray(v) && v.asJsonArray().isEmpty())).orElse(true)) {
return JsonValue.NULL;
// 15.4
} else if (!frameExpansion && JsonUtils.isNotString(value.get()) && result.containsKey(Keywords.LANGUAGE)) {
throw new JsonLdError(JsonLdErrorCode.INVALID_LANGUAGE_TAGGED_VALUE);
// 15.5
} else if (!frameExpansion
&& type.filter(t -> JsonUtils.isNotString(t)
|| UriUtils.isNotURI(((JsonString) t).getString()))
.isPresent()
) {
throw new JsonLdError(JsonLdErrorCode.INVALID_TYPED_VALUE);
}
}
return normalize(result);
}
private JsonValue normalizeType(final JsonMapBuilder result) throws JsonLdError {
result
.get(Keywords.TYPE)
.filter(JsonUtils::isNotArray)
.filter(JsonUtils::isNotNull)
.ifPresent(value -> result.put(Keywords.TYPE, JsonProvider.instance().createArrayBuilder().add(value).build()));
return normalize(result);
}
private JsonValue normalizeContainer(final JsonMapBuilder result) throws JsonLdError {
// 17.1.
if (result.size() > 2 || result.size() == 2 && !result.containsKey(Keywords.INDEX)) {
throw new JsonLdError(JsonLdErrorCode.INVALID_SET_OR_LIST_OBJECT);
}
// 17.2.
final Optional set = result.get(Keywords.SET);
if (set.filter(JsonUtils::isObject).isPresent()) {
// deepcode ignore checkIsPresent~Optional: false positive
return normalize(JsonMapBuilder.create(set.map(JsonValue::asJsonObject).get()));
} else if (set.isPresent()) {
return set.get();
}
return normalize(result);
}
private JsonValue normalize(final JsonMapBuilder result) throws JsonLdError {
// Extension: JSON-LD-STAR (Experimental)
if (result.containsKey(Keywords.ANNOTATION)
&& (StringUtils.isBlank(activeProperty)
|| Keywords.GRAPH.equals(activeProperty)
|| Keywords.INCLUDED.equals(activeProperty)
|| result.get(Keywords.ANNOTATION).filter(NodeObject::isNotAnnotationObject).isPresent())
) {
throw new JsonLdError(JsonLdErrorCode.INVALID_ANNOTATION);
}
// 18.
if (result.size() == 1 && result.containsKey(Keywords.LANGUAGE)) {
return JsonValue.NULL;
}
// 19.
if (activeProperty == null || Keywords.GRAPH.equals(activeProperty)) {
// 19.1. If result is a map which is empty, or contains only the entries @value
// or @list, set result to null
if (!frameExpansion && result.isEmpty()
|| result.containsKey(Keywords.VALUE)
|| result.containsKey(Keywords.LIST)) {
return JsonValue.NULL;
}
// 19.2. if result is a map whose only entry is @id, set result to null. When
// the frameExpansion flag is set, a map containing only the @id entry is
// retained.
if (!frameExpansion && result.size() == 1 && result.containsKey(Keywords.ID)) {
return JsonValue.NULL;
}
}
return result.build();
}
}