/**
 * Copyright (c) 2015 Source Auditor Inc.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
*/
package org.spdx.rdfparser;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;

import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.rdf.model.AnonId;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.Property;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.util.iterator.ExtendedIterator;
import org.spdx.rdfparser.license.AnyLicenseInfo;
import org.spdx.rdfparser.license.ExtractedLicenseInfo;
import org.spdx.rdfparser.license.LicenseInfoFactory;
import org.spdx.rdfparser.model.ExternalDocumentRef;
import org.spdx.rdfparser.model.IRdfModel;
import org.spdx.rdfparser.model.RdfModelObject;
import org.spdx.rdfparser.model.Relationship;
import org.spdx.rdfparser.model.SpdxDocument;
import org.spdx.rdfparser.model.SpdxElement;
import org.spdx.rdfparser.model.SpdxElementFactory;
import org.spdx.rdfparser.model.SpdxFile;
import org.spdx.rdfparser.model.SpdxPackage;
import org.spdx.rdfparser.model.SpdxSnippet;
import org.spdx.spdxspreadsheet.InvalidLicenseStringException;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;


/**
 * This class contains the SPDX Document and provides some of the basic
 * RDF model support. This class also manages the SpdxRef, ExtractedLicenseInfos,
 * and LicenseRefs.
 * 
 * Separating the container aspects of the SpdxDocument into this separate
 * class allows for the SpdxDocument to follow the rdfparser.model pattern.
 * 
 * @author Gary O'Neall
 *
 */
public class SpdxDocumentContainer implements IModelContainer, SpdxRdfConstants {

	public static final String POINT_EIGHT_SPDX_VERSION = "SPDX-0.8";
	public static final String POINT_NINE_SPDX_VERSION = "SPDX-0.9";
	public static final String ONE_DOT_ZERO_SPDX_VERSION = "SPDX-1.0";
	public static final String ONE_DOT_ONE_SPDX_VERSION = "SPDX-1.1";
	public static final String ONE_DOT_TWO_SPDX_VERSION = "SPDX-1.2";
	public static final String TWO_POINT_ZERO_VERSION = "SPDX-2.0";
	public static final String TWO_POINT_ONE_VERSION = "SPDX-2.1";
	public static final String TWO_POINT_TWO_VERSION = "SPDX-2.2";
	public static final String CURRENT_SPDX_VERSION = TWO_POINT_TWO_VERSION;
	
	public static final String CURRENT_IMPLEMENTATION_VERSION = "2.2.0";
	
	static Set<String> SUPPORTED_SPDX_VERSIONS = Sets.newHashSet();
	
	/**
	 * Keep track of all existing SPDX element references
	 */
	Set<String> spdxRefs = Sets.newHashSet();
	
	/**
	 * Map of lower case license ID to extracted license info
	 */
	Map<String, ExtractedLicenseInfo> licenseIdToExtractedLicense = Maps.newHashMap();
	
	/**
	 * Map of external document ID's to external document references
	 */
	Map<String, ExternalDocumentRef> externalDocIdToRef = Maps.newHashMap();
	/**
	 * Map of external document namespaces to external document references
	 */
	Map<String, ExternalDocumentRef> externalDocNamespaceToRef = Maps.newHashMap();
	/**
	 * Map of nodes to RDF model objects - used to improve performance by keeping track of which 
	 * nodes have more than one object associated with it.
	 */
	Map<Node, List<IRdfModel>> nodeModelMap = Maps.newHashMap();
	
	static {
		SUPPORTED_SPDX_VERSIONS.add(CURRENT_SPDX_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(POINT_EIGHT_SPDX_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(POINT_NINE_SPDX_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(ONE_DOT_ZERO_SPDX_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(ONE_DOT_ONE_SPDX_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(ONE_DOT_TWO_SPDX_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(TWO_POINT_ZERO_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(TWO_POINT_ONE_VERSION);
		SUPPORTED_SPDX_VERSIONS.add(TWO_POINT_TWO_VERSION);
	}
	private Model model;
	private String documentNamespace;
	private Node documentNode;
	private SpdxDocument spdxDocument;
	
	/**
	 * Keeps tract of the next license reference number when generating the license ID's for
	 * non-standard licenses
	 */
	protected int nextLicenseRef = 1;
	/**
	 * Keeps track of the next SPDX element reference
	 */
	private AtomicInteger nextElementRef = new AtomicInteger(0);
	
	/**
	 * Construct an SpdxDocumentContainer from an existing model which
	 * already contain an SPDX Document
	 * @param model 
	 * @throws InvalidSPDXAnalysisException 
	 * 
	 */
	public SpdxDocumentContainer(Model model) throws InvalidSPDXAnalysisException {
		this.model = model;
		this.documentNode = getSpdxDocNode();
		if (this.documentNode == null) {
			throw(new InvalidSPDXAnalysisException("Invalid model - must contain an SPDX Document"));
		}
		if (!this.documentNode.isURI()) {
			throw(new InvalidSPDXAnalysisException("SPDX Documents must have a unique URI"));
		}
		String docUri = this.documentNode.getURI();
		this.documentNamespace = this.formDocNamespace(docUri);
		initializeExternalDocumentRefs();
		this.spdxDocument = new SpdxDocument(this, this.documentNode);
		initializeNextLicenseRef();
		initializeNextElementRef();
		if (this.spdxDocument.getDocumentDescribes().length == 0) {
			upgradeDescribesToRelationship();
		}
	}
	
	/**
	 * @throws InvalidSPDXAnalysisException 
	 * 
	 */
	private void upgradeDescribesToRelationship() throws InvalidSPDXAnalysisException {
		Node p = model.getProperty(SPDX_NAMESPACE, PROP_SPDX_PACKAGE).asNode();
		Triple m = Triple.createMatch(this.documentNode, p, null);
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	
		List<SpdxPackage> describedPackages = Lists.newArrayList();
		while (tripleIter.hasNext()) {
			Triple t = tripleIter.next();
			describedPackages.add(new SpdxPackage(this, t.getObject()));
		}
		for (SpdxPackage pkg:describedPackages) {
			Relationship describes = new Relationship(pkg, 
					Relationship.RelationshipType.DESCRIBES, "");
			this.getSpdxDocument().addRelationship(describes);
		}
	}

	/**
	 * Creates a new empty SPDX Document with the current SPDX document version.
	 * Note: Follow-up calls MUST be made to add the required properties for this
	 * to be a valid SPDX document
	 * @param uri URI for the SPDX Document
	 * @throws InvalidSPDXAnalysisException 
	 */
	public SpdxDocumentContainer(String uri) throws InvalidSPDXAnalysisException {		
		this(uri, CURRENT_SPDX_VERSION);
	}
	
	/**
	 * Creates a new empty SPDX Document.
	 * Note: Follow-up calls MUST be made to add the required properties for this
	 * to be a valid SPDX document
	 * @param uri URI for the SPDX Document
	 * @param spdxVersion The version of SPDX analysis to create (impacts the data license for some versions)
	 * @throws InvalidSPDXAnalysisException 
	 */
	public SpdxDocumentContainer(String uri, String spdxVersion) throws InvalidSPDXAnalysisException {
		this.model = ModelFactory.createDefaultModel();
		String v = verifySpdxVersion(spdxVersion);
		if (v != null) {
			throw(new InvalidSPDXAnalysisException("Invalid SPDX Version: "+v));
		}
		model.setNsPrefix("spdx", SPDX_NAMESPACE);
		model.setNsPrefix("doap", DOAP_NAMESPACE);
		model.setNsPrefix("rdfs", RDFS_NAMESPACE);
		model.setNsPrefix("rdf", RDF_NAMESPACE);
		this.documentNamespace = formDocNamespace(uri);
		model.setNsPrefix("", this.documentNamespace);
		// set the default namespace to the document namespace
		Resource spdxAnalysisType = model.createResource(SPDX_NAMESPACE+CLASS_SPDX_DOCUMENT);
		model.createResource(this.documentNamespace + SPDX_DOCUMENT_ID, spdxAnalysisType);
		this.addSpdxElementRef(SPDX_DOCUMENT_ID);
		// reset the next license number and next spdx element num
		this.nextElementRef.set(1);
		this.nextLicenseRef = 1;
		this.documentNode = getSpdxDocNode();
		this.spdxDocument = new SpdxDocument(this, this.documentNode);
		// add the version
		this.spdxDocument.setSpecVersion(spdxVersion);
		// add the default data license
		if (!spdxVersion.equals(POINT_EIGHT_SPDX_VERSION) && !spdxVersion.equals(POINT_NINE_SPDX_VERSION)) { // added as a mandatory field in 1.0
			try {
				AnyLicenseInfo dataLicense;
				if (spdxVersion.equals(ONE_DOT_ZERO_SPDX_VERSION)) 
					{ 
					dataLicense = LicenseInfoFactory.parseSPDXLicenseString(SPDX_DATA_LICENSE_ID_VERSION_1_0, this);
				} else {
					dataLicense = LicenseInfoFactory.parseSPDXLicenseString(SPDX_DATA_LICENSE_ID, this);				
				}
				spdxDocument.setDataLicense(dataLicense);
			} catch (InvalidLicenseStringException e) {
				throw new InvalidSPDXAnalysisException("Unable to create data license", e);
			}
		}
	}
	
	/**
	 * Form the document namespace URI from the SPDX document URI
	 * @param docUriString String form of the SPDX document URI
	 * @return
	 */
	private String formDocNamespace(String docUriString) {
		// just remove any fragments for the DOC URI
		int fragmentIndex = docUriString.indexOf('#');
		if (fragmentIndex <= 0) {
			return docUriString + "#";
		} else {
			return docUriString.substring(0, fragmentIndex) + "#";
		}
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#getModel()
	 */
	@Override
	public Model getModel() {
		return model;
	}
	
	/**
	 * @return the spdx doc node from the model
	 */
	private Node getSpdxDocNode() {
		Node spdxDocNode = null;
		Node rdfTypePredicate = this.model.getProperty(RDF_NAMESPACE, RDF_PROP_TYPE).asNode();
		Node spdxDocObject = this.model.getProperty(SPDX_NAMESPACE, CLASS_SPDX_DOCUMENT).asNode();
		Triple m = Triple.createMatch(null, rdfTypePredicate, spdxDocObject);
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	// find the document
		while (tripleIter.hasNext()) {
			Triple docTriple = tripleIter.next();
			spdxDocNode = docTriple.getSubject();
		}
		return spdxDocNode;
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#getDocumentNamespace()
	 */
	@Override
	public String getDocumentNamespace() {
		return this.documentNamespace;
	}
	
	/**
	 * Initialize the next SPDX element reference used for creating new SPDX element URIs
	 */
	private void initializeNextElementRef() {
		int highestElementRef = 0;
		Triple m = Triple.createMatch(null, null, null);
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	// find everything
		while (tripleIter.hasNext()) {
			// iterate through everything looking for matches to this SPDX document URI
			Triple trip = tripleIter.next();
			if (trip.getSubject().isURI()) {	// check the subject
				String subjectUri = trip.getSubject().getURI();
				if (subjectUri.startsWith(this.documentNamespace)) {
					String elementRef = subjectUri.substring(this.documentNamespace.length());
					this.spdxRefs.add(elementRef);
					if (SPDX_ELEMENT_REF_PATTERN.matcher(elementRef).matches()) {
						int elementRefNum = getElementRefNumber(elementRef);
						if (elementRefNum > highestElementRef) {
							highestElementRef = elementRefNum;
						}
					}
				}
			}
			if (trip.getObject().isURI()) {		// check the object
				String objectUri = trip.getObject().getURI();
				if (objectUri.startsWith(this.documentNamespace)) {
					String elementRef = objectUri.substring(this.documentNamespace.length());
					this.spdxRefs.add(elementRef);
					if (SPDX_ELEMENT_REF_PATTERN.matcher(elementRef).matches()) {
						int elementRefNum = getElementRefNumber(elementRef);
						if (elementRefNum > highestElementRef) {
							highestElementRef = elementRefNum;
						}
					}
				}
			}
		}
		
		this.nextElementRef.set(highestElementRef + 1);
	}

	/**
	 * Parses out the reference number for an SPDX element reference
	 * @param elementReference Element reference to parse
	 * @return element reference or -1 if the element reference is not valid
	 */
	public static int getElementRefNumber(String elementReference) {
		String numPart = elementReference.substring(SPDX_ELEMENT_REF_PRENUM.length());
		try {
			return Integer.parseInt(numPart);
		} catch(Exception ex) {
			return -1;
		}
	}
	
	/**
	 * Find an element within the container by the SPDX Identifier.  
	 * Returns null if the element does not exist in the container.
	 * @param id
	 * @return
	 * @throws InvalidSPDXAnalysisException 
	 */
	public SpdxElement findElementById(String id) throws InvalidSPDXAnalysisException {
		if (SPDX_DOCUMENT_ID.equals(id)) {
			return this.spdxDocument;
		}
		String uri = null;
		if (id.contains(":")) {
			// external document reference
			String[] parts = id.split(":");
			if (parts.length != 2) {
				throw(new InvalidSPDXAnalysisException("Invalid SPDX ID: "+id+" - must be an SPDX element ID or SPDXDocument ID: SpdxElement ID"));
			}
			String nameSpace = this.externalDocumentIdToNamespace(parts[0]);
			if (nameSpace == null) {
				throw(new InvalidSPDXAnalysisException("No external document ref found for SPDX ID "+id));
			}
			uri = nameSpace + "#" + parts[1];
		} else if (!this.spdxElementRefExists(id)) {
			return null;
		} else {
			uri = this.getDocumentNamespace() + id;
		}
		Resource r = this.model.createResource(uri);
		return SpdxElementFactory.createElementFromModel(this, r.asNode());
	}


	
	public String verifySpdxVersion(String spdxVersion) {
		if (!spdxVersion.startsWith("SPDX-")) {
			return "Invalid spdx version - must start with 'SPDX-'";
		}
		Matcher docSpecVersionMatcher = SpdxRdfConstants.SPDX_VERSION_PATTERN.matcher(spdxVersion);
		if (!docSpecVersionMatcher.matches()) {
			return "Invalid spdx version format - must match 'SPDX-M.N'";
		}
		if (!SUPPORTED_SPDX_VERSIONS.contains(spdxVersion)) {
			return "Version "+spdxVersion+" is not supported by this version of the rdf parser";
		}
		return null;	// if we got here, there is no problem
	}

	/**
	 * @return
	 */
	public SpdxDocument getSpdxDocument() {
		return this.spdxDocument;
	}
	
	/**
	 * @return return the next available SPDX element reference.
	 */
	@Override
    public String getNextSpdxElementRef() {
		int nextSpdxElementNum = this.getAndIncrementNextElementRef();
		String retval = formSpdxElementRef(nextSpdxElementNum);
		while (this.spdxElementRefExists(retval)) {
			nextSpdxElementNum = this.getAndIncrementNextLicenseRef();
			retval = formSpdxElementRef(nextSpdxElementNum);
		}
		this.spdxRefs.add(retval);
		return retval;
	}
	
	public static String formSpdxElementRef(int refNum) {
		return SPDX_ELEMENT_REF_PRENUM + String.valueOf(refNum);
	}
	
	/**
	 * @return
	 */
	private int getAndIncrementNextElementRef() {
		return this.nextElementRef.getAndIncrement();
	}
	
	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#SpdxElementRefExists(java.lang.String)
	 */
	@Override
	public boolean spdxElementRefExists(String elementRef) {
		return this.spdxRefs.contains(elementRef);
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#addSpdxElementRef(java.lang.String)
	 */
	@Override
	public void addSpdxElementRef(String elementRef) throws InvalidSPDXAnalysisException {
		if (spdxElementRefExists(elementRef)) {
			throw(new InvalidSPDXAnalysisException("Duplicate SPDX element reference: "+elementRef));
		}
		this.spdxRefs.add(elementRef);
	}
	
	/**
	 * Initialize the next license reference and the cache of extracted license infos
	 * @throws InvalidSPDXAnalysisException
	 */
	public void initializeNextLicenseRef() throws InvalidSPDXAnalysisException {
		getExtractedLicenseInfosFromModel();	// this initializes the cache
		ExtractedLicenseInfo[] existingLicenses = getExtractedLicenseInfos();
		if (existingLicenses == null) {
			this.nextLicenseRef = 1;
			return;
		}
		int highestNonStdLicense = 0;
		for (int i = 0; i < existingLicenses.length; i++) {
			try {
				int idNum = getLicenseRefNum(existingLicenses[i].getLicenseId());
				if (idNum > highestNonStdLicense) {
					highestNonStdLicense = idNum;
				}
			} catch (NonNumericLicenseIdException ex) {
				// just continue
			}
		}	
		this.nextLicenseRef = highestNonStdLicense + 1;
	}

	/**
	 * @return next available license ID for an ExtractedLicenseInfo
	 */
	public synchronized String getNextLicenseRef() {
		int nextLicNum = this.getAndIncrementNextLicenseRef();
		String retval = formNonStandardLicenseID(nextLicNum);
		return retval;
	}
	
	/**
	 * Parses a license ID and return the integer representing the ID number (e.g. N in LicenseRef-N)
	 * Note that in SPDX 1.2, non-numeric license IDs are allowed. This method will throw a NonNumericException if
	 * a non numeric license ID passed as a licenseID parameter
	 * @param licenseID
	 * @return
	 * @throws NonNumericLicenseIdException If the non-standard license ID is not of the form LicenseRef-NN
	 */
	public int getLicenseRefNum(String licenseID) throws NonNumericLicenseIdException {
		Matcher matcher = LICENSE_ID_PATTERN_NUMERIC.matcher(licenseID);
		if (!matcher.matches()) {
			throw(new NonNumericLicenseIdException("Invalid license ID found in the non-standard licenses: '"+licenseID+"'"));
		}
		int numGroups = matcher.groupCount();
		if (numGroups != 1) {
			throw(new NonNumericLicenseIdException("Invalid license ID found in the non-standard licenses: '"+licenseID+"'"));
		}
		try {
			int idNum = Integer.parseInt(matcher.group(1));
			return idNum;
		} catch (NumberFormatException ex) {
			throw new NonNumericLicenseIdException("Error parsing number "+matcher.group(1));
		}
	}
	
	public static String formNonStandardLicenseID(int idNum) {
		return NON_STD_LICENSE_ID_PRENUM + String.valueOf(idNum);
	}
	
	synchronized int getAndIncrementNextLicenseRef() {
		int retval = this.nextLicenseRef;
		this.nextLicenseRef++;
		return retval;
	}
	
	/**
	 * Adds a new non-standard license containing the text provided.  Forms the license ID
	 * from the next License ID available
	 * @param licenseText
	 * @return the newly created NonStandardLicense
	 * @throws InvalidSPDXAnalysisException
	 */
	public ExtractedLicenseInfo addNewExtractedLicenseInfo(String licenseText) throws InvalidSPDXAnalysisException {
		String licenseID = getNextLicenseRef();
		ExtractedLicenseInfo retval = new ExtractedLicenseInfo(licenseID, licenseText);
		addNewExtractedLicenseInfo(retval);
		return retval;
	}

	
	/**
	 * Adds the license as a new ExtractedLicenseInfo
	 * @param license
	 * @throws InvalidSPDXAnalysisException
	 */
	public void addNewExtractedLicenseInfo(ExtractedLicenseInfo license) throws InvalidSPDXAnalysisException {
		if (extractedLicenseExists(license.getLicenseId())) {
			throw(new InvalidSPDXAnalysisException("Can not add license - ID "+license.getLicenseId()+" already exists."));
		}
		Property p = model.getProperty(SPDX_NAMESPACE, PROP_SPDX_EXTRACTED_LICENSES);
		Resource s = getResource(getSpdxDocNode());
		s.addProperty(p, license.createResource(this));		
		this.licenseIdToExtractedLicense.put(license.getLicenseId().toLowerCase(), license);
	}
	
	/**
	 * @param id
	 * @return true if the license ID is already in the model as an extracted license info
	 * @throws InvalidSPDXAnalysisException 
	 */
	public boolean extractedLicenseExists(String id) throws InvalidSPDXAnalysisException {
		return this.licenseIdToExtractedLicense.containsKey(id.toLowerCase());
	}
	
	/**
	 * @param id
	 * @return true if the license ID is already in the model as an extracted license info
	 * @throws InvalidSPDXAnalysisException 
	 */
	public ExtractedLicenseInfo getExtractedLicense(String id) throws InvalidSPDXAnalysisException {
		return this.licenseIdToExtractedLicense.get(id.toLowerCase());
	}
	
	/**
	 * Get Update the extrated license infos from the model and resynchronize it with
	 * the license cache.
	 * @throws InvalidSPDXAnalysisException 
	 */
	public void getExtractedLicenseInfosFromModel() throws InvalidSPDXAnalysisException {
		AnyLicenseInfo[] extractedAnyLicenseInfo = this.spdxDocument.findAnyLicenseInfoPropertyValues(
				SpdxRdfConstants.SPDX_NAMESPACE, 
				SpdxRdfConstants.PROP_SPDX_EXTRACTED_LICENSES);
		this.licenseIdToExtractedLicense.clear();
		
		for (int i = 0; i < extractedAnyLicenseInfo.length; i++) {
			if (!(extractedAnyLicenseInfo[i] instanceof ExtractedLicenseInfo)) {
				throw new InvalidSPDXAnalysisException("Invalid type for extracted license infos: " + extractedAnyLicenseInfo[i]);
			}
			ExtractedLicenseInfo lic = (ExtractedLicenseInfo)extractedAnyLicenseInfo[i];
			this.licenseIdToExtractedLicense.put(lic.getLicenseId().toLowerCase(), lic);
		}
	}

	/**
	 * @return
	 */
	public ExtractedLicenseInfo[] getExtractedLicenseInfos() {
		return this.licenseIdToExtractedLicense.values().toArray(
				new ExtractedLicenseInfo[this.licenseIdToExtractedLicense.values().size()]);
	}

	/**
	 * @param extractedLicenseInfos
	 * @throws InvalidSPDXAnalysisException 
	 */
	public void setExtractedLicenseInfos(
			ExtractedLicenseInfo[] extractedLicenseInfos) throws InvalidSPDXAnalysisException {
		this.spdxDocument.setPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE, 
				SpdxRdfConstants.PROP_SPDX_EXTRACTED_LICENSES, extractedLicenseInfos);
		this.initializeNextLicenseRef();
	}

	/**
	 * @param license
	 * @throws InvalidSPDXAnalysisException 
	 */
	public void addExtractedLicenseInfos(ExtractedLicenseInfo license) throws InvalidSPDXAnalysisException {
		if (license == null) {
			return;
		}
		try {
			int idNum = getLicenseRefNum(license.getLicenseId());
			if (idNum >= this.nextLicenseRef) {
				this.nextLicenseRef = idNum + 1;
			}
		} catch (NonNumericLicenseIdException ex) {
			// just continue
		}
		this.licenseIdToExtractedLicense.put(license.getLicenseId().toLowerCase(), license);
		this.spdxDocument.addPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE, 
				SpdxRdfConstants.PROP_SPDX_EXTRACTED_LICENSES, license);
	}
	
	private Resource getResource(Node node) throws InvalidSPDXAnalysisException {
		Resource s;
		if (node.isURI()) {
			s = model.createResource(node.getURI());
		} else if (node.isBlank()) {
			s = model.createResource(new AnonId(node.getBlankNodeId()));
		} else {
			throw(new InvalidSPDXAnalysisException("Node can not be a literal"));
		}
		return s;
	}

	/**
	 * get all file references contained within the container
	 * @return
	 * @throws InvalidSPDXAnalysisException 
	 */
	public SpdxFile[] getFileReferences() throws InvalidSPDXAnalysisException {
		List<SpdxFile> alFiles = Lists.newArrayList();
		Node rdfTypeNode = model.getProperty(SpdxRdfConstants.RDF_NAMESPACE, 
				SpdxRdfConstants.RDF_PROP_TYPE).asNode();
		String fileTypeUri = SPDX_NAMESPACE + CLASS_SPDX_FILE;
		Node fileTypeNode = model.getResource(fileTypeUri).asNode();
		Triple m = Triple.createMatch(null, rdfTypeNode, fileTypeNode);
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	
		while (tripleIter.hasNext()) {
			Triple t = tripleIter.next();
			alFiles.add(new SpdxFile(this, t.getSubject()));
		}
		SpdxFile[] retval = new SpdxFile[alFiles.size()];
		return alFiles.toArray(retval);
	}

	private void initializeExternalDocumentRefs() throws InvalidSPDXAnalysisException {
		this.initializeExternalDocumentRefs(
				RdfModelObject.findExternalDocRefPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_EXTERNAL_DOC_REF, this, documentNode));
	}
	
	private void initializeExternalDocumentRefs(ExternalDocumentRef[] externalDocumentRefs) throws InvalidSPDXAnalysisException {
		this.externalDocIdToRef.clear();
		this.externalDocNamespaceToRef.clear();
		if (externalDocumentRefs == null) {
			return;
		}
		for (int i = 0; i < externalDocumentRefs.length; i++) {
			this.externalDocIdToRef.put(externalDocumentRefs[i].getExternalDocumentId(), 
					externalDocumentRefs[i]);
			this.externalDocNamespaceToRef.put(externalDocumentRefs[i].getSpdxDocumentNamespace(), 
					externalDocumentRefs[i]);
		}
	}
	
	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#documentNamespaceToId(java.lang.String)
	 */
	@Override
	public String documentNamespaceToId(String externalNamespace) {
		ExternalDocumentRef ref = this.externalDocNamespaceToRef.get(externalNamespace);
		if (ref == null) {
			return null;
		} else {
			return ref.getExternalDocumentId();
		}
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#externalDocumentIdToNamespace(java.lang.String)
	 */
	@Override
	public String externalDocumentIdToNamespace(String docId) {
		ExternalDocumentRef ref = this.externalDocIdToRef.get(docId);
		if (ref == null) {
			return null;
		} else {
			return ref.getSpdxDocumentNamespace();
		}
	}

	/**
	 * @return external document refs
	 */
	public ExternalDocumentRef[] getExternalDocumentRefs() {
		return this.externalDocIdToRef.values().toArray(
						new ExternalDocumentRef[this.externalDocIdToRef.values().size()]);
	}

	/**
	 * @param externalDocumentRefs
	 * @throws InvalidSPDXAnalysisException 
	 */
	public void setExternalDocumentRefs(
			ExternalDocumentRef[] externalDocumentRefs) throws InvalidSPDXAnalysisException {
		initializeExternalDocumentRefs(externalDocumentRefs);
		this.spdxDocument.setPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_EXTERNAL_DOC_REF, externalDocumentRefs);
	}
	
	public List<SpdxPackage> findAllPackages() throws InvalidSPDXAnalysisException {
		Node rdfTypePredicate = model.getProperty(SpdxRdfConstants.RDF_NAMESPACE, 
				SpdxRdfConstants.RDF_PROP_TYPE).asNode();
		Node packageTypeObject = model.createResource(SPDX_NAMESPACE + CLASS_SPDX_PACKAGE).asNode();
		Triple m = Triple.createMatch(null, rdfTypePredicate, packageTypeObject);
		List<SpdxPackage> retval = Lists.newArrayList();
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	
		while (tripleIter.hasNext()) {
			retval.add((SpdxPackage)SpdxElementFactory.createElementFromModel(this, tripleIter.next().getSubject()));
		}
		return retval;
	}
	
	public List<SpdxFile> findAllFiles() throws InvalidSPDXAnalysisException {
		Node rdfTypePredicate = model.getProperty(SpdxRdfConstants.RDF_NAMESPACE, 
				SpdxRdfConstants.RDF_PROP_TYPE).asNode();
		Node fileTypeObject = model.createResource(SPDX_NAMESPACE + CLASS_SPDX_FILE).asNode();
		Triple m = Triple.createMatch(null, rdfTypePredicate, fileTypeObject);
		List<SpdxFile> retval = Lists.newArrayList();
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	
		while (tripleIter.hasNext()) {
			retval.add((SpdxFile)SpdxElementFactory.createElementFromModel(this, tripleIter.next().getSubject()));
		}
		return retval;
	}

	/**
	 * Add an SPDX element directly to the model without connecting it to any properties
	 * @param element
	 * @throws InvalidSPDXAnalysisException 
	 */
	public void addElement(SpdxElement element) throws InvalidSPDXAnalysisException {
		element.createResource(this, true);
	}

	/**
	 * Returns all elements in the contains
	 * @return
	 * @throws InvalidSPDXAnalysisException 
	 */
	public List<SpdxElement> findAllElements() throws InvalidSPDXAnalysisException {
		// NOTE: This needs to be updated for any new types
		List<SpdxElement> retval = Lists.newArrayList();
		retval.add(this.spdxDocument);
		retval.addAll(this.findAllFiles());
		retval.addAll(this.findAllPackages());
		retval.addAll(this.findAllSnippets());
		return retval;
	}
	
	// The following methods (createResource and ...
	// must be synchronized since it interacts with a map of nodes and objects that represent the model

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#createResource(org.apache.jena.rdf.model.Resource, java.lang.String, org.apache.jena.rdf.model.Resource)
	 */
	@Override
	public synchronized Resource createResource(Resource duplicate, String uri, Resource type, IRdfModel nodeObject) {
		Resource retval;
		if (duplicate != null) {
			retval = duplicate;
		} else if (uri == null) {			
			retval = model.createResource(type);
		} else {
			retval = model.createResource(uri, type);
		}
		Node node = retval.asNode();
		List<IRdfModel> existingModelObjects = this.nodeModelMap.get(node);
		if (existingModelObjects == null) {
			existingModelObjects = new ArrayList<IRdfModel>();
			this.nodeModelMap.put(node, existingModelObjects);
		}
		boolean found = false;
		for (IRdfModel existing:existingModelObjects) {
			if (existing == nodeObject) {
				found = true;
				break;
			}
		}
		if (!found) {
			existingModelObjects.add(nodeObject);
		} 
		if (existingModelObjects.size() == 1) {
			nodeObject.setSingleObjectForSameNode();
		} else {
			for (IRdfModel allNodeObjects:existingModelObjects) {
				allNodeObjects.setMultipleObjectsForSameNode();
			}
		}
		return retval;
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.IModelContainer#addNodeObject(org.apache.jena.graph.Node, org.spdx.rdfparser.model.IRdfModel)
	 */
	@Override
	public synchronized boolean addCheckNodeObject(Node node, IRdfModel nodeObject) {
		List<IRdfModel> existingModelObjects = this.nodeModelMap.get(node);
		if (existingModelObjects == null) {
			existingModelObjects = new ArrayList<IRdfModel>();
			this.nodeModelMap.put(node, existingModelObjects);
		}
		boolean found = false;
		for (IRdfModel existing:existingModelObjects) {
			if (existing == nodeObject) {
				found = true;
				break;
			}
		}
		if (found) {
			if (existingModelObjects.size() == 1) {
				// nothing we need to do
				return false;
			} else {
				// multiple objects
				return true;
			}
		} else {
			if (existingModelObjects.size() > 0) {
				for (IRdfModel existing:existingModelObjects) {
					existing.setMultipleObjectsForSameNode();
				}
				existingModelObjects.add(nodeObject);
				return true;
			} else {
				existingModelObjects.add(nodeObject);
				return false;
			}			
		}
	}

	/**
	 * @return all snippets in the document container
	 * @throws InvalidSPDXAnalysisException 
	 */
	public List<SpdxSnippet> findAllSnippets() throws InvalidSPDXAnalysisException {
		Node rdfTypePredicate = model.getProperty(SpdxRdfConstants.RDF_NAMESPACE, 
				SpdxRdfConstants.RDF_PROP_TYPE).asNode();
		Node snippetTypeObject = model.createResource(SPDX_NAMESPACE + CLASS_SPDX_SNIPPET).asNode();
		Triple m = Triple.createMatch(null, rdfTypePredicate, snippetTypeObject);
		List<SpdxSnippet> retval = Lists.newArrayList();
		ExtendedIterator<Triple> tripleIter = model.getGraph().find(m);	
		while (tripleIter.hasNext()) {
			retval.add((SpdxSnippet)SpdxElementFactory.createElementFromModel(this, tripleIter.next().getSubject()));
		}
		return retval;
	}
}