/**
 * Copyright (c) 2011 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.tag;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.spdx.rdfparser.InvalidSPDXAnalysisException;
import org.spdx.rdfparser.SPDXCreatorInformation;
import org.spdx.rdfparser.SPDXReview;
import org.spdx.rdfparser.SpdxDocumentContainer;
import org.spdx.rdfparser.SpdxPackageVerificationCode;
import org.spdx.rdfparser.SpdxRdfConstants;
import org.spdx.rdfparser.license.AnyLicenseInfo;
import org.spdx.rdfparser.license.ExtractedLicenseInfo;
import org.spdx.rdfparser.license.LicenseInfoFactory;
import org.spdx.rdfparser.model.Annotation;
import org.spdx.rdfparser.model.Annotation.AnnotationType;
import org.spdx.rdfparser.model.Checksum;
import org.spdx.rdfparser.model.Checksum.ChecksumAlgorithm;
import org.spdx.rdfparser.model.DoapProject;
import org.spdx.rdfparser.model.ExternalDocumentRef;
import org.spdx.rdfparser.model.ExternalRef;
import org.spdx.rdfparser.model.ExternalRef.ReferenceCategory;
import org.spdx.rdfparser.model.Relationship;
import org.spdx.rdfparser.model.Relationship.RelationshipType;
import org.spdx.rdfparser.model.SpdxDocument;
import org.spdx.rdfparser.model.SpdxElement;
import org.spdx.rdfparser.model.SpdxFile;
import org.spdx.rdfparser.model.SpdxFile.FileType;
import org.spdx.rdfparser.model.SpdxItem;
import org.spdx.rdfparser.model.SpdxNoAssertionElement;
import org.spdx.rdfparser.model.SpdxNoneElement;
import org.spdx.rdfparser.model.SpdxPackage;
import org.spdx.rdfparser.model.SpdxSnippet;
import org.spdx.rdfparser.model.pointer.ByteOffsetPointer;
import org.spdx.rdfparser.model.pointer.LineCharPointer;
import org.spdx.rdfparser.model.pointer.StartEndPointer;
import org.spdx.rdfparser.referencetype.ListedReferenceTypes;
import org.spdx.rdfparser.referencetype.ReferenceType;
import org.spdx.spdxspreadsheet.InvalidLicenseStringException;

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

/**
 * Translates an tag-value file to a an SPDX Document.
 *
 * Supports SPDX version 2.0
 *
 * 2.0 changes made by Gary O'Neall
 *
 * @author Rana Rahal, Protecode Inc.
 */
public class BuildDocument implements TagValueBehavior {

	private static class AnnotationWithId {
		private Annotation annotation;
		private String id;
		private int lineNumber;
		private AnnotationWithId(String annotator, int lineNumber) {
			this.annotation = new Annotation(annotator, null, null, null);
			this.lineNumber = lineNumber;
		}
		@SuppressWarnings("unused")
		public void setAnnotator(String annotator) {
			annotation.setAnnotator(annotator);
		}
		public void setDate(String date) throws InvalidSPDXAnalysisException {
			annotation.setAnnotationDate(date);
		}
		public void setAnnotationType(AnnotationType annotationType) throws InvalidSPDXAnalysisException {
			annotation.setAnnotationType(annotationType);
		}
		public void setComment(String comment) {
			annotation.setComment(comment);
		}
		public void setId(String id) {
			this.id = id;
		}
		public String getId() {
			return this.id;
		}
		public Annotation getAnnotation() {
			return this.annotation;
		}
		public int getLineNumber() {
			return lineNumber;
		}
	}
	private static class RelationshipWithId {
		private String id;
		private String relatedId;
		private RelationshipType relationshipType;
		private String comment;
		private int lineNumber;
		public RelationshipWithId(String id, String relatedId,
				RelationshipType relationshipType, int lineNumber) {
			this.id = id;
			this.relatedId = relatedId;
			this.relationshipType = relationshipType;
			this.lineNumber = lineNumber;
		}
		public void setComment(String comment) {
			this.comment = comment;
		}
		public String getId() {
			return id;
		}
		public String getRelatedId() {
			return relatedId;
		}
		public RelationshipType getRelationshipType() {
			return relationshipType;
		}
		public String getComment() {
			return comment;
		}
		public int getLineNumber() {
			return lineNumber;
		}
	}

	private static Pattern EXTERNAL_DOC_REF_PATTERN = Pattern.compile("(\\S+)\\s+(\\S+)\\s+SHA1:\\s+(\\S+)");
	private static Pattern RELATIONSHIP_PATTERN = Pattern.compile("(\\S+)\\s+(\\S+)\\s+(\\S+)");
	public static Pattern CHECKSUM_PATTERN = Pattern.compile("(\\S+)\\s+(\\S+)");
	private static Pattern NUMBER_RANGE_PATTERN = Pattern.compile("(\\d+):(\\d+)");
	private static Pattern EXTERNAL_REF_PATTERN = Pattern.compile("([^ ]+) ([^ ]+) (.+)");

	/**
	 * Tags used in the definition of an annotation
	 */
	private Set<String> ANNOTATION_TAGS = Sets.newHashSet();
	/**
	 * Tags used in the definition of a file
	 */
	private Set<String> FILE_TAGS = Sets.newHashSet();
	/**
	 * Tags used in the definition of a Snippet
	 */
	private Set<String> SNIPPET_TAGS = Sets.newHashSet();
	/**
	 * Tags used in the definition of a package
	 */
	/**
	 * Tags used in the definition of an extracted license
	 */
	private Set<String> EXTRACTED_LICENSE_TAGS = Sets.newHashSet();
	private Set<String> PACKAGE_TAGS = Sets.newHashSet();
	private Properties constants;
	private SpdxDocument analysis;
	private DateFormat format = new SimpleDateFormat(SpdxRdfConstants.SPDX_DATE_FORMAT);

	//When we retrieve a list from the SpdxDocument the order changes, therefore keep track of
	//the last object that we are looking at so that we can fill in all of it's information
	private SPDXReview lastReviewer = null;
	private int lastReviewerLineNumber = 0;

	private ExtractedLicenseInfo lastExtractedLicense = null;
	private int lastExtractedLicenseLineNumber = 0;
	private SpdxFile lastFile = null;
	private int lastFileLineNumber = 0;
	private SpdxSnippet lastSnippet = null;
	private int lastSnippetLineNumber = 0;
	private DoapProject lastProject = null;
	private int lastProjectLineNumber = 0;
	private Map<String, Integer> elementIdLineNumberMap = Maps.newHashMap();
	// Keep track of all file dependencies since these need to be added after all of the files
	// have been parsed.  Map of file dependency file name to the SPDX files which depends on it
	private Map<String, List<SpdxFile>> fileDependencyMap = Maps.newHashMap();
	/**
	 * Map of all snippetFileID's collected during parsing so that we can add the files
	 * at the end of the document creation once the files are actually created
	 */
	private Map<String, List<SpdxSnippet>>  snippetDependencyMap = Maps.newHashMap();
	/**
	 * Keep track of the last relationship for any following relationship related tags
	 */
	private RelationshipWithId lastRelationship = null;
	/**
	 * Keep track of all relationships and add them at the end of the parsing
	 */
	private List<RelationshipWithId> relationships = Lists.newArrayList();
	/**
	 * Keep track of the last annotation for any following annotation related tags
	 */
	private AnnotationWithId lastAnnotation;
	/**
	 * Keep track of all annotations and add them at the end of the parsing
	 */
	private List<AnnotationWithId> annotations = Lists.newArrayList();
	private SpdxDocumentContainer[] result = null;

	private String specVersion;

	private AnyLicenseInfo dataLicense;

	private String documentName;

	List<String> warningMessages;

	/**
	 * True if we have started defining a package in the tag/value file
	 */
	private boolean inPackageDefinition = false;

	/**
	 * True if we have started to define a file AT THE DOCUMENT LEVEL
	 * in the tag/value file.  Note that files defined as part of a package
	 * will have the state flag inPackageDefinition set, and inFileDefinition will be false.
	 */
	private boolean inFileDefinition = false;
	/**
	 * true if we have started to define a Snippet
	 */
	private boolean inSnippetDefinition = false;;
	/**
	 * True if we have started to define an annotation
	 * in the tag/value file.
	 */
	private boolean inAnnotation = false;
	/**
	 * True if we are building an extracted license definition
	 */
	private boolean inExtractedLicenseDefinition = false;

	/**
	 * The last (or current) package being defined by the tag/value file
	 */
	private SpdxPackage lastPackage = null;
	private int lastPackageLineNumber = 0;

	/**
	 * The last external reference found
	 */
	private ExternalRef lastExternalRef = null;

	public BuildDocument(SpdxDocumentContainer[] result, Properties constants, List<String> warnings) {
		this.constants = constants;
		this.warningMessages = warnings;
		this.result = result;
		this.ANNOTATION_TAGS.add(constants.getProperty("PROP_ANNOTATION_DATE").trim()+" ");
		this.ANNOTATION_TAGS.add(constants.getProperty("PROP_ANNOTATION_COMMENT").trim()+" ");
		this.ANNOTATION_TAGS.add(constants.getProperty("PROP_ANNOTATION_ID").trim()+" ");
		this.ANNOTATION_TAGS.add(constants.getProperty("PROP_ANNOTATION_TYPE").trim()+" ");

		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_TYPE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_LICENSE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_COPYRIGHT").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_CHECKSUM").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_SEEN_LICENSE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_LIC_COMMENTS").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_COMMENT").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_DEPENDENCY").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_CONTRIBUTOR").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_NOTICE_TEXT").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_ELEMENT_ID").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_PROJECT_NAME").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_PROJECT_HOMEPAGE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_PROJECT_URI").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_DOCUMENT_NAMESPACE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_RELATIONSHIP").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_RELATIONSHIP_COMMENT").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_ANNOTATOR").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_ANNOTATION_DATE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_ANNOTATION_COMMENT").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_ANNOTATION_ID").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_ANNOTATION_TYPE").trim()+" ");
		this.FILE_TAGS.add(constants.getProperty("PROP_FILE_ATTRIBUTION_TEXT").trim()+" ");

		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_SPDX_ID").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_FROM_FILE_ID").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_BYTE_RANGE").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_LINE_RANGE").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_CONCLUDED_LICENSE").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_LIC_COMMENTS").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_COPYRIGHT").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_COMMENT").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_NAME").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_SNIPPET_SEEN_LICENSE").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_DOCUMENT_NAMESPACE").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_RELATIONSHIP").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_RELATIONSHIP_COMMENT").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_ANNOTATOR").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_ANNOTATION_DATE").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_ANNOTATION_COMMENT").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_ANNOTATION_ID").trim()+" ");
		this.SNIPPET_TAGS.add(constants.getProperty("PROP_ANNOTATION_TYPE").trim()+" ");

		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_COMMENT").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_FILE_NAME").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_CHECKSUM").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_DOWNLOAD_URL").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_SOURCE_INFO").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_DECLARED_LICENSE").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_CONCLUDED_LICENSE").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_DECLARED_COPYRIGHT").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_SHORT_DESC").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_DESCRIPTION").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_VERIFICATION_CODE").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_LICENSE_INFO_FROM_FILES").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_LICENSE_COMMENT").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_VERSION_INFO").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_ORIGINATOR").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_SUPPLIER").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_HOMEPAGE_URL").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_ELEMENT_ID").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_FILE_NAME").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_RELATIONSHIP").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_RELATIONSHIP_COMMENT").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_ANNOTATOR").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_ANNOTATION_DATE").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_ANNOTATION_COMMENT").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_ANNOTATION_ID").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_ANNOTATION_TYPE").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_EXTERNAL_REFERENCE").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_EXTERNAL_REFERENCE_COMMENT").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_FILES_ANALYZED").trim()+" ");
		this.PACKAGE_TAGS.add(constants.getProperty("PROP_PACKAGE_ATTRIBUTION_TEXT").trim()+" ");

		this.EXTRACTED_LICENSE_TAGS.add(constants.getProperty("PROP_LICENSE_TEXT").trim()+" ");
		this.EXTRACTED_LICENSE_TAGS.add(constants.getProperty("PROP_EXTRACTED_TEXT").trim()+" ");
		this.EXTRACTED_LICENSE_TAGS.add(constants.getProperty("PROP_LICENSE_COMMENT").trim()+" ");
		this.EXTRACTED_LICENSE_TAGS.add(constants.getProperty("PROP_LICENSE_NAME").trim()+" ");
		this.EXTRACTED_LICENSE_TAGS.add(constants.getProperty("PROP_SOURCE_URLS").trim()+" ");
	}

	@Override
	public void enter() throws Exception {
		// do nothing???
	}

	@Override
	public void buildDocument(String tag, String value, int lineNumber) throws Exception {
		tag = tag.trim()+" ";
		value = trim(value.trim());
		if (this.inAnnotation && ANNOTATION_TAGS.contains(tag)) {
			buildAnnotation(tag, value, lastAnnotation);
		} else if (this.inFileDefinition && FILE_TAGS.contains(tag)) {
			buildFile(this.lastFile, tag, value, lineNumber);
		} else if (this.inSnippetDefinition && SNIPPET_TAGS.contains(tag)) {
			buildSnippet(this.lastSnippet, tag, value, lineNumber);
		} else if (this.inPackageDefinition && PACKAGE_TAGS.contains(tag)) {
			buildPackage(this.lastPackage, tag, value, lineNumber);
		} else if (this.inExtractedLicenseDefinition && EXTRACTED_LICENSE_TAGS.contains(tag)) {
			buildExtractedLicense(this.lastExtractedLicense, tag, value, lineNumber);
		} else {
			if (inExtractedLicenseDefinition && lastExtractedLicense != null) {
				verifyElement(lastExtractedLicense.verify(), "Extracted License", lastExtractedLicenseLineNumber, false);
			}
			if (inFileDefinition) {
				addLastFile();
			}
			if (inSnippetDefinition) {
				addLastSnippet();
			}
			inAnnotation = false;
			inFileDefinition = false;
			inPackageDefinition = false;
			inSnippetDefinition = false;
			buildDocumentProperties(tag, value, lineNumber);
		}
	}

	/**
	 * Add tag value properties to an existing snippet
	 * @param snippet
	 * @param tag
	 * @param value
	 * @param lineNumber
	 * @throws InvalidSpdxTagFileException
	 * @throws InvalidSPDXAnalysisException
	 * @throws InvalidLicenseStringException
	 */
	private void buildSnippet(SpdxSnippet snippet, String tag, String value, int lineNumber) throws InvalidSpdxTagFileException, InvalidSPDXAnalysisException, InvalidLicenseStringException {
		if (snippet == null) {
			throw(new InvalidSpdxTagFileException("Missing Snippet ID - An SPDX Snippet ID must be specified before the snippet properties"));
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_SPDX_ID"))) {
			snippet.setId(value);
			elementIdLineNumberMap.put(value, lineNumber);
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_FROM_FILE_ID"))) {
			// Since the files have not all been parsed, we just keep track of the
			// dependencies in a hashmap until we finish all processing and are building the package
			List<SpdxSnippet> snippetsWithThisAsADependency = this.snippetDependencyMap.get(value);
			if (snippetsWithThisAsADependency == null) {
				snippetsWithThisAsADependency = Lists.newArrayList();
				this.snippetDependencyMap.put(value, snippetsWithThisAsADependency);
			}
			snippetsWithThisAsADependency.add(snippet);
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_BYTE_RANGE"))) {
			snippet.setByteRange(parseByteRange(value, lineNumber));
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_LINE_RANGE"))) {
			snippet.setLineRange(parseLineRange(value, lineNumber));
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_CONCLUDED_LICENSE"))) {
			snippet.setLicenseConcluded(LicenseInfoFactory.parseSPDXLicenseString(value, this.analysis.getDocumentContainer()));
			// can not verify any licenses at this point since the extracted license infos may not be set
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_LIC_COMMENTS"))) {
			snippet.setLicenseComments(value);
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_COPYRIGHT"))) {
			snippet.setCopyrightText(value);
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_COMMENT"))) {
			snippet.setComment(value);
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_NAME"))) {
			snippet.setName(value);
		} else if (tag.equals(constants.getProperty("PROP_SNIPPET_SEEN_LICENSE"))) {
			snippet.setLicenseInfosFromFiles(new AnyLicenseInfo[] {LicenseInfoFactory.parseSPDXLicenseString(value, this.analysis.getDocumentContainer())});
			// can not verify any licenses at this point since the extracted license infos may not be set
		} else if (tag.equals(constants.getProperty("PROP_ANNOTATOR"))) {
			if (lastAnnotation != null) {
				annotations.add(lastAnnotation);
			}
			this.inAnnotation = true;
			lastAnnotation = new AnnotationWithId(value, lineNumber);
		} else if (tag.equals(constants.getProperty("PROP_RELATIONSHIP"))) {
			if (lastRelationship != null) {
				relationships.add(lastRelationship);
			}
			lastRelationship = parseRelationship(value, lineNumber);
		} else if (tag.equals(constants.getProperty("PROP_RELATIONSHIP_COMMENT"))) {
			if (lastRelationship == null) {
				throw(new InvalidSpdxTagFileException("Relationship comment found outside of a relationship: "+value + " at line number "+lineNumber));
			}
			lastRelationship.setComment(value);
		} else {
			throw new InvalidSPDXAnalysisException("Error parsing snippet.  Unrecognized tag: "+tag + " at line number " + lineNumber);
		}
	}

	private void verifyElement(List<String> verify, String prefix, int lineNumber) {
		verifyElement(verify, prefix, lineNumber, false);
	}

	/**
	 * Add warning messages for any element verification list
	 * @param verify List of verification warnings
	 * @param prefix Prefix to prepend the warning with
	 * @param lineNumber Line number where the error occurs
	 * @param ignoreMissingLicenseText if true, don't record any missing license text errors
	 */
	private void verifyElement(List<String> verify, String prefix, int lineNumber, boolean ignoreMissingLicenseText) {
		if (!verify.isEmpty()) {
			for (String verMsg:verify) {
				if (!ignoreMissingLicenseText || !verMsg.contains("Missing required license text")) {
					boolean found = false;
					String newWarning = prefix + " at line "+lineNumber+" invalid: "+verMsg;
					for (String existingWarn:this.warningMessages) {
						if (existingWarn.equals(newWarning)) {
							found = true;
							break;
						}
					}
					if (!found) {
						this.warningMessages.add(newWarning);
					}
				}
			}
		}
	}

	/**
	 * @param value
	 * @param lineNumber
	 * @return
	 * @throws InvalidSpdxTagFileException
	 */
	private StartEndPointer parseLineRange(String value, int lineNumber) throws InvalidSpdxTagFileException {
		Matcher matcher = NUMBER_RANGE_PATTERN.matcher(value.trim());
		if (!matcher.find()) {
			throw(new InvalidSpdxTagFileException("Invalid snippet line range: "+ value + " at line number "+lineNumber));
		}
		LineCharPointer start = null;
		try {
			start = new LineCharPointer(null, Integer.parseInt(matcher.group(1)));
		} catch (Exception ex) {
			throw new InvalidSpdxTagFileException("Non integer start to snippet line offset: "+value + " at line number "+lineNumber);
		}
		LineCharPointer end = null;
		try {
			end = new LineCharPointer(null, Integer.parseInt(matcher.group(2)));
		} catch (Exception ex) {
			throw new InvalidSpdxTagFileException("Non integer end to snippet line offset: "+value + " at line number "+lineNumber);
		}
		return new StartEndPointer(start, end);
	}

	/**
	 * @param value
	 * @param lineNumber
	 * @return
	 * @throws InvalidSpdxTagFileException
	 */
	private StartEndPointer parseByteRange(String value, int lineNumber) throws InvalidSpdxTagFileException {
		Matcher matcher = NUMBER_RANGE_PATTERN.matcher(value.trim());
		if (!matcher.find()) {
			throw(new InvalidSpdxTagFileException("Invalid snippet byte range: "+value + " at line number "+lineNumber));
		}
		ByteOffsetPointer start = null;
		try {
			start = new ByteOffsetPointer(null, Integer.parseInt(matcher.group(1)));
		} catch (Exception ex) {
			throw new InvalidSpdxTagFileException("Non integer start to snippet byte offset: "+value + " at line number "+lineNumber);
		}
		ByteOffsetPointer end = null;
		try {
			end = new ByteOffsetPointer(null, Integer.parseInt(matcher.group(2)));
		} catch (Exception ex) {
			throw new InvalidSpdxTagFileException("Non integer end to snippet byte offset: "+value + " at line number "+lineNumber);
		}
		return new StartEndPointer(start, end);
	}

	/**
	 * @param license
	 * @param tag
	 * @param value
	 * @param lineNumber
	 * @throws InvalidSpdxTagFileException
	 */
	private void buildExtractedLicense(
			ExtractedLicenseInfo license, String tag, String value, int lineNumber) throws InvalidSpdxTagFileException {
		if (tag.equals(constants.getProperty("PROP_EXTRACTED_TEXT"))) {
			if (lastExtractedLicense == null) {
				throw(new InvalidSpdxTagFileException("Missing Extracted License - An  extracted license ID must be provided before the license text at line number "+lineNumber));
			}
			license.setExtractedText(value);
		} else if (tag.equals(constants.getProperty("PROP_LICENSE_NAME"))) {
			if (lastExtractedLicense == null) {
				throw(new InvalidSpdxTagFileException("Missing Extracted License - An  extracted license ID must be provided before the license name at line number "+lineNumber));
			}
			license.setName(value);
		} else if (tag.equals(constants.getProperty("PROP_SOURCE_URLS"))) {
			if (lastExtractedLicense == null) {
				throw(new InvalidSpdxTagFileException("Missing Extracted License - An  extracted license ID must be provided before the license URL at line number "+lineNumber));
			}
			String[] values = value.split(",");
			for (int i = 0; i < values.length; i++) {
				values[i] = values[i].trim();
			}
			license.setSeeAlso(values);
		} else if (tag.equals(constants.getProperty("PROP_LICENSE_COMMENT"))) {
			if (lastExtractedLicense == null) {
				throw(new InvalidSpdxTagFileException("Missing Extracted License - An  extracted license ID must be provided before the license comment at line number "+lineNumber));
			}
			license.setComment(value);
		}
	}

	@SuppressWarnings("deprecation")
	private void buildDocumentProperties(String tag, String value, int lineNumber) throws Exception {
		if (tag.equals(constants.getProperty("PROP_SPDX_VERSION"))) {
			this.specVersion = value;
			if (analysis != null) {
				analysis.setSpecVersion(value);
			}
		} else if (tag.equals(constants.getProperty("PROP_SPDX_DATA_LICENSE"))) {
			try {
				this.dataLicense = LicenseInfoFactory.getListedLicenseById(value);
			} catch(InvalidSPDXAnalysisException ex) {
				this.dataLicense = null;
			}
			if (this.dataLicense == null) {
				this.dataLicense = new ExtractedLicenseInfo(value, "NO TEXT FOR "+value);
			}
			if (analysis != null) {
				analysis.setDataLicense(this.dataLicense);
			}
		} else if (tag.equals(constants.getProperty("PROP_DOCUMENT_NAME"))) {
			this.documentName = value;
			if (analysis != null) {
				this.analysis.setName(value);
			}
		} else if (tag.equals(constants.getProperty("PROP_DOCUMENT_NAMESPACE"))) {
			if (this.analysis != null) {
				throw(new InvalidSpdxTagFileException("More than one document namespace was specified at line number "+lineNumber));
			}
			if (this.specVersion == null) {
				result[0] = new SpdxDocumentContainer(value);
			} else {
				result[0] = new SpdxDocumentContainer(value, this.specVersion);
			}
			this.analysis = result[0].getSpdxDocument();
			if (this.dataLicense != null) {
				this.analysis.setDataLicense(this.dataLicense);
			}
			if (this.documentName != null) {
				this.analysis.setName(this.documentName);
			}
		} else if (tag.equals(constants.getProperty("PROP_ELEMENT_ID"))) {
			if (!value.equals(SpdxRdfConstants.SPDX_DOCUMENT_ID)) {
				throw(new InvalidSpdxTagFileException("SPDX Document "+value
						+" is invalid.  Document IDs must be "+SpdxRdfConstants.SPDX_DOCUMENT_ID + " at line number "+lineNumber));
			}
		} else if (tag.equals(constants.getProperty("PROP_EXTERNAL_DOC_URI"))) {
			checkAnalysisNull();
			addExternalDocRef(value, lineNumber);
		} else if (tag.equals(constants.getProperty("PROP_RELATIONSHIP"))) {
			if (lastRelationship != null) {
				relationships.add(lastRelationship);
			}
			lastRelationship = parseRelationship(value, lineNumber);
		} else if (tag.equals(constants.getProperty("PROP_RELATIONSHIP_COMMENT"))) {
			if (lastRelationship == null) {
				throw(new InvalidSpdxTagFileException("Relationship comment found outside of a relationship: "+value + " at line number "+lineNumber));
			}
			lastRelationship.setComment(value);
		} else if (tag.equals(constants.getProperty("PROP_ANNOTATOR"))) {
			if (lastAnnotation != null) {
				annotations.add(lastAnnotation);
			}
			this.inAnnotation = true;
			lastAnnotation = new AnnotationWithId(value, lineNumber);
		} else if (tag.equals(constants.getProperty("PROP_ANNOTATION_DATE"))) {
			throw(new InvalidSpdxTagFileException("Annotation date found outside of an annotation: "+value + " at line number "+lineNumber));
		} else if (tag.equals(constants.getProperty("PROP_ANNOTATION_COMMENT"))) {
			throw(new InvalidSpdxTagFileException("Annotation comment found outside of an annotation: "+value + " at line number "+lineNumber));
		} else if (tag.equals(constants.getProperty("PROP_ANNOTATION_ID"))) {
			throw(new InvalidSpdxTagFileException("Annotation ID found outside of an annotation: "+value + " at line number "+lineNumber));
		} else if (tag.equals(constants.getProperty("PROP_ANNOTATION_TYPE"))) {
			throw(new InvalidSpdxTagFileException("Annotation type found outside of an annotation: "+value + " at line number "+lineNumber));
		} else if (tag.equals(constants.getProperty("PROP_CREATION_CREATOR"))) {
			checkAnalysisNull();
			if (analysis.getCreationInfo() == null) {
				SPDXCreatorInformation creator = new SPDXCreatorInformation(new String[] { value }, "", "", "");
				analysis.setCreationInfo(creator);
			} else {
				List<String> creators = Lists.newArrayList(analysis.getCreationInfo().getCreators());
				creators.add(value);
				analysis.getCreationInfo().setCreators(creators.toArray(new String[0]));
			}
		} else if (tag.equals(constants.getProperty("PROP_CREATION_CREATED"))) {
			checkAnalysisNull();
			if (analysis.getCreationInfo() == null) {
				SPDXCreatorInformation creator = new SPDXCreatorInformation(new String[] {  }, "", "", "");
				analysis.setCreationInfo(creator);
			}
			analysis.getCreationInfo().setCreated(value);
		} else if (tag.equals(constants.getProperty("PROP_CREATION_COMMENT"))) {
			checkAnalysisNull();
			if (analysis.getCreationInfo() == null) {
				SPDXCreatorInformation creator = new SPDXCreatorInformation(new String[] { value }, "", "", "");
				analysis.setCreationInfo(creator);
			}
			analysis.getCreationInfo().setComment(value);
		} else if (tag.equals(constants.getProperty("PROP_LICENSE_LIST_VERSION"))) {
			checkAnalysisNull();
			if (analysis.getCreationInfo() == null) {
				SPDXCreatorInformation creator = new SPDXCreatorInformation(new String[] { value }, "", "", "");
				analysis.setCreationInfo(creator);
			}
			analysis.getCreationInfo().setLicenseListVersion(value);
		} else if (tag.equals(constants.getProperty("PROP_SPDX_COMMENT"))) {
			checkAnalysisNull();
			analysis.setComment(value);
		} else if (tag.equals(constants.getProperty("PROP_REVIEW_REVIEWER"))) {
			checkAnalysisNull();
			lastReviewer = new SPDXReview(value, format.format(new Date()), ""); // update date later
			List<SPDXReview> reviewers = Lists.newArrayList(analysis.getReviewers());
			reviewers.add(lastReviewer);
			analysis.setReviewers(reviewers.toArray(new SPDXReview[0]));
			this.verifyElement(lastReviewer.verify(), "Reviewer", lastReviewerLineNumber);
		} else if (tag.equals(constants.getProperty("PROP_REVIEW_DATE"))) {
			checkAnalysisNull();
			if (lastReviewer == null) {
				throw(new InvalidSpdxTagFileException("Missing Reviewer - A reviewer must be provided before a review date"));
			}
			lastReviewer.setReviewDate(value);
			this.verifyElement(lastReviewer.verify(), "Reviewer", lastReviewerLineNumber);
		} else if (tag.equals(constants.getProperty("PROP_REVIEW_COMMENT"))) {
			checkAnalysisNull();
			if (lastReviewer == null) {
				throw(new InvalidSpdxTagFileException("Missing Reviewer - A reviewer must be provided before a review comment"));
			}
			lastReviewer.setComment(value);
		} else if (tag.equals(constants.getProperty("PROP_LICENSE_ID"))) {
			checkAnalysisNull();
			if (inExtractedLicenseDefinition) {
				verifyElement(lastExtractedLicense.verify(), "Extracted License", lastExtractedLicenseLineNumber, false);
			}
			if (analysis.getDocumentContainer().extractedLicenseExists(value)) {
				lastExtractedLicense = analysis.getDocumentContainer().getExtractedLicense(value);
			} else {
				lastExtractedLicense = new ExtractedLicenseInfo(value, "WARNING: TEXT IS REQUIRED", null, null, null); //change text later
				lastExtractedLicenseLineNumber = lineNumber;
				analysis.addExtractedLicenseInfos(lastExtractedLicense);
			}
			this.inExtractedLicenseDefinition = true;
		} else if (tag.equals(constants.getProperty("PROP_PACKAGE_DECLARED_NAME"))) {
