From c89bb93ed26c4a22f874eb5762c987eb898c3789 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 18 Sep 2026 20:27:39 +0100 Subject: [PATCH 1/2] Fix stream leaks and default-charset use in CLI tools - Use try-with-resources in SpdxConverter and the RdfSchemaTo* tools - Read license and JSON files as UTF-8 (BOM-aware, legacy fallback) - Stop SpdxViewer closing System.out - Add tests for viewer, RDF schema tools and encodings Signed-off-by: Arthit Suriyawongkul --- .../spdx/tools/MatchingStandardLicenses.java | 46 +++++++++- .../spdx/tools/RdfSchemaToJsonContext.java | 30 ++---- .../org/spdx/tools/RdfSchemaToJsonSchema.java | 38 ++------ .../java/org/spdx/tools/RdfSchemaToXsd.java | 34 +------ .../java/org/spdx/tools/SpdxConverter.java | 42 +++------ .../java/org/spdx/tools/SpdxToolsHelper.java | 5 +- src/main/java/org/spdx/tools/SpdxViewer.java | 28 +++++- .../tools/MatchingStandardLicensesTest.java | 50 ++++++++++ .../org/spdx/tools/RdfSchemaToolsTest.java | 91 +++++++++++++++++++ .../org/spdx/tools/SpdxToolsHelperTest.java | 69 ++++++++++++++ .../java/org/spdx/tools/SpdxViewerTest.java | 50 ++++++++++ 11 files changed, 357 insertions(+), 126 deletions(-) create mode 100644 src/test/java/org/spdx/tools/RdfSchemaToolsTest.java create mode 100644 src/test/java/org/spdx/tools/SpdxToolsHelperTest.java create mode 100644 src/test/java/org/spdx/tools/SpdxViewerTest.java diff --git a/src/main/java/org/spdx/tools/MatchingStandardLicenses.java b/src/main/java/org/spdx/tools/MatchingStandardLicenses.java index c560643..0865d42 100644 --- a/src/main/java/org/spdx/tools/MatchingStandardLicenses.java +++ b/src/main/java/org/spdx/tools/MatchingStandardLicenses.java @@ -20,7 +20,11 @@ import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; @@ -123,12 +127,44 @@ static int run(String[] args) { } /** - * @param textFile - * @return - * @throws IOException + * Reads a text file. A UTF-8 or UTF-16 byte order mark selects the encoding and is + * removed. Otherwise the bytes are decoded as UTF-8, or with the platform default + * charset if they are not valid UTF-8 (e.g. a legacy Windows ANSI file). + * @param textFile file to read + * @return the file content, without any byte order mark + * @throws IOException on read error */ - private static String readAll(File textFile) throws IOException { - return new String(Files.readAllBytes(textFile.toPath()), Charset.defaultCharset()); + static String readAll(File textFile) throws IOException { + byte[] bytes = Files.readAllBytes(textFile.toPath()); + if (hasPrefix(bytes, 0xEF, 0xBB, 0xBF)) { + return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8); + } + if (hasPrefix(bytes, 0xFF, 0xFE)) { + return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16LE); + } + if (hasPrefix(bytes, 0xFE, 0xFF)) { + return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16BE); + } + try { + return StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(bytes)).toString(); + } catch (CharacterCodingException e) { + return new String(bytes, Charset.defaultCharset()); + } + } + + private static boolean hasPrefix(byte[] bytes, int... prefix) { + if (bytes.length < prefix.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if ((bytes[i] & 0xFF) != prefix[i]) { + return false; + } + } + return true; } private static void usage() { diff --git a/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java b/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java index 010fc54..a9811c5 100644 --- a/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java +++ b/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Objects; @@ -31,6 +30,7 @@ import org.apache.jena.ontapi.model.OntModel; import org.spdx.tools.schema.OwlToJsonContext; +import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -64,34 +64,24 @@ public static void main(String[] args) { usage(); return; } - InputStream is = null; OwlToJsonContext owlToJsonContext = null; - try { - is = new FileInputStream(fromFile); + try (InputStream is = new FileInputStream(fromFile)) { OntModel model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); model.read(is, "RDF/XML"); owlToJsonContext = new OwlToJsonContext(model); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); return; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - System.err.println("Error closing input file stream: "+e.getMessage()); - } - } + } catch (IOException e) { + System.err.println("Error closing input file stream: "+e.getMessage()); } if (Objects.isNull(owlToJsonContext)) { System.err.println("Unable to load ontology from file "+fromFile.getName()); return; } ObjectNode context = owlToJsonContext.convertToContext(); - JsonGenerator jsonGenerator = null; - try { - jsonGenerator = OwlToJsonContext.JSON_MAPPER.getFactory().createGenerator(new FileOutputStream(toFile)); - OwlToJsonContext.JSON_MAPPER.writeTree(jsonGenerator.useDefaultPrettyPrinter(), + try (JsonGenerator jsonGenerator = OwlToJsonContext.JSON_MAPPER.getFactory().createGenerator(toFile, JsonEncoding.UTF8)) { + OwlToJsonContext.JSON_MAPPER.writeTree(jsonGenerator.useDefaultPrettyPrinter(), context); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); @@ -102,14 +92,6 @@ public static void main(String[] args) { } catch (IOException e) { System.err.println("I/O error: "+e.getMessage()); return; - } finally { - if (Objects.nonNull(jsonGenerator)) { - try { - jsonGenerator.close(); - } catch (IOException e) { - System.err.println("Error closing output file stream: "+e.getMessage()); - } - } } } diff --git a/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java b/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java index 0bd4458..a67bc46 100644 --- a/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java +++ b/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java @@ -21,16 +21,15 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Objects; import org.apache.jena.ontapi.OntModelFactory; import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; import org.spdx.tools.schema.OwlToJsonSchema; +import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -66,32 +65,22 @@ public static void main(String[] args) { usage(); return; } - InputStream is = null; OntModel model = null; - try { - is = new FileInputStream(fromFile); + try (InputStream is = new FileInputStream(fromFile)) { model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); model.read(is, "RDF/XML"); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); return; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - System.err.println("Error closing input file stream: "+e.getMessage()); - } - } + } catch (IOException e) { + System.err.println("Error closing input file stream: "+e.getMessage()); } OwlToJsonSchema owlToJson = new OwlToJsonSchema(model); ObjectNode root = owlToJson.convertToJsonSchema(); ObjectMapper jsonMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); - JsonGenerator jsonGenerator = null; - try { - jsonGenerator = jsonMapper.getFactory().createGenerator(new FileOutputStream(toFile)); - jsonMapper.writeTree(jsonGenerator.useDefaultPrettyPrinter(), + try (JsonGenerator jsonGenerator = jsonMapper.getFactory().createGenerator(toFile, JsonEncoding.UTF8)) { + jsonMapper.writeTree(jsonGenerator.useDefaultPrettyPrinter(), root); } catch (JsonProcessingException e) { System.err.println("JSON error "+e.getMessage()); @@ -99,21 +88,6 @@ public static void main(String[] args) { } catch (IOException e) { System.err.println("I/O error: "+e.getMessage()); return; - } finally { - if (Objects.nonNull(is)) { - try { - is.close(); - } catch (IOException e) { - System.err.println("Error closing input file stream: "+e.getMessage()); - } - } - if (Objects.nonNull(jsonGenerator)) { - try { - jsonGenerator.close(); - } catch (IOException e) { - System.err.println("Error closing output file stream: "+e.getMessage()); - } - } } } diff --git a/src/main/java/org/spdx/tools/RdfSchemaToXsd.java b/src/main/java/org/spdx/tools/RdfSchemaToXsd.java index ba523b8..0c58637 100644 --- a/src/main/java/org/spdx/tools/RdfSchemaToXsd.java +++ b/src/main/java/org/spdx/tools/RdfSchemaToXsd.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Objects; import org.apache.jena.ontapi.OntModelFactory; import org.apache.jena.ontapi.OntSpecification; @@ -64,49 +63,24 @@ public static void main(String[] args) { usage(); return; } - InputStream is = null; OntModel model = null; - try { - is = new FileInputStream(fromFile); + try (InputStream is = new FileInputStream(fromFile)) { model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); model.read(is, "RDF/XML"); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); return; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - System.err.println("Error closing input file stream: "+e.getMessage()); - } - } + } catch (IOException e) { + System.err.println("Error closing input file stream: "+e.getMessage()); } try { OwlToXsd owlToXsd = new OwlToXsd(model); XmlSchema xmlSchema = owlToXsd.convertToXsd(); - OutputStream os = null; - try { - os = new FileOutputStream(toFile); + try (OutputStream os = new FileOutputStream(toFile)) { xmlSchema.write(os); } catch (IOException e) { System.err.println("I/O error: "+e.getMessage()); return; - } finally { - if (Objects.nonNull(is)) { - try { - is.close(); - } catch (IOException e) { - System.err.println("Error closing input file stream: "+e.getMessage()); - } - } - if (Objects.nonNull(os)) { - try { - os.close(); - } catch (IOException e) { - System.err.println("Error closing output file stream: "+e.getMessage()); - } - } } } catch (XmlSchemaSerializerException e1) { System.err.println("Error generating XSD schema: "+e1.getMessage()); diff --git a/src/main/java/org/spdx/tools/SpdxConverter.java b/src/main/java/org/spdx/tools/SpdxConverter.java index b1876c9..86aa638 100644 --- a/src/main/java/org/spdx/tools/SpdxConverter.java +++ b/src/main/java/org/spdx/tools/SpdxConverter.java @@ -24,8 +24,6 @@ import java.io.IOException; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.spdx.core.InvalidSPDXAnalysisException; import org.spdx.library.ModelCopyManager; import org.spdx.library.SpdxModelFactory; @@ -57,7 +55,6 @@ * @author Gary O'Neall */ public class SpdxConverter { - static final Logger logger = LoggerFactory.getLogger(SpdxConverter.class); static final int MIN_ARGS = 2; static final int MAX_ARGS = 5; @@ -186,8 +183,6 @@ public static void convert(String fromFilePath, String toFilePath, SerFileType f if (toFile.exists()) { throw new SpdxConverterException("Output file "+toFilePath+" already exists."); } - FileInputStream input = null; - FileOutputStream output = null; String oldXmlInputFactory = null; boolean propertySet = false; try { @@ -214,18 +209,19 @@ public static void convert(String fromFilePath, String toFilePath, SerFileType f if (toStore instanceof JsonLDStore) { ((JsonLDStore)toStore).setUseExternalListedElements(true); } - input = new FileInputStream(fromFile); - output = new FileOutputStream(toFile); - fromStore.deSerialize(input, false); - if (fromVersion == SpdxMajorVersion.VERSION_3) { - copyV3ToV3(fromStore, toStore, excludeLicenseDetails); - } else if (toVersion == SpdxMajorVersion.VERSION_3) { - copyV2ToV3(fromStore, toStore, excludeLicenseDetails); - } else { - copyV2ToV2(fromStore, toStore, excludeLicenseDetails); + try (FileInputStream input = new FileInputStream(fromFile); + FileOutputStream output = new FileOutputStream(toFile)) { + fromStore.deSerialize(input, false); + if (fromVersion == SpdxMajorVersion.VERSION_3) { + copyV3ToV3(fromStore, toStore, excludeLicenseDetails); + } else if (toVersion == SpdxMajorVersion.VERSION_3) { + copyV2ToV3(fromStore, toStore, excludeLicenseDetails); + } else { + copyV2ToV2(fromStore, toStore, excludeLicenseDetails); + } + toStore.serialize(output); } - toStore.serialize(output); - } catch (Exception ex) { + } catch (IOException | InvalidSPDXAnalysisException | SpdxConverterException | RuntimeException ex) { String msg = "Error converting SPDX file: "+ex.getClass().toString(); if (Objects.nonNull(ex.getMessage())) { msg = msg + " " + ex.getMessage(); @@ -239,20 +235,6 @@ public static void convert(String fromFilePath, String toFilePath, SerFileType f System.setProperty(SpdxToolsHelper.XML_INPUT_FACTORY_PROPERTY_KEY, oldXmlInputFactory); } } - if (Objects.nonNull(input)) { - try { - input.close(); - } catch (IOException e) { - logger.warn("Error closing input file: "+e.getMessage()); - } - } - if (Objects.nonNull(output)) { - try { - output.close(); - } catch (IOException e) { - logger.warn("Error closing output file: "+e.getMessage()); - } - } } } diff --git a/src/main/java/org/spdx/tools/SpdxToolsHelper.java b/src/main/java/org/spdx/tools/SpdxToolsHelper.java index 59ab84e..d391bdb 100644 --- a/src/main/java/org/spdx/tools/SpdxToolsHelper.java +++ b/src/main/java/org/spdx/tools/SpdxToolsHelper.java @@ -23,6 +23,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -172,7 +173,7 @@ public static SerFileType fileToFileType(File file) SerFileType retval = EXT_TO_FILETYPE.get(ext); if (SerFileType.JSON.equals(retval)) { // we need to check for a JSON-LD file type - try (Scanner scanner = new Scanner(file)) { + try (Scanner scanner = new Scanner(file, StandardCharsets.UTF_8)) { scanner.useDelimiter("\""); boolean foundContext = false; boolean foundRdfUri = false; @@ -189,7 +190,7 @@ public static SerFileType fileToFileType(File file) break; } } - } catch (FileNotFoundException e) { + } catch (IOException e) { // We'll assume it is just a JSON file } } diff --git a/src/main/java/org/spdx/tools/SpdxViewer.java b/src/main/java/org/spdx/tools/SpdxViewer.java index 5316c4e..bd33ba3 100644 --- a/src/main/java/org/spdx/tools/SpdxViewer.java +++ b/src/main/java/org/spdx/tools/SpdxViewer.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.PrintWriter; +import java.io.Writer; import java.util.List; import java.util.Objects; import java.util.Properties; @@ -115,7 +116,7 @@ static int run(String[] args) { .print("Error creating SPDX Document: " + ex.getMessage()); return ExitCode.ERROR; } - writer = new PrintWriter(System.out); + writer = new PrintWriter(new ConsoleWriter()); List verify = doc.verify(); if (verify.size() > 0) { System.out.println("This SPDX Document is not valid due to:"); @@ -137,8 +138,8 @@ static int run(String[] args) { + e.getMessage()); return ExitCode.ERROR; } finally { - if (Objects.nonNull(writer)) { - writer.close(); + if (writer != null) { + writer.flush(); } if (Objects.nonNull(store)) { try { @@ -150,4 +151,25 @@ static int run(String[] args) { } return ExitCode.SUCCESS; } + + /** + * Forwards text to {@code System.out}, so it is encoded by the console stream in the + * same charset as the tool's other output. Closing it does not close {@code System.out}. + */ + private static class ConsoleWriter extends Writer { + @Override + public void write(char[] cbuf, int off, int len) { + System.out.print(new String(cbuf, off, len)); + } + + @Override + public void flush() { + System.out.flush(); + } + + @Override + public void close() { + flush(); + } + } } diff --git a/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java b/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java index 012157c..d60fbd2 100644 --- a/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java +++ b/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java @@ -6,6 +6,7 @@ package org.spdx.tools; import java.io.File; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -67,4 +68,53 @@ public void testUsageError() { result = MatchingStandardLicenses.run(null); assertEquals(ExitCode.USAGE_ERROR, result); } + + private static File writeTemp(byte[] content) throws Exception { + File file = File.createTempFile("readall", ".txt"); + file.deleteOnExit(); + Files.write(file.toPath(), content); + return file; + } + + private static byte[] concat(byte[] first, byte[] second) { + byte[] result = new byte[first.length + second.length]; + System.arraycopy(first, 0, result, 0, first.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } + + static final String NON_ASCII_TEXT = "© Copyright 2007 über “quoted”"; + + public void testReadAllUtf8() throws Exception { + File file = writeTemp(NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_8)); + assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(file)); + } + + public void testReadAllUtf8Bom() throws Exception { + byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + File file = writeTemp(concat(bom, NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_8))); + assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(file)); + } + + public void testReadAllUtf16Bom() throws Exception { + byte[] leBom = {(byte) 0xFF, (byte) 0xFE}; + File le = writeTemp(concat(leBom, NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_16LE))); + assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(le)); + byte[] beBom = {(byte) 0xFE, (byte) 0xFF}; + File be = writeTemp(concat(beBom, NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_16BE))); + assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(be)); + } + + public void testReadAllLegacyEncodingFallsBackToDefaultCharset() throws Exception { + // 0xA9 alone is not valid UTF-8 + byte[] legacy = {'(', 'c', ')', ' ', (byte) 0xA9, ' ', 'x'}; + File file = writeTemp(legacy); + assertEquals(new String(legacy, Charset.defaultCharset()), MatchingStandardLicenses.readAll(file)); + } + + public void testReadAllEmptyAndBomOnly() throws Exception { + assertEquals("", MatchingStandardLicenses.readAll(writeTemp(new byte[0]))); + assertEquals("", MatchingStandardLicenses.readAll( + writeTemp(new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}))); + } } diff --git a/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java b/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java new file mode 100644 index 0000000..b3c3dce --- /dev/null +++ b/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java @@ -0,0 +1,91 @@ +/** + * SPDX-FileCopyrightText: 2026 SPDX Contributors + * SPDX-FileType: SOURCE + * SPDX-License-Identifier: Apache-2.0 + */ +package org.spdx.tools; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import junit.framework.TestCase; + +/** + * Test cases for the RdfSchemaTo* command line tools + */ +public class RdfSchemaToolsTest extends TestCase { + + static final String ONTOLOGY = "testResources" + File.separator + "spdx-2-2-revision-8-ontology.owl.xml"; + + /** output file name -> tool main method, invoked as main(inputFile, outputFile) */ + static final Map> TOOLS = new LinkedHashMap<>(); + static { + TOOLS.put("schema.xsd", RdfSchemaToXsd::main); + TOOLS.put("schema.json", RdfSchemaToJsonSchema::main); + TOOLS.put("context.json", RdfSchemaToJsonContext::main); + } + + Path tempDirPath; + + protected void setUp() throws Exception { + super.setUp(); + tempDirPath = Files.createTempDirectory("spdx-rdf-schema-test-"); + } + + protected void tearDown() throws Exception { + super.tearDown(); + SpdxConverterTestV3.deleteDirAndFiles(tempDirPath); + } + + public void testGeneratesOutputAndReleasesFile() throws Exception { + for (Map.Entry> tool : TOOLS.entrySet()) { + Path output = tempDirPath.resolve(tool.getKey()); + tool.getValue().accept(new String[] {ONTOLOGY, output.toString()}); + assertTrue(tool.getKey() + " output missing", Files.exists(output)); + String text = new String(Files.readAllBytes(output), StandardCharsets.UTF_8); + assertFalse(tool.getKey() + " output empty", text.isEmpty()); + if (tool.getKey().endsWith(".xsd")) { + assertTrue(text.contains("schema")); + } else { + JsonNode root = new ObjectMapper().readTree(text); + assertTrue(root.isObject() && root.size() > 0); + } + // a leaked file handle would prevent deletion on Windows + assertTrue(Files.deleteIfExists(output)); + } + } + + public void testMissingInputCreatesNoOutput() { + for (Map.Entry> tool : TOOLS.entrySet()) { + Path output = tempDirPath.resolve(tool.getKey()); + Path missing = tempDirPath.resolve("doesNotExist.owl.xml"); + tool.getValue().accept(new String[] {missing.toString(), output.toString()}); + assertFalse(Files.exists(output)); + } + } + + public void testUnwritableOutputDoesNotThrow() { + for (Map.Entry> tool : TOOLS.entrySet()) { + Path unwritable = tempDirPath.resolve("no-such-dir").resolve(tool.getKey()); + tool.getValue().accept(new String[] {ONTOLOGY, unwritable.toString()}); + assertFalse(Files.exists(unwritable)); + } + } + + public void testExistingOutputIsNotOverwritten() throws Exception { + for (Map.Entry> tool : TOOLS.entrySet()) { + Path output = tempDirPath.resolve(tool.getKey()); + Files.write(output, "keep".getBytes(StandardCharsets.UTF_8)); + tool.getValue().accept(new String[] {ONTOLOGY, output.toString()}); + assertEquals("keep", new String(Files.readAllBytes(output), StandardCharsets.UTF_8)); + } + } +} diff --git a/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java b/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java new file mode 100644 index 0000000..7bffd74 --- /dev/null +++ b/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java @@ -0,0 +1,69 @@ +/** + * SPDX-FileCopyrightText: 2026 SPDX Contributors + * SPDX-FileType: SOURCE + * SPDX-License-Identifier: Apache-2.0 + */ +package org.spdx.tools; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.spdx.tools.SpdxToolsHelper.SerFileType; + +import junit.framework.TestCase; + +/** + * Test cases for {@link SpdxToolsHelper#fileToFileType(File)} + */ +public class SpdxToolsHelperTest extends TestCase { + + static final byte[] UTF8_BOM = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + static final String JSONLD_TEXT = "{\"@context\": \"https://spdx.org/rdf/3.0.1/spdx-context.jsonld\"," + + " \"name\": \"über © “quoted”\"}"; + static final String JSON_TEXT = "{\"name\": \"über © “quoted”\"}"; + + Path tempDirPath; + + protected void setUp() throws Exception { + super.setUp(); + tempDirPath = Files.createTempDirectory("spdx-tools-helper-test-"); + } + + protected void tearDown() throws Exception { + super.tearDown(); + SpdxConverterTestV3.deleteDirAndFiles(tempDirPath); + } + + private File write(String name, boolean bom, String text) throws IOException { + Path path = tempDirPath.resolve(name); + byte[] body = text.getBytes(StandardCharsets.UTF_8); + byte[] content = new byte[(bom ? UTF8_BOM.length : 0) + body.length]; + if (bom) { + System.arraycopy(UTF8_BOM, 0, content, 0, UTF8_BOM.length); + } + System.arraycopy(body, 0, content, bom ? UTF8_BOM.length : 0, body.length); + Files.write(path, content); + return path.toFile(); + } + + public void testJsonLdDetectedWithNonAscii() throws Exception { + assertEquals(SerFileType.JSONLD, SpdxToolsHelper.fileToFileType(write("a.json", false, JSONLD_TEXT))); + } + + public void testJsonLdDetectedWithUtf8Bom() throws Exception { + assertEquals(SerFileType.JSONLD, SpdxToolsHelper.fileToFileType(write("b.json", true, JSONLD_TEXT))); + } + + public void testPlainJsonWithNonAscii() throws Exception { + assertEquals(SerFileType.JSON, SpdxToolsHelper.fileToFileType(write("c.json", false, JSON_TEXT))); + assertEquals(SerFileType.JSON, SpdxToolsHelper.fileToFileType(write("d.json", true, JSON_TEXT))); + } + + public void testUnreadableJsonFallsBackToJson() throws Exception { + File missing = tempDirPath.resolve("missing.json").toFile(); + assertEquals(SerFileType.JSON, SpdxToolsHelper.fileToFileType(missing)); + } +} diff --git a/src/test/java/org/spdx/tools/SpdxViewerTest.java b/src/test/java/org/spdx/tools/SpdxViewerTest.java new file mode 100644 index 0000000..9a643c4 --- /dev/null +++ b/src/test/java/org/spdx/tools/SpdxViewerTest.java @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2026 SPDX Contributors + * SPDX-FileType: SOURCE + * SPDX-License-Identifier: Apache-2.0 + */ +package org.spdx.tools; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import junit.framework.TestCase; + +/** + * Test cases for {@link SpdxViewer} + */ +public class SpdxViewerTest extends TestCase { + + static final String TEST_FILE = "testResources" + File.separator + "SPDXJSONExample-v2.3.spdx.json"; + + public void testViewerOutput() { + PrintStream original = System.out; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int result; + try { + System.setOut(new PrintStream(bytes, true, StandardCharsets.UTF_8)); + result = SpdxViewer.run(new String[] {TEST_FILE}); + System.out.println("MARKER-AFTER-RUN"); + } finally { + System.setOut(original); + } + assertEquals(ExitCode.SUCCESS, result); + String output = new String(bytes.toByteArray(), StandardCharsets.UTF_8); + assertTrue(output.contains("Document Name: SPDX-Tools-v2.0")); + // non-ASCII text in the document is written in the encoding of System.out + assertTrue(output.contains("© Copyright 2007 Hewlett-Packard")); + // the viewer must not close System.out + assertTrue(output.contains("MARKER-AFTER-RUN")); + } + + public void testFileNotFound() { + assertEquals(ExitCode.ERROR, + SpdxViewer.run(new String[] {"testResources" + File.separator + "doesNotExist.json"})); + } + + public void testUsageError() { + assertEquals(ExitCode.USAGE_ERROR, SpdxViewer.run(new String[] {})); + } +} From 841a939035a97e9c73c9bb90cdb11ee782a548ca Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 18 Sep 2026 22:18:13 +0100 Subject: [PATCH 2/2] Fix encoding Signed-off-by: Arthit Suriyawongkul --- .../java/org/spdx/tools/CompareSpdxDocs.java | 56 +-- .../spdx/tools/GenerateVerificationCode.java | 43 +-- .../spdx/tools/MatchingStandardLicenses.java | 46 ++- .../spdx/tools/RdfSchemaToJsonContext.java | 59 +-- .../org/spdx/tools/RdfSchemaToJsonSchema.java | 54 ++- .../java/org/spdx/tools/RdfSchemaToXsd.java | 61 +-- .../java/org/spdx/tools/SpdxConverter.java | 15 +- .../java/org/spdx/tools/SpdxToolsHelper.java | 13 +- src/main/java/org/spdx/tools/SpdxViewer.java | 35 +- src/main/java/org/spdx/tools/Verify.java | 5 +- .../compare/AbstractFileCompareSheet.java | 1 + .../org/spdx/tools/compare/AbstractSheet.java | 15 + .../compare/DocumentAnnotationSheet.java | 2 +- .../compare/DocumentRelationshipSheet.java | 35 +- .../compare/ExternalReferencesSheet.java | 2 +- .../tools/compare/ExtractedLicenseSheet.java | 4 +- .../spdx/tools/compare/FileChecksumSheet.java | 7 +- .../org/spdx/tools/compare/PackageSheet.java | 2 +- .../org/spdx/tools/compare/SnippetSheet.java | 32 +- .../spdx/tools/compare/VerificationSheet.java | 1 + .../tools/schema/AbstractOwlRdfConverter.java | 39 +- .../spdx/tools/schema/OwlToJsonSchema.java | 4 +- .../java/org/spdx/tools/schema/OwlToXsd.java | 3 +- .../spdx/tools/CompareSpdxDocsSheetsTest.java | 361 ++++++++++++++++++ .../org/spdx/tools/CompareSpdxDocsTest.java | 2 +- .../tools/GenerateVerificationCodeTest.java | 42 ++ .../tools/MatchingStandardLicensesTest.java | 92 ++++- .../org/spdx/tools/RdfSchemaToolsTest.java | 63 ++- .../org/spdx/tools/SpdxConverterTestV2.java | 56 +-- .../org/spdx/tools/SpdxConverterTestV3.java | 28 +- .../org/spdx/tools/SpdxToolsHelperTest.java | 26 +- .../java/org/spdx/tools/SpdxViewerTest.java | 50 ++- .../java/org/spdx/tools/TestFileUtils.java | 50 +++ src/test/java/org/spdx/tools/VerifyTest.java | 35 ++ .../spdx/tools/compare/CompareSheetsTest.java | 87 +++++ .../tools/schema/OwlToJsonContextTest.java | 15 +- .../tools/schema/OwlToJsonSchemaTest.java | 28 +- .../org/spdx/tools/schema/OwlToXSDTest.java | 39 +- 38 files changed, 1212 insertions(+), 296 deletions(-) create mode 100644 src/test/java/org/spdx/tools/CompareSpdxDocsSheetsTest.java create mode 100644 src/test/java/org/spdx/tools/TestFileUtils.java create mode 100644 src/test/java/org/spdx/tools/compare/CompareSheetsTest.java diff --git a/src/main/java/org/spdx/tools/CompareSpdxDocs.java b/src/main/java/org/spdx/tools/CompareSpdxDocs.java index 2e46f62..b1f1434 100644 --- a/src/main/java/org/spdx/tools/CompareSpdxDocs.java +++ b/src/main/java/org/spdx/tools/CompareSpdxDocs.java @@ -22,18 +22,16 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spdx.core.InvalidSPDXAnalysisException; -import org.spdx.library.ModelCopyManager; import org.spdx.library.model.v2.SpdxDocument; import org.spdx.spreadsheetstore.SpreadsheetException; -import org.spdx.storage.IModelStore; -import org.spdx.storage.simple.InMemSpdxStore; import org.spdx.tools.compare.MultiDocumentSpreadsheet; import org.spdx.utility.compare.SpdxCompareException; import org.spdx.utility.compare.SpdxComparer; @@ -84,7 +82,7 @@ static int run(String[] args) { return ExitCode.USAGE_ERROR; } if (args.length > MAX_ARGS) { - System.out.println("Too many SPDX documents specified. Must be less than "+String.valueOf(MAX_ARGS-1)+" document filenames"); + System.out.println("Too many SPDX documents specified. Must be at most "+String.valueOf(MAX_ARGS-1)+" document filenames"); usage(); return ExitCode.USAGE_ERROR; } @@ -117,18 +115,20 @@ public static void onlineFunction(String[] args) throws OnlineToolException { for (int i = 1; i < args.length; i++) { try { addDocToComparer(compareDocs, args[i], docNames, verificationErrors); - } catch (InvalidSPDXAnalysisException | IOException | InvalidFileNameException e) { + } catch (InvalidSPDXAnalysisException | IOException | InvalidFileNameException | RuntimeException e) { throw new OnlineToolException("Error opening SPDX document "+args[i]+": "+e.getMessage()); } } List normalizedDocNames = normalizeDocNames(docNames); MultiDocumentSpreadsheet outSheet = null; + boolean success = false; try { outSheet = new MultiDocumentSpreadsheet(outputFile, true, false); outSheet.importVerificationErrors(verificationErrors, normalizedDocNames); SpdxComparer comparer = new SpdxComparer(); comparer.compare(compareDocs); outSheet.importCompareResults(comparer, normalizedDocNames); + success = true; } catch (SpreadsheetException e) { throw new OnlineToolException("Unable to create output spreadsheet: "+e.getMessage()); } catch (InvalidSPDXAnalysisException e) { @@ -143,10 +143,16 @@ public static void onlineFunction(String[] args) throws OnlineToolException { logger.warn("Warning - error closing spreadsheet: "+e.getMessage()); } } + if (!success) { + // don't leave a partial workbook that blocks a retry + try { + Files.deleteIfExists(outputFile.toPath()); + } catch (IOException e) { + logger.warn("Warning - unable to delete incomplete output file: "+e.getMessage()); + } + } } } - - /** * Adds all SPDX documents found in the file or directory to the compareDocs list @@ -174,14 +180,8 @@ private static void addDocToComparer(List compareDocs, } } if (dupDocUri) { - // Make a unique URI by appending a UUID - String newUri = doc.getDocumentUri() + UUID.randomUUID(); - warnings.add("Duplicate Document URI: " + doc.getDocumentUri() + " changed to " + newUri); - IModelStore newStore = new InMemSpdxStore(); - ModelCopyManager copyManager = new ModelCopyManager(); - SpdxDocument newDoc = new SpdxDocument(newStore, newUri, copyManager, false); - newDoc.copyFrom(doc); - doc = newDoc; + warnings.add("Duplicate Document URI: " + doc.getDocumentUri() + + ". Document namespaces should be unique."); } compareDocs.add(doc); if (!warnings.isEmpty()) { @@ -190,10 +190,16 @@ private static void addDocToComparer(List compareDocs, verificationErrors.add(warnings); docNames.add(filePath); } else if (spdxDocOrDir.isDirectory()) { - for (File file:spdxDocOrDir.listFiles()) { + File[] files = spdxDocOrDir.listFiles(); + if (files == null) { + throw new IOException("Unable to list the files in directory "+filePath); + } + // listFiles order is filesystem dependent + Arrays.sort(files); + for (File file:files) { try { addDocToComparer(compareDocs, file.getPath(), docNames, verificationErrors); - } catch (InvalidSPDXAnalysisException | IOException | InvalidFileNameException e) { + } catch (InvalidSPDXAnalysisException | IOException | InvalidFileNameException | RuntimeException e) { System.out.println("Error deserializing "+file+". Skipping."); continue; } @@ -207,7 +213,7 @@ private static void addDocToComparer(List compareDocs, * @param uriFilePaths Un-normalized file paths or URIs * @return List of normalized doc names */ - private static List normalizeDocNames(List uriFilePaths) { + static List normalizeDocNames(List uriFilePaths) { List docNames = new ArrayList<>(); if (uriFilePaths.size() < 1) { return docNames; @@ -228,13 +234,17 @@ private static List normalizeDocNames(List uriFilePaths) { } } } - // Back up looking for the first path separator - for (int i = commonPrefixIndex; i >= 0; i--) { - if (uriFilePaths.get(0).charAt(i) == '/' || uriFilePaths.get(0).charAt(i) == '\\') { - commonPrefixIndex = i+1; + // Back up to just after the last path separator in the common prefix, + // so a name is never cut in the middle of a path segment + int lastSeparator = -1; + for (int i = commonPrefixIndex - 1; i >= 0; i--) { + char ch = uriFilePaths.get(0).charAt(i); + if (ch == '/' || ch == '\\') { + lastSeparator = i; break; } } + commonPrefixIndex = lastSeparator + 1; for (String uriFilePath:uriFilePaths) { docNames.add(uriFilePath.substring(commonPrefixIndex).replace("\\", "/")); } diff --git a/src/main/java/org/spdx/tools/GenerateVerificationCode.java b/src/main/java/org/spdx/tools/GenerateVerificationCode.java index 3cf8411..750af76 100644 --- a/src/main/java/org/spdx/tools/GenerateVerificationCode.java +++ b/src/main/java/org/spdx/tools/GenerateVerificationCode.java @@ -19,12 +19,14 @@ package org.spdx.tools; import java.io.File; +import java.nio.file.Path; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import javax.annotation.Nullable; @@ -81,8 +83,11 @@ static int run(String[] args) { SpdxPackageVerificationCode verificationCode = generateVerificationCode(directoryPath, skippedRegex); printVerificationCode(verificationCode); return ExitCode.SUCCESS; + } catch (PatternSyntaxException ex) { + error("Invalid regular expression for the skipped files: "+ex.getMessage()); + return ExitCode.USAGE_ERROR; } catch (Exception ex) { - error("Error creating verification code: "+ex.getMessage()); + System.out.println("Error creating verification code: "+ex.getMessage()); return ExitCode.ERROR; } } @@ -115,17 +120,16 @@ public static SpdxPackageVerificationCode generateVerificationCode(String direct /** * Collect files to be skipped - * @param skippedRegex Regular Expression for file paths to be skipped + * @param skippedRegex Regular Expression for file paths to be skipped. It is applied against the + * path relative to the directory, with '/' as the separator on every platform * @param dir Directory to scan for collecting skipped files * @return */ private static File[] collectSkippedFiles(String skippedRegex, File dir) { Pattern skippedPattern = Pattern.compile(skippedRegex); List skippedFiles = new ArrayList<>(); - collectSkippedFiles(skippedPattern, skippedFiles, dir.getPath(), dir); - File[] retval = new File[skippedFiles.size()]; - retval = skippedFiles.toArray(retval); - return retval; + collectSkippedFiles(skippedPattern, skippedFiles, dir.toPath(), dir); + return skippedFiles.toArray(new File[0]); } /** @@ -133,28 +137,21 @@ private static File[] collectSkippedFiles(String skippedRegex, File dir) { * @param skippedPattern * @param skippedFiles * @param rootPath - * @param dir - * @return + * @param fileOrDir */ private static void collectSkippedFiles(Pattern skippedPattern, - List skippedFiles, String rootPath, File dir) { - if (dir.isFile()) { - String relativePath = dir.getPath().substring(rootPath.length()+1); + List skippedFiles, Path rootPath, File fileOrDir) { + if (fileOrDir.isFile()) { + String relativePath = rootPath.relativize(fileOrDir.toPath()).toString() + .replace(File.separatorChar, '/'); if (skippedPattern.matcher(relativePath).matches()) { - skippedFiles.add(dir); + skippedFiles.add(fileOrDir); } - } else if (dir.isDirectory()) { - File[] children = dir.listFiles(); + } else if (fileOrDir.isDirectory()) { + File[] children = fileOrDir.listFiles(); if (children != null) { - for (int i = 0; i < children.length; i++) { - if (children[i].isFile()) { - String relativePath = children[i].getPath().substring(rootPath.length()+1); - if (skippedPattern.matcher(relativePath).matches()) { - skippedFiles.add(children[i]); - } - } else if (children[i].isDirectory()) { - collectSkippedFiles(skippedPattern, skippedFiles, rootPath, children[i]); - } + for (File child : children) { + collectSkippedFiles(skippedPattern, skippedFiles, rootPath, child); } } } diff --git a/src/main/java/org/spdx/tools/MatchingStandardLicenses.java b/src/main/java/org/spdx/tools/MatchingStandardLicenses.java index 0865d42..3fc32e6 100644 --- a/src/main/java/org/spdx/tools/MatchingStandardLicenses.java +++ b/src/main/java/org/spdx/tools/MatchingStandardLicenses.java @@ -28,6 +28,7 @@ import java.nio.file.Files; import java.util.List; +import org.apache.commons.io.ByteOrderMark; import org.spdx.core.InvalidSPDXAnalysisException; import org.spdx.utility.compare.LicenseCompareHelper; import org.spdx.utility.compare.SpdxCompareException; @@ -57,6 +58,10 @@ private MatchingStandardLicenses() { static int MIN_ARGS = 1; static int MAX_ARGS = 1; + /** UTF-32 first: the UTF-32LE BOM begins with the UTF-16LE BOM. */ + private static final ByteOrderMark[] BOMS = {ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE, + ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE}; + /** * Main entry point for the MatchingStandardLicenses tool. * Delegates to {@link #run(String[])} and terminates the JVM with its exit status. @@ -127,23 +132,32 @@ static int run(String[] args) { } /** - * Reads a text file. A UTF-8 or UTF-16 byte order mark selects the encoding and is - * removed. Otherwise the bytes are decoded as UTF-8, or with the platform default - * charset if they are not valid UTF-8 (e.g. a legacy Windows ANSI file). + * Reads a text file with the platform default charset as the fallback, see + * {@link #readAll(File, Charset)}. * @param textFile file to read * @return the file content, without any byte order mark * @throws IOException on read error */ static String readAll(File textFile) throws IOException { + return readAll(textFile, Charset.defaultCharset()); + } + + /** + * Reads a text file. A UTF-8, UTF-16 or UTF-32 byte order mark selects the encoding and + * is removed. Otherwise the bytes are decoded as UTF-8, or with {@code fallback} if they + * are not valid UTF-8 (e.g. a legacy Windows ANSI file). + * @param textFile file to read + * @param fallback charset for content that has no byte order mark and is not valid UTF-8 + * @return the file content, without any byte order mark + * @throws IOException on read error + */ + static String readAll(File textFile, Charset fallback) throws IOException { byte[] bytes = Files.readAllBytes(textFile.toPath()); - if (hasPrefix(bytes, 0xEF, 0xBB, 0xBF)) { - return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8); - } - if (hasPrefix(bytes, 0xFF, 0xFE)) { - return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16LE); - } - if (hasPrefix(bytes, 0xFE, 0xFF)) { - return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16BE); + for (ByteOrderMark bom : BOMS) { + if (startsWith(bytes, bom)) { + return new String(bytes, bom.length(), bytes.length - bom.length(), + Charset.forName(bom.getCharsetName())); + } } try { return StandardCharsets.UTF_8.newDecoder() @@ -151,16 +165,16 @@ static String readAll(File textFile) throws IOException { .onUnmappableCharacter(CodingErrorAction.REPORT) .decode(ByteBuffer.wrap(bytes)).toString(); } catch (CharacterCodingException e) { - return new String(bytes, Charset.defaultCharset()); + return new String(bytes, fallback); } } - private static boolean hasPrefix(byte[] bytes, int... prefix) { - if (bytes.length < prefix.length) { + private static boolean startsWith(byte[] bytes, ByteOrderMark bom) { + if (bytes.length < bom.length()) { return false; } - for (int i = 0; i < prefix.length; i++) { - if ((bytes[i] & 0xFF) != prefix[i]) { + for (int i = 0; i < bom.length(); i++) { + if (bytes[i] != (byte) bom.get(i)) { return false; } } diff --git a/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java b/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java index a9811c5..a677443 100644 --- a/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java +++ b/src/main/java/org/spdx/tools/RdfSchemaToJsonContext.java @@ -23,11 +23,9 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.Objects; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; +import org.spdx.tools.schema.AbstractOwlRdfConverter; import org.spdx.tools.schema.OwlToJsonContext; import com.fasterxml.jackson.core.JsonEncoding; @@ -42,64 +40,69 @@ public class RdfSchemaToJsonContext { /** + * Main entry point, terminates the JVM with the exit status of {@link #run(String[])} * @param args arg[0] RDF Schema file path; arg[1] output file path */ public static void main(String[] args) { - if (args.length != 2) { - System.err - .println("Invalid number of arguments"); + System.exit(run(args)); + } + + /** + * Runs the command logic and reports results to standard out/error, + * without terminating the JVM - allows the logic to be unit tested. + * @param args arg[0] RDF Schema file path; arg[1] output file path + * @return process exit status, see {@link ExitCode} + */ + static int run(String[] args) { + if (args == null || args.length != 2) { + System.err.println("Invalid number of arguments"); usage(); - return; + return ExitCode.USAGE_ERROR; } File fromFile = new File(args[0]); if (!fromFile.exists()) { - System.err - .println("Input file "+args[0]+" does not exist."); + System.err.println("Input file "+args[0]+" does not exist."); usage(); - return; + return ExitCode.ERROR; } File toFile = new File(args[1]); if (toFile.exists()) { System.err.println("Output file "+args[1]+" already exists."); usage(); - return; + return ExitCode.ERROR; } - OwlToJsonContext owlToJsonContext = null; + ObjectNode context; try (InputStream is = new FileInputStream(fromFile)) { - OntModel model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + OntModel model = AbstractOwlRdfConverter.createOntModel(); model.read(is, "RDF/XML"); - owlToJsonContext = new OwlToJsonContext(model); + context = new OwlToJsonContext(model).convertToContext(); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); - return; + return ExitCode.ERROR; } catch (IOException e) { System.err.println("Error closing input file stream: "+e.getMessage()); + return ExitCode.ERROR; + } catch (RuntimeException e) { + System.err.println("Unable to generate JSON context from file "+fromFile.getName()+": "+e.getMessage()); + return ExitCode.ERROR; } - if (Objects.isNull(owlToJsonContext)) { - System.err.println("Unable to load ontology from file "+fromFile.getName()); - return; - } - ObjectNode context = owlToJsonContext.convertToContext(); try (JsonGenerator jsonGenerator = OwlToJsonContext.JSON_MAPPER.getFactory().createGenerator(toFile, JsonEncoding.UTF8)) { - OwlToJsonContext.JSON_MAPPER.writeTree(jsonGenerator.useDefaultPrettyPrinter(), - context); - } catch (FileNotFoundException e) { - System.err.println("File not found for "+fromFile.getName()); - return; + OwlToJsonContext.JSON_MAPPER.writeTree(jsonGenerator.useDefaultPrettyPrinter(), context); } catch (JsonProcessingException e) { System.err.println("JSON error "+e.getMessage()); - return; + return ExitCode.ERROR; } catch (IOException e) { System.err.println("I/O error: "+e.getMessage()); - return; + return ExitCode.ERROR; } + return ExitCode.SUCCESS; } public static void usage() { System.out.println("Usage:"); System.out.println("RdfSchemaToJsonContext rdfSchemaFile jsonContextFile"); System.out.println("\trdfSchemaFile RDF schema file in RDF/XML format"); - System.out.println("\trdfSchemaFile output JSON context file"); + System.out.println("\tjsonContextFile output JSON context file"); } } diff --git a/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java b/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java index a67bc46..f362498 100644 --- a/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java +++ b/src/main/java/org/spdx/tools/RdfSchemaToJsonSchema.java @@ -24,9 +24,8 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; +import org.spdx.tools.schema.AbstractOwlRdfConverter; import org.spdx.tools.schema.OwlToJsonSchema; import com.fasterxml.jackson.core.JsonEncoding; @@ -43,53 +42,68 @@ public class RdfSchemaToJsonSchema { /** + * Main entry point, terminates the JVM with the exit status of {@link #run(String[])} * @param args arg[0] RDF Schema file path; arg[1] output file path */ public static void main(String[] args) { - if (args.length != 2) { - System.err - .println("Invalid number of arguments"); + System.exit(run(args)); + } + + /** + * Runs the command logic and reports results to standard out/error, + * without terminating the JVM - allows the logic to be unit tested. + * @param args arg[0] RDF Schema file path; arg[1] output file path + * @return process exit status, see {@link ExitCode} + */ + static int run(String[] args) { + if (args == null || args.length != 2) { + System.err.println("Invalid number of arguments"); usage(); - return; + return ExitCode.USAGE_ERROR; } File fromFile = new File(args[0]); if (!fromFile.exists()) { - System.err - .println("Input file "+args[0]+" does not exist."); + System.err.println("Input file "+args[0]+" does not exist."); usage(); - return; + return ExitCode.ERROR; } File toFile = new File(args[1]); if (toFile.exists()) { System.err.println("Output file "+args[1]+" already exists."); usage(); - return; + return ExitCode.ERROR; } OntModel model = null; try (InputStream is = new FileInputStream(fromFile)) { - model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + model = AbstractOwlRdfConverter.createOntModel(); model.read(is, "RDF/XML"); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); - return; + return ExitCode.ERROR; } catch (IOException e) { System.err.println("Error closing input file stream: "+e.getMessage()); + } catch (RuntimeException e) { + System.err.println("Unable to read ontology from file "+fromFile.getName()+": "+e.getMessage()); + return ExitCode.ERROR; + } + ObjectNode root; + try { + root = new OwlToJsonSchema(model).convertToJsonSchema(); + } catch (RuntimeException e) { + System.err.println("Error generating JSON schema: "+e.getMessage()); + return ExitCode.ERROR; } - OwlToJsonSchema owlToJson = new OwlToJsonSchema(model); - ObjectNode root = owlToJson.convertToJsonSchema(); ObjectMapper jsonMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); - try (JsonGenerator jsonGenerator = jsonMapper.getFactory().createGenerator(toFile, JsonEncoding.UTF8)) { - jsonMapper.writeTree(jsonGenerator.useDefaultPrettyPrinter(), - root); + jsonMapper.writeTree(jsonGenerator.useDefaultPrettyPrinter(), root); } catch (JsonProcessingException e) { System.err.println("JSON error "+e.getMessage()); - return; + return ExitCode.ERROR; } catch (IOException e) { System.err.println("I/O error: "+e.getMessage()); - return; + return ExitCode.ERROR; } - + return ExitCode.SUCCESS; } public static void usage() { diff --git a/src/main/java/org/spdx/tools/RdfSchemaToXsd.java b/src/main/java/org/spdx/tools/RdfSchemaToXsd.java index 0c58637..46208ac 100644 --- a/src/main/java/org/spdx/tools/RdfSchemaToXsd.java +++ b/src/main/java/org/spdx/tools/RdfSchemaToXsd.java @@ -26,11 +26,10 @@ import java.io.InputStream; import java.io.OutputStream; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; import org.apache.ws.commons.schema.XmlSchema; import org.apache.ws.commons.schema.XmlSchemaSerializer.XmlSchemaSerializerException; +import org.spdx.tools.schema.AbstractOwlRdfConverter; import org.spdx.tools.schema.OwlToXsd; import org.spdx.tools.schema.SchemaException; @@ -41,54 +40,64 @@ public class RdfSchemaToXsd { /** + * Main entry point, terminates the JVM with the exit status of {@link #run(String[])} * @param args arg[0] RDF Schema file path; arg[1] output file path */ public static void main(String[] args) { - if (args.length != 2) { - System.err - .println("Invalid number of arguments"); + System.exit(run(args)); + } + + /** + * Runs the command logic and reports results to standard out/error, + * without terminating the JVM - allows the logic to be unit tested. + * @param args arg[0] RDF Schema file path; arg[1] output file path + * @return process exit status, see {@link ExitCode} + */ + static int run(String[] args) { + if (args == null || args.length != 2) { + System.err.println("Invalid number of arguments"); usage(); - return; + return ExitCode.USAGE_ERROR; } File fromFile = new File(args[0]); if (!fromFile.exists()) { - System.err - .println("Input file "+args[0]+" does not exist."); + System.err.println("Input file "+args[0]+" does not exist."); usage(); - return; + return ExitCode.ERROR; } File toFile = new File(args[1]); if (toFile.exists()) { System.err.println("Output file "+args[1]+" already exists."); usage(); - return; + return ExitCode.ERROR; } OntModel model = null; try (InputStream is = new FileInputStream(fromFile)) { - model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + model = AbstractOwlRdfConverter.createOntModel(); model.read(is, "RDF/XML"); } catch (FileNotFoundException e) { System.err.println("File not found for "+fromFile.getName()); - return; + return ExitCode.ERROR; } catch (IOException e) { System.err.println("Error closing input file stream: "+e.getMessage()); + } catch (RuntimeException e) { + System.err.println("Unable to read ontology from file "+fromFile.getName()+": "+e.getMessage()); + return ExitCode.ERROR; } + XmlSchema xmlSchema; try { - OwlToXsd owlToXsd = new OwlToXsd(model); - XmlSchema xmlSchema = owlToXsd.convertToXsd(); - try (OutputStream os = new FileOutputStream(toFile)) { - xmlSchema.write(os); - } catch (IOException e) { - System.err.println("I/O error: "+e.getMessage()); - return; - } - } catch (XmlSchemaSerializerException e1) { - System.err.println("Error generating XSD schema: "+e1.getMessage()); - } catch (SchemaException e1) { - System.err.println("Error generating XSD schema: "+e1.getMessage()); + xmlSchema = new OwlToXsd(model).convertToXsd(); + } catch (XmlSchemaSerializerException | SchemaException | RuntimeException e) { + System.err.println("Error generating XSD schema: "+e.getMessage()); + return ExitCode.ERROR; } - - + try (OutputStream os = new FileOutputStream(toFile)) { + xmlSchema.write(os); + } catch (IOException e) { + System.err.println("I/O error: "+e.getMessage()); + return ExitCode.ERROR; + } + return ExitCode.SUCCESS; } public static void usage() { diff --git a/src/main/java/org/spdx/tools/SpdxConverter.java b/src/main/java/org/spdx/tools/SpdxConverter.java index 86aa638..261d5ae 100644 --- a/src/main/java/org/spdx/tools/SpdxConverter.java +++ b/src/main/java/org/spdx/tools/SpdxConverter.java @@ -22,6 +22,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.Objects; import org.spdx.core.InvalidSPDXAnalysisException; @@ -83,10 +84,10 @@ static int run(String[] args) { return ExitCode.USAGE_ERROR; } if (args.length > MAX_ARGS) { - System.out.printf("Warning: Extra arguments will be ignored"); + System.out.println("Warning: Extra arguments will be ignored"); } if (args.length == 3) { - System.out.printf("Warning: only the input file type specified - it will be ignored"); + System.out.println("Warning: only the input file type specified - it will be ignored"); } boolean excludeLicenseDetails = false; if (args.length == 5 && "excludelicensedetails".equals(args[4].toLowerCase())) { @@ -185,6 +186,7 @@ public static void convert(String fromFilePath, String toFilePath, SerFileType f } String oldXmlInputFactory = null; boolean propertySet = false; + boolean outputCreated = false; try { ISerializableModelStore fromStore = SpdxToolsHelper.fileTypeToStore(fromFileType); ISerializableModelStore toStore = SpdxToolsHelper.fileTypeToStore(toFileType); @@ -211,6 +213,7 @@ public static void convert(String fromFilePath, String toFilePath, SerFileType f } try (FileInputStream input = new FileInputStream(fromFile); FileOutputStream output = new FileOutputStream(toFile)) { + outputCreated = true; fromStore.deSerialize(input, false); if (fromVersion == SpdxMajorVersion.VERSION_3) { copyV3ToV3(fromStore, toStore, excludeLicenseDetails); @@ -226,6 +229,14 @@ public static void convert(String fromFilePath, String toFilePath, SerFileType f if (Objects.nonNull(ex.getMessage())) { msg = msg + " " + ex.getMessage(); } + if (outputCreated) { + // the streams are already closed; don't leave a partial file that blocks a retry + try { + Files.deleteIfExists(toFile.toPath()); + } catch (IOException | RuntimeException deleteFailure) { + ex.addSuppressed(deleteFailure); + } + } throw new SpdxConverterException(msg, ex); } finally { if (propertySet) { diff --git a/src/main/java/org/spdx/tools/SpdxToolsHelper.java b/src/main/java/org/spdx/tools/SpdxToolsHelper.java index d391bdb..471849d 100644 --- a/src/main/java/org/spdx/tools/SpdxToolsHelper.java +++ b/src/main/java/org/spdx/tools/SpdxToolsHelper.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Scanner; @@ -150,23 +151,23 @@ public static SerFileType fileToFileType(File file) throw new InvalidFileNameException( "Can not convert file to file type - no file extension for file "+file.getPath()); } - String ext = fileName.substring(fileName.lastIndexOf(".") + 1) - .toLowerCase(); + String lowerCaseName = fileName.toLowerCase(Locale.ROOT); + String ext = lowerCaseName.substring(lowerCaseName.lastIndexOf(".") + 1); if ("xml".equals(ext)) { - if (fileName.endsWith("rdf.xml")) { + if (lowerCaseName.endsWith(".rdf.xml")) { ext = "rdf.xml"; } } if ("ttl".equals(ext)) { - if (fileName.endsWith("rdf.ttl")) { + if (lowerCaseName.endsWith(".rdf.ttl")) { ext = "rdf.ttl"; } } if ("json".equals(ext)) { - if (fileName.endsWith("jsonld.json")) { + if (lowerCaseName.endsWith(".jsonld.json")) { ext = "jsonld.json"; } - if (fileName.endsWith("spdx3.json")) { + if (lowerCaseName.endsWith(".spdx3.json")) { ext = "spdx3.json"; } } diff --git a/src/main/java/org/spdx/tools/SpdxViewer.java b/src/main/java/org/spdx/tools/SpdxViewer.java index bd33ba3..830bf71 100644 --- a/src/main/java/org/spdx/tools/SpdxViewer.java +++ b/src/main/java/org/spdx/tools/SpdxViewer.java @@ -83,7 +83,6 @@ static int run(String[] args) { SpdxToolsHelper.initialize(); SpdxDocument doc = null; ISerializableModelStore store = null; - PrintWriter writer = null; try { File file = new File(args[0]); if (!file.exists()) { @@ -116,7 +115,6 @@ static int run(String[] args) { .print("Error creating SPDX Document: " + ex.getMessage()); return ExitCode.ERROR; } - writer = new PrintWriter(new ConsoleWriter()); List verify = doc.verify(); if (verify.size() > 0) { System.out.println("This SPDX Document is not valid due to:"); @@ -128,7 +126,12 @@ static int run(String[] args) { Properties constants = CommonCode .getTextFromProperties("org/spdx/tag/SpdxViewerConstants.properties"); // print document to system output using human readable format - CommonCode.printDoc(doc, writer, constants); + PrintWriter writer = new PrintWriter(new ConsoleWriter()); + try { + CommonCode.printDoc(doc, writer, constants); + } finally { + writer.flush(); + } } catch (InvalidSPDXAnalysisException e) { System.out.print("Error pretty printing SPDX Document: " + e.getMessage()); @@ -138,9 +141,6 @@ static int run(String[] args) { + e.getMessage()); return ExitCode.ERROR; } finally { - if (writer != null) { - writer.flush(); - } if (Objects.nonNull(store)) { try { store.close(); @@ -153,20 +153,35 @@ static int run(String[] args) { } /** - * Forwards text to {@code System.out}, so it is encoded by the console stream in the - * same charset as the tool's other output. Closing it does not close {@code System.out}. + * Buffers text and forwards it to {@code System.out}, so it is encoded by the console + * stream in the same charset as the tool's other output. Closing it does not close + * {@code System.out}. */ - private static class ConsoleWriter extends Writer { + static class ConsoleWriter extends Writer { + static final int FLUSH_THRESHOLD = 8192; + private final StringBuilder buffer = new StringBuilder(); + @Override public void write(char[] cbuf, int off, int len) { - System.out.print(new String(cbuf, off, len)); + buffer.append(cbuf, off, len); + if (buffer.length() >= FLUSH_THRESHOLD) { + drain(); + } } @Override public void flush() { + drain(); System.out.flush(); } + private void drain() { + if (buffer.length() > 0) { + System.out.print(buffer.toString()); + buffer.setLength(0); + } + } + @Override public void close() { flush(); diff --git a/src/main/java/org/spdx/tools/Verify.java b/src/main/java/org/spdx/tools/Verify.java index 64056b7..d05fce5 100644 --- a/src/main/java/org/spdx/tools/Verify.java +++ b/src/main/java/org/spdx/tools/Verify.java @@ -179,6 +179,9 @@ public static List verify(String filePath, SerFileType fileType) throws throw new SpdxVerificationException("IO Error reading SPDX file",e); } catch (InvalidSPDXAnalysisException e) { throw new SpdxVerificationException("Analysis exception processing SPDX file: "+e.getMessage(),e); + } catch (RuntimeException e) { + // e.g. a parser error in an RDF file + throw new SpdxVerificationException("Error reading SPDX file: "+e.getMessage(),e); } List retval = new ArrayList(); if (store instanceof TagValueStore) { @@ -189,7 +192,7 @@ public static List verify(String filePath, SerFileType fileType) throws try { String jsonSchemaResource; if (SerFileType.JSON.equals(fileType)) { - jsonSchemaResource = Version.versionLessThan(Version.TWO_POINT_THREE_VERSION, doc.getSpecVersion()) ? + jsonSchemaResource = Version.versionLessThan(doc.getSpecVersion(), Version.TWO_POINT_THREE_VERSION) ? JSON_SCHEMA_RESOURCE_V2_2 : JSON_SCHEMA_RESOURCE_V2_3; } else { jsonSchemaResource = JSON_SCHEMA_RESOURCE_V3; diff --git a/src/main/java/org/spdx/tools/compare/AbstractFileCompareSheet.java b/src/main/java/org/spdx/tools/compare/AbstractFileCompareSheet.java index 6d1c1dc..10a4268 100644 --- a/src/main/java/org/spdx/tools/compare/AbstractFileCompareSheet.java +++ b/src/main/java/org/spdx/tools/compare/AbstractFileCompareSheet.java @@ -144,6 +144,7 @@ public void importCompareResults(SpdxComparer comparer, List> fil allValuesMatch = false; } lastFile = files.get(i).get(fileIndexes[i]); + lastDocIndex = i; if (val.length() > MAX_VALUE_LENGTH) { val = val.substring(0, MAX_VALUE_LENGTH-9) + "[more...]"; } diff --git a/src/main/java/org/spdx/tools/compare/AbstractSheet.java b/src/main/java/org/spdx/tools/compare/AbstractSheet.java index 207d7b6..54825c4 100644 --- a/src/main/java/org/spdx/tools/compare/AbstractSheet.java +++ b/src/main/java/org/spdx/tools/compare/AbstractSheet.java @@ -23,6 +23,7 @@ import java.text.AttributedString; import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellType; @@ -50,6 +51,7 @@ public abstract class AbstractSheet { static final String CHECKBOX_FONT_NAME = "Wingdings 2"; static final String CHECKBOX = "P"; private static final short MAX_ROW_LINES = 10; + private static final String TRUNCATED_MARKER = "[more...]"; protected CellStyle checkboxStyle; protected CellStyle dateStyle; protected CellStyle greenWrapped; @@ -155,6 +157,19 @@ protected Row addRow() { return row; } + /** + * Truncates text that is longer than the maximum cell text length of the workbook format. + * @param text text for a cell + * @return text that fits in a cell, ending with a marker if it was shortened + */ + static String truncateCellText(String text) { + int max = SpreadsheetVersion.EXCEL2007.getMaxTextLength(); + if (text == null || text.length() <= max) { + return text; + } + return text.substring(0, max - TRUNCATED_MARKER.length()) + TRUNCATED_MARKER; + } + /** * Clears all data from the worksheet */ diff --git a/src/main/java/org/spdx/tools/compare/DocumentAnnotationSheet.java b/src/main/java/org/spdx/tools/compare/DocumentAnnotationSheet.java index a8ec948..88beb4a 100644 --- a/src/main/java/org/spdx/tools/compare/DocumentAnnotationSheet.java +++ b/src/main/java/org/spdx/tools/compare/DocumentAnnotationSheet.java @@ -131,7 +131,7 @@ public static void create(Workbook wb, String sheetName) { commentHeaderCell.setCellStyle(headerStyle); commentHeaderCell.setCellValue(COMMENT_COL_TEXT_TITLE); - for (int i = FIRST_DATE_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS; i++) { + for (int i = FIRST_DATE_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS + FIRST_DATE_COL; i++) { sheet.setColumnWidth(i, DATE_COL_WIDTH*256); sheet.setDefaultColumnStyle(i, defaultStyle); Cell cell = row.createCell(i); diff --git a/src/main/java/org/spdx/tools/compare/DocumentRelationshipSheet.java b/src/main/java/org/spdx/tools/compare/DocumentRelationshipSheet.java index b7f1dae..1dae52f 100644 --- a/src/main/java/org/spdx/tools/compare/DocumentRelationshipSheet.java +++ b/src/main/java/org/spdx/tools/compare/DocumentRelationshipSheet.java @@ -65,22 +65,11 @@ public int compare(Relationship o1, Relationship o2) { } Optional relatedElement1 = r1.getRelatedSpdxElement(); Optional relatedElement2 = r2.getRelatedSpdxElement(); - if (relatedElement1.isPresent() && !relatedElement2.isPresent()) { - return 1; - } else if (!relatedElement1.isPresent() && relatedElement2.isPresent()) { - return -1; - } else if (relatedElement1.get().equivalent(relatedElement2.get())) { - return 0; - } - Optional name1 = relatedElement1.get().getName(); - Optional name2 = relatedElement2.get().getName(); - if (name1.isPresent() && - name2.isPresent()) { - return name1.get().compareTo( - name2.get()); - } else { - return relatedElement1.get().getId().compareTo(relatedElement2.get().getId()); + if (!relatedElement1.isPresent() || !relatedElement2.isPresent()) { + return Boolean.compare(relatedElement1.isPresent(), relatedElement2.isPresent()); } + return compareElements(relatedElement1.get().getName(), relatedElement1.get().getId(), + relatedElement2.get().getName(), relatedElement2.get().getId()); } else { return 1; } @@ -95,6 +84,20 @@ public int compare(Relationship o1, Relationship o2) { } } + /** + * Total order for related elements: elements without a name first, then by name. + * Elements without a name are ordered by ID. + */ + static int compareElements(Optional name1, String id1, Optional name2, String id2) { + if (name1.isPresent() && name2.isPresent()) { + return name1.get().compareTo(name2.get()); + } + if (name1.isPresent() != name2.isPresent()) { + return Boolean.compare(name1.isPresent(), name2.isPresent()); + } + return id1.compareTo(id2); + } + RelationshipComparator relationshipComparator = new RelationshipComparator(); static final int TYPE_COL = 0; @@ -127,7 +130,7 @@ public static void create(Workbook wb, String sheetName) { typeHeaderCell.setCellStyle(headerStyle); typeHeaderCell.setCellValue(TYPE_COL_TEXT_TITLE); - for (int i = FIRST_RELATIONSHIP_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS; i++) { + for (int i = FIRST_RELATIONSHIP_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS + FIRST_RELATIONSHIP_COL; i++) { sheet.setColumnWidth(i, FIRST_RELATIONSHIP_COL_WIDTH*256); sheet.setDefaultColumnStyle(i, defaultStyle); Cell cell = row.createCell(i); diff --git a/src/main/java/org/spdx/tools/compare/ExternalReferencesSheet.java b/src/main/java/org/spdx/tools/compare/ExternalReferencesSheet.java index 4cac713..a6f5583 100644 --- a/src/main/java/org/spdx/tools/compare/ExternalReferencesSheet.java +++ b/src/main/java/org/spdx/tools/compare/ExternalReferencesSheet.java @@ -137,7 +137,7 @@ public static void create(Workbook wb, String sheetName) { checksumHeaderCell.setCellStyle(headerStyle); checksumHeaderCell.setCellValue(CHECKSUM_TEXT_TITLE); - for (int i = FIRST_DOC_ID_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS; i++) { + for (int i = FIRST_DOC_ID_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS + FIRST_DOC_ID_COL; i++) { sheet.setColumnWidth(i, DOC_ID_COL_WIDTH*256); sheet.setDefaultColumnStyle(i, defaultStyle); Cell cell = row.createCell(i); diff --git a/src/main/java/org/spdx/tools/compare/ExtractedLicenseSheet.java b/src/main/java/org/spdx/tools/compare/ExtractedLicenseSheet.java index 5971296..9f91e3a 100644 --- a/src/main/java/org/spdx/tools/compare/ExtractedLicenseSheet.java +++ b/src/main/java/org/spdx/tools/compare/ExtractedLicenseSheet.java @@ -129,7 +129,7 @@ public static void create(Workbook wb, String sheetName) { Cell extractedHeaderCell = row.createCell(EXTRACTED_TEXT_COL); extractedHeaderCell.setCellStyle(headerStyle); extractedHeaderCell.setCellValue(EXTRACTED_TEXT_TITLE); - for (int i = FIRST_LIC_ID_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS; i++) { + for (int i = FIRST_LIC_ID_COL; i < MultiDocumentSpreadsheet.MAX_DOCUMENTS + FIRST_LIC_ID_COL; i++) { sheet.setColumnWidth(i, LIC_ID_COL_WIDTH*256); sheet.setDefaultColumnStyle(i, defaultStyle); Cell cell = row.createCell(i); @@ -162,7 +162,7 @@ public void importCompareResults(SpdxComparer comparer, List docNames) t Row currentRow = this.addRow(); String extractedLicenseText = getNextExtractedLicenseText(extractedLicenses, licenseIndexes); Cell licenseTextCell = currentRow.createCell(EXTRACTED_TEXT_COL); - licenseTextCell.setCellValue(extractedLicenseText); + licenseTextCell.setCellValue(truncateCellText(extractedLicenseText)); for (int i = 0; i < extractedLicenses.length; i++) { if (extractedLicenses[i].length > licenseIndexes[i]) { if (extractedLicenses[i][licenseIndexes[i]] instanceof ExtractedLicenseInfo) { diff --git a/src/main/java/org/spdx/tools/compare/FileChecksumSheet.java b/src/main/java/org/spdx/tools/compare/FileChecksumSheet.java index d2d11bd..59ee537 100644 --- a/src/main/java/org/spdx/tools/compare/FileChecksumSheet.java +++ b/src/main/java/org/spdx/tools/compare/FileChecksumSheet.java @@ -33,7 +33,7 @@ */ public class FileChecksumSheet extends AbstractFileCompareSheet { - private static final int CHECKSUM_COL_WIDTH = 41; + private static final int CHECKSUM_COL_WIDTH = 80; /** * @param workbook @@ -52,7 +52,7 @@ static void create(Workbook wb, String sheetName) { */ @Override String getFileValue(SpdxFile spdxFile) throws InvalidSPDXAnalysisException { - return spdxFile.getSha1(); + return CompareHelper.checksumsToString(spdxFile.getChecksums()); } /* (non-Javadoc) @@ -61,6 +61,7 @@ String getFileValue(SpdxFile spdxFile) throws InvalidSPDXAnalysisException { @Override boolean valuesMatch(SpdxComparer comparer, SpdxFile fileA, int docIndexA, SpdxFile fileB, int docIndexB) throws SpdxCompareException, InvalidSPDXAnalysisException { - return Objects.equals(fileA.getSha1(), fileB.getSha1()); + return Objects.equals(CompareHelper.checksumsToString(fileA.getChecksums()), + CompareHelper.checksumsToString(fileB.getChecksums())); } } diff --git a/src/main/java/org/spdx/tools/compare/PackageSheet.java b/src/main/java/org/spdx/tools/compare/PackageSheet.java index f0c5c5e..48ee45b 100644 --- a/src/main/java/org/spdx/tools/compare/PackageSheet.java +++ b/src/main/java/org/spdx/tools/compare/PackageSheet.java @@ -310,7 +310,7 @@ private void addPackageToSheet(SpdxPackageComparer comparer, } Row attributionRow = this.addRow(); attributionRow.createCell(FIELD_COL).setCellValue(ATTRIBUTION_FIELD_TEXT); - if (comparer.isAnnotationsEquals()) { + if (comparer.isAttributionTextEquals()) { setCellEqualValue(attributionRow.createCell(EQUALS_COL), allDocsPresent); } else { setCellDifferentValue(attributionRow.createCell(EQUALS_COL)); diff --git a/src/main/java/org/spdx/tools/compare/SnippetSheet.java b/src/main/java/org/spdx/tools/compare/SnippetSheet.java index d7ea26d..93885d0 100644 --- a/src/main/java/org/spdx/tools/compare/SnippetSheet.java +++ b/src/main/java/org/spdx/tools/compare/SnippetSheet.java @@ -20,7 +20,9 @@ import java.util.Arrays; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.poi.ss.usermodel.Cell; @@ -137,18 +139,30 @@ public void importCompareResults(SpdxComparer comparer, List docNames) t headerCell.setCellValue(docNames.get(i)); } + List docs = comparer.getSpdxDocuments(); SpdxSnippetComparer[] snippetComparers = comparer.getSnippetComparers(); - Arrays.sort(snippetComparers, new Comparator() { + Map sortKeys = new IdentityHashMap<>(); + for (SpdxSnippetComparer snippetComparer : snippetComparers) { + sortKeys.put(snippetComparer, sortKey(snippetComparer, docs)); + } + Arrays.sort(snippetComparers, Comparator.comparing(sortKeys::get)); + for (int i = 0; i < snippetComparers.length; i++) { + addSnippetToSheet(snippetComparers[i], docs); + } + } - @Override - public int compare(SpdxSnippetComparer o1, SpdxSnippetComparer o2) { - return o1.toString().compareTo(o2.toString()); + /** + * @return name and ID of the snippet from the first document that has it + */ + private static String sortKey(SpdxSnippetComparer snippetComparer, List docs) + throws InvalidSPDXAnalysisException { + for (SpdxDocument doc : docs) { + SpdxSnippet snippet = snippetComparer.getDocSnippet(doc); + if (snippet != null) { + return snippet.getName().orElse("") + "\n" + snippet.getId(); } - - }); - for (int i = 0; i < snippetComparers.length; i++) { - addSnippetToSheet(snippetComparers[i], comparer.getSpdxDocuments()); } + return ""; } /** @@ -249,7 +263,7 @@ private void addSnippetToSheet(SpdxSnippetComparer comparer, } else { licenseCommentRow.createCell(FIRST_DOC_COL+i).setCellValue(""); } - copyrightRow.createCell(FIRST_DOC_COL+i).setCellValue(snippet.getCopyrightText()); + copyrightRow.createCell(FIRST_DOC_COL+i).setCellValue(truncateCellText(snippet.getCopyrightText())); SpdxFile snippetFromFile = snippet.getSnippetFromFile(); if (snippetFromFile != null) { snippetFromFileRow.createCell(FIRST_DOC_COL+i).setCellValue(snippetFromFile.toString()); diff --git a/src/main/java/org/spdx/tools/compare/VerificationSheet.java b/src/main/java/org/spdx/tools/compare/VerificationSheet.java index 945d487..338b9a8 100644 --- a/src/main/java/org/spdx/tools/compare/VerificationSheet.java +++ b/src/main/java/org/spdx/tools/compare/VerificationSheet.java @@ -116,6 +116,7 @@ public void importVerificationErrors( } } } + lastRowNum = Math.max(lastRowNum, lastRowCreated); } } diff --git a/src/main/java/org/spdx/tools/schema/AbstractOwlRdfConverter.java b/src/main/java/org/spdx/tools/schema/AbstractOwlRdfConverter.java index 778b815..c584e1f 100644 --- a/src/main/java/org/spdx/tools/schema/AbstractOwlRdfConverter.java +++ b/src/main/java/org/spdx/tools/schema/AbstractOwlRdfConverter.java @@ -264,18 +264,37 @@ private List getRestrictionsFromSuperclasses( if (property.equals(r.getProperty())) { retval.add(r); } + } else { + // restrictions are inherited from the named superclasses + retval.addAll(getRestrictionsFromSuperclasses(superClass, property)); } }); return retval; } + /** + * Properties listed by the model are not typed as object or data properties, + * only those are able to list the restrictions that refer to them + * @return the object or data property with the same URI, or null if there is none + */ + private org.apache.jena.ontapi.model.OntRelationalProperty toRelationalProperty(OntProperty property) { + if (property instanceof org.apache.jena.ontapi.model.OntRelationalProperty) { + return (org.apache.jena.ontapi.model.OntRelationalProperty) property; + } + if (!property.isURIResource()) { + return null; + } + org.apache.jena.ontapi.model.OntRelationalProperty result = model.getObjectProperty(property.getURI()); + return Objects.nonNull(result) ? result : model.getDataProperty(property.getURI()); + } + public PropertyRestrictions(OntProperty property) { Objects.requireNonNull(property, "Missing required property"); this.property = property; List propertyRestrictions = new ArrayList<>(); - if (property instanceof org.apache.jena.ontapi.model.OntRelationalProperty) { - ((org.apache.jena.ontapi.model.OntRelationalProperty) property) - .referringRestrictions().forEach(propertyRestrictions::add); + org.apache.jena.ontapi.model.OntRelationalProperty relationalProperty = toRelationalProperty(property); + if (Objects.nonNull(relationalProperty)) { + relationalProperty.referringRestrictions().forEach(propertyRestrictions::add); } interpretRestrictions(propertyRestrictions); } @@ -367,6 +386,14 @@ public boolean isSingleProperty() { OntID ontology; + /** + * @return an empty model to read the SPDX ontology into. The ontology has restrictions + * on annotation properties (e.g. rdfs:comment), which only the OWL Full profile accepts. + */ + public static OntModel createOntModel() { + return org.apache.jena.ontapi.OntModelFactory.createModel(org.apache.jena.ontapi.OntSpecification.OWL2_FULL_MEM); + } + public AbstractOwlRdfConverter(OntModel model) { Objects.requireNonNull(model, "Model must not be null"); this.model = model; @@ -383,10 +410,12 @@ public AbstractOwlRdfConverter(OntModel model) { intersectionOfProperty = model.createProperty("http://www.w3.org/2002/07/owl#intersectionOf"); firstResource = model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"); restResource = model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"); - ontology = model.getID(); - if (Objects.isNull(ontology)) { + // getID() creates an ontology header when there is none, so check for it first + if (!model.contains(null, model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + model.createResource("http://www.w3.org/2002/07/owl#Ontology"))) { throw new RuntimeException("No ontologies defined in RDF OWL"); } + ontology = model.getID(); } public PropertyRestrictions getPropertyRestrictions(OntClass ontClass, OntProperty property) { diff --git a/src/main/java/org/spdx/tools/schema/OwlToJsonSchema.java b/src/main/java/org/spdx/tools/schema/OwlToJsonSchema.java index ac157e6..c46de70 100644 --- a/src/main/java/org/spdx/tools/schema/OwlToJsonSchema.java +++ b/src/main/java/org/spdx/tools/schema/OwlToJsonSchema.java @@ -393,14 +393,14 @@ private ObjectNode derivePropertySchema(OntProperty property, PropertyRestrictio } } else { OntClass typeClass = model.getOntClass(restrictions.getTypeUri()); + Objects.requireNonNull(typeClass, "No type class found for "+restrictions.getTypeUri()); + propertySchema = ontClassToJsonSchema(typeClass); commentStatement = typeClass.getProperty(commentProperty); if (Objects.nonNull(commentStatement) && Objects.nonNull(commentStatement.getObject()) && commentStatement.getObject().isLiteral()) { // replace the property comment with the class comment propertySchema.put("description", commentStatement.getObject().asLiteral().getString()); } - Objects.requireNonNull(typeClass, "No type class found for "+restrictions.getTypeUri()); - propertySchema = ontClassToJsonSchema(typeClass); if (SINGLE_POINTER_URI.equals(restrictions.getTypeUri())) { // Need to add in the line and offset properties // These are not in the OWL schema since the generic OffsetPointer in the range diff --git a/src/main/java/org/spdx/tools/schema/OwlToXsd.java b/src/main/java/org/spdx/tools/schema/OwlToXsd.java index 0063eb3..3175b3f 100644 --- a/src/main/java/org/spdx/tools/schema/OwlToXsd.java +++ b/src/main/java/org/spdx/tools/schema/OwlToXsd.java @@ -118,7 +118,8 @@ private XmlSchemaType addComplexTypeToSchema(XmlSchema schema, OntClass type) th XmlSchemaComplexContentExtension schemaExtension = null; java.util.List superClasses = type.superClasses(true) .filter(sc -> sc.isURIResource() - && !"http://www.w3.org/2000/01/rdf-schema#Container".equals(sc.getURI())) + && !"http://www.w3.org/2000/01/rdf-schema#Container".equals(sc.getURI()) + && !"http://www.w3.org/2002/07/owl#Thing".equals(sc.getURI())) .collect(java.util.stream.Collectors.toList()); if (!superClasses.isEmpty()) { diff --git a/src/test/java/org/spdx/tools/CompareSpdxDocsSheetsTest.java b/src/test/java/org/spdx/tools/CompareSpdxDocsSheetsTest.java new file mode 100644 index 0000000..2e6b756 --- /dev/null +++ b/src/test/java/org/spdx/tools/CompareSpdxDocsSheetsTest.java @@ -0,0 +1,361 @@ +/** + * SPDX-FileCopyrightText: 2026 SPDX Contributors + * SPDX-FileType: SOURCE + * SPDX-License-Identifier: Apache-2.0 + */ +package org.spdx.tools; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import junit.framework.TestCase; + +/** + * Test cases for the comparison spreadsheet written by {@link CompareSpdxDocs}, + * using small SPDX 2.3 JSON documents generated in a temporary directory + */ +public class CompareSpdxDocsSheetsTest extends TestCase { + + static final ObjectMapper MAPPER = new ObjectMapper(); + static final String CHECKSUM_SHA1 = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"; + + Path tempDirPath; + + protected void setUp() throws Exception { + super.setUp(); + tempDirPath = Files.createTempDirectory("spdx-compare-sheets-test-"); + } + + protected void tearDown() throws Exception { + super.tearDown(); + TestFileUtils.deleteDirAndFiles(tempDirPath); + } + + /** + * @param index makes the document namespace unique + * @param licenseId license ID of the extracted license and of the concluded license of the file ./a.c + * @param extractedText extracted text of that license + */ + private ObjectNode createDoc(int index, String licenseId, String extractedText) { + ObjectNode doc = MAPPER.createObjectNode(); + doc.put("SPDXID", "SPDXRef-DOCUMENT"); + doc.put("spdxVersion", "SPDX-2.3"); + doc.put("name", "doc" + index); + doc.put("dataLicense", "CC0-1.0"); + doc.put("documentNamespace", "http://example.com/spdxdocs/doc" + index); + ObjectNode creationInfo = doc.putObject("creationInfo"); + creationInfo.put("created", "2020-01-01T00:00:00Z"); + creationInfo.putArray("creators").add("Tool: test"); + ObjectNode extracted = doc.putArray("hasExtractedLicensingInfos").addObject(); + extracted.put("licenseId", licenseId); + extracted.put("extractedText", extractedText); + ObjectNode file = doc.putArray("files").addObject(); + file.put("SPDXID", "SPDXRef-File"); + file.put("fileName", "./a.c"); + ObjectNode checksum = file.putArray("checksums").addObject(); + checksum.put("algorithm", "SHA1"); + checksum.put("checksumValue", CHECKSUM_SHA1); + file.put("licenseConcluded", licenseId); + file.putArray("licenseInfoInFiles").add(licenseId); + file.put("copyrightText", "NONE"); + addDescribes(doc, "SPDXRef-File"); + return doc; + } + + private void addDescribes(ObjectNode doc, String elementId) { + ArrayNode relationships = doc.has("relationships") ? (ArrayNode) doc.get("relationships") + : doc.putArray("relationships"); + ObjectNode relationship = relationships.addObject(); + relationship.put("spdxElementId", "SPDXRef-DOCUMENT"); + relationship.put("relationshipType", "DESCRIBES"); + relationship.put("relatedSpdxElement", elementId); + } + + private String write(String fileName, ObjectNode doc) throws IOException { + Path path = tempDirPath.resolve(fileName); + Files.write(path, MAPPER.writeValueAsBytes(doc)); + return path.toString(); + } + + private String[] compareArgs(String outputName, List docPaths) { + List args = new ArrayList<>(); + args.add(tempDirPath.resolve(outputName).toString()); + args.addAll(docPaths); + return args.toArray(new String[0]); + } + + private List dataRows(String outputName, String sheetName) throws IOException { + try (Workbook workbook = WorkbookFactory.create(tempDirPath.resolve(outputName).toFile())) { + Sheet sheet = workbook.getSheet(sheetName); + assertNotNull("missing sheet " + sheetName, sheet); + List rows = new ArrayList<>(); + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + if (sheet.getRow(i) != null) { + rows.add(sheet.getRow(i)); + } + } + return rows; + } + } + + private static String text(Row row, int col) { + Cell cell = row.getCell(col); + return cell == null ? "" : cell.getStringCellValue(); + } + + public void testMaximumNumberOfDocuments() throws Exception { + List paths = new ArrayList<>(); + for (int i = 0; i < CompareSpdxDocs.MAX_ARGS - 1; i++) { + paths.add(write("doc" + i + ".spdx.json", createDoc(i, "LicenseRef-L", "Some license text"))); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("max.xlsx", paths))); + assertTrue(Files.exists(tempDirPath.resolve("max.xlsx"))); + } + + public void testTooManyDocuments() throws Exception { + List paths = new ArrayList<>(); + for (int i = 0; i < CompareSpdxDocs.MAX_ARGS; i++) { + paths.add("doc" + i + ".spdx.json"); + } + assertEquals(ExitCode.USAGE_ERROR, CompareSpdxDocs.run(compareArgs("many.xlsx", paths))); + } + + public void testSingleDocumentIsAnErrorAndLeavesNoOutput() throws Exception { + String path = write("one.spdx.json", createDoc(0, "LicenseRef-L", "Some license text")); + assertEquals(ExitCode.ERROR, CompareSpdxDocs.run(compareArgs("one.xlsx", Arrays.asList(path)))); + assertFalse(Files.exists(tempDirPath.resolve("one.xlsx"))); + } + + public void testExtractedLicenseTextLongerThanCellLimit() throws Exception { + StringBuilder longText = new StringBuilder(); + while (longText.length() < 40000) { + longText.append("word "); + } + List paths = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + paths.add(write("doc" + i + ".spdx.json", createDoc(i, "LicenseRef-L" + i, longText.toString()))); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("long.xlsx", paths))); + List rows = dataRows("long.xlsx", "Extracted Licenses"); + assertFalse(rows.isEmpty()); + String cellText = text(rows.get(0), 0); + assertTrue(cellText.length() <= 32767); + assertTrue(cellText.endsWith("[more...]")); + } + + public void testFileLicensesMappedAcrossThreeDocuments() throws Exception { + // each document names the same license differently: the extracted text is what makes them equal + List paths = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + paths.add(write("doc" + i + ".spdx.json", createDoc(i, "LicenseRef-L" + i, "Some license text"))); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("three.xlsx", paths))); + for (String sheetName : new String[] {"File Concluded", "File Found Licenses"}) { + List rows = dataRows("three.xlsx", sheetName); + assertEquals(sheetName, 1, rows.size()); + assertEquals(sheetName, "Equal", text(rows.get(0), 1)); + } + } + + private ObjectNode createDocWithPackage(int index, Consumer packageCustomizer) { + ObjectNode doc = createDoc(index, "LicenseRef-L", "Some license text"); + ObjectNode pkg = doc.putArray("packages").addObject(); + pkg.put("SPDXID", "SPDXRef-Package"); + pkg.put("name", "pkg"); + pkg.put("downloadLocation", "NOASSERTION"); + pkg.put("filesAnalyzed", false); + pkg.put("licenseConcluded", "NOASSERTION"); + pkg.put("licenseDeclared", "NOASSERTION"); + pkg.put("copyrightText", "NONE"); + packageCustomizer.accept(pkg); + addDescribes(doc, "SPDXRef-Package"); + return doc; + } + + private String equalsCellOfRow(List rows, String fieldName) { + for (Row row : rows) { + if (fieldName.equals(text(row, 0))) { + return text(row, 1); + } + } + fail("no row for " + fieldName); + return null; + } + + public void testPackageAttributionDifferenceIsReported() throws Exception { + List paths = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + final int docIndex = i; + paths.add(write("doc" + i + ".spdx.json", createDocWithPackage(i, + pkg -> pkg.putArray("attributionTexts").add("attribution " + docIndex)))); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("attr.xlsx", paths))); + List rows = dataRows("attr.xlsx", "Package"); + assertEquals("Diff", equalsCellOfRow(rows, "Attributions")); + assertEquals("Equal", equalsCellOfRow(rows, "Annotations")); + } + + public void testDuplicateDocumentNamespacesAreComparedAndReported() throws Exception { + List paths = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + final int docIndex = i; + ObjectNode doc = createDocWithPackage(0, + pkg -> pkg.putArray("attributionTexts").add("attribution " + docIndex)); + paths.add(write("doc" + i + ".spdx.json", doc)); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("dup.xlsx", paths))); + // documents that share a namespace are still told apart + assertEquals("Diff", equalsCellOfRow(dataRows("dup.xlsx", "Package"), "Attributions")); + boolean reported = false; + for (Row row : dataRows("dup.xlsx", "Verification Errors")) { + for (Cell cell : row) { + if (cell.getCellType() == org.apache.poi.ss.usermodel.CellType.STRING + && cell.getStringCellValue().contains("Duplicate Document URI")) { + reported = true; + } + } + } + assertTrue("duplicate namespace not reported", reported); + } + + private List writeDocsWithFileMd5(String... md5Values) throws IOException { + List paths = new ArrayList<>(); + for (int i = 0; i < md5Values.length; i++) { + ObjectNode doc = createDoc(i, "LicenseRef-L", "Some license text"); + ObjectNode checksum = ((ObjectNode) doc.get("files").get(0)).withArray("checksums").addObject(); + checksum.put("algorithm", "MD5"); + checksum.put("checksumValue", md5Values[i]); + paths.add(write("doc" + i + ".spdx.json", doc)); + } + return paths; + } + + public void testFileChecksumsOtherThanSha1AreCompared() throws Exception { + List paths = writeDocsWithFileMd5("d41d8cd98f00b204e9800998ecf8427e", + "0cc175b9c0f1b6a831c399e269772661"); + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("md5.xlsx", paths))); + List rows = dataRows("md5.xlsx", "File Checksum"); + assertEquals(1, rows.size()); + assertEquals("Different", text(rows.get(0), 1)); + assertTrue(text(rows.get(0), 2).contains("MD5 d41d8cd98f00b204e9800998ecf8427e")); + assertTrue(text(rows.get(0), 2).contains("SHA1 " + CHECKSUM_SHA1)); + } + + public void testEqualFileChecksums() throws Exception { + List paths = writeDocsWithFileMd5("d41d8cd98f00b204e9800998ecf8427e", + "d41d8cd98f00b204e9800998ecf8427e"); + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("md5same.xlsx", paths))); + assertEquals("Equal", text(dataRows("md5same.xlsx", "File Checksum").get(0), 1)); + } + + public void testPackageEqualAttributions() throws Exception { + List paths = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + paths.add(write("doc" + i + ".spdx.json", createDocWithPackage(i, + pkg -> pkg.putArray("attributionTexts").add("same attribution")))); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("sameattr.xlsx", paths))); + assertEquals("Equal", equalsCellOfRow(dataRows("sameattr.xlsx", "Package"), "Attributions")); + } + + public void testSnippetsAreListedInNameOrder() throws Exception { + String[] names = {"snippet-f", "snippet-b", "snippet-e", "snippet-a", "snippet-d", "snippet-c"}; + List paths = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + ObjectNode doc = createDoc(i, "LicenseRef-L", "Some license text"); + ArrayNode snippets = doc.putArray("snippets"); + for (String name : names) { + ObjectNode snippet = snippets.addObject(); + snippet.put("SPDXID", "SPDXRef-" + name); + snippet.put("name", name); + snippet.put("snippetFromFile", "SPDXRef-File"); + ObjectNode range = snippet.putArray("ranges").addObject(); + range.putObject("startPointer").put("offset", 1).put("reference", "SPDXRef-File"); + range.putObject("endPointer").put("offset", 2).put("reference", "SPDXRef-File"); + snippet.put("licenseConcluded", "NOASSERTION"); + snippet.put("copyrightText", "NONE"); + } + paths.add(write("doc" + i + ".spdx.json", doc)); + } + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("snippets.xlsx", paths))); + List listed = new ArrayList<>(); + for (Row row : dataRows("snippets.xlsx", "Snippets")) { + if ("Snippet Name".equals(text(row, 0))) { + listed.add(text(row, 2)); + } + } + List expected = new ArrayList<>(Arrays.asList(names)); + expected.sort(null); + assertEquals(expected, listed); + } + + public void testMalformedFileInDirectoryIsSkipped() throws Exception { + Path dir = Files.createDirectory(tempDirPath.resolve("docs")); + for (int i = 0; i < 2; i++) { + Files.write(dir.resolve("good" + i + ".spdx.json"), + MAPPER.writeValueAsBytes(createDoc(i, "LicenseRef-L", "Some license text"))); + } + Files.write(dir.resolve("bad.rdf.xml"), "not rdf".getBytes(StandardCharsets.UTF_8)); + assertEquals(ExitCode.SUCCESS, CompareSpdxDocs.run(compareArgs("dir.xlsx", Arrays.asList(dir.toString())))); + } + + public void testMalformedFileArgumentIsAnError() throws Exception { + Path bad = tempDirPath.resolve("bad.rdf.xml"); + Files.write(bad, "not rdf".getBytes(StandardCharsets.UTF_8)); + assertEquals(ExitCode.ERROR, CompareSpdxDocs.run(compareArgs("bad.xlsx", Arrays.asList(bad.toString())))); + } + + public void testNormalizeDocNamesSingleDocument() { + assertEquals(Arrays.asList("doc.spdx.json"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("dir/sub/doc.spdx.json"))); + assertEquals(Arrays.asList("doc.spdx.json"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("doc.spdx.json"))); + } + + public void testNormalizeDocNamesIdenticalPaths() { + assertEquals(Arrays.asList("doc.json", "doc.json"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("dir/doc.json", "dir/doc.json"))); + } + + public void testNormalizeDocNamesRemovesCommonDirectory() { + assertEquals(Arrays.asList("a/x.json", "b/y.json"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("/tmp/run/a/x.json", "/tmp/run/b/y.json"))); + assertEquals(Arrays.asList("a/x.json", "b/y.json"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("C:\\run\\a\\x.json", "C:\\run\\b\\y.json"))); + } + + public void testNormalizeDocNamesKeepsWholeFileNames() { + assertEquals(Arrays.asList("doc-1.json", "doc-2.json"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("doc-1.json", "doc-2.json"))); + assertEquals(Arrays.asList("doc1.spdx.json", "doc2.spdx"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("dir/doc1.spdx.json", "dir/doc2.spdx"))); + } + + public void testNormalizeDocNamesOnePathIsPrefixOfAnother() { + assertEquals(Arrays.asList("b", "bc"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("a/b", "a/bc"))); + assertEquals(Arrays.asList("bc", "b"), + CompareSpdxDocs.normalizeDocNames(Arrays.asList("a/bc", "a/b"))); + } + + public void testNormalizeDocNamesNoDocuments() { + assertTrue(CompareSpdxDocs.normalizeDocNames(new ArrayList()).isEmpty()); + } +} diff --git a/src/test/java/org/spdx/tools/CompareSpdxDocsTest.java b/src/test/java/org/spdx/tools/CompareSpdxDocsTest.java index d72556c..33f4f4d 100644 --- a/src/test/java/org/spdx/tools/CompareSpdxDocsTest.java +++ b/src/test/java/org/spdx/tools/CompareSpdxDocsTest.java @@ -84,7 +84,7 @@ protected void setUp() throws Exception { */ protected void tearDown() throws Exception { super.tearDown(); - SpdxConverterTestV2.deleteDirAndFiles(tempDirPath); + TestFileUtils.deleteDirAndFiles(tempDirPath); } public void testCompareDocumentsv23() throws OnlineToolException, InvalidSPDXAnalysisException, IOException, InvalidFileNameException { diff --git a/src/test/java/org/spdx/tools/GenerateVerificationCodeTest.java b/src/test/java/org/spdx/tools/GenerateVerificationCodeTest.java index 5e3b753..aa7796d 100644 --- a/src/test/java/org/spdx/tools/GenerateVerificationCodeTest.java +++ b/src/test/java/org/spdx/tools/GenerateVerificationCodeTest.java @@ -6,6 +6,10 @@ package org.spdx.tools; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import org.spdx.core.DefaultModelStore; import org.spdx.core.InvalidSPDXAnalysisException; @@ -45,4 +49,42 @@ public void testGenerateVerificationCode() throws OnlineToolException, InvalidSP assertEquals(0, result2.getExcludedFileNames().size()); } + private Path createTree() throws IOException { + Path root = Files.createTempDirectory("spdx-verification-code-test-"); + Files.write(root.resolve("a.txt"), "a".getBytes(StandardCharsets.UTF_8)); + Files.createDirectories(root.resolve("sub").resolve("deep")); + Files.write(root.resolve("sub").resolve("b.txt"), "b".getBytes(StandardCharsets.UTF_8)); + Files.write(root.resolve("sub").resolve("deep").resolve("c.txt"), "c".getBytes(StandardCharsets.UTF_8)); + return root; + } + + public void testSkippedRegexUsesForwardSlashSeparators() throws Exception { + Path root = createTree(); + try { + SpdxPackageVerificationCode result = + GenerateVerificationCode.generateVerificationCode(root.toString(), "sub/.*"); + assertEquals(2, result.getExcludedFileNames().size()); + assertTrue(result.getExcludedFileNames().contains("./sub/b.txt")); + assertTrue(result.getExcludedFileNames().contains("./sub/deep/c.txt")); + result = GenerateVerificationCode.generateVerificationCode(root.toString(), "sub/deep/c\\.txt"); + assertEquals(1, result.getExcludedFileNames().size()); + assertTrue(result.getExcludedFileNames().contains("./sub/deep/c.txt")); + result = GenerateVerificationCode.generateVerificationCode(root.toString(), "a\\.txt"); + assertEquals(1, result.getExcludedFileNames().size()); + assertTrue(result.getExcludedFileNames().contains("./a.txt")); + } finally { + TestFileUtils.deleteDirAndFiles(root); + } + } + + public void testInvalidRegexIsAUsageError() throws Exception { + Path root = createTree(); + try { + assertEquals(ExitCode.USAGE_ERROR, GenerateVerificationCode.run(new String[] {root.toString(), "["})); + assertEquals(ExitCode.ERROR, GenerateVerificationCode.run(new String[] {root.resolve("missing").toString()})); + assertEquals(ExitCode.SUCCESS, GenerateVerificationCode.run(new String[] {root.toString()})); + } finally { + TestFileUtils.deleteDirAndFiles(root); + } + } } diff --git a/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java b/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java index d60fbd2..56e45f8 100644 --- a/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java +++ b/src/test/java/org/spdx/tools/MatchingStandardLicensesTest.java @@ -83,7 +83,30 @@ private static byte[] concat(byte[] first, byte[] second) { return result; } - static final String NON_ASCII_TEXT = "© Copyright 2007 über “quoted”"; + static final String NON_ASCII_TEXT = "\u00A9 Copyright 2007 \u00FCber \u201Cquoted\u201D"; + + /** U+1F600, outside the BMP: two chars in UTF-16 and four bytes in UTF-8 and UTF-32 */ + static final String SUPPLEMENTARY_TEXT = "x \uD83D\uDE00 y"; + + private static byte[] bytes(int... values) { + byte[] result = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = (byte) values[i]; + } + return result; + } + + private static final byte[] UTF8_BOM = bytes(0xEF, 0xBB, 0xBF); + private static final byte[] UTF16LE_BOM = bytes(0xFF, 0xFE); + private static final byte[] UTF16BE_BOM = bytes(0xFE, 0xFF); + private static final byte[] UTF32LE_BOM = bytes(0xFF, 0xFE, 0x00, 0x00); + private static final byte[] UTF32BE_BOM = bytes(0x00, 0x00, 0xFE, 0xFF); + + private static void assertReadAll(String expected, byte[] bom, String text, Charset charset) + throws Exception { + File file = writeTemp(concat(bom, text.getBytes(charset))); + assertEquals(charset.name() + " with BOM", expected, MatchingStandardLicenses.readAll(file)); + } public void testReadAllUtf8() throws Exception { File file = writeTemp(NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_8)); @@ -91,30 +114,69 @@ public void testReadAllUtf8() throws Exception { } public void testReadAllUtf8Bom() throws Exception { - byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; - File file = writeTemp(concat(bom, NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_8))); - assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(file)); + assertReadAll(NON_ASCII_TEXT, UTF8_BOM, NON_ASCII_TEXT, StandardCharsets.UTF_8); } public void testReadAllUtf16Bom() throws Exception { - byte[] leBom = {(byte) 0xFF, (byte) 0xFE}; - File le = writeTemp(concat(leBom, NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_16LE))); - assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(le)); - byte[] beBom = {(byte) 0xFE, (byte) 0xFF}; - File be = writeTemp(concat(beBom, NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_16BE))); - assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(be)); + assertReadAll(NON_ASCII_TEXT, UTF16LE_BOM, NON_ASCII_TEXT, StandardCharsets.UTF_16LE); + assertReadAll(NON_ASCII_TEXT, UTF16BE_BOM, NON_ASCII_TEXT, StandardCharsets.UTF_16BE); + assertReadAll(SUPPLEMENTARY_TEXT, UTF16LE_BOM, SUPPLEMENTARY_TEXT, StandardCharsets.UTF_16LE); + assertReadAll(SUPPLEMENTARY_TEXT, UTF16BE_BOM, SUPPLEMENTARY_TEXT, StandardCharsets.UTF_16BE); + } + + public void testReadAllUtf32Bom() throws Exception { + Charset utf32le = Charset.forName("UTF-32LE"); + Charset utf32be = Charset.forName("UTF-32BE"); + assertReadAll(NON_ASCII_TEXT, UTF32LE_BOM, NON_ASCII_TEXT, utf32le); + assertReadAll(NON_ASCII_TEXT, UTF32BE_BOM, NON_ASCII_TEXT, utf32be); + assertReadAll(SUPPLEMENTARY_TEXT, UTF32LE_BOM, SUPPLEMENTARY_TEXT, utf32le); + assertReadAll(SUPPLEMENTARY_TEXT, UTF32BE_BOM, SUPPLEMENTARY_TEXT, utf32be); + } + + public void testReadAllUtf16LeIsNotTakenForUtf32Le() throws Exception { + // FF FE 41 00 starts like the UTF-32LE BOM (FF FE 00 00) only up to the second byte + assertReadAll("Apache", UTF16LE_BOM, "Apache", StandardCharsets.UTF_16LE); } - public void testReadAllLegacyEncodingFallsBackToDefaultCharset() throws Exception { - // 0xA9 alone is not valid UTF-8 + public void testReadAllBomlessUtf8StartingWithBomLikeChars() throws Exception { + // U+00FF U+00FE encoded as UTF-8 is C3 BF C3 BE, not a BOM + String text = "\u00FF\u00FE text"; + assertEquals(text, MatchingStandardLicenses.readAll( + writeTemp(text.getBytes(StandardCharsets.UTF_8)))); + } + + public void testReadAllLegacyEncodingUsesFallbackCharset() throws Exception { + // 0xA9 alone is not valid UTF-8; it is U+00A9 in ISO-8859-1 and windows-1252 byte[] legacy = {'(', 'c', ')', ' ', (byte) 0xA9, ' ', 'x'}; File file = writeTemp(legacy); - assertEquals(new String(legacy, Charset.defaultCharset()), MatchingStandardLicenses.readAll(file)); + assertEquals("(c) \u00A9 x", MatchingStandardLicenses.readAll(file, StandardCharsets.ISO_8859_1)); + assertEquals("(c) \u00A9 x", MatchingStandardLicenses.readAll(file, Charset.forName("windows-1252"))); + } + + public void testReadAllValidUtf8IgnoresFallbackCharset() throws Exception { + File file = writeTemp(NON_ASCII_TEXT.getBytes(StandardCharsets.UTF_8)); + assertEquals(NON_ASCII_TEXT, MatchingStandardLicenses.readAll(file, StandardCharsets.ISO_8859_1)); + } + + public void testReadAllDefaultFallbackIsPlatformCharset() throws Exception { + byte[] legacy = {'(', 'c', ')', ' ', (byte) 0xA9, ' ', 'x'}; + File file = writeTemp(legacy); + assertEquals(MatchingStandardLicenses.readAll(file, Charset.defaultCharset()), + MatchingStandardLicenses.readAll(file)); } public void testReadAllEmptyAndBomOnly() throws Exception { assertEquals("", MatchingStandardLicenses.readAll(writeTemp(new byte[0]))); - assertEquals("", MatchingStandardLicenses.readAll( - writeTemp(new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}))); + for (byte[] bom : new byte[][] {UTF8_BOM, UTF16LE_BOM, UTF16BE_BOM, UTF32LE_BOM, UTF32BE_BOM}) { + assertEquals("", MatchingStandardLicenses.readAll(writeTemp(bom))); + } + } + + public void testReadAllTruncatedBomPrefixIsNotABom() throws Exception { + // a single 0xFF (or 0xEF 0xBB) is not a complete BOM and not valid UTF-8: decoded by the fallback + assertEquals("\u00FF", MatchingStandardLicenses.readAll( + writeTemp(bytes(0xFF)), StandardCharsets.ISO_8859_1)); + assertEquals("\u00EF\u00BB", MatchingStandardLicenses.readAll( + writeTemp(bytes(0xEF, 0xBB)), StandardCharsets.ISO_8859_1)); } } diff --git a/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java b/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java index b3c3dce..d10cc78 100644 --- a/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java +++ b/src/test/java/org/spdx/tools/RdfSchemaToolsTest.java @@ -11,7 +11,7 @@ import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.Map; -import java.util.function.Consumer; +import java.util.function.Function; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,12 +25,12 @@ public class RdfSchemaToolsTest extends TestCase { static final String ONTOLOGY = "testResources" + File.separator + "spdx-2-2-revision-8-ontology.owl.xml"; - /** output file name -> tool main method, invoked as main(inputFile, outputFile) */ - static final Map> TOOLS = new LinkedHashMap<>(); + /** output file name -> tool run method, invoked as run(inputFile, outputFile) */ + static final Map> TOOLS = new LinkedHashMap<>(); static { - TOOLS.put("schema.xsd", RdfSchemaToXsd::main); - TOOLS.put("schema.json", RdfSchemaToJsonSchema::main); - TOOLS.put("context.json", RdfSchemaToJsonContext::main); + TOOLS.put("schema.xsd", RdfSchemaToXsd::run); + TOOLS.put("schema.json", RdfSchemaToJsonSchema::run); + TOOLS.put("context.json", RdfSchemaToJsonContext::run); } Path tempDirPath; @@ -42,13 +42,14 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { super.tearDown(); - SpdxConverterTestV3.deleteDirAndFiles(tempDirPath); + TestFileUtils.deleteDirAndFiles(tempDirPath); } public void testGeneratesOutputAndReleasesFile() throws Exception { - for (Map.Entry> tool : TOOLS.entrySet()) { + for (Map.Entry> tool : TOOLS.entrySet()) { Path output = tempDirPath.resolve(tool.getKey()); - tool.getValue().accept(new String[] {ONTOLOGY, output.toString()}); + assertEquals(tool.getKey(), ExitCode.SUCCESS, + tool.getValue().apply(new String[] {ONTOLOGY, output.toString()}).intValue()); assertTrue(tool.getKey() + " output missing", Files.exists(output)); String text = new String(Files.readAllBytes(output), StandardCharsets.UTF_8); assertFalse(tool.getKey() + " output empty", text.isEmpty()); @@ -64,28 +65,60 @@ public void testGeneratesOutputAndReleasesFile() throws Exception { } public void testMissingInputCreatesNoOutput() { - for (Map.Entry> tool : TOOLS.entrySet()) { + for (Map.Entry> tool : TOOLS.entrySet()) { Path output = tempDirPath.resolve(tool.getKey()); Path missing = tempDirPath.resolve("doesNotExist.owl.xml"); - tool.getValue().accept(new String[] {missing.toString(), output.toString()}); + assertEquals(ExitCode.ERROR, tool.getValue().apply(new String[] {missing.toString(), output.toString()}).intValue()); assertFalse(Files.exists(output)); } } public void testUnwritableOutputDoesNotThrow() { - for (Map.Entry> tool : TOOLS.entrySet()) { + for (Map.Entry> tool : TOOLS.entrySet()) { Path unwritable = tempDirPath.resolve("no-such-dir").resolve(tool.getKey()); - tool.getValue().accept(new String[] {ONTOLOGY, unwritable.toString()}); + assertEquals(ExitCode.ERROR, tool.getValue().apply(new String[] {ONTOLOGY, unwritable.toString()}).intValue()); assertFalse(Files.exists(unwritable)); } } public void testExistingOutputIsNotOverwritten() throws Exception { - for (Map.Entry> tool : TOOLS.entrySet()) { + for (Map.Entry> tool : TOOLS.entrySet()) { Path output = tempDirPath.resolve(tool.getKey()); Files.write(output, "keep".getBytes(StandardCharsets.UTF_8)); - tool.getValue().accept(new String[] {ONTOLOGY, output.toString()}); + assertEquals(ExitCode.ERROR, tool.getValue().apply(new String[] {ONTOLOGY, output.toString()}).intValue()); assertEquals("keep", new String(Files.readAllBytes(output), StandardCharsets.UTF_8)); } } + + public void testUsageError() { + for (Map.Entry> tool : TOOLS.entrySet()) { + assertEquals(ExitCode.USAGE_ERROR, tool.getValue().apply(new String[] {}).intValue()); + assertEquals(ExitCode.USAGE_ERROR, tool.getValue().apply(new String[] {ONTOLOGY}).intValue()); + assertEquals(ExitCode.USAGE_ERROR, tool.getValue().apply(new String[] {"a", "b", "c"}).intValue()); + } + } + + public void testMalformedOntologyIsAnError() throws Exception { + Path input = tempDirPath.resolve("bad.owl.xml"); + Files.write(input, "not rdf".getBytes(StandardCharsets.UTF_8)); + for (Map.Entry> tool : TOOLS.entrySet()) { + Path output = tempDirPath.resolve("bad-" + tool.getKey()); + assertEquals(tool.getKey(), ExitCode.ERROR, + tool.getValue().apply(new String[] {input.toString(), output.toString()}).intValue()); + assertFalse(Files.exists(output)); + } + } + + public void testOntologyWithoutHeaderIsAnError() throws Exception { + Path input = tempDirPath.resolve("noheader.owl.xml"); + Files.write(input, ("") + .getBytes(StandardCharsets.UTF_8)); + for (Map.Entry> tool : TOOLS.entrySet()) { + Path output = tempDirPath.resolve("noheader-" + tool.getKey()); + assertEquals(tool.getKey(), ExitCode.ERROR, + tool.getValue().apply(new String[] {input.toString(), output.toString()}).intValue()); + assertFalse(Files.exists(output)); + } + } } diff --git a/src/test/java/org/spdx/tools/SpdxConverterTestV2.java b/src/test/java/org/spdx/tools/SpdxConverterTestV2.java index ce8402c..50bce4c 100644 --- a/src/test/java/org/spdx/tools/SpdxConverterTestV2.java +++ b/src/test/java/org/spdx/tools/SpdxConverterTestV2.java @@ -20,12 +20,9 @@ import java.io.File; import java.io.IOException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.Arrays; -import java.util.Objects; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -77,30 +74,7 @@ protected void setUp() throws Exception { */ protected void tearDown() throws Exception { super.tearDown(); - deleteDirAndFiles(tempDirPath); - } - - public static void deleteDirAndFiles(Path dirOrFile) { - if (Objects.isNull(dirOrFile)) { - return; - } - if (!Files.exists(dirOrFile, LinkOption.NOFOLLOW_LINKS)) { - return; - } - if (Files.isDirectory(dirOrFile, LinkOption.NOFOLLOW_LINKS)) { - try (DirectoryStream files = Files.newDirectoryStream(dirOrFile)) { - for (Path file : files) { - deleteDirAndFiles(file); - } - } catch (IOException e) { - System.err.println("IO error deleting directory or file "+e.getMessage()); - } - } - try { - Files.delete(dirOrFile); - } catch (IOException e) { - System.err.println("IO error deleting directory or file "+e.getMessage()); - } + TestFileUtils.deleteDirAndFiles(tempDirPath); } // Supported file types: JSON, XLS, XLSX, TAG, RDFXML, YAML or XML @@ -412,4 +386,32 @@ public void testLicenseDetailsRdf() throws SpdxConverterException, InvalidSPDXAn Resource noDetailException = noDetailModel.createResource(exceptionUri); assertFalse(noDetailModel.contains(noDetailException, detailExceptionTextProperty)); } + + public void testFailedConversionDoesNotLeavePartialOutput() throws Exception { + Path badInput = tempDirPath.resolve("bad.spdx.json"); + Files.write(badInput, "not json".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + Path output = tempDirPath.resolve("out.spdx.yaml"); + try { + SpdxConverter.convert(badInput.toString(), output.toString(), SerFileType.JSON, SerFileType.YAML); + fail("expected SpdxConverterException"); + } catch (SpdxConverterException e) { + // expected + } + assertFalse("partial output left behind", Files.exists(output)); + // a retry to the same output path is accepted + SpdxConverter.convert(TEST_JSON_FILE_PATH, output.toString(), SerFileType.JSON, SerFileType.YAML); + assertTrue(Files.size(output) > 0); + } + + public void testMissingInputCreatesNoOutput() throws Exception { + Path output = tempDirPath.resolve("never.spdx.yaml"); + try { + SpdxConverter.convert(tempDirPath.resolve("missing.spdx.json").toString(), output.toString(), + SerFileType.JSON, SerFileType.YAML); + fail("expected SpdxConverterException"); + } catch (SpdxConverterException e) { + // expected + } + assertFalse(Files.exists(output)); + } } diff --git a/src/test/java/org/spdx/tools/SpdxConverterTestV3.java b/src/test/java/org/spdx/tools/SpdxConverterTestV3.java index 473ea49..42ab3e3 100644 --- a/src/test/java/org/spdx/tools/SpdxConverterTestV3.java +++ b/src/test/java/org/spdx/tools/SpdxConverterTestV3.java @@ -7,12 +7,9 @@ import java.io.File; import java.io.IOException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -64,30 +61,7 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { super.tearDown(); - deleteDirAndFiles(tempDirPath); - } - - public static void deleteDirAndFiles(Path dirOrFile) { - if (Objects.isNull(dirOrFile)) { - return; - } - if (!Files.exists(dirOrFile, LinkOption.NOFOLLOW_LINKS)) { - return; - } - if (Files.isDirectory(dirOrFile, LinkOption.NOFOLLOW_LINKS)) { - try (DirectoryStream files = Files.newDirectoryStream(dirOrFile)) { - for (Path file : files) { - deleteDirAndFiles(file); - } - } catch (IOException e) { - System.err.println("IO error deleting directory or file "+e.getMessage()); - } - } - try { - Files.delete(dirOrFile); - } catch (IOException e) { - System.err.println("IO error deleting directory or file "+e.getMessage()); - } + TestFileUtils.deleteDirAndFiles(tempDirPath); } public void testV2JsonToV3JsonLD() throws SpdxConverterException, InvalidSPDXAnalysisException, IOException, SpdxCompareException { diff --git a/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java b/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java index 7bffd74..5e8bb0c 100644 --- a/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java +++ b/src/test/java/org/spdx/tools/SpdxToolsHelperTest.java @@ -22,8 +22,8 @@ public class SpdxToolsHelperTest extends TestCase { static final byte[] UTF8_BOM = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; static final String JSONLD_TEXT = "{\"@context\": \"https://spdx.org/rdf/3.0.1/spdx-context.jsonld\"," - + " \"name\": \"über © “quoted”\"}"; - static final String JSON_TEXT = "{\"name\": \"über © “quoted”\"}"; + + " \"name\": \"\u00FCber \u00A9 \u201Cquoted\u201D\"}"; + static final String JSON_TEXT = "{\"name\": \"\u00FCber \u00A9 \u201Cquoted\u201D\"}"; Path tempDirPath; @@ -34,7 +34,7 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { super.tearDown(); - SpdxConverterTestV3.deleteDirAndFiles(tempDirPath); + TestFileUtils.deleteDirAndFiles(tempDirPath); } private File write(String name, boolean bom, String text) throws IOException { @@ -66,4 +66,24 @@ public void testUnreadableJsonFallsBackToJson() throws Exception { File missing = tempDirPath.resolve("missing.json").toFile(); assertEquals(SerFileType.JSON, SpdxToolsHelper.fileToFileType(missing)); } + + public void testFileTypeFromExtensionIsCaseInsensitive() throws Exception { + assertEquals(SerFileType.RDFXML, SpdxToolsHelper.fileToFileType(new File("x.RDF.XML"))); + assertEquals(SerFileType.RDFXML, SpdxToolsHelper.fileToFileType(new File("x.Rdf.Xml"))); + assertEquals(SerFileType.RDFTTL, SpdxToolsHelper.fileToFileType(new File("x.Rdf.Ttl"))); + assertEquals(SerFileType.XLSX, SpdxToolsHelper.fileToFileType(new File("x.XLSX"))); + assertEquals(SerFileType.YAML, SpdxToolsHelper.fileToFileType(new File("x.YAML"))); + } + + public void testCompoundExtensionNeedsADot() throws Exception { + assertEquals(SerFileType.XML, SpdxToolsHelper.fileToFileType(new File("cardf.xml"))); + assertEquals(SerFileType.RDFXML, SpdxToolsHelper.fileToFileType(new File("card.rdf.xml"))); + } + + public void testJsonLdCompoundExtensions() throws Exception { + File spdx3 = write("upper.SPDX3.JSON", false, JSON_TEXT); + assertEquals(SerFileType.JSONLD, SpdxToolsHelper.fileToFileType(spdx3)); + File jsonld = write("upper.JSONLD.JSON", false, JSON_TEXT); + assertEquals(SerFileType.JSONLD, SpdxToolsHelper.fileToFileType(jsonld)); + } } diff --git a/src/test/java/org/spdx/tools/SpdxViewerTest.java b/src/test/java/org/spdx/tools/SpdxViewerTest.java index 9a643c4..5f5477c 100644 --- a/src/test/java/org/spdx/tools/SpdxViewerTest.java +++ b/src/test/java/org/spdx/tools/SpdxViewerTest.java @@ -34,7 +34,7 @@ public void testViewerOutput() { String output = new String(bytes.toByteArray(), StandardCharsets.UTF_8); assertTrue(output.contains("Document Name: SPDX-Tools-v2.0")); // non-ASCII text in the document is written in the encoding of System.out - assertTrue(output.contains("© Copyright 2007 Hewlett-Packard")); + assertTrue(output.contains("\u00A9 Copyright 2007 Hewlett-Packard")); // the viewer must not close System.out assertTrue(output.contains("MARKER-AFTER-RUN")); } @@ -47,4 +47,52 @@ public void testFileNotFound() { public void testUsageError() { assertEquals(ExitCode.USAGE_ERROR, SpdxViewer.run(new String[] {})); } + + private static String captureConsoleWriter(String[] chunks) throws Exception { + PrintStream original = System.out; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(bytes, true, StandardCharsets.UTF_8)); + SpdxViewer.ConsoleWriter writer = new SpdxViewer.ConsoleWriter(); + for (String chunk : chunks) { + writer.write(chunk.toCharArray(), 0, chunk.length()); + } + writer.flush(); + } finally { + System.setOut(original); + } + return new String(bytes.toByteArray(), StandardCharsets.UTF_8); + } + + public void testConsoleWriterSurrogatePairSplitAtFlushBoundary() throws Exception { + String padding = "a".repeat(SpdxViewer.ConsoleWriter.FLUSH_THRESHOLD - 1); + // the high surrogate ends the first flushed chunk, its low surrogate starts the next + String output = captureConsoleWriter(new String[] {padding + "\uD83D", "\uDE00 end"}); + assertEquals(padding + "\uD83D\uDE00 end", output); + } + + public void testConsoleWriterBuffersUntilFlush() throws Exception { + PrintStream original = System.out; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(bytes, true, StandardCharsets.UTF_8)); + SpdxViewer.ConsoleWriter writer = new SpdxViewer.ConsoleWriter(); + writer.write("small".toCharArray(), 0, 5); + assertEquals(0, bytes.size()); + writer.close(); + assertEquals("small", new String(bytes.toByteArray(), StandardCharsets.UTF_8)); + } finally { + System.setOut(original); + } + } + + public void testConsoleWriterLargeOutputIsComplete() throws Exception { + StringBuilder expected = new StringBuilder(); + String[] chunks = new String[3000]; + for (int i = 0; i < chunks.length; i++) { + chunks[i] = "line " + i + " \u00A9 \uD83D\uDE00\n"; + expected.append(chunks[i]); + } + assertEquals(expected.toString(), captureConsoleWriter(chunks)); + } } diff --git a/src/test/java/org/spdx/tools/TestFileUtils.java b/src/test/java/org/spdx/tools/TestFileUtils.java new file mode 100644 index 0000000..be59bc7 --- /dev/null +++ b/src/test/java/org/spdx/tools/TestFileUtils.java @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2026 SPDX Contributors + * SPDX-FileType: SOURCE + * SPDX-License-Identifier: Apache-2.0 + */ +package org.spdx.tools; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Objects; + +/** + * File helpers shared by the tests + */ +final class TestFileUtils { + + private TestFileUtils() { + } + + /** + * Deletes a file, or a directory with its contents. Symbolic links are deleted, not followed. + * Failures are reported to stderr, not thrown. + * @param dirOrFile path to delete; null or a missing path is ignored + */ + static void deleteDirAndFiles(Path dirOrFile) { + if (Objects.isNull(dirOrFile)) { + return; + } + if (!Files.exists(dirOrFile, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (Files.isDirectory(dirOrFile, LinkOption.NOFOLLOW_LINKS)) { + try (DirectoryStream files = Files.newDirectoryStream(dirOrFile)) { + for (Path file : files) { + deleteDirAndFiles(file); + } + } catch (IOException e) { + System.err.println("IO error deleting directory or file " + e.getMessage()); + } + } + try { + Files.delete(dirOrFile); + } catch (IOException e) { + System.err.println("IO error deleting directory or file " + e.getMessage()); + } + } +} diff --git a/src/test/java/org/spdx/tools/VerifyTest.java b/src/test/java/org/spdx/tools/VerifyTest.java index f2d03fe..f515d4b 100644 --- a/src/test/java/org/spdx/tools/VerifyTest.java +++ b/src/test/java/org/spdx/tools/VerifyTest.java @@ -6,6 +6,8 @@ package org.spdx.tools; import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; import org.spdx.core.DefaultModelStore; @@ -16,6 +18,9 @@ import org.spdx.storage.simple.InMemSpdxStore; import org.spdx.tools.SpdxToolsHelper.SerFileType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import junit.framework.TestCase; public class VerifyTest extends TestCase { @@ -106,4 +111,34 @@ public void testVerifyDouble() throws SpdxVerificationException { List result = Verify.verify(DOUBLE_JSON_LD_FILE_PATH, SerFileType.JSONLD); assertEquals(0, result.size()); } + + public void testV22JsonIsValidatedAgainstV22Schema() throws Exception { + // primaryPackagePurpose is new in 2.3, so the 2.2 schema rejects it and the 2.3 schema accepts it + ObjectNode doc = (ObjectNode) new ObjectMapper().readTree(new File(JSON_V2_2_FILE_PATH)); + ((ObjectNode) doc.get("packages").get(0)).put("primaryPackagePurpose", "LIBRARY"); + File file = File.createTempFile("verify-v22-", ".spdx.json"); + file.deleteOnExit(); + Files.write(file.toPath(), new ObjectMapper().writeValueAsBytes(doc)); + List result = Verify.verify(file.getPath(), SerFileType.JSON); + boolean schemaError = false; + for (String message : result) { + if (message.contains("primaryPackagePurpose") && message.contains("$.")) { + schemaError = true; + } + } + assertTrue("expected a JSON schema error, got " + result, schemaError); + } + + public void testMalformedRdfFileIsAVerificationException() throws Exception { + File file = File.createTempFile("verify-bad-", ".rdf.xml"); + file.deleteOnExit(); + Files.write(file.toPath(), "not rdf".getBytes(StandardCharsets.UTF_8)); + try { + Verify.verify(file.getPath(), SerFileType.RDFXML); + fail("expected SpdxVerificationException"); + } catch (SpdxVerificationException e) { + // expected + } + assertEquals(ExitCode.ERROR, Verify.run(new String[] {file.getPath()})); + } } diff --git a/src/test/java/org/spdx/tools/compare/CompareSheetsTest.java b/src/test/java/org/spdx/tools/compare/CompareSheetsTest.java new file mode 100644 index 0000000..74f7517 --- /dev/null +++ b/src/test/java/org/spdx/tools/compare/CompareSheetsTest.java @@ -0,0 +1,87 @@ +/** + * SPDX-FileCopyrightText: 2026 SPDX Contributors + * SPDX-FileType: SOURCE + * SPDX-License-Identifier: Apache-2.0 + */ +package org.spdx.tools.compare; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.spdx.spreadsheetstore.SpreadsheetException; + +import junit.framework.TestCase; + +/** + * Test cases for the helpers of the comparison sheets + */ +public class CompareSheetsTest extends TestCase { + + private static class Element { + final Optional name; + final String id; + + Element(String name, String id) { + this.name = Optional.ofNullable(name); + this.id = id; + } + } + + private static int compare(Element a, Element b) { + return DocumentRelationshipSheet.compareElements(a.name, a.id, b.name, b.id); + } + + public void testElementOrderIsATotalOrder() { + // named and unnamed elements, with equal names, equal IDs and different IDs for the same name + List elements = new ArrayList<>(); + for (String name : new String[] {null, "a", "b"}) { + for (String id : new String[] {"SPDXRef-1", "SPDXRef-2", "SPDXRef-3"}) { + elements.add(new Element(name, id)); + } + } + for (Element a : elements) { + assertEquals(0, compare(a, a)); + for (Element b : elements) { + assertEquals(Integer.signum(compare(a, b)), -Integer.signum(compare(b, a))); + for (Element c : elements) { + if (compare(a, b) <= 0 && compare(b, c) <= 0) { + assertTrue(compare(a, c) <= 0); + } + } + } + } + } + + public void testElementOrder() { + assertTrue(compare(new Element(null, "SPDXRef-9"), new Element("a", "SPDXRef-1")) < 0); + assertTrue(compare(new Element("a", "SPDXRef-9"), new Element("b", "SPDXRef-1")) < 0); + assertTrue(compare(new Element(null, "SPDXRef-1"), new Element(null, "SPDXRef-2")) < 0); + // elements with the same name are aligned in the same row, whatever their IDs + assertEquals(0, compare(new Element("a", "SPDXRef-1"), new Element("a", "SPDXRef-2"))); + } + + public void testVerificationSheetRowCount() throws SpreadsheetException { + try (Workbook workbook = new XSSFWorkbook()) { + VerificationSheet.create(workbook, "Verification"); + VerificationSheet sheet = new VerificationSheet(workbook, "Verification"); + assertEquals(0, sheet.getNumDataRows()); + List> errors = new ArrayList<>(); + errors.add(Arrays.asList("first error", "second error", "third error")); + errors.add(Arrays.asList("only error")); + sheet.importVerificationErrors(errors, Arrays.asList("doc1", "doc2")); + assertEquals(3, sheet.getNumDataRows()); + assertEquals("third error", sheet.getSheet().getRow(3).getCell(0).getStringCellValue()); + assertEquals("only error", sheet.getSheet().getRow(1).getCell(1).getStringCellValue()); + sheet.resizeRows(); + sheet.clear(); + assertEquals(0, sheet.getNumDataRows()); + assertNull(sheet.getSheet().getRow(1)); + } catch (java.io.IOException e) { + throw new SpreadsheetException("unable to close the workbook: " + e.getMessage()); + } + } +} diff --git a/src/test/java/org/spdx/tools/schema/OwlToJsonContextTest.java b/src/test/java/org/spdx/tools/schema/OwlToJsonContextTest.java index 909932f..74e8020 100644 --- a/src/test/java/org/spdx/tools/schema/OwlToJsonContextTest.java +++ b/src/test/java/org/spdx/tools/schema/OwlToJsonContextTest.java @@ -10,10 +10,9 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import junit.framework.TestCase; @@ -29,8 +28,7 @@ public class OwlToJsonContextTest extends TestCase { public void testConvertToContext() throws IOException { OwlToJsonContext otjc = null; try (InputStream is = new FileInputStream(new File(OWL_FILE_PATH))) { - OntModel model = OntModelFactory - .createModel(OntSpecification.OWL2_DL_MEM); + OntModel model = AbstractOwlRdfConverter.createOntModel(); model.read(is, "RDF/XML"); otjc = new OwlToJsonContext(model); } @@ -42,5 +40,14 @@ public void testConvertToContext() throws IOException { assertTrue(context.size() > 0); assertTrue(context.has("Document")); + // the properties of the ontology, not only the fixed entries + assertTrue("too few entries: " + context.size(), context.size() > 80); + JsonNode annotations = context.get("annotations"); + assertEquals("spdx:annotations", annotations.get("@id").asText()); + assertEquals("spdx:Annotation", annotations.get("@type").asText()); + assertEquals("@set", annotations.get("@container").asText()); + assertEquals("xs:hexBinary", context.get("checksumValue").get("@type").asText()); + // rdfs:comment is an annotation property, its type is set explicitly + assertEquals("xs:string", context.get("comment").get("@type").asText()); } } diff --git a/src/test/java/org/spdx/tools/schema/OwlToJsonSchemaTest.java b/src/test/java/org/spdx/tools/schema/OwlToJsonSchemaTest.java index c8b28d6..f8ffe8a 100644 --- a/src/test/java/org/spdx/tools/schema/OwlToJsonSchemaTest.java +++ b/src/test/java/org/spdx/tools/schema/OwlToJsonSchemaTest.java @@ -10,10 +10,9 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -30,8 +29,7 @@ public class OwlToJsonSchemaTest extends TestCase { public void testConvertToJsonSchema() throws IOException { OwlToJsonSchema otjs = null; try (InputStream is = new FileInputStream(new File(OWL_FILE_PATH))) { - OntModel model = OntModelFactory - .createModel(OntSpecification.OWL2_DL_MEM); + OntModel model = AbstractOwlRdfConverter.createOntModel(); model.read(is, "RDF/XML"); otjs = new OwlToJsonSchema(model); } @@ -55,5 +53,27 @@ public void testConvertToJsonSchema() throws IOException { assertTrue(result.has("required")); ArrayNode required = (ArrayNode) result.get("required"); assertTrue(required.size() > 0); + + // restrictions inherited from the superclasses (SpdxElement, SpdxItem) are included + JsonNode packageSchema = properties.get("packages").get("items"); + assertEquals("array", packageSchema.get("properties").get("annotations").get("type").asText()); + assertTrue(packageSchema.get("properties").has("comment")); + assertTrue(contains(packageSchema.get("required"), "name")); + assertTrue(contains(packageSchema.get("required"), "copyrightText")); + assertTrue(contains(packageSchema.get("required"), "licenseConcluded")); + JsonNode fileSchema = properties.get("files").get("items"); + assertTrue(contains(fileSchema.get("required"), "copyrightText")); + assertTrue(contains(fileSchema.get("required"), "licenseConcluded")); + JsonNode extractedLicense = properties.get("hasExtractedLicensingInfos").get("items"); + assertEquals("array", extractedLicense.get("properties").get("seeAlsos").get("type").asText()); + } + + private static boolean contains(JsonNode array, String value) { + for (JsonNode element : array) { + if (value.equals(element.asText())) { + return true; + } + } + return false; } } diff --git a/src/test/java/org/spdx/tools/schema/OwlToXSDTest.java b/src/test/java/org/spdx/tools/schema/OwlToXSDTest.java index 8a69988..afa32a7 100644 --- a/src/test/java/org/spdx/tools/schema/OwlToXSDTest.java +++ b/src/test/java/org/spdx/tools/schema/OwlToXSDTest.java @@ -10,10 +10,13 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.model.OntModel; import org.apache.ws.commons.schema.XmlSchema; +import org.apache.ws.commons.schema.XmlSchemaComplexContent; +import org.apache.ws.commons.schema.XmlSchemaComplexContentExtension; +import org.apache.ws.commons.schema.XmlSchemaComplexType; +import org.apache.ws.commons.schema.XmlSchemaContentModel; +import org.apache.ws.commons.schema.XmlSchemaObject; import org.apache.ws.commons.schema.XmlSchemaSerializer.XmlSchemaSerializerException; import junit.framework.TestCase; @@ -36,7 +39,7 @@ protected void tearDown() throws Exception { public void testConvertToXsd() throws IOException, XmlSchemaSerializerException, SchemaException { OwlToXsd otx = null; try (InputStream is = new FileInputStream(new File(OWL_FILE_PATH))) { - OntModel model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + OntModel model = AbstractOwlRdfConverter.createOntModel(); model.read(is, "RDF/XML"); otx = new OwlToXsd(model); } @@ -46,6 +49,36 @@ public void testConvertToXsd() throws IOException, XmlSchemaSerializerException, assertNotNull(result.getElementByName("Document")); String expectedIRI = "http://spdx.org/rdf/terms"; assertEquals(expectedIRI, result.getTargetNamespace()); + + // every base type is defined in the schema (owl:Thing is not) + int extensions = 0; + for (XmlSchemaObject item : result.getItems()) { + if (item instanceof XmlSchemaComplexType) { + XmlSchemaContentModel model = ((XmlSchemaComplexType) item).getContentModel(); + if (model instanceof XmlSchemaComplexContent + && ((XmlSchemaComplexContent) model).getContent() instanceof XmlSchemaComplexContentExtension) { + XmlSchemaComplexContentExtension extension = + (XmlSchemaComplexContentExtension) ((XmlSchemaComplexContent) model).getContent(); + extensions++; + assertNotNull("undefined base type " + extension.getBaseTypeName(), + result.getTypeByName(extension.getBaseTypeName())); + } + } + } + assertTrue(extensions > 0); + } + + public void testOntologyWithoutHeaderIsRejected() { + OntModel model = AbstractOwlRdfConverter.createOntModel(); + model.read(new java.io.StringReader(""), + null, "RDF/XML"); + try { + new OwlToXsd(model); + fail("expected an exception for a model without an ontology"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("No ontologies")); + } } }