diff --git a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java b/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java index f83f32f8..b7c41d84 100644 --- a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java +++ b/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java @@ -21,6 +21,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; @@ -29,6 +30,7 @@ import org.spdx.core.IModelCopyManager; import org.spdx.core.InvalidSPDXAnalysisException; import org.spdx.library.ListedLicenses; +import org.spdx.library.SpdxModelFactory; import org.spdx.library.model.v2.SpdxConstantsCompatV2; import org.spdx.library.model.v2.SpdxCreatorInformation; import org.spdx.library.model.v2.pointer.ByteOffsetPointer; @@ -698,11 +700,58 @@ public SpdxDocument convertAndStore(org.spdx.library.model.v2.SpdxDocument fromD } ).collect(Collectors.toList())); for (org.spdx.library.model.v2.license.ExtractedLicenseInfo extractedLicense:fromDoc.getExtractedLicenseInfos()) { - convertAndStore(extractedLicense); + toDoc.getRootElements().add(convertAndStore(extractedLicense)); } + convertOrphanElements(fromDoc, toDoc); return toDoc; } + /** + * Converts any SPDX spec version 2 packages, files, snippets, listed licenses, and listed license + * exceptions belonging to fromDoc which are not reachable through documentDescribes, a relationship, + * or a license expression (i.e. have no edges connecting them to anything else in the document). + * Without this, such elements would be silently dropped from the conversion since they would never + * be visited by the relationship graph traversal performed elsewhere in this class. + * Any such elements found are converted, stored, and added to the toDoc's root elements. + * @param fromDoc SPDX spec version 2 document being converted from + * @param toDoc SPDX spec version 3 document being converted to + * @throws InvalidSPDXAnalysisException on any errors converting the orphan elements + */ + @SuppressWarnings("unchecked") + private void convertOrphanElements(org.spdx.library.model.v2.SpdxDocument fromDoc, SpdxDocument toDoc) throws InvalidSPDXAnalysisException { + String documentUri = fromDoc.getDocumentUri(); + IModelStore fromModelStore = fromDoc.getModelStore(); + for (String typeFilter : new String[] {SpdxConstantsCompatV2.CLASS_SPDX_PACKAGE, + SpdxConstantsCompatV2.CLASS_SPDX_FILE, SpdxConstantsCompatV2.CLASS_SPDX_SNIPPET}) { + List fromElements = ((Stream) + SpdxModelFactory.getSpdxObjects(fromModelStore, copyManager, typeFilter, documentUri, null)) + .collect(Collectors.toList()); + for (org.spdx.library.model.v2.SpdxElement fromElement : fromElements) { + if (!alreadyCopied(fromElement.getObjectUri())) { + toDoc.getRootElements().add(convertAndStore(fromElement)); + } + } + } + // Listed licenses and listed license exceptions are stored under the SPDX License List namespace + // rather than the document's namespace, so no object URI prefix filter is used to find them. + List fromListedLicenses = ((Stream) + SpdxModelFactory.getSpdxObjects(fromModelStore, copyManager, SpdxConstantsCompatV2.CLASS_SPDX_LISTED_LICENSE, null, null)) + .collect(Collectors.toList()); + for (org.spdx.library.model.v2.license.SpdxListedLicense fromListedLicense : fromListedLicenses) { + if (!alreadyCopied(fromListedLicense.getObjectUri())) { + toDoc.getRootElements().add(convertAndStore(fromListedLicense)); + } + } + List fromListedExceptions = ((Stream) + SpdxModelFactory.getSpdxObjects(fromModelStore, copyManager, SpdxConstantsCompatV2.CLASS_SPDX_LISTED_LICENSE_EXCEPTION, null, null)) + .collect(Collectors.toList()); + for (org.spdx.library.model.v2.license.ListedLicenseException fromListedException : fromListedExceptions) { + if (!alreadyCopied(fromListedException.getObjectUri())) { + toDoc.getRootElements().add(convertAndStore(fromListedException)); + } + } + } + /** * Converts the externalDocRef to a NamespaceMap and store the NamespaceMap. * The document information is also retained in the externalDocRefMap such that any subsequent diff --git a/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java b/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java index 30f8f507..a646e183 100644 --- a/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java +++ b/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java @@ -23,10 +23,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import javax.annotation.Nullable; @@ -606,9 +608,21 @@ public void testConvertAndStoreSpdxDocument() throws InvalidSPDXAnalysisExceptio assertEquals(annotationComment, resultAnnotation.getStatement().get()); Element[] rootElements = result.getRootElements().toArray(new Element[result.getRootElements().size()]); - assertEquals(1, rootElements.length); - assertTrue(rootElements[0] instanceof SpdxPackage); - SpdxPackage resultPkg = (SpdxPackage)rootElements[0]; + assertEquals(2, rootElements.length); + SpdxPackage resultPkg = null; + CustomLicense rootCustomLicense = null; + for (Element rootElement:rootElements) { + if (rootElement instanceof SpdxPackage) { + resultPkg = (SpdxPackage)rootElement; + } else if (rootElement instanceof CustomLicense) { + rootCustomLicense = (CustomLicense)rootElement; + } else { + fail("Unexpected root element type "+rootElement.getClass().getName()); + } + } + assertTrue(Objects.nonNull(resultPkg)); + assertTrue(Objects.nonNull(rootCustomLicense)); + assertEquals(extractedLicName, rootCustomLicense.getName().get()); assertEquals(pkgName, resultPkg.getName().get()); assertEquals(pkgDownloadLocation, resultPkg.getDownloadLocation().get()); assertEquals(pkgCopyright, resultPkg.getCopyrightText().get()); @@ -671,6 +685,135 @@ public void testConvertAndStoreSpdxDocument() throws InvalidSPDXAnalysisExceptio assertTrue(verify.isEmpty()); } + /** + * Test method for {@link org.spdx.library.conversion.Spdx2to3Converter#convertAndStore(org.spdx.library.model.v2.SpdxDocument)}. + * Verifies that a package with no relationships and not included in documentDescribes (i.e. no edges + * connecting it to anything else in the document) is still converted rather than silently dropped. + * @throws InvalidSPDXAnalysisException + */ + @Test + public void testConvertAndStoreSpdxDocumentOrphanPackage() throws InvalidSPDXAnalysisException { + String describedPkgName = "described package"; + String orphanPkgName = "orphan package"; + + org.spdx.library.model.v2.SpdxDocument doc = new org.spdx.library.model.v2.SpdxDocument(fromModelStore, DOCUMENT_URI, copyManager, true); + doc.setCreationInfo(doc.createCreationInfo(Arrays.asList(new String[] {SpdxConstantsCompatV2.CREATOR_PREFIX_TOOL + "test"}), + "2010-01-29T18:30:22Z")); + org.spdx.library.model.v2.license.AnyLicenseInfo dataLicense = + LicenseInfoFactory.parseSPDXLicenseStringCompatV2("CC0-1.0", fromModelStore, DOCUMENT_URI, copyManager); + doc.setDataLicense(dataLicense); + + org.spdx.library.model.v2.license.AnyLicenseInfo noAssertion = + new org.spdx.library.model.v2.license.SpdxNoAssertionLicense(); + org.spdx.library.model.v2.SpdxPackage describedPkg = doc.createPackage(fromModelStore.getNextId(IdType.SpdxId), + describedPkgName, noAssertion, "copyright", noAssertion) + .setFilesAnalyzed(false) + .setDownloadLocation("NOASSERTION") + .build(); + doc.setDocumentDescribes(Arrays.asList(new org.spdx.library.model.v2.SpdxItem[] {describedPkg})); + + // orphan package - no relationships, not part of documentDescribes + org.spdx.library.model.v2.SpdxPackage orphanPkg = doc.createPackage(fromModelStore.getNextId(IdType.SpdxId), + orphanPkgName, noAssertion, "copyright", noAssertion) + .setFilesAnalyzed(false) + .setDownloadLocation("NOASSERTION") + .build(); + + Spdx2to3Converter converter = new Spdx2to3Converter(toModelStore, copyManager, defaultCreationInfo, + SpdxModelFactory.getLatestSpecVersion(), DEFAULT_PREFIX, true); + converter.convertAndStore(doc); + + List resultPackages = new ArrayList<>(); + SpdxModelFactory.getSpdxObjects(toModelStore, copyManager, SpdxConstantsV3.SOFTWARE_SPDX_PACKAGE, DEFAULT_PREFIX, DEFAULT_PREFIX) + .forEach(pkg -> resultPackages.add((SpdxPackage)pkg)); + assertEquals(2, resultPackages.size()); + Set resultPackageNames = new HashSet<>(); + for (SpdxPackage pkg:resultPackages) { + resultPackageNames.add(pkg.getName().get()); + } + assertTrue(resultPackageNames.contains(describedPkgName)); + assertTrue(resultPackageNames.contains(orphanPkgName)); + } + + /** + * Test method for {@link org.spdx.library.conversion.Spdx2to3Converter#convertAndStore(org.spdx.library.model.v2.SpdxDocument)}. + * Verifies that extracted licensing info, listed licenses, and listed license exceptions stored in the + * document's model store but not referenced by any package, file, or snippet are still converted and + * added as root elements rather than silently dropped. + * @throws InvalidSPDXAnalysisException + */ + @Test + public void testConvertAndStoreSpdxDocumentOrphanLicenses() throws InvalidSPDXAnalysisException { + String pkgName = "package"; + String extractedLicName = "Extracted License Name"; + String extractedLicText = "Extracted license text"; + String orphanLicenseId = "Apache-2.0"; + String orphanExceptionId = "Classpath-exception-2.0"; + + org.spdx.library.model.v2.SpdxDocument doc = new org.spdx.library.model.v2.SpdxDocument(fromModelStore, DOCUMENT_URI, copyManager, true); + doc.setCreationInfo(doc.createCreationInfo(Arrays.asList(new String[] {SpdxConstantsCompatV2.CREATOR_PREFIX_TOOL + "test"}), + "2010-01-29T18:30:22Z")); + org.spdx.library.model.v2.license.AnyLicenseInfo dataLicense = + LicenseInfoFactory.parseSPDXLicenseStringCompatV2("CC0-1.0", fromModelStore, DOCUMENT_URI, copyManager); + doc.setDataLicense(dataLicense); + + org.spdx.library.model.v2.license.AnyLicenseInfo noAssertion = + new org.spdx.library.model.v2.license.SpdxNoAssertionLicense(); + org.spdx.library.model.v2.SpdxPackage pkg = doc.createPackage(fromModelStore.getNextId(IdType.SpdxId), + pkgName, noAssertion, "copyright", noAssertion) + .setFilesAnalyzed(false) + .setDownloadLocation("NOASSERTION") + .build(); + doc.setDocumentDescribes(Arrays.asList(new org.spdx.library.model.v2.SpdxItem[] {pkg})); + + // extracted license info stored at the document level but not referenced by the package + org.spdx.library.model.v2.license.ExtractedLicenseInfo extractedLicense = + new org.spdx.library.model.v2.license.ExtractedLicenseInfo(fromModelStore, DOCUMENT_URI, + fromModelStore.getNextId(IdType.LicenseRef), copyManager, true); + extractedLicense.setExtractedText(extractedLicText); + extractedLicense.setName(extractedLicName); + doc.setExtractedLicenseInfos(Arrays.asList(new org.spdx.library.model.v2.license.ExtractedLicenseInfo[] {extractedLicense})); + + // listed license and listed license exception stored in the document's model store but not + // referenced by the package, a relationship, or documentDescribes + org.spdx.library.model.v2.license.SpdxListedLicense orphanLicense = + new org.spdx.library.model.v2.license.SpdxListedLicense(fromModelStore, + SpdxConstantsCompatV2.LISTED_LICENSE_NAMESPACE_PREFIX, orphanLicenseId, copyManager, true); + org.spdx.library.model.v2.license.ListedLicenseException orphanException = + new org.spdx.library.model.v2.license.ListedLicenseException(fromModelStore, + SpdxConstantsCompatV2.LISTED_LICENSE_NAMESPACE_PREFIX, orphanExceptionId, copyManager, true); + + Spdx2to3Converter converter = new Spdx2to3Converter(toModelStore, copyManager, defaultCreationInfo, + SpdxModelFactory.getLatestSpecVersion(), DEFAULT_PREFIX, true); + SpdxDocument result = converter.convertAndStore(doc); + + Element[] rootElements = result.getRootElements().toArray(new Element[result.getRootElements().size()]); + assertEquals(4, rootElements.length); + boolean foundPkg = false; + boolean foundCustomLicense = false; + boolean foundListedLicense = false; + boolean foundListedException = false; + for (Element rootElement:rootElements) { + if (rootElement instanceof SpdxPackage) { + foundPkg = true; + assertEquals(pkgName, ((SpdxPackage)rootElement).getName().get()); + } else if (rootElement instanceof CustomLicense) { + foundCustomLicense = true; + assertEquals(extractedLicName, ((CustomLicense)rootElement).getName().get()); + } else if (rootElement instanceof ListedLicense) { + foundListedLicense = true; + } else if (rootElement instanceof ListedLicenseException) { + foundListedException = true; + } else { + fail("Unexpected root element type "+rootElement.getClass().getName()); + } + } + assertTrue(foundPkg); + assertTrue(foundCustomLicense); + assertTrue(foundListedLicense); + assertTrue(foundListedException); + } + @Test public void testConvertAndStoreExternalDocRef() throws InvalidSPDXAnalysisException { String externalDocumentId = SpdxConstantsCompatV2.EXTERNAL_DOC_REF_PRENUM + "external";