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

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

There is a newer version: 0.13.6
Show newest version
package com.github.jsonldjava.core;

import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

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

import com.github.jsonldjava.utils.JSONUtils;
import com.github.jsonldjava.utils.Obj;
import com.github.jsonldjava.utils.URL;

import static com.github.jsonldjava.core.JSONLDConsts.*;
import static com.github.jsonldjava.core.JSONLDUtils.*;
import static com.github.jsonldjava.core.RDFDatasetUtils.*;

public class JSONLDProcessor {

    private static final Logger LOG = LoggerFactory.getLogger(JSONLDProcessor.class);
	
    Options opts;

    public JSONLDProcessor() {
        opts = new Options("");
    }

    public JSONLDProcessor(Options opts) {
        if (opts == null) {
            opts = new Options("");
        } else {
            this.opts = opts;
        }
    }

    /**
     * Processes a local context and returns a new active context.
     *
     * @param activeCtx the current active context.
     * @param localCtx the local context to process.
     * @param options the context processing options.
     *
     * @return the new active context.
     */
    ActiveContext processContext(ActiveContext activeCtx, Object localCtx) throws JSONLDProcessingError {
    	
    	// TODO: get context from cache if available
    	
    	// initialize the resulting context
        ActiveContext rval = activeCtx.clone();

        // normalize local context to an array of @context objects
        if (localCtx instanceof Map && ((Map) localCtx).containsKey("@context") && ((Map) localCtx).get("@context") instanceof List) {
            localCtx = ((Map) localCtx).get("@context");
        }

        List> ctxs;
        if (localCtx instanceof List) {
            ctxs = (List>) localCtx;
        } else {
            ctxs = new ArrayList>();
            ctxs.add((Map) localCtx);
        }

        // process each context in order
        for (Object ctx : ctxs) {
        	if (ctx == null) {
        		// reset to initial context
        		rval = new ActiveContext(opts);
        		continue;
            }
        	
        	// context must be an object by now, all URLs resolved before this call
        	if (ctx instanceof Map) {
        		// dereference @context key if present
                if (((Map) ctx).containsKey("@context")) {
                    ctx = (Map) ((Map) ctx).get("@context");
                }
            } else {
            	// context must be an object by now, all URLs resolved before this call
                throw new JSONLDProcessingError("@context must be an object")
                	.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
                	.setDetail("context", ctx);
            }
        	
        	// define context mappings for keys in local context
            Map defined = new LinkedHashMap();
            
            // helper for access to ctx as a map
            Map ctxm = (Map)ctx;
            // handle @base
            if (ctxm.containsKey("@base")) {
            	Object base = ctxm.get("@base");
            	
            	// reset base
            	if (base == null) {
            		base = opts.base;
            	}
            	else if (!isString(base)) {
            		throw new JSONLDProcessingError(
            				"Invalid JSON-LD syntax; the value of \"@base\" in a " +
            				"@context must be a string or null.")
            			.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
            			.setDetail("context", ctx);
            	} 
            	else if (!"".equals(base) && !isAbsoluteIri((String)base)) {
            		throw new JSONLDProcessingError(
            				"Invalid JSON-LD syntax; the value of \"@base\" in a " +
            				"@context must be an absolute IRI or the empty string.")
            			.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
            			.setDetail("context", ctx);
            	}
            	
            	base = URL.parse((String)base);
            	rval.put("@base", base);
            	defined.put("@base", true);
            }
            
            // handle @vocab
            if (ctxm.containsKey("@vocab")) {
            	Object value = ctxm.get("@vocab");
            	if (value == null) {
            		rval.remove("@vocab");
            	}
            	else if (!isString(value)) {
            		throw new JSONLDProcessingError(
            				"Invalid JSON-LD syntax; the value of \"@vocab\" in a " +
            				"@context must be a string or null.")
            			.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
            			.setDetail("context", ctx);
            	}
        		else if (!isAbsoluteIri((String)value)) {
            		throw new JSONLDProcessingError(
            				"Invalid JSON-LD syntax; the value of \"@vocab\" in a " +
            				"@context must be an absolute IRI.")
            			.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
            			.setDetail("context", ctx);
            	}
        		else {
        			rval.put("@vocab", value);
        		}
            	defined.put("@vocab", true);
            }
            
            // handle @language
            if (ctxm.containsKey("@language")) {
            	Object value = ctxm.get("@language");
            	if (value == null) {
            		rval.remove("@language");
            	}
            	else if (!isString(value)) {
            		throw new JSONLDProcessingError(
            				"Invalid JSON-LD syntax; the value of \"@language\" in a " +
            				"@context must be a string or null.")
            			.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
            			.setDetail("context", ctx);
            	}
            	else {
            		rval.put("@language", ((String)value).toLowerCase());
            	}
            	defined.put("@language", true);
            }
            
            // process all other keys
            for (String key : ctxm.keySet()) {
            	createTermDefinition(rval, ctxm, key, defined);
            }
        }
        
        // TODO: cache results

        return rval;
    }
   
    /**
     * Recursively expands an element using the given context. Any context in
     * the element will be removed. All context URLs must have been retrieved
     * before calling this method.
     *
     * @param activeCtx the context to use.
     * @param activeProperty the property for the element, null for none.
     * @param element the element to expand.
     * @param options the expansion options.
     * @param insideList true if the element is a list, false if not.
     *
     * @return the expanded value.
     * 
     * TODO:
     *  - does this function always return a map, or can it also return a list, the expandedValue variable below seems to assume a map, but in javascript, `in` will just return false if the result is a list
     */
    public Object expand(ActiveContext activeCtx, String activeProperty, Object element, Boolean insideList) throws JSONLDProcessingError {
    	// nothing to expand
    	if (element == null) {
    		return null;
    	}

    	// recursively expand array
    	if (element instanceof List) {
    		List rval = new ArrayList();
    		for (Object i : (List) element) {
    			// expand element
    			Object e = expand(activeCtx, activeProperty, i, insideList);
    			if (insideList && (isArray(e) || isList(e))) {
    				// lists of lists are illegal
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; lists of lists are not permitted.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
    				// drop null values
    			} else if (e != null) {
    				if (isArray(e)) {
    					rval.addAll((Collection) e);
    				} else {
    					rval.add(e);
    				}
    			}
    		}
    		return rval;
    	}

    	// recursively expand object
    	if (isObject(element)) {
    		// access helper
    		Map elem = (Map) element;

    		// if element has a context, process it
    		if (elem.containsKey("@context")) {
    			activeCtx = processContext(activeCtx, elem.get("@context"));
    			//elem.remove("@context");
    		}

    		// expand the active property
    		String expandedActiveProperty = expandIri(activeCtx, activeProperty, false, true, null, null); //  {vocab: true}

    		Object rval = new LinkedHashMap();
    		Map mval = (Map) rval; // to make things easier while we know rval is a map
    		List keys = new ArrayList(elem.keySet());
    		Collections.sort(keys);
    		for (String key : keys) {
    			Object value = elem.get(key);
    			Object expandedValue;

    			// skip @context
    			if (key.equals("@context")) {
    				continue;
    			}

    			// expand key to IRI
    			String expandedProperty = expandIri(activeCtx, key, false, true, null, null); //  {vocab: true}

    			// drop non-absolute IRI keys that aren't keywords
    			if (expandedProperty == null || !(isAbsoluteIri(expandedProperty) || isKeyword(expandedProperty))) {
    				continue;
    			}

    			if (isKeyword(expandedProperty) && "@reverse".equals(expandedActiveProperty)) {
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; a keyword cannot be used as a @reverse propery.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("value", value);
    			}

    			if ("@id".equals(expandedProperty) && !isString(value)) {
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@id\" value must a string.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("value", value);
    			}

    			// validate @type value
    			if ("@type".equals(expandedProperty)) {
    				validateTypeValue(value);
    			}

    			// @graph must be an array or an object
    			if ("@graph".equals(expandedProperty) && !(isObject(value) || isArray(value))) {
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@graph\" value must be an object or an array.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("value", value);
    			}

    			// @value must not be an object or an array
    			if ("@value".equals(expandedProperty) && (value instanceof Map || value instanceof List)) {
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@value\" value must not be an object or an array.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("value", value);
    			}

    			// @language must be a string
    			if ("@language".equals(expandedProperty) && !(value instanceof String)) {
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@language\" value must be a string.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("value", value);
    			}

    			// @index must be a string
    			if ("@index".equals(expandedProperty) && !(value instanceof String)) {
    				throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@index\" value must be a string.")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("value", value);
    			}

    			// @reverse must be an object
    			if ("@reverse".equals(expandedProperty)) {
    				if (!isObject(value)) {
    					throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@reverse\" value must be an object.")
    					.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    					.setDetail("value", value);
    				}

    				expandedValue = expand(activeCtx, "@reverse", value, insideList);

	    			// properties double-reversed
	    			if (expandedValue instanceof Map && ((Map) expandedValue).containsKey("@reverse")) {
	    				// TODO: javascript seems to assume that the value of reverse will always be an object, may need to add a check here if this turns out to be the case
	    				Map rev = (Map) ((Map) expandedValue).get("@reverse");
	    				for (String property : rev.keySet()) {
	    					addValue(mval, property, rev.get(property), true);
	    				}
	
	    			}

	    			// FIXME: can this be merged with the code below to simplify?
	    			// merge in all reversed properties
	    			if (expandedValue instanceof Map) { // TODO: javascript doesn't make this check, can we assume expandedValue is always going to be an object?
	    				Map reverseMap = (Map) mval.get("@reverse");
	    				for (String property : ((Map) expandedValue).keySet()) {
	    					if ("@reverse".equals(property)) {
	    						continue;
	    					}
	    					if (reverseMap == null) {
	    						reverseMap = new LinkedHashMap();
	    						mval.put("@reverse", reverseMap);
	    					}
	    					addValue(reverseMap, property, new ArrayList(), true);
	    					List items = (List) ((Map) expandedValue).get(property);
	    					for (Object item : items) {
	    						if (isValue(item) || isList(item)) {
	    							throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@reverse\" value must not be a @value or an @list.")
	    							.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
	    							.setDetail("value", expandedValue);
	    						}
	    						addValue(reverseMap, property, item, true);
	    					}
	    				}
	    			}
	    			continue;
	    		}

    			String container = (String) activeCtx.getContextValue(key, "@container");

    			// handle language map container (skip if value is not an object)
    			if ("@language".equals(container) && isObject(value)) {
    				expandedValue = expandLanguageMap((Map)value);
    			}
    			// handle index container (skip if value is not an object)
    			else if ("@index".equals(container) && isObject(value)) {
    				// NOTE: implementing embeded function expandIndexMap from javascript as rolled out code here
    				// as it doesn't call itself and needs access to this instance's expand method.
    				// using eim_ prefix for variables to avoid clashes
    				String eim_activeProperty = key;
    				List eim_rval = new ArrayList();
    		    	for (String eim_key: ((Map) value).keySet()) {
    		    		List eim_val;
    					if (!isArray(((Map) value).get(eim_key))) {
    						eim_val = new ArrayList();
    						eim_val.add(((Map) value).get(eim_key));
    					} else {
    						eim_val = (List) ((Map) value).get(eim_key);
    					}
    					// NOTE: javascript assumes list result here, so I am as well
    					eim_val = (List) expand(activeCtx, eim_activeProperty, eim_val, false);
    					for (Object eim_item : eim_val) {
    						if (isObject(eim_item)) {
    							if (!((Map) eim_item).containsKey("@index")) {
    								((Map) eim_item).put("@index", eim_key);
    							}
    							eim_rval.add(eim_item);
    						}
    					}
    		    	}
    				expandedValue = eim_rval;
    			} else {
    				// recurse into @list or @set
    				Boolean isList = "@list".equals(expandedProperty);
    				if (isList || "@set".equals(expandedProperty)) {
    					String nextActiveProperty = activeProperty;
    					if (isList && "@graph".equals(expandedActiveProperty)) {
    						nextActiveProperty = null;
    					}
    					expandedValue = expand(activeCtx, nextActiveProperty, value, isList);
    					if (isList && isList(expandedValue)) {
    						throw new JSONLDProcessingError("Invalid JSON-LD syntax; lists of lists are not permitted.")
    						.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
    					}
    				} 
    				else {
    					// recursively expand value with key as new active property
    					expandedValue = expand(activeCtx, key, value, false);
    				}
    			}

    			// drop null values if property is not @value
    			if (expandedValue == null && !"@value".equals(expandedProperty)) {
    				continue;
    			}

    			// convert expanded value to @list if container specified it
    			if (!"@list".equals(expandedProperty) && !isList(expandedValue) && "@list".equals(container)) {
    				// ensure expanded value is an array
    				Map tm = new LinkedHashMap();
    				List tl;
    				if (isArray(expandedValue)) {
    					tl = (List) expandedValue;
    				} else {
    					tl = new ArrayList();
    					tl.add(expandedValue);
    				}
    				tm.put("@list", tl);
    				expandedValue = tm;
    			}

    			// FIXME: can this be merged with the code above to simplify?
    			// merge in all reversed properties
    			if (Boolean.TRUE.equals(Obj.get(activeCtx.mappings, key, "reverse"))) {
    				Map reverseMap = new LinkedHashMap();
    				mval.put("@reverse", reverseMap);
    				if (!isArray(expandedValue)) {
    					List tmp = new ArrayList();
    					tmp.add(expandedValue);
    					expandedValue = tmp;
    				}
    				for (Object item : (List)expandedValue) {
    					if (isValue(item) || isList(item)) {
    						throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@reverse\" value must not be a @value or an @list.")
    						.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    						.setDetail("value", expandedValue);
    					}
    					addValue(reverseMap, expandedProperty, item, true);
    				}
    				continue;
    			}

    			// add value for property
    			// use an array except for certain keywords
    			Boolean useArray = !("@index".equals(expandedProperty) || "@id".equals(expandedProperty) || 
    					"@type".equals(expandedProperty) || "@value".equals(expandedProperty) || 
    					"@language".equals(expandedProperty));
    			addValue(mval, expandedProperty, expandedValue, useArray);

    		}

    		// get property count on expanded output
    		int count = mval.size(); 

    		// @value must only have @language or @type
    		if (mval.containsKey("@value")) {
    			// @value must only have @language or @type
    			if (mval.containsKey("@type") && mval.containsKey("@language")) {
    				throw new JSONLDProcessingError(
    						"Invalid JSON-LD syntax; an element containing \"@value\" may not contain both \"@type\" and \"@language\".")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("element", mval);
    			}
    			int validCount = count -1;
    			if (mval.containsKey("@type") || mval.containsKey("@language")) {
    				validCount -= 1;
    			}
    			if (mval.containsKey("@index")) {
    				validCount -= 1;
    			}
    			if (validCount != 0) {
    				throw new JSONLDProcessingError(
    						"Invalid JSON-LD syntax; an element containing \"@value\" may only have an \"@index\" property " +
    						"and at most one other property which can be \"@type\" or \"@language\".")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("element", mval);
    			}
    			
    			// drop null @values
    			if (mval.get("@value") == null) {
    				rval = null; mval = null;
    			}
    			// drop @language if @value isn't a string
    			else if (mval.containsKey("@language") && !isString(mval.get("@value"))) {
    				mval.remove("@language");
    			}
    		}
    		// convert @type to an array
    		else if (mval.containsKey("@type") && !isArray(mval.get("@type"))) {
    			List tmp = new ArrayList();
    			tmp.add(mval.get("@type"));
    			mval.put("@type", tmp);
    		}
    		// handle @set and @list
    		else if (mval.containsKey("@set") || mval.containsKey("@list")) {
    			if (count > 1 && (count != 2 && mval.containsKey("@index"))) {
    				throw new JSONLDProcessingError(
    						"Invalid JSON-LD syntax; if an element has the property \"@set\" or \"@list\", then it can have " +
    						"at most one other property that is \"@index\".")
    				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
    				.setDetail("element", mval);
    			}
    			// optimize away @set
    			if (mval.containsKey("@set")) {
    				rval = mval.get("@set");
    				mval = null; // result is no longer a map, so don't allow this to be used anymore
    				count = ((Collection)rval).size(); // TODO: i'm sure the result here should be a List, but Collection works, so it'll do for now
    			}
    		} 
    		// drop objects with only @language
    		else if (mval.containsKey("@language") && count == 1) {
    			rval = null; mval = null;
    		}

    		// drop certain top-level object that do not occur in lists
    		if (isObject(rval) && !opts.keepFreeFloatingNodes && !insideList &&
    				(activeProperty == null || "@graph".equals(expandedActiveProperty))) {
    			// drop empty object or top-level @value
    			if (count == 0 || mval.containsKey("@value")) {
    				rval = null; mval = null;
    			} else {
    				// drop nodes that generate no triples
    				boolean hasTriples = false;
    				for (String key: mval.keySet()) {
    					if (hasTriples) {
    						break;
    					}
    					if (!isKeyword(key) || "@graph".equals(key) || "@type".equals(key)) {
    						hasTriples = true;
    					}
    				}
    				if (!hasTriples) {
    					rval = null; mval = null;
    				}
    			}
    		}
    		
    		return rval;
    	}

    	// drop top-level scalars that are not in lists
    	if (!insideList && (activeProperty == null || "@graph".equals(expandIri(activeCtx, activeProperty, false, true, null, null)))) {
    		return null;
    	}
    	
    	// expand element according to value expansion rules
    	return expandValue(activeCtx, activeProperty, element);
    }

    /**
     * Recursively compacts an element using the given active context. All values
     * must be in expanded form before this method is called.
     *
     * @param activeCtx the active context to use.
     * @param activeProperty the compacted property associated with the element
     *          to compact, null for none.
     * @param element the element to compact.
     * @param options the compaction options.
     *
     * @return the compacted value.
     */
    public Object compact(ActiveContext activeCtx, String activeProperty, Object element) throws JSONLDProcessingError {

    	// recursively compact array
        if (isArray(element)) {
            List rval = new ArrayList();
            for (Object i : (List) element) {
            	// compact, dropping any null values
                Object compacted = compact(activeCtx, activeProperty, i);
                if (compacted != null) {
                    rval.add(compacted);
                }
            }
            if (opts.compactArrays && rval.size() == 1) {
            	// use single element if no container is specified
                Object container = activeCtx.getContextValue(activeProperty, "@container");
                if (container == null) {
                    return rval.get(0);
                }
            }
            return rval;
        }

        // recursively compact object
        if (isObject(element)) {
        	// access helper
            Map elem = (Map) element;
            
            // do value compaction on @value and subject references
            if (isValue(element) || isSubjectReference(element)) {
            	return compactValue(activeCtx, activeProperty, element);
            }
            
            // FIXME: avoid misuse of active property as an expanded property?
            boolean insideReverse = ("@reverse".equals(activeProperty));
            
            // process element keys in order
            List keys = new ArrayList(elem.keySet());
            Collections.sort(keys);
            Map rval = new LinkedHashMap();
            for (String expandedProperty : keys) {
                Object expandedValue = elem.get(expandedProperty);
                
                /* TODO:
                // handle ignored keys
                if (opts.isIgnored(key)) {
                	//JSONLDUtils.addValue(rval, key, value, false);
                	rval.put(key, value);
                	continue;
                }
                */
                
                // compact @id and @type(s)
                if ("@id".equals(expandedProperty) || "@type".equals(expandedProperty)) {
                	Object compactedValue;
                	
                	// compact single @id
                    if (isString(expandedValue)) {
                        compactedValue = compactIri(activeCtx, (String)expandedValue, null, "@type".equals(expandedProperty), false);
                    }
                    // expanded value must be a @type array
                    else {
                        List types = new ArrayList();
                        for (String i : (List) expandedValue) {
                            types.add(compactIri(activeCtx, i, null, true, false));
                        }
                        compactedValue = types;
                    }

                    // use keyword alias and add value
                    String alias = compactIri(activeCtx, expandedProperty);
                    addValue(rval, alias, compactedValue, 
                    		isArray(compactedValue) && ((List) expandedValue).size() == 0);
                    continue;
                }

                // handle @reverse
                if ("@reverse".equals(expandedProperty)) {
                	// recursively compact expanded value
                	// TODO: i'm assuming this will always be a map due to the rest of the code 
                	Map compactedValue = (Map) compact(activeCtx, "@reverse", expandedValue);
                	
                	// handle double-reversed properties
                	for (String compactedProperty : compactedValue.keySet()) {
                		
                		if (Boolean.TRUE.equals(Obj.get(activeCtx.mappings, compactedProperty, "reverse"))) {
                			if (!rval.containsKey(compactedProperty) && !opts.compactArrays) {
                				rval.put(compactedProperty, new ArrayList());
                			}
                			addValue(rval, compactedProperty, compactedValue.get(compactedProperty));
                			compactedValue.remove(compactedProperty);
                		}
                	}
                	
                	if (compactedValue.size() > 0) {
                		// use keyword alias and add value
                		addValue(rval, compactIri(activeCtx, expandedProperty), compactedValue);
                	}
                	
                	continue;
                }

                // handle @index property
                if ("@index".equals(expandedProperty)) {
                	// drop @index if inside an @index container
                	String container = (String) activeCtx.getContextValue(activeProperty, "@container");
                	if ("@index".equals(container)) {
                		continue;
                	}
                	
                	// use keyword alias and add value
                	addValue(rval, compactIri(activeCtx, expandedProperty), expandedValue);
                	continue;
                }
                
                // NOTE: expanded value must be an array due to expansion algorithm.
                
                // preserve empty arrays
                if (((List) expandedValue).size() == 0) {
                	addValue(rval, compactIri(activeCtx, expandedProperty, expandedValue, true, insideReverse), expandedValue, true);
                }
               
                // recusively process array values
                for (Object expandedItem : (List) expandedValue) {
                	// compact property and get container type
                	String itemActiveProperty = compactIri(activeCtx, expandedProperty, expandedItem, true, insideReverse);
                    String container = (String) activeCtx.getContextValue(itemActiveProperty, "@container");
                    
                    // get @list value if appropriate
                    boolean isList = isList(expandedItem);
                    Object list = null;
                    if (isList) {
                    	list = ((Map) expandedItem).get("@list");
                    }
                    
                    // recursively compact expanded item
                    Object compactedItem = compact(activeCtx, itemActiveProperty, isList ? list : expandedItem);
                    
                    // handle @list
                    if (isList) {
                    	// ensure @list value is an array
                    	if (!isArray(compactedItem)) {
                    		List tmp = new ArrayList();
                    		tmp.add(compactedItem);
                    		compactedItem = tmp;
                    	}
                    	
                    	if (!"@list".equals(container)) {
                    		// wrap using @list alias
                    		Map wrapper = new LinkedHashMap();
                    		wrapper.put(compactIri(activeCtx, "@list"), compactedItem);
                    		compactedItem = wrapper;
                    		
                    		// include @index from expanded @list, if any
                    		if (((Map) expandedItem).containsKey("@index")) {
                    			((Map) compactedItem).put(compactIri(activeCtx, "@index"), ((Map) expandedItem).get("@index"));
                    		}
                    	}
                    	// can't use @list container for more than 1 list
                    	else if (rval.containsKey(itemActiveProperty)) {
                    		throw new JSONLDProcessingError(
            						"Invalid JSON-LD compact error; property has a \"@list\" @container " +
            						"rule but there is more than a single @list that matches " +
            						"the compacted term in the document. Compaction might mix " +
            						"unwanted items into the list.")
            					.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
                    	}
                    }
                    
                    // handle language and index maps
                    if ("@language".equals(container) || "@index".equals(container)) {
                    	// get or create the map object
                    	Map mapObject;
                    	if (rval.containsKey(itemActiveProperty)) {
                    		mapObject = (Map) rval.get(itemActiveProperty);
                    	}
                    	else {
                    		mapObject = new LinkedHashMap();
                    		rval.put(itemActiveProperty, mapObject);
                    	}
                    	
                    	// if container is a language map, simplify compacted value to
                    	// a simple string
                    	if ("@language".equals(container) && isValue(compactedItem)) {
                    		compactedItem = ((Map) compactedItem).get("@value");
                    	}
                    	
                    	// add compact value to map object using key from expanded value
                    	// based on the container type
                    	addValue(mapObject, (String)((Map) expandedItem).get(container), compactedItem);
                    }
                    else {
                    	// use an array if: compactArrays flag is false,
                    	// @container is @set or @list, value is an empty
                    	// array, or key is @graph
                    	Boolean isArray = (!opts.compactArrays || "@set".equals(container) || "@list".equals(container) ||
                    			(isArray(compactedItem) && ((List) compactedItem).size() == 0) ||
                    			"@list".equals(expandedProperty) || "@graph".equals(expandedProperty));
                    	
                    	// add compact value
                    	addValue(rval, itemActiveProperty, compactedItem, isArray);
                    }
                }
            }
            
            return rval;
        }
        
        // only primatives remain which are already compact
        return element;
    }

	private class FramingContext {
    	public Map embeds = null;
    	public Map graphs = null;
    	public Map subjects = null;
    	public Options options = opts;
    }
    
	/**
	 * 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 Object frame(Object input, Object frame) throws JSONLDProcessingError {
    	// create framing state
    	FramingContext state = new FramingContext();
    	//Map state = new HashMap();
    	//state.put("options", this.opts);
    	state.graphs = new LinkedHashMap();
    	state.graphs.put("@default", new LinkedHashMap());
    	state.graphs.put("@merged", new LinkedHashMap());
    	
    	// produce a map of all graphs and name each bnode
    	// FIXME: currently uses subjects from @merged graph only
    	UniqueNamer namer = new UniqueNamer("_:b");
    	createNodeMap(input, state.graphs, "@merged", namer);
    	state.subjects = (Map) state.graphs.get("@merged");
    	
    	// frame the subjects
        List framed = new ArrayList();
        List sortedKeys = new ArrayList(state.subjects.keySet());
        Collections.sort(sortedKeys);
        frame(state, sortedKeys, frame, 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, Collection subjects,
			Object frame, Object parent, String property) throws JSONLDProcessingError {
		// validate the frame
    	validateFrame(state, frame);
    	// NOTE: once validated we move to the function where the frame is specifically a map
    	frame(state, subjects, (Map)((List)frame).get(0), parent, property);
    }
    
    private void frame(FramingContext state, Collection subjects, 
    		Map frame, Object parent, String property) throws JSONLDProcessingError {
    	// filter out subjects that match the frame
    	Map matches = filterSubjects(state, subjects, frame);
    	
    	// get flags for current frame
    	Options options = state.options;
    	Boolean embedOn = (frame.containsKey("@embed")) ? (Boolean)((List)frame.get("@embed")).get(0) : options.embed;
    	Boolean explicicOn = (frame.containsKey("@explicit")) ? (Boolean)((List)frame.get("@explicit")).get(0) : options.explicit;
    	
    	// add matches to output
    	List ids = new ArrayList(matches.keySet());
    	Collections.sort(ids);
    	for (String id: ids) {
    		
    		// Note: In order to treat each top-level match as a compartmentalized
    	    // result, create an independent copy of the embedded subjects map when the
    	    // property is null, which only occurs at the top-level.
    		if (property == null) {
    			state.embeds = new LinkedHashMap();
    		}
    		
    		// start output
    		Map output = new LinkedHashMap();
    		output.put("@id", id);
    		
    		// prepare embed meta info
    		Map embed = new LinkedHashMap();
    		embed.put("parent", parent);
    		embed.put("property", property);
    		
    		// if embed is on and there is an existing embed
    		if (embedOn && state.embeds.containsKey(id)) {
    			// only overwrite an existing embed if it has already been added to its
    			// parent -- otherwise its parent is somewhere up the tree from this
    			// embed and the embed would occur twice once the tree is added
    			embedOn = false;
    			
    			// existing embed's parent is an array
    			Map existing = (Map) state.embeds.get(id);
    			if (isArray(existing.get("parent"))) {
    				for (Object o: (List)existing.get("parent")) {
    					if (compareValues(output, o)) {
    						embedOn = true;
    						break;
    					}
    				}
    			}
    			// existing embed's parent is an object
    			else if (hasValue((Map)existing.get("parent"), (String)existing.get("property"), output)) {
    				embedOn = true;
    			}
    			
    			// 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, embed);
    			
    			// iterate over subject properties
    			Map subject = (Map) matches.get(id);
    			List props = new ArrayList(subject.keySet());
    			Collections.sort(props);
    			for (String prop: props) {

    				// handle ignored keys
    				if (opts.isIgnored(prop)) {
    					output.put((String) prop, JSONLDUtils.clone(subject.get(prop)));
    					continue;
    				}
    				
    				// copy keywords to output
    				if (isKeyword(prop)) {
    					output.put((String) prop, JSONLDUtils.clone(subject.get(prop)));
    					continue;
    				}
    				
    				// if property isn't in the frame
    				if (!frame.containsKey(prop)) {
    					// if explicit is off, embed values
    					if (!explicicOn) {
    						embedValues(state, subject, prop, output);
    					}
    					continue;
    				}
    				
    				// add objects
    				Object objects = subject.get(prop); 
    				// TODO: i've done some crazy stuff here because i'm unsure if objects is always a list or if it can
    				// be a map as well. I think it's always a map, but i'll get it working like this first
    				for (Object i: objects instanceof List ? (List)objects : ((Map)objects).keySet()) {
    					Object o = objects instanceof List ? i : ((Map)objects).get(i);
    					
    					// recurse into list
    					if (isList(o)) {
    						// add empty list
    						Map list = new LinkedHashMap();
    						list.put("@list", new ArrayList());
    						addFrameOutput(state, output, prop, list);
    						
    						// add list objects
    						List src = (List)((Map)o).get("@list");
    						for (Object n: src) {
    							// recurse into subject reference
    							if (isSubjectReference(o)) {
    								List tmp = new ArrayList();
    								tmp.add(((Map)n).get("@id"));
    								frame(state, tmp, frame.get(prop), list, "@list");
    							} else {
    								// include other values automatcially
    								addFrameOutput(state, list, "@list", (Map) JSONLDUtils.clone(n));
    							}
    						}
    						continue;
    					}
    					
    					// recurse into subject reference
    					if (isSubjectReference(o)) {
    						List tmp = new ArrayList();
							tmp.add(((Map)o).get("@id"));
							frame(state, tmp, frame.get(prop), output, prop);
    					} else {
    						// include other values automatically
    						addFrameOutput(state, output, prop, (Map) JSONLDUtils.clone(o));
    					}
    				}
    			}
    			
    			// handle defaults
    			props = new ArrayList(frame.keySet());
    			Collections.sort(props);
    			for (String prop: props) {
    				// skip keywords
    				if (isKeyword(prop)) {
    					continue;
    				}
    				
    				// if omit default is off, then include default values for properties
    		        // that appear in the next frame but are not in the matching subject
    				Map next = (Map) ((List) frame.get(prop)).get(0);
    				boolean omitDefaultOn = 
    						(next.containsKey("@omitDefault")) ? (Boolean)((List)next.get("@omitDefault")).get(0) : options.omitDefault ;
    				if (!omitDefaultOn && !output.containsKey(prop)) {
    					Object preserve = "@null";
    					if (next.containsKey("@default")) {
    						preserve = JSONLDUtils.clone(next.get("@default"));
    					}
    					if (!isArray(preserve)) {
    						List tmp = new ArrayList();
    						tmp.add(preserve);
        					preserve = tmp;
    					}
    					Map tmp1 = new LinkedHashMap();
    					tmp1.put("@preserve", preserve);
    					List tmp2 = new ArrayList();
    					tmp2.add(tmp1);
    					output.put(prop, tmp2);
    				}
    			}
    			
    			// add output to parent
    			addFrameOutput(state, parent, property, output);
    		}
    	}
	}
    
    /**
     * Embeds values for the given subject and property into the given output
     * during the framing algorithm.
     *
     * @param state the current framing state.
     * @param subject the subject.
     * @param property the property.
     * @param output the output.
     */
    private void embedValues(FramingContext state,
			Map subject, String property, Object output) {
    	// embed subject properties in output
    	Object objects = subject.get(property);
    	
    	// TODO: more craziness due to lack of knowledge about whether objects should
    	// be an array or an object
    	for (Object i: objects instanceof List ? (List)objects : ((Map)objects).keySet()) {
			Object o = objects instanceof List ? i : ((Map)objects).get(i);
			
			// recurse into @list
			if (isList(o)) {
				Map list = new LinkedHashMap();
				list.put("@list", new ArrayList());
				addFrameOutput(state, output, property, list);
				embedValues(state, (Map)o, "@list", list.get("@list"));
				return;
			}
			
			// handle subject reference
			if (isSubjectReference(o)) {
				String id = (String) ((Map) o).get("@id");
				
				// embed full subject if isn't already embedded
				if (!state.embeds.containsKey(id)) {
					// add embed
					Map embed = new LinkedHashMap();
					embed.put("parent", output);
					embed.put("property", property);
					state.embeds.put(id, embed);
					
					// recurse into subject
					o = new LinkedHashMap();
					Map s = (Map) state.subjects.get(id);
					for (String prop: s.keySet()) {
						// copy keywords
						if (isKeyword(prop) || opts.isIgnored(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));
			}
    	}
		
	}

	/**
     * 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 (isObject(parent)) {
			addValue((Map)parent, property, output, true);
		} else {
			((List)parent).add(output);
		}
		
	}

    /**
     * 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
		Map embeds = state.embeds;
		Map embed = (Map) embeds.get(id);
		Object parent = embed.get("parent");
		String property = (String) embed.get("property");
		
		// create reference to replace embed
		Map subject = new LinkedHashMap();
		subject.put("@id", id);
		
		// remove existing embed
		if (isArray(parent)) {
			// replace subject with reference
			for (int i = 0; i < ((List)parent).size(); i++) {
				if (compareValues(((List)parent).get(i), subject)) {
					((List)parent).set(i, subject);
					break;
				}
			}
		} else {
			// replace subject with reference
			removeValue(((Map)parent), property, subject, ((Map) parent).get(property) instanceof List);
			addValue(((Map)parent), property, subject, ((Map) parent).get(property) instanceof List);
		}
		
		// 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
    	Set ids = embeds.keySet();
    	for (String next: ids) {
    		if (embeds.containsKey(next) && 
    				((Map) embeds.get(next)).get("parent") instanceof Map &&
    				id.equals(((Map) ((Map) embeds.get(next)).get("parent")).get("@id"))) {
    			embeds.remove(next);
    			removeDependents(embeds, next);
    		}
    	}
    }

	/**
     * Returns a map of all of the subjects that match a parsed frame.
     *
     * @param state the current framing state.
     * @param subjects the set of subjects to filter.
     * @param frame the parsed frame.
     *
     * @return all of the matched subjects.
     */
    private static Map filterSubjects(FramingContext state,
			Collection subjects, Map frame) {
    	// filter subjects in @id order
		Map rval = new LinkedHashMap();
		for (String id: subjects) {
			Map subject = (Map) state.subjects.get(id);
			if (filterSubject(subject, frame)) {
				rval.put(id, subject);
			}
		}
		return rval;
	}

    /**
     * Returns true if the given subject matches the given frame.
     *
     * @param subject the subject to check.
     * @param frame the frame to check.
     *
     * @return true if the subject matches, false if not.
     */
	private static boolean filterSubject(Map subject, Map frame) {
		// check @type (object value means 'any' type, fall through to ducktyping)
		Object t = frame.get("@type");
		// TODO: it seems @type should always be a list
		if (frame.containsKey("@type") && !(t instanceof List && ((List)t).size() == 1 && ((List)t).get(0) instanceof Map)) {
			for (Object i: (List)t) {
				if (hasValue(subject, "@type", i)) {
					return true;
				}
			}
			return false;
		}
		
		// check ducktype
		for (String key: frame.keySet()) {
			if ("@id".equals(key) || !isKeyword(key) && !(subject.containsKey(key))) {
				return false;
			}
		}
		return true;
	}

	/**
     * Validates a JSON-LD frame, throwing an exception if the frame is invalid.
     *
     * @param state the current frame state.
     * @param frame the frame to validate.
	 * @throws JSONLDProcessingError 
     */
	private static void validateFrame(FramingContext state, Object frame) throws JSONLDProcessingError {
		if (!(frame instanceof List) || ((List)frame).size() != 1 || !(((List) frame).get(0) instanceof Map)) {
			throw new JSONLDProcessingError("Invalid JSON-LD syntax; a JSON-LD frame must be a single object.")
				.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
				.setDetail("frame", frame);
		}
	}
	
	 /**
     * 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 JSONLDProcessingError {
		// create quads and map bnodes to their associated quads
		List quads = new ArrayList();
		Map bnodes = new LinkedHashMap();
		for (String graphName : dataset.keySet()) {
			List> triples = (List>) dataset.get(graphName);
			if ("@default".equals(graphName)) {
				graphName = null;
			}
			for (Map quad : triples) {
				if (graphName != null) {
					if (graphName.indexOf("_:") == 0) {
						Map tmp = new LinkedHashMap();
						tmp.put("type", "blank node");
						tmp.put("value", graphName);
						quad.put("name", tmp);
					} else {
						Map tmp = new LinkedHashMap();
						tmp.put("type", "IRI");
						tmp.put("value", graphName);
						quad.put("name", tmp);
					}
				}
				quads.add(quad);
				
				String[] attrs = new String[] { "subject", "object", "name" };
				for (String attr : attrs) {
					if (quad.containsKey(attr) && "blank node".equals(((Map) quad.get(attr)).get("type"))) {
						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
		NormalizeUtils normalizeUtils = new NormalizeUtils(quads, bnodes, new UniqueNamer("_:c14n"), opts);
		return normalizeUtils.hashBlankNodes(bnodes.keySet());
	}
	
	/**
	 * Adds RDF triples for each graph in the given node map to an RDF dataset.
	 *
	 * @param nodeMap the node map.
	 *
	 * @return the RDF dataset.
	 */
	public Map toRDF(Map nodeMap) {
		UniqueNamer namer = new UniqueNamer("_:b");
		Map dataset = new LinkedHashMap();
		for (String graphName : nodeMap.keySet()) {
			Map graph = (Map) nodeMap.get(graphName);
			if (graphName.indexOf("_:") == 0) {
				graphName = namer.getName(graphName);
			}
			dataset.put(graphName, graphToRDF(graph, namer));
		}
		return dataset;
	}
	
	/**
	 * 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 Map dataset) throws JSONLDProcessingError {
		final Map defaultGraph = new LinkedHashMap();
		final Map> graphMap = new LinkedHashMap>() {{
			put("@default", defaultGraph);
		}};
		
		// For each graph in RDF dataset
		for (final String name : dataset.keySet()) {
			
			final List> graph = (List>)dataset.get(name);
			
			// If graph map has no name member, create one and set its value to an empty JSON object.
			Map nodeMap;
			if (!graphMap.containsKey(name)) {
				nodeMap = new LinkedHashMap();
				graphMap.put(name, nodeMap);
			} else {
				nodeMap = graphMap.get(name);
			}
			
			// If graph is not the default graph and default graph does not have a name member, create such 
			// a member and initialize its value to a new JSON object with a single member @id whose value is name.
			if (!"@default".equals(name) && !Obj.contains(defaultGraph, name)) {
				Obj.put(defaultGraph, name, new LinkedHashMap() {{
					put("@id", name);
				}});
			}
			
			// For each RDF triple in graph consisting of subject, predicate, and object
			for (Map triple: graph) {
				final String subject = (String)Obj.get(triple, "subject", "value");
				final String predicate = (String)Obj.get(triple, "predicate", "value");
				final Map object = (Map) triple.get("object");
				
				// If node map does not have a subject member, create one and initialize its value to a new JSON object 
				// consisting of a single member @id whose value is set to subject.
				Map node;
				if (!nodeMap.containsKey(subject)) {
					node = new LinkedHashMap() {{
						put("@id", subject);
					}};
					nodeMap.put(subject, node);
				} else {
					node = (Map) nodeMap.get(subject);
				}
				
				// If object is an IRI or blank node identifier, does not equal rdf:nil, and node map does not have an object member, 
				// create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
				if (("IRI".equals(object.get("type")) || ((String)object.get("value")).startsWith("_:")) 
						&& !RDF_NIL.equals(object.get("value")) && !nodeMap.containsKey(object.get("value"))) {
					nodeMap.put((String)object.get("value"), new LinkedHashMap() {{ 
						put("@id", object.get("value"));
					}});
				}
				
				// If predicate equals rdf:type, and object is an IRI or blank node identifier, append object to the value of the
				// @type member of node. If no such member exists, create one and initialize it to an array whose only item is object. 
				// Finally, continue to the next RDF triple
				if (RDF_TYPE.equals(predicate) && ("IRI".equals(object.get("type")) || ((String)object.get("value")).startsWith("_:"))) {
					addValue(node, "@type", object.get("value"), true);
					continue;
				}
				
				// If object equals rdf:nil and predicate does not equal rdf:rest, set value to a new JSON object 
				// consisting of a single member @list whose value is set to an empty array.
				Map value;
				if (RDF_NIL.equals(object.get("value")) && !RDF_REST.equals(predicate)) {
					value = new LinkedHashMap() {{
						put("@list", new ArrayList());
					}};
				} else {
					// Otherwise, set value to the result of using the RDF to Object Conversion algorithm, passing object and use native types.
					value = RDFToObject(object, opts.useNativeTypes);
				}
				
				// If node does not have an predicate member, create one and initialize its value to an empty array.
				// Add a reference to value to the to the array associated with the predicate member of node.
				addValue(node, predicate, value, true);
				
				// If object is a blank node identifier and predicate equals neither rdf:first nor rdf:rest, it might represent the head of a RDF list
				if ("blank node".equals(object.get("type")) && !RDF_FIRST.equals(predicate) && !RDF_REST.equals(predicate)) {
					// If the object member of node map has an usages member, add a reference to value to it; 
					// otherwise create such a member and set its value to an array whose only item is a reference to value.
					addValue((Map)nodeMap.get(object.get("value")), "usages", value, true);
				}
			}
		}
		
		// build @lists
		for (String name: graphMap.keySet()) {
			Map graph = graphMap.get(name);
			
			List subjects = new ArrayList(graph.keySet());
			
			for (String subj: subjects) {
				// If graph object does not have a subj member, it has been removed as it was part of a list. Continue with the next subj.
				if (!graph.containsKey(subj)) {
					continue;
				}
				
				// If node has no usages member or its value is not an array consisting of one item, continue with the next subj.
				Map node = (Map) graph.get(subj);
				if (!node.containsKey("usages") || !(node.get("usages") instanceof List) || ((List)node.get("usages")).size() != 1) {
					continue;
				}
				Map value = (Map) ((List)node.get("usages")).get(0);
				List list = new ArrayList();
				List listNodes = new ArrayList();
				String subject = subj;
				
				while (!RDF_NIL.equals(subject) && list != null) {
					// If node is null; the value of its @id member does not begin with _: ...
					boolean test = node == null || ((String)node.get("@id")).indexOf("_:") != 0;
					if (!test) {
						int cnt = 0;
						for (String i : new String[] { "@id", "usages", RDF_FIRST, RDF_REST }) {
							if (node.containsKey(i)) {
								cnt++;
							}
						}
						// it has members other than @id, usages, rdf:first, and rdf:rest ...
						test = (node.keySet().size() > cnt);
						if (!test) {
							// the value of its rdf:first member is not an array consisting of a single item ...
							test = !(node.get(RDF_FIRST) instanceof List) || ((List)node.get(RDF_FIRST)).size() != 1;
							if (!test) {
								// or the value of its rdf:rest member is not an array containing a single item which is a JSON object that has an @id member ...
								test = !(node.get(RDF_REST) instanceof List) || ((List)node.get(RDF_REST)).size() != 1;
								if (!test) {
									Object o = ((List)node.get(RDF_REST)).get(0);
									test = (!(o instanceof Map && ((Map) o).containsKey("@id")));
								}
							}
						}
					}
					if (test) {
						// it is not a valid list node. Set list to null
						list = null;
					} else {
						list.add(((List) node.get(RDF_FIRST)).get(0));
						listNodes.add((String) node.get("@id"));
						subject = (String) ((Map) ((List) node.get(RDF_REST)).get(0)).get("@id");
						node = (Map) graph.get(subject);
						if (listNodes.contains(subject)) {
							list = null;
						}
					}
				}
				
				// If list is null, continue with the next subj.
				if (list == null) {
					continue;
				}
				
				// Remove the @id member from value.
				value.remove("@id");
				
				// Add an @list member to value and initialize it to list.
				value.put("@list", list);
				
				for (String subject_ : listNodes) {
					graph.remove(subject_);
				}
				
			}
		}
		
		List result = new ArrayList();
		List ids = new ArrayList(defaultGraph.keySet());
		Collections.sort(ids);
		for (String subject : ids) {
			Map node = (Map) defaultGraph.get(subject);
			if (graphMap.containsKey(subject)) {
				node.put("@graph", new ArrayList());
				List keys = new ArrayList(graphMap.get(subject).keySet());
				Collections.sort(keys);
				for (String s : keys) {
					Map n = (Map) graphMap.get(subject).get(s);
					n.remove("usages");
					((List) node.get("@graph")).add(n);
				}
			}
			node.remove("usages");
			result.add(node);
		}
		
		return result;
	}
    
    /**
     * Performs JSON-LD flattening.
     *
     * @param input the expanded JSON-LD to flatten.
     *
     * @return the flattened output.
     * @throws JSONLDProcessingError 
     */
	public List flatten(List input) throws JSONLDProcessingError {
		// produce a map of all subjects and name each bnode
		UniqueNamer namer = new UniqueNamer("_:b");
		Map graphs = new LinkedHashMap() {{
			put("@default", new LinkedHashMap());
		}};
		createNodeMap(input, graphs, "@default", namer);
		
		// add all non-default graphs to default graph
		Map defaultGraph = (Map) graphs.get("@default");
		List graphNames = new ArrayList(graphs.keySet());
		Collections.sort(graphNames);
		for (String graphName : graphNames) {
			if ("@default".equals(graphName)) {
				continue;
			}
			Map nodeMap = (Map) graphs.get(graphName);
			Map subject = (Map) defaultGraph.get(graphName);
			if (subject == null) {
				subject = new LinkedHashMap();
				subject.put("@id", graphName);
				subject.put("@graph", new ArrayList());
				defaultGraph.put(graphName, subject);
			}
			else if (!subject.containsKey("@graph")) {
				subject.put("@graph", new ArrayList());
			}
			List graph = (List) subject.get("@graph");
			List ids = new ArrayList(nodeMap.keySet());
			for (String id : ids) {
				graph.add(nodeMap.get(id));
			}
		}
		
		// produce flattened output
		List flattened = new ArrayList();
		List keys = new ArrayList(defaultGraph.keySet());
		Collections.sort(keys);
		for (String key : keys) {
			flattened.add(defaultGraph.get(key));
		}
		return flattened;
	}

	/**
     * Generates a unique simplified key from a URI and add it to the context 
     * 
     * @param key to full URI to generate the simplified key from
     * @param ctx the context to add the simplified key too
     * @param isid whether to set the type to @id
     */
    private static void processKeyVal(Map ctx, String key, Object val) {
        int idx = key.lastIndexOf('#');
        if (idx < 0) {
            idx = key.lastIndexOf('/');
        }
        String skey = key.substring(idx + 1);
        Object keyval = key;
        Map entry = new LinkedHashMap();
        entry.put("@id", keyval);
        Object v = val;
    	while (true) {
    		if (v instanceof List && ((List)v).size() > 0) {
    			// use the first entry as a reference
    			v = ((List)v).get(0);
    			continue;
    		}
    		if (v instanceof Map && ((Map)v).containsKey("@list")) {
    			v = ((Map)v).get("@list");
    			entry.put("@container", "@list");
    			continue;
    		}
    		if (v instanceof Map && ((Map)v).containsKey("@set")) {
    			v = ((Map)v).get("@set");
    			entry.put("@container", "@set");
    			continue;
    		}
    		break;
    	}
    	if (v instanceof Map && ((Map) v).containsKey("@id")) {
    		entry.put("@type", "@id");
    	}
    	if (entry.size() == 1) {
    		keyval = entry.get("@id");
    	} else {
    		keyval = entry;
    	}
        while (true) {
            // check if the key is already in the frame ctx
            if (ctx.containsKey(skey)) {
                // if so, check if the values are the same
                if (JSONUtils.equals(ctx.get(skey), keyval)) {
                    // if they are, skip adding this
                    break;
                }
                // if not, add a _ to the simple key and try again
                skey += "_";
            } else {
                ctx.put(skey, keyval);
                break;
            }
        }
    }
	
	/**
     * Generates the context to be used by simplify.
     * 
     * @param input
     * @param ctx
     */
    private static void generateSimplifyContext(Object input, Map ctx) {
        if (input instanceof List) {
            for (Object o : (List) input) {
                generateSimplifyContext(o, ctx);
            }
        } else if (input instanceof Map) {
            Map o = (Map) input;
            Map localCtx = (Map) o.remove("@context");
            for (String key : o.keySet()) {
                Object val = o.get(key);
                if (key.matches("^https?://.+$")) {
                	processKeyVal(ctx, key, val);
                }
                if ("@type".equals(key)) {
                    if (!(val instanceof List)) {
                        List tmp = new ArrayList();
                        tmp.add(val);
                        val = tmp;
                    }
                    for (Object t : (List) val) {
                        if (t instanceof String) {
                            processKeyVal(ctx, (String) t, new LinkedHashMap() {{
                            	put("@id", "");
                            }});
                        } else {
                            throw new RuntimeException("TODO: don't yet know how to handle non-string types in @type");
                        }
                    }
                } else if (val instanceof Map || val instanceof List) {
                    generateSimplifyContext(val, ctx);
                }
            }
        }
    }

    /**
     * Automatically builds a context which attempts to simplify the keys and values as much as possible
     * and uses that context to compact the input
     * 
     * NOTE: this is experimental and only built for specific conditions
     * 
     * @param input
     * @return the simplified version of input
     * @throws JSONLDProcessingError 
     */
    public Object simplify(Object input) throws JSONLDProcessingError {

        Object expanded = JSONLD.expand(input, opts);
        Map ctx = new LinkedHashMap();
        
        generateSimplifyContext(expanded, ctx);

        Map tmp = new LinkedHashMap();
        tmp.put("@context", ctx);
        
        // add optimize flag to opts (clone the opts so we don't change the flag for the base processor)
        Options opts1 = opts.clone();
        //opts1.optimize = true;
        return JSONLD.compact(input, tmp, opts1);
    }
    
}