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

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

package com.github.jsonldjava.core;

import static com.github.jsonldjava.core.JSONLDConsts.RDF_FIRST;
import static com.github.jsonldjava.core.JSONLDConsts.RDF_LIST;
import static com.github.jsonldjava.core.JSONLDConsts.RDF_NIL;
import static com.github.jsonldjava.core.JSONLDConsts.RDF_REST;
import static com.github.jsonldjava.core.JSONLDConsts.RDF_TYPE;
import static com.github.jsonldjava.core.JsonLdUtils.isKeyword;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class JsonLdApi {

    private static final Logger LOG = LoggerFactory.getLogger(JsonLdApi.class);

    JsonLdOptions opts;
    Object value = null;
    Context context = null;

    public JsonLdApi() {
        opts = new JsonLdOptions("");
    }

    public JsonLdApi(Object input, JsonLdOptions opts) throws JsonLdError {
        initialize(input, null, opts);
    }

    public JsonLdApi(Object input, Object context, JsonLdOptions opts) throws JsonLdError {
        initialize(input, null, opts);
    }

    public JsonLdApi(JsonLdOptions opts) {
        if (opts == null) {
            opts = new JsonLdOptions("");
        } else {
            this.opts = opts;
        }
    }

    private void initialize(Object input, Object context, JsonLdOptions opts) throws JsonLdError {
        // set option defaults (TODO: clone?)
        // NOTE: sane defaults should be set in JsonLdOptions constructor
        this.opts = opts;

        if (input instanceof List || input instanceof Map) {
            this.value = JsonLdUtils.clone(input);
        }
        // TODO: string/IO input
        this.context = new Context(opts);
        if (context != null) {
            this.context = this.context.parse(context);
        }
    }

    /***
     * ____ _ _ _ _ _ _ / ___|___ _ __ ___ _ __ __ _ ___| |_ / \ | | __ _ ___ _
     * __(_) |_| |__ _ __ ___ | | / _ \| '_ ` _ \| '_ \ / _` |/ __| __| / _ \ |
     * |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ | |__| (_) | | | | | | |_) | (_|
     * | (__| |_ / ___ \| | (_| | (_) | | | | |_| | | | | | | | | \____\___/|_|
     * |_| |_| .__/ \__,_|\___|\__| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_|
     * |_| |_| |___/
     */

    /**
     * Compaction Algorithm
     * 
     * http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
     * 
     * 
     * @param activeCtx
     * @param activeProperty
     * @param element
     * @param compactArrays
     * @return
     */
    public Object compact(Context activeCtx, String activeProperty, Object element,
            boolean compactArrays) throws JsonLdError {
        // 2)
        if (element instanceof List) {
            // 2.1)
            final List result = new ArrayList();
            // 2.2)
            for (final Object item : (List) element) {
                // 2.2.1)
                final Object compactedItem = compact(activeCtx, activeProperty, item, compactArrays);
                // 2.2.2)
                if (compactedItem != null) {
                    result.add(compactedItem);
                }
            }
            // 2.3)
            if (compactArrays && result.size() == 1
                    && activeCtx.getContainer(activeProperty) == null) {
                return result.get(0);
            }
            // 2.4)
            return result;
        }

        // 3)
        if (element instanceof Map) {
            // access helper
            final Map elem = (Map) element;

            // 4
            if (elem.containsKey("@value") || elem.containsKey("@id")) {
                final Object compactedValue = activeCtx.compactValue(activeProperty, elem);
                if (!(compactedValue instanceof Map || compactedValue instanceof List)) {
                    return compactedValue;
                }
            }
            // 5)
            final boolean insideReverse = ("@reverse".equals(activeProperty));

            // 6)
            final Map result = new LinkedHashMap();
            // 7)
            final List keys = new ArrayList(elem.keySet());
            Collections.sort(keys);
            for (final String expandedProperty : keys) {
                final Object expandedValue = elem.get(expandedProperty);

                // 7.1)
                if ("@id".equals(expandedProperty) || "@type".equals(expandedProperty)) {
                    Object compactedValue;

                    // 7.1.1)
                    if (expandedValue instanceof String) {
                        compactedValue = activeCtx.compactIri((String) expandedValue,
                                "@type".equals(expandedProperty));
                    }
                    // 7.1.2)
                    else {
                        final List types = new ArrayList();
                        // 7.1.2.2)
                        for (final String expandedType : (List) expandedValue) {
                            types.add(activeCtx.compactIri(expandedType, true));
                        }
                        // 7.1.2.3)
                        if (types.size() == 1) {
                            compactedValue = types.get(0);
                        } else {
                            compactedValue = types;
                        }
                    }

                    // 7.1.3)
                    final String alias = activeCtx.compactIri(expandedProperty, true);
                    // 7.1.4)
                    result.put(alias, compactedValue);
                    continue;
                    // TODO: old add value code, see if it's still relevant?
                    // addValue(rval, alias, compactedValue,
                    // isArray(compactedValue)
                    // && ((List) expandedValue).size() == 0);
                }

                // 7.2)
                if ("@reverse".equals(expandedProperty)) {
                    // 7.2.1)
                    final Map compactedValue = (Map) compact(
                            activeCtx, "@reverse", expandedValue, compactArrays);

                    // 7.2.2)
                    for (final String property : compactedValue.keySet()) {
                        final Object value = compactedValue.get(property);
                        // 7.2.2.1)
                        if (activeCtx.isReverseProperty(property)) {
                            // 7.2.2.1.1)
                            if (("@set".equals(activeCtx.getContainer(property)) || !compactArrays)
                                    && !(value instanceof List)) {
                                final List tmp = new ArrayList();
                                tmp.add(value);
                                result.put(property, tmp);
                            }
                            // 7.2.2.1.2)
                            if (!result.containsKey(property)) {
                                result.put(property, value);
                            }
                            // 7.2.2.1.3)
                            else {
                                if (!(result.get(property) instanceof List)) {
                                    final List tmp = new ArrayList();
                                    tmp.add(result.put(property, tmp));
                                }
                                if (value instanceof List) {
                                    ((List) result.get(property))
                                            .addAll((List) value);
                                } else {
                                    ((List) result.get(property)).add(value);
                                }
                            }
                            // 7.2.2.1.4) TODO: this doesn't seem safe (i.e.
                            // modifying the map being used to drive the loop)!
                            compactedValue.remove(property);
                        }
                    }
                    // 7.2.3)
                    if (!compactedValue.isEmpty()) {
                        // 7.2.3.1)
                        final String alias = activeCtx.compactIri("@reverse", true);
                        // 7.2.3.2)
                        result.put(alias, compactedValue);
                    }
                    // 7.2.4)
                    continue;
                }

                // 7.3)
                if ("@index".equals(expandedProperty)
                        && "@index".equals(activeCtx.getContainer(activeProperty))) {
                    continue;
                }
                // 7.4)
                else if ("@index".equals(expandedProperty) || "@value".equals(expandedProperty)
                        || "@language".equals(expandedProperty)) {
                    // 7.4.1)
                    final String alias = activeCtx.compactIri(expandedProperty, true);
                    // 7.4.2)
                    result.put(alias, expandedValue);
                    continue;
                }

                // NOTE: expanded value must be an array due to expansion
                // algorithm.

                // 7.5)
                if (((List) expandedValue).size() == 0) {
                    // 7.5.1)
                    final String itemActiveProperty = activeCtx.compactIri(expandedProperty,
                            expandedValue, true, insideReverse);
                    // 7.5.2)
                    if (!result.containsKey(itemActiveProperty)) {
                        result.put(itemActiveProperty, new ArrayList());
                    } else {
                        final Object value = result.get(itemActiveProperty);
                        if (!(value instanceof List)) {
                            final List tmp = new ArrayList();
                            tmp.add(value);
                            result.put(itemActiveProperty, tmp);
                        }
                    }
                }

                // 7.6)
                for (final Object expandedItem : (List) expandedValue) {
                    // 7.6.1)
                    final String itemActiveProperty = activeCtx.compactIri(expandedProperty,
                            expandedItem, true, insideReverse);
                    // 7.6.2)
                    final String container = activeCtx.getContainer(itemActiveProperty);

                    // get @list value if appropriate
                    final boolean isList = (expandedItem instanceof Map && ((Map) expandedItem)
                            .containsKey("@list"));
                    Object list = null;
                    if (isList) {
                        list = ((Map) expandedItem).get("@list");
                    }

                    // 7.6.3)
                    Object compactedItem = compact(activeCtx, itemActiveProperty, isList ? list
                            : expandedItem, compactArrays);

                    // 7.6.4)
                    if (isList) {
                        // 7.6.4.1)
                        if (!(compactedItem instanceof List)) {
                            final List tmp = new ArrayList();
                            tmp.add(compactedItem);
                            compactedItem = tmp;
                        }
                        // 7.6.4.2)
                        if (!"@list".equals(container)) {
                            // 7.6.4.2.1)
                            final Map wrapper = new LinkedHashMap();
                            // TODO: SPEC: no mention of vocab = true
                            wrapper.put(activeCtx.compactIri("@list", true), compactedItem);
                            compactedItem = wrapper;

                            // 7.6.4.2.2)
                            if (((Map) expandedItem).containsKey("@index")) {
                                ((Map) compactedItem).put(
                                        // TODO: SPEC: no mention of vocab =
                                        // true
                                        activeCtx.compactIri("@index", true),
                                        ((Map) expandedItem).get("@index"));
                            }
                        }
                        // 7.6.4.3)
                        else if (result.containsKey(itemActiveProperty)) {
                            throw new JsonLdError(Error.COMPACTION_TO_LIST_OF_LISTS,
                                    "There cannot be two list objects associated with an active property that has a container mapping");
                        }
                    }

                    // 7.6.5)
                    if ("@language".equals(container) || "@index".equals(container)) {
                        // 7.6.5.1)
                        Map mapObject;
                        if (result.containsKey(itemActiveProperty)) {
                            mapObject = (Map) result.get(itemActiveProperty);
                        } else {
                            mapObject = new LinkedHashMap();
                            result.put(itemActiveProperty, mapObject);
                        }

                        // 7.6.5.2)
                        if ("@language".equals(container)
                                && (compactedItem instanceof Map && ((Map) compactedItem)
                                        .containsKey("@value"))) {
                            compactedItem = ((Map) compactedItem).get("@value");
                        }

                        // 7.6.5.3)
                        final String mapKey = (String) ((Map) expandedItem)
                                .get(container);
                        // 7.6.5.4)
                        if (!mapObject.containsKey(mapKey)) {
                            mapObject.put(mapKey, compactedItem);
                        } else {
                            List tmp;
                            if (!(mapObject.get(mapKey) instanceof List)) {
                                tmp = new ArrayList();
                                tmp.add(mapObject.put(mapKey, tmp));
                            } else {
                                tmp = (List) mapObject.get(mapKey);
                            }
                            tmp.add(compactedItem);
                        }
                    }
                    // 7.6.6)
                    else {
                        // 7.6.6.1)
                        final Boolean check = (!compactArrays || "@set".equals(container)
                                || "@list".equals(container) || "@list".equals(expandedProperty) || "@graph"
                                    .equals(expandedProperty))
                                && (!(compactedItem instanceof List));
                        if (check) {
                            final List tmp = new ArrayList();
                            tmp.add(compactedItem);
                            compactedItem = tmp;
                        }
                        // 7.6.6.2)
                        if (!result.containsKey(itemActiveProperty)) {
                            result.put(itemActiveProperty, compactedItem);
                        } else {
                            if (!(result.get(itemActiveProperty) instanceof List)) {
                                final List tmp = new ArrayList();
                                tmp.add(result.put(itemActiveProperty, tmp));
                            }
                            if (compactedItem instanceof List) {
                                ((List) result.get(itemActiveProperty))
                                        .addAll((List) compactedItem);
                            } else {
                                ((List) result.get(itemActiveProperty)).add(compactedItem);
                            }
                        }

                    }
                }
            }
            // 8)
            return result;
        }

        // 2)
        return element;
    }

    public Object compact(Context activeCtx, String activeProperty, Object element)
            throws JsonLdError {
        return compact(activeCtx, activeProperty, element, true);
    }

    /***
     * _____ _ _ _ _ _ _ | ____|_ ___ __ __ _ _ __ __| | / \ | | __ _ ___ _
     * __(_) |_| |__ _ __ ___ | _| \ \/ / '_ \ / _` | '_ \ / _` | / _ \ | |/ _`
     * |/ _ \| '__| | __| '_ \| '_ ` _ \ | |___ > <| |_) | (_| | | | | (_| | /
     * ___ \| | (_| | (_) | | | | |_| | | | | | | | | |_____/_/\_\ .__/ \__,_|_|
     * |_|\__,_| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| |_| |___/
     */

    /**
     * Expansion Algorithm
     * 
     * http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
     * 
     * @param activeCtx
     * @param activeProperty
     * @param element
     * @return
     * @throws JsonLdError
     */
    public Object expand(Context activeCtx, String activeProperty, Object element)
            throws JsonLdError {
        // 1)
        if (element == null) {
            return null;
        }

        // 3)
        if (element instanceof List) {
            // 3.1)
            final List result = new ArrayList();
            // 3.2)
            for (final Object item : (List) element) {
                // 3.2.1)
                final Object v = expand(activeCtx, activeProperty, item);
                // 3.2.2)
                if (("@list".equals(activeProperty) || "@list".equals(activeCtx
                        .getContainer(activeProperty)))
                        && (v instanceof List || (v instanceof Map && ((Map) v)
                                .containsKey("@list")))) {
                    throw new JsonLdError(Error.LIST_OF_LISTS, "lists of lists are not permitted.");
                }
                // 3.2.3)
                else if (v != null) {
                    if (v instanceof List) {
                        result.addAll((Collection) v);
                    } else {
                        result.add(v);
                    }
                }
            }
            // 3.3)
            return result;
        }
        // 4)
        else if (element instanceof Map) {
            // access helper
            final Map elem = (Map) element;
            // 5)
            if (elem.containsKey("@context")) {
                activeCtx = activeCtx.parse(elem.get("@context"));
            }
            // 6)
            Map result = new LinkedHashMap();
            // 7)
            final List keys = new ArrayList(elem.keySet());
            Collections.sort(keys);
            for (final String key : keys) {
                final Object value = elem.get(key);
                // 7.1)
                if (key.equals("@context")) {
                    continue;
                }
                // 7.2)
                final String expandedProperty = activeCtx.expandIri(key, false, true, null, null);
                Object expandedValue = null;
                // 7.3)
                if (expandedProperty == null
                        || (!expandedProperty.contains(":") && !isKeyword(expandedProperty))) {
                    continue;
                }
                // 7.4)
                if (isKeyword(expandedProperty)) {
                    // 7.4.1)
                    if ("@reverse".equals(activeProperty)) {
                        throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_MAP,
                                "a keyword cannot be used as a @reverse propery");
                    }
                    // 7.4.2)
                    if (result.containsKey(expandedProperty)) {
                        throw new JsonLdError(Error.COLLIDING_KEYWORDS, expandedProperty
                                + " already exists in result");
                    }
                    // 7.4.3)
                    if ("@id".equals(expandedProperty)) {
                        if (!(value instanceof String)) {
                            throw new JsonLdError(Error.INVALID_ID_VALUE,
                                    "value of @id must be a string");
                        }
                        expandedValue = activeCtx
                                .expandIri((String) value, true, false, null, null);
                    }
                    // 7.4.4)
                    else if ("@type".equals(expandedProperty)) {
                        if (value instanceof List) {
                            expandedValue = new ArrayList();
                            for (final Object v : (List) value) {
                                if (!(v instanceof String)) {
                                    throw new JsonLdError(Error.INVALID_TYPE_VALUE,
                                            "@type value must be a string or array of strings");
                                }
                                ((List) expandedValue).add(activeCtx.expandIri((String) v,
                                        true, true, null, null));
                            }
                        } else if (value instanceof String) {
                            expandedValue = activeCtx.expandIri((String) value, true, true, null,
                                    null);
                        }
                        // TODO: SPEC: no mention of empty map check
                        else if (value instanceof Map) {
                            if (((Map) value).size() != 0) {
                                throw new JsonLdError(Error.INVALID_TYPE_VALUE,
                                        "@type value must be a an empty object for framing");
                            }
                            expandedValue = value;
                        } else {
                            throw new JsonLdError(Error.INVALID_TYPE_VALUE,
                                    "@type value must be a string or array of strings");
                        }
                    }
                    // 7.4.5)
                    else if ("@graph".equals(expandedProperty)) {
                        expandedValue = expand(activeCtx, "@graph", value);
                    }
                    // 7.4.6)
                    else if ("@value".equals(expandedProperty)) {
                        if (value != null && (value instanceof Map || value instanceof List)) {
                            throw new JsonLdError(Error.INVALID_VALUE_OBJECT_VALUE, "value of "
                                    + expandedProperty + " must be a scalar or null");
                        }
                        expandedValue = value;
                        if (expandedValue == null) {
                            result.put("@value", null);
                            continue;
                        }
                    }
                    // 7.4.7)
                    else if ("@language".equals(expandedProperty)) {
                        if (!(value instanceof String)) {
                            throw new JsonLdError(Error.INVALID_LANGUAGE_TAGGED_STRING, "Value of "
                                    + expandedProperty + " must be a string");
                        }
                        expandedValue = ((String) value).toLowerCase();
                    }
                    // 7.4.8)
                    else if ("@index".equals(expandedProperty)) {
                        if (!(value instanceof String)) {
                            throw new JsonLdError(Error.INVALID_INDEX_VALUE, "Value of "
                                    + expandedProperty + " must be a string");
                        }
                        expandedValue = value;
                    }
                    // 7.4.9)
                    else if ("@list".equals(expandedProperty)) {
                        // 7.4.9.1)
                        if (activeProperty == null || "@graph".equals(activeProperty)) {
                            continue;
                        }
                        // 7.4.9.2)
                        expandedValue = expand(activeCtx, activeProperty, value);

                        // NOTE: step not in the spec yet
                        if (!(expandedValue instanceof List)) {
                            final List tmp = new ArrayList();
                            tmp.add(expandedValue);
                            expandedValue = tmp;
                        }

                        // 7.4.9.3)
                        for (final Object o : (List) expandedValue) {
                            if (o instanceof Map && ((Map) o).containsKey("@list")) {
                                throw new JsonLdError(Error.LIST_OF_LISTS,
                                        "A list may not contain another list");
                            }
                        }
                    }
                    // 7.4.10)
                    else if ("@set".equals(expandedProperty)) {
                        expandedValue = expand(activeCtx, activeProperty, value);
                    }
                    // 7.4.11)
                    else if ("@reverse".equals(expandedProperty)) {
                        if (!(value instanceof Map)) {
                            throw new JsonLdError(Error.INVALID_REVERSE_VALUE,
                                    "@reverse value must be an object");
                        }
                        // 7.4.11.1)
                        expandedValue = expand(activeCtx, "@reverse", value);
                        // NOTE: algorithm assumes the result is a map
                        // 7.4.11.2)
                        if (((Map) expandedValue).containsKey("@reverse")) {
                            final Map reverse = (Map) ((Map) expandedValue)
                                    .get("@reverse");
                            for (final String property : reverse.keySet()) {
                                final Object item = reverse.get(property);
                                // 7.4.11.2.1)
                                if (!result.containsKey(property)) {
                                    result.put(property, new ArrayList());
                                }
                                // 7.4.11.2.2)
                                if (item instanceof List) {
                                    ((List) result.get(property))
                                            .addAll((List) item);
                                } else {
                                    ((List) result.get(property)).add(item);
                                }
                            }
                        }
                        // 7.4.11.3)
                        if (((Map) expandedValue).size() > (((Map) expandedValue)
                                .containsKey("@reverse") ? 1 : 0)) {
                            // 7.4.11.3.1)
                            if (!result.containsKey("@reverse")) {
                                result.put("@reverse", new LinkedHashMap());
                            }
                            // 7.4.11.3.2)
                            final Map reverseMap = (Map) result
                                    .get("@reverse");
                            // 7.4.11.3.3)
                            for (final String property : ((Map) expandedValue)
                                    .keySet()) {
                                if ("@reverse".equals(property)) {
                                    continue;
                                }
                                // 7.4.11.3.3.1)
                                final List items = (List) ((Map) expandedValue)
                                        .get(property);
                                for (final Object item : items) {
                                    // 7.4.11.3.3.1.1)
                                    if (item instanceof Map
                                            && (((Map) item).containsKey("@value") || ((Map) item)
                                                    .containsKey("@list"))) {
                                        throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_VALUE);
                                    }
                                    // 7.4.11.3.3.1.2)
                                    if (!reverseMap.containsKey(property)) {
                                        reverseMap.put(property, new ArrayList());
                                    }
                                    // 7.4.11.3.3.1.3)
                                    ((List) reverseMap.get(property)).add(item);
                                }
                            }
                        }
                        // 7.4.11.4)
                        continue;
                    }
                    // TODO: SPEC no mention of @explicit etc in spec
                    else if ("@explicit".equals(expandedProperty)
                            || "@default".equals(expandedProperty)
                            || "@embed".equals(expandedProperty)
                            || "@embedChildren".equals(expandedProperty)
                            || "@omitDefault".equals(expandedProperty)) {
                        expandedValue = expand(activeCtx, expandedProperty, value);
                    }
                    // 7.4.12)
                    if (expandedValue != null) {
                        result.put(expandedProperty, expandedValue);
                    }
                    // 7.4.13)
                    continue;
                }
                // 7.5
                else if ("@language".equals(activeCtx.getContainer(key)) && value instanceof Map) {
                    // 7.5.1)
                    expandedValue = new ArrayList();
                    // 7.5.2)
                    for (final String language : ((Map) value).keySet()) {
                        Object languageValue = ((Map) value).get(language);
                        // 7.5.2.1)
                        if (!(languageValue instanceof List)) {
                            final Object tmp = languageValue;
                            languageValue = new ArrayList();
                            ((List) languageValue).add(tmp);
                        }
                        // 7.5.2.2)
                        for (final Object item : (List) languageValue) {
                            // 7.5.2.2.1)
                            if (!(item instanceof String)) {
                                throw new JsonLdError(Error.INVALID_LANGUAGE_MAP_VALUE, "Expected "
                                        + item.toString() + " to be a string");
                            }
                            // 7.5.2.2.2)
                            final Map tmp = new LinkedHashMap();
                            tmp.put("@value", item);
                            tmp.put("@language", language.toLowerCase());
                            ((List) expandedValue).add(tmp);
                        }
                    }
                }
                // 7.6)
                else if ("@index".equals(activeCtx.getContainer(key)) && value instanceof Map) {
                    // 7.6.1)
                    expandedValue = new ArrayList();
                    // 7.6.2)
                    final List indexKeys = new ArrayList(
                            ((Map) value).keySet());
                    Collections.sort(indexKeys);
                    for (final String index : indexKeys) {
                        Object indexValue = ((Map) value).get(index);
                        // 7.6.2.1)
                        if (!(indexValue instanceof List)) {
                            final Object tmp = indexValue;
                            indexValue = new ArrayList();
                            ((List) indexValue).add(tmp);
                        }
                        // 7.6.2.2)
                        indexValue = expand(activeCtx, key, indexValue);
                        // 7.6.2.3)
                        for (final Map item : (List>) indexValue) {
                            // 7.6.2.3.1)
                            if (!item.containsKey("@index")) {
                                item.put("@index", index);
                            }
                            // 7.6.2.3.2)
                            ((List) expandedValue).add(item);
                        }
                    }
                }
                // 7.7)
                else {
                    expandedValue = expand(activeCtx, key, value);
                }
                // 7.8)
                if (expandedValue == null) {
                    continue;
                }
                // 7.9)
                if ("@list".equals(activeCtx.getContainer(key))) {
                    if (!(expandedValue instanceof Map)
                            || !((Map) expandedValue).containsKey("@list")) {
                        Object tmp = expandedValue;
                        if (!(tmp instanceof List)) {
                            tmp = new ArrayList();
                            ((List) tmp).add(expandedValue);
                        }
                        expandedValue = new LinkedHashMap();
                        ((Map) expandedValue).put("@list", tmp);
                    }
                }
                // 7.10)
                if (activeCtx.isReverseProperty(key)) {
                    // 7.10.1)
                    if (!result.containsKey("@reverse")) {
                        result.put("@reverse", new LinkedHashMap());
                    }
                    // 7.10.2)
                    final Map reverseMap = (Map) result
                            .get("@reverse");
                    // 7.10.3)
                    if (!(expandedValue instanceof List)) {
                        final Object tmp = expandedValue;
                        expandedValue = new ArrayList();
                        ((List) expandedValue).add(tmp);
                    }
                    // 7.10.4)
                    for (final Object item : (List) expandedValue) {
                        // 7.10.4.1)
                        if (item instanceof Map
                                && (((Map) item).containsKey("@value") || ((Map) item)
                                        .containsKey("@list"))) {
                            throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_VALUE);
                        }
                        // 7.10.4.2)
                        if (!reverseMap.containsKey(expandedProperty)) {
                            reverseMap.put(expandedProperty, new ArrayList());
                        }
                        // 7.10.4.3)
                        if (item instanceof List) {
                            ((List) reverseMap.get(expandedProperty))
                                    .addAll((List) item);
                        } else {
                            ((List) reverseMap.get(expandedProperty)).add(item);
                        }
                    }
                }
                // 7.11)
                else {
                    // 7.11.1)
                    if (!result.containsKey(expandedProperty)) {
                        result.put(expandedProperty, new ArrayList());
                    }
                    // 7.11.2)
                    if (expandedValue instanceof List) {
                        ((List) result.get(expandedProperty))
                                .addAll((List) expandedValue);
                    } else {
                        ((List) result.get(expandedProperty)).add(expandedValue);
                    }
                }
            }
            // 8)
            if (result.containsKey("@value")) {
                // 8.1)
                // TODO: is this method faster than just using containsKey for
                // each?
                final Set keySet = new HashSet(result.keySet());
                keySet.remove("@value");
                keySet.remove("@index");
                final boolean langremoved = keySet.remove("@language");
                final boolean typeremoved = keySet.remove("@type");
                if ((langremoved && typeremoved) || !keySet.isEmpty()) {
                    throw new JsonLdError(Error.INVALID_VALUE_OBJECT,
                            "value object has unknown keys");
                }
                // 8.2)
                final Object rval = result.get("@value");
                if (rval == null) {
                    // nothing else is possible with result if we set it to
                    // null, so simply return it
                    return null;
                }
                // 8.3)
                if (!(rval instanceof String) && result.containsKey("@language")) {
                    throw new JsonLdError(Error.INVALID_LANGUAGE_TAGGED_VALUE,
                            "when @language is used, @value must be a string");
                }
                // 8.4)
                else if (result.containsKey("@type")) {
                    // TODO: is this enough for "is an IRI"
                    if (!(result.get("@type") instanceof String)
                            || ((String) result.get("@type")).startsWith("_:")
                            || !((String) result.get("@type")).contains(":")) {
                        throw new JsonLdError(Error.INVALID_TYPED_VALUE,
                                "value of @type must be an IRI");
                    }
                }
            }
            // 9)
            else if (result.containsKey("@type")) {
                final Object rtype = result.get("@type");
                if (!(rtype instanceof List)) {
                    final List tmp = new ArrayList();
                    tmp.add(rtype);
                    result.put("@type", tmp);
                }
            }
            // 10)
            else if (result.containsKey("@set") || result.containsKey("@list")) {
                // 10.1)
                if (result.size() > (result.containsKey("@index") ? 2 : 1)) {
                    throw new JsonLdError(Error.INVALID_SET_OR_LIST_OBJECT,
                            "@set or @list may only contain @index");
                }
                // 10.2)
                if (result.containsKey("@set")) {
                    // result becomes an array here, thus the remaining checks
                    // will never be true from here on
                    // so simply return the value rather than have to make
                    // result an object and cast it with every
                    // other use in the function.
                    return result.get("@set");
                }
            }
            // 11)
            if (result.containsKey("@language") && result.size() == 1) {
                result = null;
            }
            // 12)
            if (activeProperty == null || "@graph".equals(activeProperty)) {
                // 12.1)
                if (result != null
                        && (result.size() == 0 || result.containsKey("@value") || result
                                .containsKey("@list"))) {
                    result = null;
                }
                // 12.2)
                else if (result != null && result.containsKey("@id") && result.size() == 1) {
                    result = null;
                }
            }
            // 13)
            return result;
        }
        // 2) If element is a scalar
        else {
            // 2.1)
            if (activeProperty == null || "@graph".equals(activeProperty)) {
                return null;
            }
            return activeCtx.expandValue(activeProperty, element);
        }
    }

    public Object expand(Context activeCtx, Object element) throws JsonLdError {
        return expand(activeCtx, null, element);
    }

    /***
     * _____ _ _ _ _ _ _ _ _ | ___| | __ _| |_| |_ ___ _ __ / \ | | __ _ ___ _
     * __(_) |_| |__ _ __ ___ | |_ | |/ _` | __| __/ _ \ '_ \ / _ \ | |/ _` |/ _
     * \| '__| | __| '_ \| '_ ` _ \ | _| | | (_| | |_| || __/ | | | / ___ \| |
     * (_| | (_) | | | | |_| | | | | | | | | |_| |_|\__,_|\__|\__\___|_| |_| /_/
     * \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| |___/
     */

    void generateNodeMap(Object element, Map nodeMap) throws JsonLdError {
        generateNodeMap(element, nodeMap, "@default", null, null, null);
    }

    void generateNodeMap(Object element, Map nodeMap, String activeGraph)
            throws JsonLdError {
        generateNodeMap(element, nodeMap, activeGraph, null, null, null);
    }

    void generateNodeMap(Object element, Map nodeMap, String activeGraph,
            Object activeSubject, String activeProperty, Map list)
            throws JsonLdError {
        // 1)
        if (element instanceof List) {
            // 1.1)
            for (final Object item : (List) element) {
                generateNodeMap(item, nodeMap, activeGraph, activeSubject, activeProperty, list);
            }
            return;
        }

        // for convenience
        final Map elem = (Map) element;

        // 2)
        if (!nodeMap.containsKey(activeGraph)) {
            nodeMap.put(activeGraph, new LinkedHashMap());
        }
        final Map graph = (Map) nodeMap.get(activeGraph);
        Map node = (Map) (activeSubject == null ? null : graph
                .get(activeSubject));

        // 3)
        if (elem.containsKey("@type")) {
            // 3.1)
            List oldTypes;
            final List newTypes = new ArrayList();
            if (elem.get("@type") instanceof List) {
                oldTypes = (List) elem.get("@type");
            } else {
                oldTypes = new ArrayList();
                oldTypes.add((String) elem.get("@type"));
            }
            for (final String item : oldTypes) {
                if (item.startsWith("_:")) {
                    newTypes.add(generateBlankNodeIdentifier(item));
                } else {
                    newTypes.add(item);
                }
            }
            if (elem.get("@type") instanceof List) {
                elem.put("@type", newTypes);
            } else {
                elem.put("@type", newTypes.get(0));
            }
        }

        // 4)
        if (elem.containsKey("@value")) {
            // 4.1)
            if (list == null) {
                JsonLdUtils.mergeValue(node, activeProperty, elem);
            }
            // 4.2)
            else {
                JsonLdUtils.mergeValue(list, "@list", elem);
            }
        }

        // 5)
        else if (elem.containsKey("@list")) {
            // 5.1)
            final Map result = new LinkedHashMap();
            result.put("@list", new ArrayList());
            // 5.2)
            //for (final Object item : (List) elem.get("@list")) {
            //    generateNodeMap(item, nodeMap, activeGraph, activeSubject, activeProperty, result);
            //}
            generateNodeMap(elem.get("@list"), nodeMap, activeGraph, activeSubject, activeProperty, result);
            // 5.3)
            JsonLdUtils.mergeValue(node, activeProperty, result);
        }

        // 6)
        else {
            // 6.1)
            String id = (String) elem.remove("@id");
            if (id != null) {
                if (id.startsWith("_:")) {
                    id = generateBlankNodeIdentifier(id);
                }
            }
            // 6.2)
            else {
                id = generateBlankNodeIdentifier(null);
            }
            // 6.3)
            if (!graph.containsKey(id)) {
                final Map tmp = new LinkedHashMap();
                tmp.put("@id", id);
                graph.put(id, tmp);
            }
            // 6.4) TODO: SPEC this line is asked for by the spec, but it breaks various tests
            //node = (Map) graph.get(id);
            // 6.5)
            if (activeSubject instanceof Map) {
            	// 6.5.1)
            	JsonLdUtils.mergeValue((Map) graph.get(id), activeProperty, activeSubject);
            }
            // 6.6)
            else if (activeProperty != null) {
                final Map reference = new LinkedHashMap();
                reference.put("@id", id);
                // 6.6.2)
                if (list == null) {
                    // 6.6.2.1+2)
                    JsonLdUtils.mergeValue(node, activeProperty, reference);
                }
                // 6.6.3) TODO: SPEC says to add ELEMENT to @list member, should
                // be REFERENCE
                else {
                    JsonLdUtils.mergeValue(list, "@list", reference);
                }
            }
            // TODO: SPEC this is removed in the spec now, but it's still needed (see 6.4)
            node = (Map) graph.get(id);
            // 6.7)
            if (elem.containsKey("@type")) {
                for (final Object type : (List) elem.remove("@type")) {
                    JsonLdUtils.mergeValue(node, "@type", type);
                }
            }
            // 6.8)
            if (elem.containsKey("@index")) {
                final Object elemIndex = elem.remove("@index");
                if (node.containsKey("@index")) {
                    if (!JsonLdUtils.deepCompare(node.get("@index"), elemIndex)) {
                        throw new JsonLdError(Error.CONFLICTING_INDEXES);
                    }
                } else {
                    node.put("@index", elemIndex);
                }
            }
            // 6.9)
            if (elem.containsKey("@reverse")) {
            	// 6.9.1)
            	final Map referencedNode = new LinkedHashMap();
                referencedNode.put("@id", id);
                // 6.9.2+6.9.4)
                final Map reverseMap = (Map) elem
                        .remove("@reverse");
                // 6.9.3)
                for (final String property : reverseMap.keySet()) {
                    final List values = (List) reverseMap.get(property);
                    // 6.9.3.1)
                    for (final Object value : values) {
                        // 6.9.3.1.1)
                        generateNodeMap(value, nodeMap, activeGraph, referencedNode, property, null);
                    }
                }
            }
            // 6.10)
            if (elem.containsKey("@graph")) {
                generateNodeMap(elem.remove("@graph"), nodeMap, id, null, null, null);
            }
            // 6.11)
            final List keys = new ArrayList(elem.keySet());
            Collections.sort(keys);
            for (String property : keys) {
                final Object value = elem.get(property);
                // 6.11.1)
                if (property.startsWith("_:")) {
                    property = generateBlankNodeIdentifier(property);
                }
                // 6.11.2)
                if (!node.containsKey(property)) {
                    node.put(property, new ArrayList());
                }
                // 6.11.3)
                generateNodeMap(value, nodeMap, activeGraph, id, property, null);
            }
        }
    }

    private final Map blankNodeIdentifierMap = new LinkedHashMap();
    private int blankNodeCounter = 0;

    String generateBlankNodeIdentifier(String id) {
        if (id != null && blankNodeIdentifierMap.containsKey(id)) {
            return blankNodeIdentifierMap.get(id);
        }
        final String bnid = "_:b" + blankNodeCounter++;
        if (id != null) {
            blankNodeIdentifierMap.put(id, bnid);
        }
        return bnid;
    }

    String generateBlankNodeIdentifier() {
        return generateBlankNodeIdentifier(null);
    }

    /***
     * _____ _ _ _ _ _ _ | ___| __ __ _ _ __ ___ (_)_ __ __ _ / \ | | __ _ ___ _
     * __(_) |_| |__ _ __ ___ | |_ | '__/ _` | '_ ` _ \| | '_ \ / _` | / _ \ |
     * |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ | _|| | | (_| | | | | | | | | | |
     * (_| | / ___ \| | (_| | (_) | | | | |_| | | | | | | | | |_| |_| \__,_|_|
     * |_| |_|_|_| |_|\__, | /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
     * |___/ |___/
     */

    private class FramingContext {
        public boolean embed;
        public boolean explicit;
        public boolean omitDefault;

        public FramingContext() {
            embed = true;
            explicit = false;
            omitDefault = false;
            embeds = null;
        }

        public Map embeds = null;
    }

    private class EmbedNode {
        public Object parent = null;
        public String property = null;
    }

    private Map nodeMap;

    /**
     * Performs JSON-LD framing.
     * 
     * @param input
     *            the expanded JSON-LD to frame.
     * @param frame
     *            the expanded JSON-LD frame to use.
     * @param options
     *            the framing options.
     * 
     * @return the framed output.
     * @throws JSONLDProcessingError
     */
    public List frame(Object input, List frame) throws JsonLdError {
        // create framing state
        final FramingContext state = new FramingContext();
        if (this.opts.getEmbed() != null) {
            state.embed = this.opts.getEmbed();
        }
        if (this.opts.getExplicit() != null) {
            state.embed = this.opts.getExplicit();
        }
        if (this.opts.getOmitDefault() != null) {
            state.embed = this.opts.getOmitDefault();
        }

        // use tree map so keys are sotred by default
        final Map nodes = new TreeMap();
        generateNodeMap(input, nodes);
        this.nodeMap = (Map) nodes.get("@default");

        final List framed = new ArrayList();
        // NOTE: frame validation is done by the function not allowing anything
        // other than list to me passed
        frame(state, this.nodeMap,
                (frame != null && frame.size() > 0 ? (Map) frame.get(0)
                        : new LinkedHashMap()), framed, null);

        return framed;
    }

    /**
     * Frames subjects according to the given frame.
     * 
     * @param state
     *            the current framing state.
     * @param subjects
     *            the subjects to filter.
     * @param frame
     *            the frame.
     * @param parent
     *            the parent subject or top-level array.
     * @param property
     *            the parent property, initialized to null.
     * @throws JSONLDProcessingError
     */
    private void frame(FramingContext state, Map nodes, Map frame,
            Object parent, String property) throws JsonLdError {

        // filter out subjects that match the frame
        final Map matches = filterNodes(state, nodes, frame);

        // get flags for current frame
        Boolean embedOn = getFrameFlag(frame, "@embed", state.embed);
        final Boolean explicicOn = getFrameFlag(frame, "@explicit", state.explicit);

        // add matches to output
        final List ids = new ArrayList(matches.keySet());
        Collections.sort(ids);
        for (final String id : ids) {
            if (property == null) {
                state.embeds = new LinkedHashMap();
            }

            // start output
            final Map output = new LinkedHashMap();
            output.put("@id", id);

            // prepare embed meta info
            final EmbedNode embeddedNode = new EmbedNode();
            embeddedNode.parent = parent;
            embeddedNode.property = property;

            // if embed is on and there is an existing embed
            if (embedOn && state.embeds.containsKey(id)) {
                final EmbedNode existing = state.embeds.get(id);
                embedOn = false;

                if (existing.parent instanceof List) {
                    for (final Object p : (List) existing.parent) {
                        if (JsonLdUtils.compareValues(output, p)) {
                            embedOn = true;
                            break;
                        }
                    }
                }
                // existing embed's parent is an object
                else {
                    if (((Map) existing.parent).containsKey(existing.property)) {
                        for (final Object v : (List) ((Map) existing.parent)
                                .get(existing.property)) {
                            if (v instanceof Map
                                    && Obj.equals(id, ((Map) v).get("@id"))) {
                                embedOn = true;
                                break;
                            }
                        }
                    }
                }

                // existing embed has already been added, so allow an overwrite
                if (embedOn) {
                    removeEmbed(state, id);
                }
            }

            // not embedding, add output without any other properties
            if (!embedOn) {
                addFrameOutput(state, parent, property, output);
            } else {
                // add embed meta info
                state.embeds.put(id, embeddedNode);

                // iterate over subject properties
                final Map element = (Map) matches.get(id);
                List props = new ArrayList(element.keySet());
                Collections.sort(props);
                for (final String prop : props) {

                    // copy keywords to output
                    if (isKeyword(prop)) {
                        output.put(prop, JsonLdUtils.clone(element.get(prop)));
                        continue;
                    }

                    // if property isn't in the frame
                    if (!frame.containsKey(prop)) {
                        // if explicit is off, embed values
                        if (!explicicOn) {
                            embedValues(state, element, prop, output);
                        }
                        continue;
                    }

                    // add objects
                    final List value = (List) element.get(prop);

                    for (final Object item : value) {

                        // recurse into list
                        if ((item instanceof Map)
                                && ((Map) item).containsKey("@list")) {
                            // add empty list
                            final Map list = new LinkedHashMap();
                            list.put("@list", new ArrayList());
                            addFrameOutput(state, output, prop, list);

                            // add list objects
                            for (final Object listitem : (List) ((Map) item)
                                    .get("@list")) {
                                // recurse into subject reference
                                if (JsonLdUtils.isNodeReference(listitem)) {
                                    final Map tmp = new LinkedHashMap();
                                    final String itemid = (String) ((Map) listitem)
                                            .get("@id");
                                    // TODO: nodes may need to be node_map,
                                    // which is global
                                    tmp.put(itemid, this.nodeMap.get(itemid));
                                    frame(state, tmp,
                                            (Map) ((List) frame.get(prop))
                                                    .get(0), list, "@list");
                                } else {
                                    // include other values automatcially (TODO:
                                    // may need JsonLdUtils.clone(n))
                                    addFrameOutput(state, list, "@list", listitem);
                                }
                            }
                        }

                        // recurse into subject reference
                        else if (JsonLdUtils.isNodeReference(item)) {
                            final Map tmp = new LinkedHashMap();
                            final String itemid = (String) ((Map) item).get("@id");
                            // TODO: nodes may need to be node_map, which is
                            // global
                            tmp.put(itemid, this.nodeMap.get(itemid));
                            frame(state, tmp,
                                    (Map) ((List) frame.get(prop)).get(0),
                                    output, prop);
                        } else {
                            // include other values automatically (TODO: may
                            // need JsonLdUtils.clone(o))
                            addFrameOutput(state, output, prop, item);
                        }
                    }
                }

                // handle defaults
                props = new ArrayList(frame.keySet());
                Collections.sort(props);
                for (final String prop : props) {
                    // skip keywords
                    if (isKeyword(prop)) {
                        continue;
                    }

                    final List pf = (List) frame.get(prop);
                    Map propertyFrame = pf.size() > 0 ? (Map) pf
                            .get(0) : null;
                    if (propertyFrame == null) {
                        propertyFrame = new LinkedHashMap();
                    }
                    final boolean omitDefaultOn = getFrameFlag(propertyFrame, "@omitDefault",
                            state.omitDefault);
                    if (!omitDefaultOn && !output.containsKey(prop)) {
                        Object def = "@null";
                        if (propertyFrame.containsKey("@default")) {
                            def = JsonLdUtils.clone(propertyFrame.get("@default"));
                        }
                        if (!(def instanceof List)) {
                            final List tmp = new ArrayList();
                            tmp.add(def);
                            def = tmp;
                        }
                        final Map tmp1 = new LinkedHashMap();
                        tmp1.put("@preserve", def);
                        final List tmp2 = new ArrayList();
                        tmp2.add(tmp1);
                        output.put(prop, tmp2);
                    }
                }

                // add output to parent
                addFrameOutput(state, parent, property, output);
            }
        }
    }

    private Boolean getFrameFlag(Map frame, String name, boolean thedefault) {
        Object value = frame.get(name);
        if (value instanceof List) {
            if (((List) value).size() > 0) {
                value = ((List) value).get(0);
            }
        }
        if (value instanceof Map && ((Map) value).containsKey("@value")) {
            value = ((Map) value).get("@value");
        }
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        return thedefault;
    }

    /**
     * Removes an existing embed.
     * 
     * @param state
     *            the current framing state.
     * @param id
     *            the @id of the embed to remove.
     */
    private static void removeEmbed(FramingContext state, String id) {
        // get existing embed
        final Map embeds = state.embeds;
        final EmbedNode embed = embeds.get(id);
        final Object parent = embed.parent;
        final String property = embed.property;

        // create reference to replace embed
        final Map node = new LinkedHashMap();
        node.put("@id", id);

        // remove existing embed
        if (JsonLdUtils.isNode(parent)) {
            // replace subject with reference
            final List newvals = new ArrayList();
            final List oldvals = (List) ((Map) parent)
                    .get(property);
            for (final Object v : oldvals) {
                if (v instanceof Map && Obj.equals(((Map) v).get("@id"), id)) {
                    newvals.add(node);
                } else {
                    newvals.add(v);
                }
            }
            ((Map) parent).put(property, newvals);
        }
        // recursively remove dependent dangling embeds
        removeDependents(embeds, id);
    }

    private static void removeDependents(Map embeds, String id) {
        // get embed keys as a separate array to enable deleting keys in map
        for (final String id_dep : embeds.keySet()) {
            final EmbedNode e = embeds.get(id_dep);
            final Object p = e.parent != null ? e.parent : new LinkedHashMap();
            if (!(p instanceof Map)) {
                continue;
            }
            final String pid = (String) ((Map) p).get("@id");
            if (Obj.equals(id, pid)) {
                embeds.remove(id_dep);
                removeDependents(embeds, id_dep);
            }
        }
    }

    private Map filterNodes(FramingContext state, Map nodes,
            Map frame) throws JsonLdError {
        final Map rval = new LinkedHashMap();
        for (final String id : nodes.keySet()) {
            final Map element = (Map) nodes.get(id);
            if (element != null && filterNode(state, element, frame)) {
                rval.put(id, element);
            }
        }
        return rval;
    }

    private boolean filterNode(FramingContext state, Map node,
            Map frame) throws JsonLdError {
        final Object types = frame.get("@type");
        if (types != null) {
            if (!(types instanceof List)) {
                throw new JsonLdError(Error.SYNTAX_ERROR, "frame @type must be an array");
            }
            Object nodeTypes = node.get("@type");
            if (nodeTypes == null) {
                nodeTypes = new ArrayList();
            } else if (!(nodeTypes instanceof List)) {
                throw new JsonLdError(Error.SYNTAX_ERROR, "node @type must be an array");
            }
            if (((List) types).size() == 1 && ((List) types).get(0) instanceof Map
                    && ((Map) ((List) types).get(0)).size() == 0) {
                return !((List) nodeTypes).isEmpty();
            } else {
                for (final Object i : (List) nodeTypes) {
                    for (final Object j : (List) types) {
                        if (JsonLdUtils.deepCompare(i, j)) {
                            return true;
                        }
                    }
                }
                return false;
            }
        } else {
            for (final String key : frame.keySet()) {
                if ("@id".equals(key) || !isKeyword(key) && !(node.containsKey(key))) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Adds framing output to the given parent.
     * 
     * @param state
     *            the current framing state.
     * @param parent
     *            the parent to add to.
     * @param property
     *            the parent property.
     * @param output
     *            the output to add.
     */
    private static void addFrameOutput(FramingContext state, Object parent, String property,
            Object output) {
        if (parent instanceof Map) {
            List prop = (List) ((Map) parent).get(property);
            if (prop == null) {
                prop = new ArrayList();
                ((Map) parent).put(property, prop);
            }
            prop.add(output);
        } else {
            ((List) parent).add(output);
        }
    }

    /**
     * Embeds values for the given subject and property into the given output
     * during the framing algorithm.
     * 
     * @param state
     *            the current framing state.
     * @param element
     *            the subject.
     * @param property
     *            the property.
     * @param output
     *            the output.
     */
    private void embedValues(FramingContext state, Map element, String property,
            Object output) {
        // embed subject properties in output
        final List objects = (List) element.get(property);
        for (Object o : objects) {
            // handle subject reference
            if (JsonLdUtils.isNodeReference(o)) {
                final String sid = (String) ((Map) o).get("@id");

                // embed full subject if isn't already embedded
                if (!state.embeds.containsKey(sid)) {
                    // add embed
                    final EmbedNode embed = new EmbedNode();
                    embed.parent = output;
                    embed.property = property;
                    state.embeds.put(sid, embed);

                    // recurse into subject
                    o = new LinkedHashMap();
                    Map s = (Map) this.nodeMap.get(sid);
                    if (s == null) {
                        s = new LinkedHashMap();
                        s.put("@id", sid);
                    }
                    for (final String prop : s.keySet()) {
                        // copy keywords
                        if (isKeyword(prop)) {
                            ((Map) o).put(prop, JsonLdUtils.clone(s.get(prop)));
                            continue;
                        }
                        embedValues(state, s, prop, o);
                    }
                }
                addFrameOutput(state, output, property, o);
            }
            // copy non-subject value
            else {
                addFrameOutput(state, output, property, JsonLdUtils.clone(o));
            }
        }
    }

    /***
     * ____ _ __ ____ ____ _____ _ _ _ _ _ / ___|___ _ ____ _____ _ __| |_ / _|_
     * __ ___ _ __ ___ | _ \| _ \| ___| / \ | | __ _ ___ _ __(_) |_| |__ _ __
     * ___ | | / _ \| '_ \ \ / / _ \ '__| __| | |_| '__/ _ \| '_ ` _ \ | |_) | |
     * | | |_ / _ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ | |__| (_) | | | \
     * V / __/ | | |_ | _| | | (_) | | | | | | | _ <| |_| | _| / ___ \| | (_| |
     * (_) | | | | |_| | | | | | | | | \____\___/|_| |_|\_/ \___|_| \__| |_| |_|
     * \___/|_| |_| |_| |_| \_\____/|_| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_|
     * |_| |_| |___/
     */

    /**
     * Helper class for node usages
     * 
     * @author tristan
     */
    private class UsagesNode {
        public UsagesNode(NodeMapNode node, String property, Map value) {
            this.node = node;
            this.property = property;
            this.value = value;
        }

        public NodeMapNode node = null;
        public String property = null;
        public Map value = null;
    }

    private class NodeMapNode extends LinkedHashMap {
        public List usages = new ArrayList();

        public NodeMapNode(String id) {
            super();
            this.put("@id", id);
        }

        // helper fucntion for 4.3.3
        public boolean isWellFormedListNode() {
            if (usages.size() != 1) {
                return false;
            }
            int keys = 0;
            if (containsKey(RDF_FIRST)) {
                keys++;
                if (!(get(RDF_FIRST) instanceof List && ((List) get(RDF_FIRST)).size() == 1)) {
                    return false;
                }
            }
            if (containsKey(RDF_REST)) {
                keys++;
                if (!(get(RDF_REST) instanceof List && ((List) get(RDF_REST)).size() == 1)) {
                    return false;
                }
            }
            if (containsKey("@type")) {
                keys++;
                if (!(get("@type") instanceof List && ((List) get("@type")).size() == 1)
                        && RDF_LIST.equals(((List) get("@type")).get(0))) {
                    return false;
                }
            }
            // TODO: SPEC: 4.3.3 has no mention of @id
            if (containsKey("@id")) {
                keys++;
            }
            if (keys < size()) {
                return false;
            }
            return true;
        }

        // return this node without the usages variable
        public Map serialize() {
            return new LinkedHashMap(this);
        }
    }

    /**
     * Converts RDF statements into JSON-LD.
     * 
     * @param statements
     *            the RDF statements.
     * @param options
     *            the RDF conversion options.
     * @param callback
     *            (err, output) called once the operation completes.
     * @throws JSONLDProcessingError
     */
    public List fromRDF(final RDFDataset dataset) throws JsonLdError {
        // 1)
        final Map defaultGraph = new LinkedHashMap();
        // 2)
        final Map> graphMap = new LinkedHashMap>();
        graphMap.put("@default", defaultGraph);

        // 3/3.1)
        for (final String name : dataset.graphNames()) {

            final List graph = dataset.getQuads(name);

            // 3.2+3.4)
            Map nodeMap;
            if (!graphMap.containsKey(name)) {
                nodeMap = new LinkedHashMap();
                graphMap.put(name, nodeMap);
            } else {
                nodeMap = graphMap.get(name);
            }

            // 3.3)
            if (!"@default".equals(name) && !Obj.contains(defaultGraph, name)) {
                defaultGraph.put(name, new NodeMapNode(name));
            }

            // 3.5)
            for (final RDFDataset.Quad triple : graph) {
                final String subject = triple.getSubject().getValue();
                final String predicate = triple.getPredicate().getValue();
                final RDFDataset.Node object = triple.getObject();

                // 3.5.1+3.5.2)
                NodeMapNode node;
                if (!nodeMap.containsKey(subject)) {
                    node = new NodeMapNode(subject);
                    nodeMap.put(subject, node);
                } else {
                    node = nodeMap.get(subject);
                }

                // 3.5.3)
                if ((object.isIRI() || object.isBlankNode())
                        && !nodeMap.containsKey(object.getValue())) {
                    nodeMap.put(object.getValue(), new NodeMapNode(object.getValue()));
                }

                // 3.5.4)
                if (RDF_TYPE.equals(predicate) && (object.isIRI() || object.isBlankNode())
                        && !opts.getUseRdfType()) {
                    JsonLdUtils.mergeValue(node, "@type", object.getValue());
                    continue;
                }

                // 3.5.5)
                final Map value = object.toObject(opts.getUseNativeTypes());

                // 3.5.6+7)
                JsonLdUtils.mergeValue(node, predicate, value);

                // 3.5.8)
                if (object.isBlankNode() || object.isIRI()) {
                    // 3.5.8.1-3)
                    nodeMap.get(object.getValue()).usages
                            .add(new UsagesNode(node, predicate, value));
                }
            }
        }

        // 4)
        for (final String name : graphMap.keySet()) {
            final Map graph = graphMap.get(name);

            // 4.1)
            if (!graph.containsKey(RDF_NIL)) {
                continue;
            }

            // 4.2)
            final NodeMapNode nil = graph.get(RDF_NIL);
            // 4.3)
            for (final UsagesNode usage : nil.usages) {
                // 4.3.1)
                NodeMapNode node = usage.node;
                String property = usage.property;
                Map head = usage.value;
                // 4.3.2)
                final List list = new ArrayList();
                final List listNodes = new ArrayList();
                // 4.3.3)
                while (RDF_REST.equals(property) && node.isWellFormedListNode()) {
                    // 4.3.3.1)
                    list.add(((List) node.get(RDF_FIRST)).get(0));
                    // 4.3.3.2)
                    listNodes.add((String) node.get("@id"));
                    // 4.3.3.3)
                    final UsagesNode nodeUsage = node.usages.get(0);
                    // 4.3.3.4)
                    node = nodeUsage.node;
                    property = nodeUsage.property;
                    head = nodeUsage.value;
                    // 4.3.3.5)
                    if (!JsonLdUtils.isBlankNode(node)) {
                        break;
                    }
                }
                // 4.3.4)
                if (RDF_FIRST.equals(property)) {
                    // 4.3.4.1)
                    if (RDF_NIL.equals(node.get("@id"))) {
                        continue;
                    }
                    // 4.3.4.3)
                    final String headId = (String) head.get("@id");
                    // 4.3.4.4-5)
                    head = (Map) ((List) graph.get(headId).get(RDF_REST))
                            .get(0);
                    // 4.3.4.6)
                    list.remove(list.size() - 1);
                    listNodes.remove(listNodes.size() - 1);
                }
                // 4.3.5)
                head.remove("@id");
                // 4.3.6)
                Collections.reverse(list);
                // 4.3.7)
                head.put("@list", list);
                // 4.3.8)
                for (final String nodeId : listNodes) {
                    graph.remove(nodeId);
                }
            }
        }

        // 5)
        final List result = new ArrayList();
        // 6)
        final List ids = new ArrayList(defaultGraph.keySet());
        Collections.sort(ids);
        for (final String subject : ids) {
            final NodeMapNode node = defaultGraph.get(subject);
            // 6.1)
            if (graphMap.containsKey(subject)) {
                // 6.1.1)
                node.put("@graph", new ArrayList());
                // 6.1.2)
                final List keys = new ArrayList(graphMap.get(subject).keySet());
                Collections.sort(keys);
                for (final String s : keys) {
                    final NodeMapNode n = graphMap.get(subject).get(s);
                    if (n.size() == 1 && n.containsKey("@id")) {
                        continue;
                    }
                    ((List) node.get("@graph")).add(n.serialize());
                }
            }
            // 6.2)
            if (node.size() == 1 && node.containsKey("@id")) {
                continue;
            }
            result.add(node.serialize());
        }

        return result;
    }

    /***
     * ____ _ _ ____ ____ _____ _ _ _ _ _ / ___|___ _ ____ _____ _ __| |_ | |_
     * ___ | _ \| _ \| ___| / \ | | __ _ ___ _ __(_) |_| |__ _ __ ___ | | / _ \|
     * '_ \ \ / / _ \ '__| __| | __/ _ \ | |_) | | | | |_ / _ \ | |/ _` |/ _ \|
     * '__| | __| '_ \| '_ ` _ \ | |__| (_) | | | \ V / __/ | | |_ | || (_) | |
     * _ <| |_| | _| / ___ \| | (_| | (_) | | | | |_| | | | | | | | |
     * \____\___/|_| |_|\_/ \___|_| \__| \__\___/ |_| \_\____/|_| /_/ \_\_|\__,
     * |\___/|_| |_|\__|_| |_|_| |_| |_| |___/
     */

    /**
     * Adds RDF triples for each graph in the given node map to an RDF dataset.
     * 
     * @return the RDF dataset.
     * @throws JsonLdError
     */
    public RDFDataset toRDF() throws JsonLdError {
        // TODO: make the default generateNodeMap call (i.e. without a
        // graphName) create and return the nodeMap
        final Map nodeMap = new LinkedHashMap();
        nodeMap.put("@default", new LinkedHashMap());
        generateNodeMap(this.value, nodeMap);

        final RDFDataset dataset = new RDFDataset(this);

        for (final String graphName : nodeMap.keySet()) {
            // 4.1)
            if (JsonLdUtils.isRelativeIri(graphName)) {
                continue;
            }
            final Map graph = (Map) nodeMap.get(graphName);
            dataset.graphToRDF(graphName, graph);
        }

        return dataset;
    }

    /***
     * _ _ _ _ _ _ _ _ _ _ _ | \ | | ___ _ __ _ __ ___ __ _| (_)______ _| |_(_)
     * ___ _ __ / \ | | __ _ ___ _ __(_) |_| |__ _ __ ___ | \| |/ _ \| '__| '_ `
     * _ \ / _` | | |_ / _` | __| |/ _ \| '_ \ / _ \ | |/ _` |/ _ \| '__| | __|
     * '_ \| '_ ` _ \ | |\ | (_) | | | | | | | | (_| | | |/ / (_| | |_| | (_) |
     * | | | / ___ \| | (_| | (_) | | | | |_| | | | | | | | | |_| \_|\___/|_|
     * |_| |_| |_|\__,_|_|_/___\__,_|\__|_|\___/|_| |_| /_/ \_\_|\__, |\___/|_|
     * |_|\__|_| |_|_| |_| |_| |___/
     */

    /**
     * Performs RDF normalization on the given JSON-LD input.
     * 
     * @param input
     *            the expanded JSON-LD object to normalize.
     * @param options
     *            the normalization options.
     * @param callback
     *            (err, normalized) called once the operation completes.
     * @throws JSONLDProcessingError
     */
    public Object normalize(Map dataset) throws JsonLdError {
        // create quads and map bnodes to their associated quads
        final List quads = new ArrayList();
        final Map bnodes = new LinkedHashMap();
        for (String graphName : dataset.keySet()) {
            final List> triples = (List>) dataset
                    .get(graphName);
            if ("@default".equals(graphName)) {
                graphName = null;
            }
            for (final Map quad : triples) {
                if (graphName != null) {
                    if (graphName.indexOf("_:") == 0) {
                        final Map tmp = new LinkedHashMap();
                        tmp.put("type", "blank node");
                        tmp.put("value", graphName);
                        quad.put("name", tmp);
                    } else {
                        final Map tmp = new LinkedHashMap();
                        tmp.put("type", "IRI");
                        tmp.put("value", graphName);
                        quad.put("name", tmp);
                    }
                }
                quads.add(quad);

                final String[] attrs = new String[] { "subject", "object", "name" };
                for (final String attr : attrs) {
                    if (quad.containsKey(attr)
                            && "blank node".equals(((Map) quad.get(attr))
                                    .get("type"))) {
                        final String id = (String) ((Map) quad.get(attr))
                                .get("value");
                        if (!bnodes.containsKey(id)) {
                            bnodes.put(id, new LinkedHashMap>() {
                                {
                                    put("quads", new ArrayList());
                                }
                            });
                        }
                        ((List) ((Map) bnodes.get(id)).get("quads"))
                                .add(quad);
                    }
                }
            }
        }

        // mapping complete, start canonical naming
        final NormalizeUtils normalizeUtils = new NormalizeUtils(quads, bnodes, new UniqueNamer(
                "_:c14n"), opts);
        return normalizeUtils.hashBlankNodes(bnodes.keySet());
    }

}