All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jsonldjava.core.Context Maven / Gradle / Ivy

package com.github.jsonldjava.core;

import static com.github.jsonldjava.core.JsonLdUtils.compareShortestLeast;
import static com.github.jsonldjava.utils.Obj.newMap;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.github.jsonldjava.core.JsonLdError.Error;
import com.github.jsonldjava.utils.JsonLdUrl;
import com.github.jsonldjava.utils.Obj;

/**
 * A helper class which still stores all the values in a map but gives member
 * variables easily access certain keys
 *
 * @author tristan
 *
 */
public class Context extends LinkedHashMap {

    private static final long serialVersionUID = 2894534897574805571L;

    private static final Pattern URL_PATTERN = Pattern.compile("^https?://.*$", Pattern.CASE_INSENSITIVE);
    private JsonLdOptions options;
    private Map termDefinitions;
    public Map inverse = null;

    public Context() {
        this(new JsonLdOptions());
    }

    public Context(JsonLdOptions opts) {
        super();
        init(opts);
    }

    public Context(Map map, JsonLdOptions opts) {
        super(map);
        checkEmptyKey(map);
        init(opts);
    }

    public Context(Map map) {
        super(map);
        checkEmptyKey(map);
        init(new JsonLdOptions());
    }

    public Context(Object context, JsonLdOptions opts) {
        // TODO: load remote context
        super(context instanceof Map ? (Map) context : null);
        init(opts);
    }

    private void init(JsonLdOptions options) {
        this.options = options;
        if (options.getBase() != null) {
            this.put(JsonLdConsts.BASE, options.getBase());
        }
        this.termDefinitions = newMap();
    }

    /**
     * Value Compaction Algorithm
     *
     * http://json-ld.org/spec/latest/json-ld-api/#value-compaction
     *
     * @param activeProperty
     *            The Active Property
     * @param value
     *            The value to compact
     * @return The compacted value
     */
    public Object compactValue(String activeProperty, Map value) {
        // 1)
        int numberMembers = value.size();
        // 2)
        if (value.containsKey(JsonLdConsts.INDEX)
                && JsonLdConsts.INDEX.equals(this.getContainer(activeProperty))) {
            numberMembers--;
        }
        // 3)
        if (numberMembers > 2) {
            return value;
        }
        // 4)
        final String typeMapping = getTypeMapping(activeProperty);
        final String languageMapping = getLanguageMapping(activeProperty);
        if (value.containsKey(JsonLdConsts.ID)) {
            // 4.1)
            if (numberMembers == 1 && JsonLdConsts.ID.equals(typeMapping)) {
                return compactIri((String) value.get(JsonLdConsts.ID));
            }
            // 4.2)
            if (numberMembers == 1 && JsonLdConsts.VOCAB.equals(typeMapping)) {
                return compactIri((String) value.get(JsonLdConsts.ID), true);
            }
            // 4.3)
            return value;
        }
        final Object valueValue = value.get(JsonLdConsts.VALUE);
        // 5)
        if (value.containsKey(JsonLdConsts.TYPE)
                && Obj.equals(value.get(JsonLdConsts.TYPE), typeMapping)) {
            return valueValue;
        }
        // 6)
        if (value.containsKey(JsonLdConsts.LANGUAGE)) {
            // TODO: SPEC: doesn't specify to check default language as well
            if (Obj.equals(value.get(JsonLdConsts.LANGUAGE), languageMapping) || Obj
                    .equals(value.get(JsonLdConsts.LANGUAGE), this.get(JsonLdConsts.LANGUAGE))) {
                return valueValue;
            }
        }
        // 7)
        if (numberMembers == 1 && (!(valueValue instanceof String)
                || !this.containsKey(JsonLdConsts.LANGUAGE)
                || (termDefinitions.containsKey(activeProperty)
                        && getTermDefinition(activeProperty).containsKey(JsonLdConsts.LANGUAGE)
                        && languageMapping == null))) {
            return valueValue;
        }
        // 8)
        return value;
    }

    /**
     * Context Processing Algorithm
     *
     * http://json-ld.org/spec/latest/json-ld-api/#context-processing-algorithms
     *
     * @param localContext
     *            The Local Context object.
     * @param remoteContexts
     *            The list of Strings denoting the remote Context URLs.
     * @return The parsed and merged Context.
     * @throws JsonLdError
     *             If there is an error parsing the contexts.
     */
    public Context parse(Object localContext, List remoteContexts) throws JsonLdError {
        if (remoteContexts == null) {
            remoteContexts = new ArrayList();
        }
        return parse(localContext, remoteContexts, false);
    }

    /**
     * Helper method used to work around logic errors related to the recursive
     * nature of the JSONLD-API Context Processing Algorithm.
     *
     * @param localContext
     *            The Local Context object.
     * @param remoteContexts
     *            The list of Strings denoting the remote Context URLs.
     * @param parsingARemoteContext
     *            True if localContext represents a remote context that has been
     *            parsed and sent into this method and false otherwise. This
     *            must be set to know whether to propagate the @code{@base} key
     *            from the context to the result.
     * @return The parsed and merged Context.
     * @throws JsonLdError
     *             If there is an error parsing the contexts.
     */
    private Context parse(Object localContext, final List remoteContexts,
            boolean parsingARemoteContext) throws JsonLdError {
        // 1. Initialize result to the result of cloning active context.
        Context result = this.clone(); // TODO: clone?
        // 2)
        if (!(localContext instanceof List)) {
            final Object temp = localContext;
            localContext = new ArrayList();
            ((List) localContext).add(temp);
        }
        // 3)
        for (final Object context : ((List) localContext)) {
            // 3.1)
            if (context == null) {
                result = new Context(this.options);
                continue;
            } else if (context instanceof Context) {
                result = ((Context) context).clone();
            }
            // 3.2)
            else if (context instanceof String) {
                String uri = null;
                // @base is ignored when processing remote contexts, https://github.com/jsonld-java/jsonld-java/issues/304
                if (!URL_PATTERN.matcher(context.toString()).matches()) {
                    uri = (String) result.get(JsonLdConsts.BASE);
                }
                uri = JsonLdUrl.resolve(uri, (String) context);
                // 3.2.2
                if (remoteContexts.contains(uri)) {
                    throw new JsonLdError(Error.RECURSIVE_CONTEXT_INCLUSION, uri);
                }
                List nextRemoteContexts = new ArrayList<>(remoteContexts);
                nextRemoteContexts.add(uri);

                // 3.2.3: Dereference context
                final RemoteDocument rd = this.options.getDocumentLoader().loadDocument(uri);
                final Object remoteContext = rd.getDocument();
                if (!(remoteContext instanceof Map) || !((Map) remoteContext)
                        .containsKey(JsonLdConsts.CONTEXT)) {
                    // If the dereferenced document has no top-level JSON object
                    // with an @context member
                    throw new JsonLdError(Error.INVALID_REMOTE_CONTEXT, context);
                }
                final Object tempContext = ((Map) remoteContext)
                        .get(JsonLdConsts.CONTEXT);

                // 3.2.4
                result = result.parse(tempContext, nextRemoteContexts, true);
                // 3.2.5
                continue;
            } else if (!(context instanceof Map)) {
                // 3.3
                throw new JsonLdError(Error.INVALID_LOCAL_CONTEXT, context);
            }
            checkEmptyKey((Map) context);
            // 3.4
            if (!parsingARemoteContext
                    && ((Map) context).containsKey(JsonLdConsts.BASE)) {
                // 3.4.1
                final Object value = ((Map) context).get(JsonLdConsts.BASE);
                // 3.4.2
                if (value == null) {
                    result.remove(JsonLdConsts.BASE);
                } else if (value instanceof String) {
                    // 3.4.3
                    if (JsonLdUtils.isAbsoluteIri((String) value)) {
                        result.put(JsonLdConsts.BASE, value);
                    } else {
                        // 3.4.4
                        final String baseUri = (String) result.get(JsonLdConsts.BASE);
                        if (!JsonLdUtils.isAbsoluteIri(baseUri)) {
                            throw new JsonLdError(Error.INVALID_BASE_IRI, baseUri);
                        }
                        result.put(JsonLdConsts.BASE, JsonLdUrl.resolve(baseUri, (String) value));
                    }
                } else {
                    // 3.4.5
                    throw new JsonLdError(JsonLdError.Error.INVALID_BASE_IRI,
                            "@base must be a string");
                }
            }

            // 3.5
            if (((Map) context).containsKey(JsonLdConsts.VOCAB)) {
                final Object value = ((Map) context).get(JsonLdConsts.VOCAB);
                if (value == null) {
                    result.remove(JsonLdConsts.VOCAB);
                } else if (value instanceof String) {
                    if (JsonLdUtils.isAbsoluteIri((String) value)) {
                        result.put(JsonLdConsts.VOCAB, value);
                    } else {
                        throw new JsonLdError(Error.INVALID_VOCAB_MAPPING,
                                "@value must be an absolute IRI");
                    }
                } else {
                    throw new JsonLdError(Error.INVALID_VOCAB_MAPPING,
                            "@vocab must be a string or null");
                }
            }

            // 3.6
            if (((Map) context).containsKey(JsonLdConsts.LANGUAGE)) {
                final Object value = ((Map) context).get(JsonLdConsts.LANGUAGE);
                if (value == null) {
                    result.remove(JsonLdConsts.LANGUAGE);
                } else if (value instanceof String) {
                    result.put(JsonLdConsts.LANGUAGE, ((String) value).toLowerCase());
                } else {
                    throw new JsonLdError(Error.INVALID_DEFAULT_LANGUAGE, value);
                }
            }

            // 3.7
            final Map defined = new LinkedHashMap();
            for (final String key : ((Map) context).keySet()) {
                if (JsonLdConsts.BASE.equals(key) || JsonLdConsts.VOCAB.equals(key)
                        || JsonLdConsts.LANGUAGE.equals(key)) {
                    continue;
                }
                result.createTermDefinition((Map) context, key, defined);
            }
        }
        return result;
    }

    private void checkEmptyKey(final Map map) {
        if (map.containsKey("")) {
            // the term MUST NOT be an empty string ("")
            // https://www.w3.org/TR/json-ld/#h3_terms
            throw new JsonLdError(Error.INVALID_TERM_DEFINITION,
                    String.format("empty key for value '%s'", map.get("")));
        }
    }

    public Context parse(Object localContext) throws JsonLdError {
        return this.parse(localContext, new ArrayList());
    }

    /**
     * Create Term Definition Algorithm
     *
     * http://json-ld.org/spec/latest/json-ld-api/#create-term-definition
     *
     * @param context
     * @param defined
     * @throws JsonLdError
     */
    private void createTermDefinition(Map context, String term,
            Map defined) throws JsonLdError {
        if (defined.containsKey(term)) {
            if (Boolean.TRUE.equals(defined.get(term))) {
                return;
            }
            throw new JsonLdError(Error.CYCLIC_IRI_MAPPING, term);
        }

        defined.put(term, false);

        if (JsonLdUtils.isKeyword(term)
                && !(options.getAllowContainerSetOnType() && JsonLdConsts.TYPE.equals(term)
                        && !(context.get(term)).toString().contains(JsonLdConsts.ID))) {
            throw new JsonLdError(Error.KEYWORD_REDEFINITION, term);
        }

        this.termDefinitions.remove(term);
        Object value = context.get(term);
        if (value == null || (value instanceof Map
                && ((Map) value).containsKey(JsonLdConsts.ID)
                && ((Map) value).get(JsonLdConsts.ID) == null)) {
            this.termDefinitions.put(term, null);
            defined.put(term, true);
            return;
        }

        if (value instanceof String) {
            value = newMap(JsonLdConsts.ID, value);
        }

        if (!(value instanceof Map)) {
            throw new JsonLdError(Error.INVALID_TERM_DEFINITION, value);
        }

        // casting the value so it doesn't have to be done below everytime
        final Map val = (Map) value;

        // 9) create a new term definition
        final Map definition = newMap();

        // 10)
        if (val.containsKey(JsonLdConsts.TYPE)) {
            if (!(val.get(JsonLdConsts.TYPE) instanceof String)) {
                throw new JsonLdError(Error.INVALID_TYPE_MAPPING, val.get(JsonLdConsts.TYPE));
            }
            String type = (String) val.get(JsonLdConsts.TYPE);
            try {
                type = this.expandIri((String) val.get(JsonLdConsts.TYPE), false, true, context,
                        defined);
            } catch (final JsonLdError error) {
                if (error.getType() != Error.INVALID_IRI_MAPPING) {
                    throw error;
                }
                throw new JsonLdError(Error.INVALID_TYPE_MAPPING, type, error);
            }
            // TODO: fix check for absoluteIri (blank nodes shouldn't count, at
            // least not here!)
            if (JsonLdConsts.ID.equals(type) || JsonLdConsts.VOCAB.equals(type)
                    || (!type.startsWith(JsonLdConsts.BLANK_NODE_PREFIX)
                            && JsonLdUtils.isAbsoluteIri(type))) {
                definition.put(JsonLdConsts.TYPE, type);
            } else {
                throw new JsonLdError(Error.INVALID_TYPE_MAPPING, type);
            }
        }

        // 11)
        if (val.containsKey(JsonLdConsts.REVERSE)) {
            if (val.containsKey(JsonLdConsts.ID)) {
                throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY, val);
            }
            if (!(val.get(JsonLdConsts.REVERSE) instanceof String)) {
                throw new JsonLdError(Error.INVALID_IRI_MAPPING,
                        "Expected String for @reverse value. got "
                                + (val.get(JsonLdConsts.REVERSE) == null ? "null"
                                        : val.get(JsonLdConsts.REVERSE).getClass()));
            }
            final String reverse = this.expandIri((String) val.get(JsonLdConsts.REVERSE), false,
                    true, context, defined);
            if (!JsonLdUtils.isAbsoluteIri(reverse)) {
                throw new JsonLdError(Error.INVALID_IRI_MAPPING,
                        "Non-absolute @reverse IRI: " + reverse);
            }
            definition.put(JsonLdConsts.ID, reverse);
            if (val.containsKey(JsonLdConsts.CONTAINER)) {
                final String container = (String) val.get(JsonLdConsts.CONTAINER);
                if (container == null || JsonLdConsts.SET.equals(container)
                        || JsonLdConsts.INDEX.equals(container)) {
                    definition.put(JsonLdConsts.CONTAINER, container);
                } else {
                    throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY,
                            "reverse properties only support set- and index-containers");
                }
            }
            definition.put(JsonLdConsts.REVERSE, true);
            this.termDefinitions.put(term, definition);
            defined.put(term, true);
            return;
        }

        // 12)
        definition.put(JsonLdConsts.REVERSE, false);

        // 13)
        if (val.get(JsonLdConsts.ID) != null && !term.equals(val.get(JsonLdConsts.ID))) {
            if (!(val.get(JsonLdConsts.ID) instanceof String)) {
                throw new JsonLdError(Error.INVALID_IRI_MAPPING,
                        "expected value of @id to be a string");
            }

            final String res = this.expandIri((String) val.get(JsonLdConsts.ID), false, true,
                    context, defined);
            if (JsonLdUtils.isKeyword(res) || JsonLdUtils.isAbsoluteIri(res)) {
                if (JsonLdConsts.CONTEXT.equals(res)) {
                    throw new JsonLdError(Error.INVALID_KEYWORD_ALIAS, "cannot alias @context");
                }
                definition.put(JsonLdConsts.ID, res);
            } else {
                throw new JsonLdError(Error.INVALID_IRI_MAPPING,
                        "resulting IRI mapping should be a keyword, absolute IRI or blank node");
            }
        }

        // 14)
        else if (term.indexOf(":") >= 0) {
            final int colIndex = term.indexOf(":");
            final String prefix = term.substring(0, colIndex);
            final String suffix = term.substring(colIndex + 1);
            if (context.containsKey(prefix)) {
                this.createTermDefinition(context, prefix, defined);
            }
            if (termDefinitions.containsKey(prefix)) {
                definition.put(JsonLdConsts.ID,
                        ((Map) termDefinitions.get(prefix)).get(JsonLdConsts.ID)
                                + suffix);
            } else {
                definition.put(JsonLdConsts.ID, term);
            }
            // 15)
        } else if (this.containsKey(JsonLdConsts.VOCAB)) {
            definition.put(JsonLdConsts.ID, this.get(JsonLdConsts.VOCAB) + term);
        } else if (!JsonLdConsts.TYPE.equals(term)) {
            throw new JsonLdError(Error.INVALID_IRI_MAPPING,
                    "relative term definition without vocab mapping");
        }

        // 16)
        if (val.containsKey(JsonLdConsts.CONTAINER)) {
            final String container = (String) val.get(JsonLdConsts.CONTAINER);
            if (!JsonLdConsts.LIST.equals(container) && !JsonLdConsts.SET.equals(container)
                    && !JsonLdConsts.INDEX.equals(container)
                    && !JsonLdConsts.LANGUAGE.equals(container)) {
                throw new JsonLdError(Error.INVALID_CONTAINER_MAPPING,
                        "@container must be either @list, @set, @index, or @language");
            }
            definition.put(JsonLdConsts.CONTAINER, container);
            if (JsonLdConsts.TYPE.equals(term)) {
                definition.put(JsonLdConsts.ID, "type");
            }
        }

        // 17)
        if (val.containsKey(JsonLdConsts.LANGUAGE) && !val.containsKey(JsonLdConsts.TYPE)) {
            if (val.get(JsonLdConsts.LANGUAGE) == null
                    || val.get(JsonLdConsts.LANGUAGE) instanceof String) {
                final String language = (String) val.get(JsonLdConsts.LANGUAGE);
                definition.put(JsonLdConsts.LANGUAGE,
                        language != null ? language.toLowerCase() : null);
            } else {
                throw new JsonLdError(Error.INVALID_LANGUAGE_MAPPING,
                        "@language must be a string or null");
            }
        }

        // 18)
        this.termDefinitions.put(term, definition);
        defined.put(term, true);
    }

    /**
     * IRI Expansion Algorithm
     *
     * http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
     *
     * @param value
     * @param relative
     * @param vocab
     * @param context
     * @param defined
     * @return
     * @throws JsonLdError
     */
    String expandIri(String value, boolean relative, boolean vocab, Map context,
            Map defined) throws JsonLdError {
        // 1)
        if (value == null || JsonLdUtils.isKeyword(value)) {
            return value;
        }
        // 2)
        if (context != null && context.containsKey(value)
                && !Boolean.TRUE.equals(defined.get(value))) {
            this.createTermDefinition(context, value, defined);
        }
        // 3)
        if (vocab && this.termDefinitions.containsKey(value)) {
            final Map td = (Map) this.termDefinitions.get(value);
            if (td != null) {
                return (String) td.get(JsonLdConsts.ID);
            } else {
                return null;
            }
        }
        // 4)
        final int colIndex = value.indexOf(":");
        if (colIndex >= 0) {
            // 4.1)
            final String prefix = value.substring(0, colIndex);
            final String suffix = value.substring(colIndex + 1);
            // 4.2)
            if ("_".equals(prefix) || suffix.startsWith("//")) {
                return value;
            }
            // 4.3)
            if (context != null && context.containsKey(prefix)
                    && (!defined.containsKey(prefix) || defined.get(prefix) == false)) {
                this.createTermDefinition(context, prefix, defined);
            }
            // 4.4)
            if (this.termDefinitions.containsKey(prefix)) {
                return (String) ((Map) this.termDefinitions.get(prefix))
                        .get(JsonLdConsts.ID) + suffix;
            }
            // 4.5)
            return value;
        }
        // 5)
        if (vocab && this.containsKey(JsonLdConsts.VOCAB)) {
            return this.get(JsonLdConsts.VOCAB) + value;
        }
        // 6)
        else if (relative) {
            return JsonLdUrl.resolve((String) this.get(JsonLdConsts.BASE), value);
        } else if (context != null && JsonLdUtils.isRelativeIri(value)) {
            throw new JsonLdError(Error.INVALID_IRI_MAPPING, "not an absolute IRI: " + value);
        }
        // 7)
        return value;
    }

    /**
     * IRI Compaction Algorithm
     *
     * http://json-ld.org/spec/latest/json-ld-api/#iri-compaction
     *
     * Compacts an IRI or keyword into a term or prefix if it can be. If the IRI
     * has an associated value it may be passed.
     *
     * @param iri
     *            the IRI to compact.
     * @param value
     *            the value to check or null.
     * @param relativeToVocab
     *            options for how to compact IRIs: vocab: true to split
     *            after @vocab, false not to.
     * @param reverse
     *            true if a reverse property is being compacted, false if not.
     *
     * @return the compacted term, prefix, keyword alias, or the original IRI.
     */
    String compactIri(String iri, Object value, boolean relativeToVocab, boolean reverse) {
        // 1)
        if (iri == null) {
            return null;
        }

        // 2)
        if (relativeToVocab && getInverse().containsKey(iri)) {
            // 2.1)
            String defaultLanguage = (String) this.get(JsonLdConsts.LANGUAGE);
            if (defaultLanguage == null) {
                defaultLanguage = JsonLdConsts.NONE;
            }

            // 2.2)
            final List containers = new ArrayList();
            // 2.3)
            String typeLanguage = JsonLdConsts.LANGUAGE;
            String typeLanguageValue = JsonLdConsts.NULL;

            // 2.4)
            if (value instanceof Map
                    && ((Map) value).containsKey(JsonLdConsts.INDEX)) {
                containers.add(JsonLdConsts.INDEX);
            }

            // 2.5)
            if (reverse) {
                typeLanguage = JsonLdConsts.TYPE;
                typeLanguageValue = JsonLdConsts.REVERSE;
                containers.add(JsonLdConsts.SET);
            }
            // 2.6)
            else if (value instanceof Map
                    && ((Map) value).containsKey(JsonLdConsts.LIST)) {
                // 2.6.1)
                if (!((Map) value).containsKey(JsonLdConsts.INDEX)) {
                    containers.add(JsonLdConsts.LIST);
                }
                // 2.6.2)
                final List list = (List) ((Map) value)
                        .get(JsonLdConsts.LIST);
                // 2.6.3)
                String commonLanguage = (list.size() == 0) ? defaultLanguage : null;
                String commonType = null;
                // 2.6.4)
                for (final Object item : list) {
                    // 2.6.4.1)
                    String itemLanguage = JsonLdConsts.NONE;
                    String itemType = JsonLdConsts.NONE;
                    // 2.6.4.2)
                    if (JsonLdUtils.isValue(item)) {
                        // 2.6.4.2.1)
                        if (((Map) item).containsKey(JsonLdConsts.LANGUAGE)) {
                            itemLanguage = (String) ((Map) item)
                                    .get(JsonLdConsts.LANGUAGE);
                        }
                        // 2.6.4.2.2)
                        else if (((Map) item).containsKey(JsonLdConsts.TYPE)) {
                            itemType = (String) ((Map) item).get(JsonLdConsts.TYPE);
                        }
                        // 2.6.4.2.3)
                        else {
                            itemLanguage = JsonLdConsts.NULL;
                        }
                    }
                    // 2.6.4.3)
                    else {
                        itemType = JsonLdConsts.ID;
                    }
                    // 2.6.4.4)
                    if (commonLanguage == null) {
                        commonLanguage = itemLanguage;
                    }
                    // 2.6.4.5)
                    else if (!commonLanguage.equals(itemLanguage) && JsonLdUtils.isValue(item)) {
                        commonLanguage = JsonLdConsts.NONE;
                    }
                    // 2.6.4.6)
                    if (commonType == null) {
                        commonType = itemType;
                    }
                    // 2.6.4.7)
                    else if (!commonType.equals(itemType)) {
                        commonType = JsonLdConsts.NONE;
                    }
                    // 2.6.4.8)
                    if (JsonLdConsts.NONE.equals(commonLanguage)
                            && JsonLdConsts.NONE.equals(commonType)) {
                        break;
                    }
                }
                // 2.6.5)
                commonLanguage = (commonLanguage != null) ? commonLanguage : JsonLdConsts.NONE;
                // 2.6.6)
                commonType = (commonType != null) ? commonType : JsonLdConsts.NONE;
                // 2.6.7)
                if (!JsonLdConsts.NONE.equals(commonType)) {
                    typeLanguage = JsonLdConsts.TYPE;
                    typeLanguageValue = commonType;
                }
                // 2.6.8)
                else {
                    typeLanguageValue = commonLanguage;
                }
            }
            // 2.7)
            else {
                // 2.7.1)
                if (value instanceof Map
                        && ((Map) value).containsKey(JsonLdConsts.VALUE)) {
                    // 2.7.1.1)
                    if (((Map) value).containsKey(JsonLdConsts.LANGUAGE)
                            && !((Map) value).containsKey(JsonLdConsts.INDEX)) {
                        containers.add(JsonLdConsts.LANGUAGE);
                        typeLanguageValue = (String) ((Map) value)
                                .get(JsonLdConsts.LANGUAGE);
                    }
                    // 2.7.1.2)
                    else if (((Map) value).containsKey(JsonLdConsts.TYPE)) {
                        typeLanguage = JsonLdConsts.TYPE;
                        typeLanguageValue = (String) ((Map) value)
                                .get(JsonLdConsts.TYPE);
                    }
                }
                // 2.7.2)
                else {
                    typeLanguage = JsonLdConsts.TYPE;
                    typeLanguageValue = JsonLdConsts.ID;
                }
                // 2.7.3)
                containers.add(JsonLdConsts.SET);
            }

            // 2.8)
            containers.add(JsonLdConsts.NONE);
            // 2.9)
            if (typeLanguageValue == null) {
                typeLanguageValue = JsonLdConsts.NULL;
            }
            // 2.10)
            final List preferredValues = new ArrayList();
            // 2.11)
            if (JsonLdConsts.REVERSE.equals(typeLanguageValue)) {
                preferredValues.add(JsonLdConsts.REVERSE);
            }
            // 2.12)
            if ((JsonLdConsts.REVERSE.equals(typeLanguageValue)
                    || JsonLdConsts.ID.equals(typeLanguageValue)) && (value instanceof Map)
                    && ((Map) value).containsKey(JsonLdConsts.ID)) {
                // 2.12.1)
                final String result = this.compactIri(
                        (String) ((Map) value).get(JsonLdConsts.ID), null, true,
                        true);
                if (termDefinitions.containsKey(result)
                        && ((Map) termDefinitions.get(result))
                                .containsKey(JsonLdConsts.ID)
                        && ((Map) value).get(JsonLdConsts.ID)
                                .equals(((Map) termDefinitions.get(result))
                                        .get(JsonLdConsts.ID))) {
                    preferredValues.add(JsonLdConsts.VOCAB);
                    preferredValues.add(JsonLdConsts.ID);
                }
                // 2.12.2)
                else {
                    preferredValues.add(JsonLdConsts.ID);
                    preferredValues.add(JsonLdConsts.VOCAB);
                }
            }
            // 2.13)
            else {
                preferredValues.add(typeLanguageValue);
            }
            preferredValues.add(JsonLdConsts.NONE);

            // 2.14)
            final String term = selectTerm(iri, containers, typeLanguage, preferredValues);
            // 2.15)
            if (term != null) {
                return term;
            }
        }

        // 3)
        if (relativeToVocab && this.containsKey(JsonLdConsts.VOCAB)) {
            // determine if vocab is a prefix of the iri
            final String vocab = (String) this.get(JsonLdConsts.VOCAB);
            // 3.1)
            if (iri.indexOf(vocab) == 0 && !iri.equals(vocab)) {
                // use suffix as relative iri if it is not a term in the
                // active context
                final String suffix = iri.substring(vocab.length());
                if (!termDefinitions.containsKey(suffix)) {
                    return suffix;
                }
            }
        }

        // 4)
        String compactIRI = null;
        // 5)
        for (final String term : termDefinitions.keySet()) {
            final Map termDefinition = (Map) termDefinitions
                    .get(term);
            // 5.1)
            if (term.contains(":")) {
                continue;
            }
            // 5.2)
            if (termDefinition == null || iri.equals(termDefinition.get(JsonLdConsts.ID))
                    || !iri.startsWith((String) termDefinition.get(JsonLdConsts.ID))) {
                continue;
            }

            // 5.3)
            final String candidate = term + ":"
                    + iri.substring(((String) termDefinition.get(JsonLdConsts.ID)).length());
            // 5.4)
            compactIRI = _iriCompactionStep5point4(iri, value, compactIRI, candidate,
                    termDefinitions);
        }

        // 6)
        if (compactIRI != null) {
            return compactIRI;
        }

        // 7)
        if (!relativeToVocab) {
            return JsonLdUrl.removeBase(this.get(JsonLdConsts.BASE), iri);
        }

        // 8)
        return iri;
    }

    /*
     * This method is only visible for testing.
     */
    public static String _iriCompactionStep5point4(String iri, Object value, String compactIRI,
            final String candidate, Map termDefinitions) {

        final boolean condition1 = (compactIRI == null
                || compareShortestLeast(candidate, compactIRI) < 0);

        final boolean condition2 = (!termDefinitions.containsKey(candidate) || (iri
                .equals(((Map) termDefinitions.get(candidate)).get(JsonLdConsts.ID))
                && value == null));

        if (condition1 && condition2) {
            compactIRI = candidate;
        }
        return compactIRI;
    }

    /**
     * Return a map of potential RDF prefixes based on the JSON-LD Term
     * Definitions in this context.
     * 

* No guarantees of the prefixes are given, beyond that it will not contain * ":". * * @param onlyCommonPrefixes * If true, the result will not include "not so * useful" prefixes, such as "term1": "http://example.com/term1", * e.g. all IRIs will end with "/" or "#". If false, * all potential prefixes are returned. * * @return A map from prefix string to IRI string */ public Map getPrefixes(boolean onlyCommonPrefixes) { final Map prefixes = new LinkedHashMap(); for (final String term : termDefinitions.keySet()) { if (term.contains(":")) { continue; } final Map termDefinition = (Map) termDefinitions .get(term); if (termDefinition == null) { continue; } final String id = (String) termDefinition.get(JsonLdConsts.ID); if (id == null) { continue; } if (term.startsWith("@") || id.startsWith("@")) { continue; } if (!onlyCommonPrefixes || id.endsWith("/") || id.endsWith("#")) { prefixes.put(term, id); } } return prefixes; } String compactIri(String iri, boolean relativeToVocab) { return compactIri(iri, null, relativeToVocab, false); } String compactIri(String iri) { return compactIri(iri, null, false, false); } @Override public Context clone() { final Context rval = (Context) super.clone(); // TODO: is this shallow copy enough? probably not, but it passes all // the tests! rval.termDefinitions = new LinkedHashMap(this.termDefinitions); return rval; } /** * Inverse Context Creation * * http://json-ld.org/spec/latest/json-ld-api/#inverse-context-creation * * Generates an inverse context for use in the compaction algorithm, if not * already generated for the given active context. * * @return the inverse context. */ public Map getInverse() { // lazily create inverse if (inverse != null) { return inverse; } // 1) inverse = newMap(); // 2) String defaultLanguage = (String) this.get(JsonLdConsts.LANGUAGE); if (defaultLanguage == null) { defaultLanguage = JsonLdConsts.NONE; } // create term selections for each mapping in the context, ordererd by // shortest and then lexicographically least final List terms = new ArrayList(termDefinitions.keySet()); Collections.sort(terms, new Comparator() { @Override public int compare(String a, String b) { return compareShortestLeast(a, b); } }); for (final String term : terms) { final Map definition = (Map) termDefinitions.get(term); // 3.1) if (definition == null) { continue; } // 3.2) String container = (String) definition.get(JsonLdConsts.CONTAINER); if (container == null) { container = JsonLdConsts.NONE; } // 3.3) final String iri = (String) definition.get(JsonLdConsts.ID); // 3.4 + 3.5) Map containerMap = (Map) inverse.get(iri); if (containerMap == null) { containerMap = newMap(); inverse.put(iri, containerMap); } // 3.6 + 3.7) Map typeLanguageMap = (Map) containerMap.get(container); if (typeLanguageMap == null) { typeLanguageMap = newMap(); typeLanguageMap.put(JsonLdConsts.LANGUAGE, newMap()); typeLanguageMap.put(JsonLdConsts.TYPE, newMap()); containerMap.put(container, typeLanguageMap); } // 3.8) if (Boolean.TRUE.equals(definition.get(JsonLdConsts.REVERSE))) { final Map typeMap = (Map) typeLanguageMap .get(JsonLdConsts.TYPE); if (!typeMap.containsKey(JsonLdConsts.REVERSE)) { typeMap.put(JsonLdConsts.REVERSE, term); } // 3.9) } else if (definition.containsKey(JsonLdConsts.TYPE)) { final Map typeMap = (Map) typeLanguageMap .get(JsonLdConsts.TYPE); if (!typeMap.containsKey(definition.get(JsonLdConsts.TYPE))) { typeMap.put((String) definition.get(JsonLdConsts.TYPE), term); } // 3.10) } else if (definition.containsKey(JsonLdConsts.LANGUAGE)) { final Map languageMap = (Map) typeLanguageMap .get(JsonLdConsts.LANGUAGE); String language = (String) definition.get(JsonLdConsts.LANGUAGE); if (language == null) { language = JsonLdConsts.NULL; } if (!languageMap.containsKey(language)) { languageMap.put(language, term); } // 3.11) } else { // 3.11.1) final Map languageMap = (Map) typeLanguageMap .get(JsonLdConsts.LANGUAGE); // 3.11.2) if (!languageMap.containsKey(JsonLdConsts.LANGUAGE)) { languageMap.put(JsonLdConsts.LANGUAGE, term); } // 3.11.3) if (!languageMap.containsKey(JsonLdConsts.NONE)) { languageMap.put(JsonLdConsts.NONE, term); } // 3.11.4) final Map typeMap = (Map) typeLanguageMap .get(JsonLdConsts.TYPE); // 3.11.5) if (!typeMap.containsKey(JsonLdConsts.NONE)) { typeMap.put(JsonLdConsts.NONE, term); } } } // 4) return inverse; } /** * Term Selection * * http://json-ld.org/spec/latest/json-ld-api/#term-selection * * This algorithm, invoked via the IRI Compaction algorithm, makes use of an * active context's inverse context to find the term that is best used to * compact an IRI. Other information about a value associated with the IRI * is given, including which container mappings and which type mapping or * language mapping would be best used to express the value. * * @return the selected term. */ private String selectTerm(String iri, List containers, String typeLanguage, List preferredValues) { final Map inv = getInverse(); // 1) final Map containerMap = (Map) inv.get(iri); // 2) for (final String container : containers) { // 2.1) if (!containerMap.containsKey(container)) { continue; } // 2.2) final Map typeLanguageMap = (Map) containerMap .get(container); // 2.3) final Map valueMap = (Map) typeLanguageMap .get(typeLanguage); // 2.4 ) for (final String item : preferredValues) { // 2.4.1 if (!valueMap.containsKey(item)) { continue; } // 2.4.2 return (String) valueMap.get(item); } } // 3) return null; } /** * Retrieve container mapping. * * @param property * The Property to get a container mapping for. * @return The container mapping if any, else null */ public String getContainer(String property) { if (property == null) { return null; } if (JsonLdConsts.GRAPH.equals(property)) { return JsonLdConsts.SET; } if (!property.equals(JsonLdConsts.TYPE) && JsonLdUtils.isKeyword(property)) { return property; } final Map td = (Map) termDefinitions.get(property); if (td == null) { return null; } return (String) td.get(JsonLdConsts.CONTAINER); } public Boolean isReverseProperty(String property) { final Map td = (Map) termDefinitions.get(property); if (td == null) { return false; } final Object reverse = td.get(JsonLdConsts.REVERSE); return reverse != null && (Boolean) reverse; } public String getTypeMapping(String property) { final Map td = (Map) termDefinitions.get(property); if (td == null) { return null; } return (String) td.get(JsonLdConsts.TYPE); } public String getLanguageMapping(String property) { final Map td = (Map) termDefinitions.get(property); if (td == null) { return null; } return (String) td.get(JsonLdConsts.LANGUAGE); } Map getTermDefinition(String key) { return ((Map) termDefinitions.get(key)); } public Object expandValue(String activeProperty, Object value) throws JsonLdError { final Map rval = newMap(); final Map td = getTermDefinition(activeProperty); // 1) if (td != null && JsonLdConsts.ID.equals(td.get(JsonLdConsts.TYPE))) { // TODO: i'm pretty sure value should be a string if the @type is // @id rval.put(JsonLdConsts.ID, expandIri(value.toString(), true, false, null, null)); return rval; } // 2) if (td != null && JsonLdConsts.VOCAB.equals(td.get(JsonLdConsts.TYPE))) { // TODO: same as above rval.put(JsonLdConsts.ID, expandIri(value.toString(), true, true, null, null)); return rval; } // 3) rval.put(JsonLdConsts.VALUE, value); // 4) if (td != null && td.containsKey(JsonLdConsts.TYPE)) { rval.put(JsonLdConsts.TYPE, td.get(JsonLdConsts.TYPE)); } // 5) else if (value instanceof String) { // 5.1) if (td != null && td.containsKey(JsonLdConsts.LANGUAGE)) { final String lang = (String) td.get(JsonLdConsts.LANGUAGE); if (lang != null) { rval.put(JsonLdConsts.LANGUAGE, lang); } } // 5.2) else if (this.get(JsonLdConsts.LANGUAGE) != null) { rval.put(JsonLdConsts.LANGUAGE, this.get(JsonLdConsts.LANGUAGE)); } } return rval; } @Deprecated public Map serialize() { final Map ctx = newMap(); if (this.get(JsonLdConsts.BASE) != null && !this.get(JsonLdConsts.BASE).equals(options.getBase())) { ctx.put(JsonLdConsts.BASE, this.get(JsonLdConsts.BASE)); } if (this.get(JsonLdConsts.LANGUAGE) != null) { ctx.put(JsonLdConsts.LANGUAGE, this.get(JsonLdConsts.LANGUAGE)); } if (this.get(JsonLdConsts.VOCAB) != null) { ctx.put(JsonLdConsts.VOCAB, this.get(JsonLdConsts.VOCAB)); } for (final String term : termDefinitions.keySet()) { final Map definition = (Map) termDefinitions.get(term); if (definition.get(JsonLdConsts.LANGUAGE) == null && definition.get(JsonLdConsts.CONTAINER) == null && definition.get(JsonLdConsts.TYPE) == null && (definition.get(JsonLdConsts.REVERSE) == null || Boolean.FALSE.equals(definition.get(JsonLdConsts.REVERSE)))) { final String cid = this.compactIri((String) definition.get(JsonLdConsts.ID)); ctx.put(term, term.equals(cid) ? definition.get(JsonLdConsts.ID) : cid); } else { final Map defn = newMap(); final String cid = this.compactIri((String) definition.get(JsonLdConsts.ID)); final Boolean reverseProperty = Boolean.TRUE .equals(definition.get(JsonLdConsts.REVERSE)); if (!(term.equals(cid) && !reverseProperty)) { defn.put(reverseProperty ? JsonLdConsts.REVERSE : JsonLdConsts.ID, cid); } final String typeMapping = (String) definition.get(JsonLdConsts.TYPE); if (typeMapping != null) { defn.put(JsonLdConsts.TYPE, JsonLdUtils.isKeyword(typeMapping) ? typeMapping : compactIri(typeMapping, true)); } if (definition.get(JsonLdConsts.CONTAINER) != null) { defn.put(JsonLdConsts.CONTAINER, definition.get(JsonLdConsts.CONTAINER)); } final Object lang = definition.get(JsonLdConsts.LANGUAGE); if (definition.get(JsonLdConsts.LANGUAGE) != null) { defn.put(JsonLdConsts.LANGUAGE, Boolean.FALSE.equals(lang) ? null : lang); } ctx.put(term, defn); } } final Map rval = newMap(); if (!(ctx == null || ctx.isEmpty())) { rval.put(JsonLdConsts.CONTEXT, ctx); } return rval; } }