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

org.spdx.jacksonstore.JacksonDeSerializer Maven / Gradle / Ivy

Go to download

Storage for SPDX documents utilizing Jackson Databind. This store supports serializing and deserializing files in JSON, YAML and XML formats.

There is a newer version: 2.0.0-RC1
Show newest version
/**
 * Copyright (c) 2020 Source Auditor Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 * 
 *   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.jacksonstore;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;

import javax.annotation.Nullable;

import org.spdx.jacksonstore.MultiFormatStore.Format;
import org.spdx.library.InvalidSPDXAnalysisException;
import org.spdx.library.SpdxConstants;
import org.spdx.library.model.ExternalSpdxElement;
import org.spdx.library.model.IndividualUriValue;
import org.spdx.library.model.ModelStorageClassConverter;
import org.spdx.library.model.ReferenceType;
import org.spdx.library.model.SimpleUriValue;
import org.spdx.library.model.SpdxDocument;
import org.spdx.library.model.SpdxElement;
import org.spdx.library.model.SpdxModelFactory;
import org.spdx.library.model.TypedValue;
import org.spdx.library.model.enumerations.RelationshipType;
import org.spdx.library.model.license.AnyLicenseInfo;
import org.spdx.library.model.license.LicenseInfoFactory;
import org.spdx.storage.IModelStore;
import org.spdx.storage.IModelStore.IModelStoreLock;
import org.spdx.storage.IModelStore.IdType;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;

/**
 * Converts a Jackson node for a document to a stored document in a model store
 * 
 * @author Gary O'Neall
 *
 */
public class JacksonDeSerializer {
	
	/**
	 * Properties that should not be restored as part of the deserialization
	 */
	static final Set SKIPPED_PROPERTIES = Collections.unmodifiableSet(new HashSet(Arrays.asList(new String[] {
			SpdxConstants.PROP_DOCUMENT_DESCRIBES, SpdxConstants.PROP_DOCUMENT_PACKAGES, SpdxConstants.PROP_DOCUMENT_FILES,
			SpdxConstants.PROP_DOCUMENT_SNIPPETS, SpdxConstants.SPDX_IDENTIFIER, SpdxConstants.PROP_DOCUMENT_RELATIONSHIPS
	})));

	private IModelStore store;
	@SuppressWarnings("unused")
	private Format format;

	/**
	 * @param store store to store any documents in
	 */
	public JacksonDeSerializer(IModelStore store, Format format) {
		Objects.requireNonNull(store, "Model store can not be null");
		Objects.requireNonNull(format, "Format can not be null");
		this.store = store;
		this.format = format;
	}

	/**
	 * Stores an SPDX document converted from the JsonNode doc
	 * @param documentNamespace namespace for the document
	 * @param doc JsonNode containing the SPDX document
	 * @throws InvalidSPDXAnalysisException
	 */
	@SuppressWarnings("unchecked")
	public void storeDocument(String documentNamespace, JsonNode doc) throws InvalidSPDXAnalysisException {
		Objects.requireNonNull(documentNamespace, "Null required document namespace");
		Objects.requireNonNull(doc, "Null document JSON Node");
		IModelStoreLock lock = store.enterCriticalSection(documentNamespace, false);
		try {
			Map spdxIdProperties = new HashMap<>();	// properties which contain an SPDX id which needs to be replaced
			store.create(documentNamespace, SpdxConstants.SPDX_DOCUMENT_ID, SpdxConstants.CLASS_SPDX_DOCUMENT);
			restoreObjectPropertyValues(documentNamespace, SpdxConstants.SPDX_DOCUMENT_ID, doc, spdxIdProperties);
			// restore the packages
			Map addedElements = new HashMap<>();
			addedElements.put(SpdxConstants.SPDX_DOCUMENT_ID, new TypedValue(SpdxConstants.SPDX_DOCUMENT_ID, SpdxConstants.CLASS_SPDX_DOCUMENT));
			restoreElements(documentNamespace, SpdxConstants.CLASS_SPDX_PACKAGE, 
					doc.get(SpdxConstants.PROP_DOCUMENT_PACKAGES), addedElements, spdxIdProperties);
			restoreElements(documentNamespace, SpdxConstants.CLASS_SPDX_FILE, 
					doc.get(SpdxConstants.PROP_DOCUMENT_FILES), addedElements, spdxIdProperties);
			restoreElements(documentNamespace, SpdxConstants.CLASS_SPDX_SNIPPET, 
					doc.get(SpdxConstants.PROP_DOCUMENT_SNIPPETS), addedElements, spdxIdProperties);
			restoreRelationships(documentNamespace, doc.get(SpdxConstants.PROP_DOCUMENT_RELATIONSHIPS),
					addedElements);
			// fix up the ID's
			for (Entry propertyToFix:spdxIdProperties.entrySet()) {
				Optional idToReplace = store.getValue(documentNamespace, propertyToFix.getKey(), propertyToFix.getValue());
				if (!idToReplace.isPresent()) {
					throw new InvalidSPDXAnalysisException("Missing SPDX ID for "+propertyToFix.getKey() + " " + propertyToFix.getValue());
				}
				if (idToReplace.get() instanceof Collection) {
					Collection replacements = new HashSet<>();
					for (Object spdxId:(Collection)(idToReplace.get())) {
						if (!(spdxId instanceof String)) {
							throw new InvalidSPDXAnalysisException("Can not replace the SPDX ID with value due to invalid type for "+propertyToFix.getKey() + " " + propertyToFix.getValue());
						}
						replacements.add(idToObjectValue(documentNamespace, (String)spdxId, addedElements));

					}
					store.clearValueCollection(documentNamespace, propertyToFix.getKey(), propertyToFix.getValue());
					for (Object replacement:replacements) {
						store.addValueToCollection(documentNamespace, propertyToFix.getKey(), propertyToFix.getValue(), replacement);
					}
					
				} else {
					if (!(idToReplace.get() instanceof String)) {
						throw new InvalidSPDXAnalysisException("Can not replace the SPDX ID with value due to invalid type for "+propertyToFix.getKey() + " " + propertyToFix.getValue());
					}
					String spdxId = (String)idToReplace.get();
					store.setValue(documentNamespace, propertyToFix.getKey(), propertyToFix.getValue(), idToObjectValue(documentNamespace, spdxId, addedElements));
				}
			}
		} finally {
			store.leaveCriticalSection(lock);
		}
	}
	
	/**
	 * Restores a single SPDX element of a specific type
	 * @param documentUri
	 * @param type
	 * @param jsonNode
	 * @param addedElements
	 * @param spdxIdProperties Properties which contain an SPDX ID which needs to be replaced
	 * @throws InvalidSPDXAnalysisException
	 */
	private void restoreElement(String documentUri, String type, @Nullable JsonNode jsonNode,
			Map addedElements, Map spdxIdProperties) throws InvalidSPDXAnalysisException {
		if (Objects.isNull(jsonNode)) {
			return;
		}
		if (!jsonNode.isObject()) {
			throw new InvalidSPDXAnalysisException("Invalid JSON node type for SPDX element");
		}
		JsonNode idNode = jsonNode.get(SpdxConstants.SPDX_IDENTIFIER);
		if (Objects.isNull(idNode) || !idNode.isTextual()) {
			throw new InvalidSPDXAnalysisException("Missing SPDX ID");
		}
		String id = idNode.asText();
		if (Objects.isNull(id) || id.isEmpty()) {
			throw new InvalidSPDXAnalysisException("Missing SPDX ID");
		}
		if (addedElements.containsKey(id)) {
			throw new InvalidSPDXAnalysisException("Duplicate SPDX ID: "+id);
		}
		store.create(documentUri, id, type);
		restoreObjectPropertyValues(documentUri, id, jsonNode, spdxIdProperties);
		addedElements.put(id, new TypedValue(id, type));
	}

	/**
	 * Restores SPDX elements of a specific type
	 * @param documentUri
	 * @param type
	 * @param jsonNode
	 * @param addedElements
	 * @param spdxIdProperties Properties which contain an SPDX ID which needs to be replaced
	 * @throws InvalidSPDXAnalysisException
	 */
	private void restoreElements(String documentUri, String type, @Nullable JsonNode jsonNode,
			Map addedElements, Map spdxIdProperties) throws InvalidSPDXAnalysisException {
		if (Objects.isNull(jsonNode)) {
			return;
		}
		if (jsonNode.isArray()) {
			Iterator iter = jsonNode.elements();
			while (iter.hasNext()) {
				restoreElement(documentUri, type, iter.next(), addedElements, spdxIdProperties);
			}
		} else {
			// This can occur if there is only a single element in an XML document
			restoreElement(documentUri, type, jsonNode, addedElements, spdxIdProperties);
		}
	}
	

	/**
	 * Restore the relationships adding them as properites to the correct elements
	 * @param documentNamespace
	 * @param jsonNode
	 * @param addedElements
	 * @throws InvalidSPDXAnalysisException 
	 */
	private void restoreRelationships(String documentNamespace, JsonNode jsonNode,
			Map addedElements) throws InvalidSPDXAnalysisException {
		if (Objects.isNull(jsonNode)) {
			return;
		}
		if (!jsonNode.isArray()) {
			throw new InvalidSPDXAnalysisException("Relationships are expected to be in an array for type Relationship");
		}
		Iterator iter = jsonNode.elements();
		while (iter.hasNext()) {
			JsonNode relationship = iter.next();
			JsonNode elementIdNode = relationship.get(SpdxConstants.PROP_SPDX_ELEMENTID);
			if (Objects.isNull(elementIdNode) || !elementIdNode.isTextual()) {
				throw new InvalidSPDXAnalysisException("Missing SPDX element ID");
			}
			TypedValue element = addedElements.get(elementIdNode.asText());
			if (Objects.isNull(element)) {
				throw new InvalidSPDXAnalysisException("Missing SPDX element for ID "+elementIdNode.asText());
			}
			JsonNode relationshipTypeNode = relationship.get(SpdxConstants.PROP_RELATIONSHIP_TYPE);
			if (Objects.isNull(relationshipTypeNode) || !relationshipTypeNode.isTextual()) {
				throw new InvalidSPDXAnalysisException("Missing required relationship type");
			}
			String relationshipTypeUri = null;
			try {
				relationshipTypeUri = RelationshipType.valueOf(relationshipTypeNode.asText()).getIndividualURI();
			} catch(Exception ex) {
				throw new InvalidSPDXAnalysisException("Unknown relationship type: "+relationshipTypeNode.asText());
			}
			SimpleUriValue relationshipType = new SimpleUriValue(relationshipTypeUri);
			JsonNode relatedElementNode = relationship.get(SpdxConstants.PROP_RELATED_SPDX_ELEMENT);
			if (Objects.isNull(relatedElementNode) || !relatedElementNode.isTextual()) {
				throw new InvalidSPDXAnalysisException("Missing required related element");
			}
			Object relatedElement = idToObjectValue(documentNamespace, relatedElementNode.asText(), addedElements);
			if (Objects.isNull(relatedElement)) {
				throw new InvalidSPDXAnalysisException("Missing SPDX element for ID "+relatedElementNode.asText());
			}
			String relationshipId = store.getNextId(IdType.Anonymous, documentNamespace);
			store.create(documentNamespace, relationshipId, SpdxConstants.CLASS_RELATIONSHIP);
			store.setValue(documentNamespace, relationshipId, SpdxConstants.PROP_RELATIONSHIP_TYPE, relationshipType);
			store.setValue(documentNamespace, relationshipId, SpdxConstants.PROP_RELATED_SPDX_ELEMENT, relatedElement);
			store.addValueToCollection(documentNamespace, element.getId(), SpdxConstants.PROP_RELATIONSHIP, 
					new TypedValue(relationshipId, SpdxConstants.CLASS_RELATIONSHIP));
		}
	}

	/**
	 * Restore all the property values within the JsonNode
	 * @param documentUri
	 * @param id
	 * @param node
	 * @param spdxIdProperties Properties which contain an SPDX ID which needs to be replaced
	 * @throws InvalidSPDXAnalysisException 
	 */
	private void restoreObjectPropertyValues(String documentUri, String id, JsonNode node, 
			Map spdxIdProperties) throws InvalidSPDXAnalysisException {
		Iterator>  fieldIterator = node.fields();
		while (fieldIterator.hasNext()) {
			Entry field = fieldIterator.next();
			if (SKIPPED_PROPERTIES.contains(field.getKey())) {
				continue;
			}
			setPropertyValueForJsonNode(documentUri, id, field.getKey(), field.getValue(), spdxIdProperties, false);
		}
	}

	/**
	 * Set the property value for the property associated with the ID with the value stored in the JsonNode value
	 * @param documentUri document URI
	 * @param id ID of the object to store the value
	 * @param property property name
	 * @param value JSON node containing the value
	 * @param spdxIdProperties Properties which contain an SPDX ID which needs to be replaced
	 * @param list true if this property is a list type
	 * @throws InvalidSPDXAnalysisException
	 */
	private void setPropertyValueForJsonNode(String documentUri, String id, String property, JsonNode value,
			Map spdxIdProperties, boolean list) throws InvalidSPDXAnalysisException {
		if (SpdxJsonLDContext.getInstance().isList(property)) {
			list = true;
		}if (JsonNodeType.ARRAY.equals(value.getNodeType())) {
			Iterator iter = value.elements();
			while (iter.hasNext()) {
				setPropertyValueForJsonNode(documentUri, id, property, iter.next(), spdxIdProperties, true);
			}
		} else if (!JsonNodeType.NULL.equals(value.getNodeType())) {
			// ignore te null;
			Optional propertyType = SpdxJsonLDContext.getInstance().getType(property);
			if (list) {
				store.addValueToCollection(documentUri, id, 
						MultiFormatStore.collectionPropertyNameToPropertyName(property), 
						toStoredObject(documentUri, id, 
								property, value, propertyType, spdxIdProperties, list));
			} else {
				store.setValue(documentUri, id, property, toStoredObject(documentUri, id, 
						property, value, propertyType, spdxIdProperties, list));
			}
		}
	}
	/**
	 * Convert the value to the appropriate type to be stored
	 * @param documentUri document URI
	 * @param id ID of the object to store the value
	 * @param property property name
	 * @param value JSON node containing the value
	 * @param propertyType type of property
	 * @param spdxIdProperties Properties which contain an SPDX ID which needs to be replaced
	 * @param list true if this property is a list type
	 * @return the object to be stored
	 * @throws InvalidSPDXAnalysisException
	 */
	private Object toStoredObject(String documentUri, String id, String property, JsonNode value, 
			Optional propertyType, Map spdxIdProperties, boolean list) throws InvalidSPDXAnalysisException {
		switch (value.getNodeType()) {
		case ARRAY:
			throw new InvalidSPDXAnalysisException("Can not convert a JSON array to a stored object");
		case BOOLEAN:
			if (propertyType.isPresent()) {
				Class toStoreClass = SpdxJsonLDContext.XMLSCHEMA_TYPE_TO_JAVA_CLASS.get(propertyType.get());
				if (Objects.isNull(toStoreClass)) {
					// assume it is a boolean type
					return value.asBoolean();
				} else if (String.class.equals(toStoreClass)) {
					return Boolean.toString(value.asBoolean());
				} else if (Boolean.class.equals(toStoreClass)) {
					return value.asBoolean();
				} else {
					throw new InvalidSPDXAnalysisException("Can not convert a JSON BOOLEAN to a "+toStoreClass.toString());
				}
			} else {
				return value.asBoolean();
			}
		case NULL: throw new InvalidSPDXAnalysisException("Can not convert a JSON NULL to a stored object");
		case NUMBER: {
			if (propertyType.isPresent()) {
				Class toStoreClass = SpdxJsonLDContext.XMLSCHEMA_TYPE_TO_JAVA_CLASS.get(propertyType.get());
				if (Objects.isNull(toStoreClass)) {
					// assume it is a integer type
					return value.asInt();
				} else if (String.class.equals(toStoreClass)) {
					return Double.toString(value.asDouble());
				} else if (Integer.class.equals(toStoreClass)) {
					return value.asInt();
				} else {
					throw new InvalidSPDXAnalysisException("Can not convert a JSON NUMBER to a "+toStoreClass.toString());
				}
			} else {
				return value.asInt();
			}
		}
		case OBJECT: {
			if (!propertyType.isPresent()) {
				throw new InvalidSPDXAnalysisException("Unknown type for property " + property);
			}
			if (SpdxConstants.CLASS_SINGLE_POINTER.equals(propertyType.get())) {
				// need to determine whether a byte or line pointer type
				// A bit of Duck Typing is in order
				if (Objects.nonNull(value.get(SpdxConstants.PROP_POINTER_OFFSET))) {
					propertyType = Optional.of(SpdxConstants.CLASS_POINTER_BYTE_OFFSET_POINTER);
				} else if (Objects.nonNull(value.get(SpdxConstants.PROP_POINTER_LINE_NUMBER))) {
					propertyType = Optional.of(SpdxConstants.CLASS_POINTER_LINE_CHAR_POINTER);
				} else {
					throw new InvalidSPDXAnalysisException("Can not determine type for snippet pointer");
				}
			}
			String objectId = findObjectIdInJsonObject(documentUri, value);
			store.create(documentUri, objectId, propertyType.get());
			restoreObjectPropertyValues(documentUri, objectId, value, spdxIdProperties);
			return new TypedValue(objectId, propertyType.get());
		}
		case STRING:
			return getStringPropertyValueForJsonNode(documentUri, id, property, value, 
					propertyType, spdxIdProperties, list);
		case BINARY:
		case MISSING:
		case POJO:
		default: throw new InvalidSPDXAnalysisException("Unsupported JSON node type: "+value.toString());
		}
	}
	
	/**
	 * @param spdxIdProperties Properties which contain an SPDX ID which needs to be replaced
	 * 
	 * @throws InvalidSPDXAnalysisException
	 */
	/**
	 * Gets the property value for a string JsonNode
	 * @param documentUri document URI
	 * @param id ID of the object to store the value
	 * @param property property name
	 * @param value JSON node containing the value
	 * @param propertyType
	 * @param list true if this property is a list type
	 * @return the appropriate object to store
	 * @throws InvalidSPDXAnalysisException
	 */
	private Object getStringPropertyValueForJsonNode(String documentUri, String id, String property, JsonNode value,
			Optional propertyType, Map spdxIdProperties, boolean list) throws InvalidSPDXAnalysisException {
		Class clazz = null;
		if (propertyType.isPresent()) {
			// check for SPDX model types
			clazz = SpdxModelFactory.SPDX_TYPE_TO_CLASS.get(propertyType.get());
			if (Objects.isNull(clazz)) {
				// check for primitive types
				clazz = SpdxJsonLDContext.XMLSCHEMA_TYPE_TO_JAVA_CLASS.get(propertyType.get());
			}
		}
		if (Objects.isNull(clazz)) {
			// Just return the string value
			return value.asText();
		} else {
			// check for SPDX model classes
			if (AnyLicenseInfo.class.isAssignableFrom(clazz)) {
				// convert license expressions to their model object form
				AnyLicenseInfo parsedLicense = LicenseInfoFactory.parseSPDXLicenseString(value.asText(), store, documentUri, null);
				return ModelStorageClassConverter.modelObjectToStoredObject(parsedLicense, documentUri, store, null);			
			} else if (SpdxDocument.class.isAssignableFrom(clazz) || ReferenceType.class.isAssignableFrom(clazz)) {
				// Convert any IndividualUriValue values
				final String uriValue = value.asText();
				return new IndividualUriValue() {

					@Override
					public String getIndividualURI() {
						return uriValue;
					}
					
				};
			} else if (SpdxElement.class.isAssignableFrom(clazz)) {
				// store the ID and save it in the spdxIdProperties to replace with the actual class later
				// once everything is restored
				if (list) {
					spdxIdProperties.put(id, MultiFormatStore.collectionPropertyNameToPropertyName(property));
				} else {
					spdxIdProperties.put(id, property);
				}
				return value.asText();
			} else if (clazz.isEnum()) {
				for (Object enumConst:clazz.getEnumConstants()) {
					if (enumConst instanceof IndividualUriValue && value.asText().equals(enumConst.toString())) {
						return new SimpleUriValue((IndividualUriValue)enumConst);
					}
				}
				throw new InvalidSPDXAnalysisException("Could not find enum constants for "+value.asText()+" property "+property);
			} else if (String.class.equals(clazz)) {
				return value.asText();
			} else if (Boolean.class.equals(clazz)) {
				try {
					return Boolean.parseBoolean(value.asText());
				} catch (Exception ex) {
					throw new InvalidSPDXAnalysisException("Unable to convert "+value.asText()+" to boolean for property "+property);
				}
			} else if (Integer.class.equals(clazz)) {
				try {
					return Integer.parseInt(value.asText());
				} catch (Exception ex) {
					throw new InvalidSPDXAnalysisException("Unable to convert "+value.asText()+" to integer for property "+property);
				}
			}	else {
				throw new InvalidSPDXAnalysisException("Unknown type: "+propertyType.get()+" for property "+property);
			}
		}
	}


	/**
	 * @param documentUri
	 * @param jsonObject
	 * @return the ID for the JSON object based on what property values are available
	 * @throws InvalidSPDXAnalysisException
	 */
	private String findObjectIdInJsonObject(String documentUri, JsonNode jsonObject) throws InvalidSPDXAnalysisException {
		JsonNode retval = jsonObject.get(SpdxConstants.SPDX_IDENTIFIER);
		if (Objects.isNull(retval) || !retval.isTextual()) {
			retval = jsonObject.get(SpdxConstants.PROP_LICENSE_ID);
		}
		if (Objects.isNull(retval) || !retval.isTextual()) {
			retval = jsonObject.get(SpdxConstants.PROP_LICENSE_EXCEPTION_ID);
		}
		if (Objects.isNull(retval) || !retval.isTextual()) {
			retval = jsonObject.get(SpdxConstants.EXTERNAL_DOCUMENT_REF_IDENTIFIER);
		}
		if (Objects.isNull(retval) || !retval.isTextual()) {
			return store.getNextId(IdType.Anonymous, documentUri);
		} else {
			return retval.asText();
		}
	}
	

	/**
	 * Convert an ID into the value object to be stored
	 * @param documentNamespace
	 * @param spdxId ID to be replaced by the actual object
	 * @param addedElements SPDX elements added
	 * @return
	 * @throws InvalidSPDXAnalysisException 
	 */
	private Object idToObjectValue(String documentNamespace, String spdxId, Map addedElements) throws InvalidSPDXAnalysisException {
		TypedValue fixedValue = addedElements.get(spdxId);
		if (Objects.isNull(fixedValue)) {
			if (spdxId.equals(SpdxConstants.NONE_VALUE)) {
				return new IndividualUriValue() {

					@Override
					public String getIndividualURI() {
						return SpdxConstants.URI_VALUE_NONE;
					}
					
				};
			} else if (spdxId.equals(SpdxConstants.NOASSERTION_VALUE)) {
				return new IndividualUriValue() {

					@Override
					public String getIndividualURI() {
						return SpdxConstants.URI_VALUE_NOASSERTION;
					}
					
				};
			} else if (spdxId.startsWith("DocumentRef-")) {
				final IModelStore modelStore = store;
				IndividualUriValue spdxExternalElementRef = new IndividualUriValue() {

					@Override
					public String getIndividualURI() {
						try {
							return ExternalSpdxElement.externalSpdxElementIdToURI(spdxId, modelStore, documentNamespace, null);
						} catch (InvalidSPDXAnalysisException e) {
							throw new RuntimeException(e);
						}
					}
					
				};
				return spdxExternalElementRef;
			} else {
				throw new InvalidSPDXAnalysisException("No SPDX element found for SPDX ID "+spdxId);
			}
		} else {
			return fixedValue;
		}
	}
}