From 651e97df7c1b6f89aaf6b768a6b1b64797640b17 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Sun, 15 Sep 2024 10:21:53 +0000 Subject: [PATCH] Add index generation By specifying the MULTIPLE_FILE_INDEX_ONLY stylesheet, the xhtml command can now generate an XML index that maps document identifiers to locations in output files. Fix: https://github.com/io7m-com/xstructural/issues/29 --- README-CHANGES.xml | 9 +- .../api/XSProcessorRequestType.java | 16 +- .../com/io7m/xstructural/api/XSSchemas.java | 12 +- .../io7m/xstructural/api/package-info.java | 2 +- .../xstructural/tests/XSCommandLineTest.java | 11 +- .../xstructural/tests/XSProcessorTest.java | 141 ++++++++++++++++++ .../xstructural/tests/example1_index_80.xml | 48 ++++++ .../vanilla/internal/XSEntityResolver.java | 1 + .../vanilla/internal/XSSAXParsers.java | 1 + .../vanilla/internal/XSTransformer.java | 44 ++++-- .../io7m/xstructural/xml/SXMLResources.java | 14 +- .../io7m/xstructural/xml/package-info.java | 2 +- .../xstructural/xml/xstructural-8-index.xsd | 61 ++++++++ .../xstructural/xml/s8/xstructural8-index.xsl | 97 ++++++++++++ .../xml/s8/xstructural8-web-multi.xsl | 1 + 15 files changed, 436 insertions(+), 24 deletions(-) create mode 100644 com.io7m.xstructural.tests/src/main/resources/com/io7m/xstructural/tests/example1_index_80.xml create mode 100644 com.io7m.xstructural.xml/src/main/xsd/com/io7m/xstructural/xml/xstructural-8-index.xsd create mode 100644 com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-index.xsl diff --git a/README-CHANGES.xml b/README-CHANGES.xml index 6d3de6d..1baa2d2 100644 --- a/README-CHANGES.xml +++ b/README-CHANGES.xml @@ -114,7 +114,7 @@ - + @@ -126,11 +126,16 @@ - + + + + + + diff --git a/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSProcessorRequestType.java b/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSProcessorRequestType.java index 100c18d..f726b37 100644 --- a/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSProcessorRequestType.java +++ b/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSProcessorRequestType.java @@ -129,7 +129,21 @@ enum Stylesheet * use in an EPUB file. */ - EPUB + EPUB, + + /** + *

The multiple file index stylesheet. Produces a single index file + * that contains references to elements as they would appear had they + * been handled by the {@link #MULTIPLE_FILE} stylesheet.

+ * + *

Essentially, the index answers the question "Given an element in my + * source document, in which file does that element appear in the output + * document?".

+ * + * @since 1.9.0 + */ + + MULTIPLE_FILE_INDEX_ONLY } /** diff --git a/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSSchemas.java b/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSSchemas.java index bb815d0..0ac4d3d 100644 --- a/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSSchemas.java +++ b/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/XSSchemas.java @@ -38,7 +38,8 @@ public static Set namespaces() { return Set.of( namespace7p0(), - namespace8p0() + namespace8p0(), + namespace8Index() ); } @@ -69,6 +70,15 @@ public static URI namespace8p0() return URI.create("urn:com.io7m.structural:8:0"); } + /** + * @return The structural 8.0 index schema + */ + + public static URI namespace8Index() + { + return URI.create("urn:com.io7m.structural.index:1:0"); + } + /** * @return The structural 8.0 schema */ diff --git a/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/package-info.java b/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/package-info.java index 7f8b9ed..e9f854c 100644 --- a/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/package-info.java +++ b/com.io7m.xstructural.api/src/main/java/com/io7m/xstructural/api/package-info.java @@ -19,7 +19,7 @@ */ @Export -@Version("1.1.0") +@Version("1.2.0") package com.io7m.xstructural.api; import org.osgi.annotation.bundle.Export; diff --git a/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSCommandLineTest.java b/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSCommandLineTest.java index 90a3b92..fee6951 100644 --- a/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSCommandLineTest.java +++ b/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSCommandLineTest.java @@ -570,11 +570,12 @@ public void testSchema() .map(Path::toString) .collect(Collectors.toList()); - Assertions.assertEquals(4L, (long) files.size()); + Assertions.assertEquals(5L, (long) files.size()); Assertions.assertTrue(files.contains("xml.xsd")); Assertions.assertTrue(files.contains("dc.xsd")); Assertions.assertTrue(files.contains("xstructural-7.xsd")); Assertions.assertTrue(files.contains("xstructural-8.xsd")); + Assertions.assertTrue(files.contains("xstructural-8-index.xsd")); } @Test @@ -591,7 +592,7 @@ public void testSchemaNoReplace() XSOutputCaptured.capture(main::run); Assertions.assertEquals(0, main.exitCode()); - Assertions.assertEquals(4L, Files.list(this.outputDirectory).count()); + Assertions.assertEquals(5L, Files.list(this.outputDirectory).count()); Files.list(this.outputDirectory) .forEach(path -> { @@ -609,7 +610,7 @@ public void testSchemaNoReplace() }); mainAgain.run(); Assertions.assertEquals(0, mainAgain.exitCode()); - Assertions.assertEquals(4L, Files.list(this.outputDirectory).count()); + Assertions.assertEquals(5L, Files.list(this.outputDirectory).count()); Files.list(this.outputDirectory) .forEach(path -> { @@ -636,7 +637,7 @@ public void testSchemaReplace() XSOutputCaptured.capture(main::run); Assertions.assertEquals(0, main.exitCode()); - Assertions.assertEquals(4L, Files.list(this.outputDirectory).count()); + Assertions.assertEquals(5L, Files.list(this.outputDirectory).count()); Files.list(this.outputDirectory) .forEach(path -> { @@ -656,7 +657,7 @@ public void testSchemaReplace() }); mainAgain.run(); Assertions.assertEquals(0, mainAgain.exitCode()); - Assertions.assertEquals(4L, Files.list(this.outputDirectory).count()); + Assertions.assertEquals(5L, Files.list(this.outputDirectory).count()); Files.list(this.outputDirectory) .forEach(path -> { diff --git a/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSProcessorTest.java b/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSProcessorTest.java index 122f837..ae14c49 100644 --- a/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSProcessorTest.java +++ b/com.io7m.xstructural.tests/src/main/java/com/io7m/xstructural/tests/XSProcessorTest.java @@ -19,19 +19,27 @@ import com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder; import com.io7m.xstructural.api.XSProcessorRequest; import com.io7m.xstructural.api.XSProcessorRequestType; +import com.io7m.xstructural.api.XSTransformException; import com.io7m.xstructural.api.XSValidationException; import com.io7m.xstructural.vanilla.XSProcessors; +import com.io7m.xstructural.vanilla.internal.XSValidator; +import com.io7m.xstructural.xml.SXMLResources; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.w3c.dom.Element; +import javax.xml.parsers.DocumentBuilderFactory; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + public final class XSProcessorTest { private static final Duration TIMEOUT = Duration.ofSeconds(15L); @@ -344,4 +352,137 @@ public void testCompileSingleExampleWindows0_70() final var processor = this.processors.create(request); Assertions.assertTimeout(TIMEOUT, processor::execute); } + + @Test + public void testCompileMultipleIndex0_70() + throws Exception + { + final var request = + XSProcessorRequest.builder() + .setOutputDirectory(this.outputDirectory) + .setSourceFile(XSTestDirectories.resourceOf( + XSProcessorTest.class, + this.sourceDirectory, + "example1_70.xml")) + .setTraceFile(this.directory.resolve("trace.xml")) + .setMessageFile(this.directory.resolve("messages.txt")) + .setStylesheet(XSProcessorRequestType.Stylesheet.MULTIPLE_FILE_INDEX_ONLY) + .build(); + + final var processor = + this.processors.create(request); + final var x = + Assertions.assertThrows(XSTransformException.class, processor::execute); + assertTrue(x.getMessage().contains("Unsupported configuration")); + } + + @Test + public void testCompileMultipleIndex0_80() + throws Exception + { + final var request = + XSProcessorRequest.builder() + .setOutputDirectory(this.outputDirectory) + .setSourceFile(XSTestDirectories.resourceOf( + XSProcessorTest.class, + this.sourceDirectory, + "example1_index_80.xml")) + .setTraceFile(this.directory.resolve("trace.xml")) + .setMessageFile(this.directory.resolve("messages.txt")) + .setStylesheet(XSProcessorRequestType.Stylesheet.MULTIPLE_FILE_INDEX_ONLY) + .build(); + + final var processor = this.processors.create(request); + Assertions.assertTimeout(TIMEOUT, processor::execute); + + final var indexFile = + this.outputDirectory.resolve("xstructural-index.xml"); + + assertTrue(Files.isRegularFile(indexFile)); + + final var validator = + new XSValidator( + new SXMLResources(), + XSProcessorRequest.builder() + .setTask(XSProcessorRequestType.Task.VALIDATE) + .setOutputDirectory(this.outputDirectory) + .setSourceFile(indexFile) + .setStylesheet(XSProcessorRequestType.Stylesheet.MULTIPLE_FILE_INDEX_ONLY) + .build() + ); + + validator.execute(); + + { + final var d = + DocumentBuilderFactory.newNSInstance() + .newDocumentBuilder() + .parse(indexFile.toFile()); + + final var items = d.getElementsByTagNameNS("urn:com.io7m.structural.index:1:0", "Item"); + assertEquals(8, items.getLength()); + + { + final var i = (Element) items.item(0); + assertEquals("d0e32.xhtml", i.getAttribute("File")); + assertEquals("59259ed7-a88d-4411-b397-b9aa20aec3a0", i.getAttribute("ID")); + assertEquals("Section", i.getAttribute("Type")); + assertEquals("Section-A", i.getAttribute("Title")); + } + + { + final var i = (Element) items.item(1); + assertEquals("d0e32.xhtml", i.getAttribute("File")); + assertEquals("27174ab0-0c36-43c9-9f7e-1e140ade8510", i.getAttribute("ID")); + assertEquals("Subsection", i.getAttribute("Type")); + assertEquals("Subsection-A", i.getAttribute("Title")); + } + + { + final var i = (Element) items.item(2); + assertEquals("d0e32.xhtml", i.getAttribute("File")); + assertEquals("24cafbdf-cfb6-4fee-a8d6-496d98b4def7", i.getAttribute("ID")); + assertEquals("Paragraph", i.getAttribute("Type")); + } + + { + final var i = (Element) items.item(3); + assertEquals("d0e32.xhtml", i.getAttribute("File")); + assertEquals("cdbeb82b-ba49-4504-9c7f-86fe5186184d", i.getAttribute("ID")); + assertEquals("FormalItem", i.getAttribute("Type")); + assertEquals("Formal-A", i.getAttribute("Title")); + } + + { + final var i = (Element) items.item(4); + assertEquals("d0e46.xhtml", i.getAttribute("File")); + assertEquals("1b3fed5d-31dd-42bd-98c1-30bfaa873de4", i.getAttribute("ID")); + assertEquals("Section", i.getAttribute("Type")); + assertEquals("Section-B", i.getAttribute("Title")); + } + + { + final var i = (Element) items.item(5); + assertEquals("d0e46.xhtml", i.getAttribute("File")); + assertEquals("5455c2c4-5468-4dc3-8f0f-27d8da7331cd", i.getAttribute("ID")); + assertEquals("Subsection", i.getAttribute("Type")); + assertEquals("Subsection-B", i.getAttribute("Title")); + } + + { + final var i = (Element) items.item(6); + assertEquals("d0e46.xhtml", i.getAttribute("File")); + assertEquals("ee1f2ec7-d958-4a31-8773-36235e63c9ab", i.getAttribute("ID")); + assertEquals("Paragraph", i.getAttribute("Type")); + } + + { + final var i = (Element) items.item(7); + assertEquals("d0e46.xhtml", i.getAttribute("File")); + assertEquals("b8bc55d6-3f1e-491e-a3f1-8e6c40461a51", i.getAttribute("ID")); + assertEquals("FormalItem", i.getAttribute("Type")); + assertEquals("Formal-B", i.getAttribute("Title")); + } + } + } } diff --git a/com.io7m.xstructural.tests/src/main/resources/com/io7m/xstructural/tests/example1_index_80.xml b/com.io7m.xstructural.tests/src/main/resources/com/io7m/xstructural/tests/example1_index_80.xml new file mode 100644 index 0000000..51b78dd --- /dev/null +++ b/com.io7m.xstructural.tests/src/main/resources/com/io7m/xstructural/tests/example1_index_80.xml @@ -0,0 +1,48 @@ + + + + + + + + + + A Contributor + A Creator + 2020-04-23T10:19:25+00:00 + An example article. + c65fa00d-0870-405e-8f13-258e5cf4992d + English + A Publisher + A Relation + CC-0 + A Source + Example Document + + +
+ + + Paragraphy. + + + Hello! + + +
+ +
+ + + Paragraphy. + + + Hello! + + +
+ +
diff --git a/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSEntityResolver.java b/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSEntityResolver.java index 2d7ab6a..643f0b8 100644 --- a/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSEntityResolver.java +++ b/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSEntityResolver.java @@ -97,6 +97,7 @@ public InputSource resolveEntity( case "xstructural-8.xsd": case "xstructural-7.xsd": + case "xstructural-8-index.xsd": case "xml.xsd": case "dc.xsd": { final var source = new InputSource(systemId); diff --git a/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSSAXParsers.java b/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSSAXParsers.java index cc5f6fc..92e6fb8 100644 --- a/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSSAXParsers.java +++ b/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSSAXParsers.java @@ -242,6 +242,7 @@ public XMLReader createXMLReader( schemas.put(XSSchemas.namespace7p0(), "xstructural-7.xsd"); schemas.put(XSSchemas.namespace8p0(), "xstructural-8.xsd"); + schemas.put(XSSchemas.namespace8Index(), "xstructural-8-index.xsd"); schemas.forEach((key, value) -> { locations.append(key); locations.append(' '); diff --git a/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSTransformer.java b/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSTransformer.java index 7f62f28..ffb2d35 100644 --- a/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSTransformer.java +++ b/com.io7m.xstructural.vanilla/src/main/java/com/io7m/xstructural/vanilla/internal/XSTransformer.java @@ -93,6 +93,18 @@ private static XSTransformException epub7NotSupported() return new XSTransformException(builder.toString()); } + private static XSTransformException index7NotSupported() + { + final var lineSeparator = System.lineSeparator(); + final var builder = new StringBuilder(128); + builder.append("Unsupported configuration."); + builder.append(lineSeparator); + builder.append( + " Problem: Producing index files from 7.0 documents is unsupported. Use 8.0 or newer."); + builder.append(lineSeparator); + return new XSTransformException(builder.toString()); + } + @Override public void execute() throws XSTransformException @@ -224,7 +236,8 @@ public void startPrefixMapping( final var builder = new StringBuilder(128); builder.append("Ambiguous document."); builder.append(lineSeparator); - builder.append(" Problem: The input document uses multiple xstructural schemas"); + builder.append( + " Problem: The input document uses multiple xstructural schemas"); builder.append(lineSeparator); builder.append(" Cannot determine which XSLT stylesheet to use!"); builder.append(lineSeparator); @@ -275,40 +288,47 @@ private URL selectStylesheet( final URI target) throws XSTransformException { - switch (this.request.stylesheet()) { - case SINGLE_FILE: { + return switch (this.request.stylesheet()) { + case SINGLE_FILE -> { if (Objects.equals(target, XSSchemas.namespace7p0())) { - return this.resources.s7Single(); + yield this.resources.s7Single(); } if (Objects.equals(target, XSSchemas.namespace8p0())) { - return this.resources.s8Single(); + yield this.resources.s8Single(); } throw new IllegalStateException(); } - case MULTIPLE_FILE: { + case MULTIPLE_FILE -> { if (Objects.equals(target, XSSchemas.namespace7p0())) { - return this.resources.s7Multi(); + yield this.resources.s7Multi(); } if (Objects.equals(target, XSSchemas.namespace8p0())) { - return this.resources.s8Multi(); + yield this.resources.s8Multi(); } throw new IllegalStateException(); } - case EPUB: { + case EPUB -> { if (Objects.equals(target, XSSchemas.namespace7p0())) { throw epub7NotSupported(); } if (Objects.equals(target, XSSchemas.namespace8p0())) { - return this.resources.s8Epub(); + yield this.resources.s8Epub(); } throw new IllegalStateException(); } - default: { + + case MULTIPLE_FILE_INDEX_ONLY -> { + if (Objects.equals(target, XSSchemas.namespace7p0())) { + throw index7NotSupported(); + } + if (Objects.equals(target, XSSchemas.namespace8p0())) { + yield this.resources.s8Index(); + } throw new IllegalStateException(); } - } + }; } private XSLTTraceListener createTraceListener() diff --git a/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/SXMLResources.java b/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/SXMLResources.java index bfea1b2..8bfdde1 100644 --- a/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/SXMLResources.java +++ b/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/SXMLResources.java @@ -176,7 +176,8 @@ public Stream xstructuralResources() "dc.xsd", "xml.xsd", "xstructural-7.xsd", - "xstructural-8.xsd" + "xstructural-8.xsd", + "xstructural-8-index.xsd" ); } @@ -303,6 +304,17 @@ public URL s8Multi() return SXMLResources.class.getResource("s8/xstructural8-web-multi.xsl"); } + /** + * @return The xstructural 8.0 multi-page index XSL stylesheet + * + * @since 1.9.0 + */ + + public URL s8Index() + { + return SXMLResources.class.getResource("s8/xstructural8-index.xsl"); + } + /** * @return The xstructural 8.0 EPUB XSL stylesheet * diff --git a/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/package-info.java b/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/package-info.java index b43e1bc..c017bc3 100644 --- a/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/package-info.java +++ b/com.io7m.xstructural.xml/src/main/java/com/io7m/xstructural/xml/package-info.java @@ -19,7 +19,7 @@ */ @Export -@Version("1.1.0") +@Version("1.2.0") package com.io7m.xstructural.xml; import org.osgi.annotation.bundle.Export; diff --git a/com.io7m.xstructural.xml/src/main/xsd/com/io7m/xstructural/xml/xstructural-8-index.xsd b/com.io7m.xstructural.xml/src/main/xsd/com/io7m/xstructural/xml/xstructural-8-index.xsd new file mode 100644 index 0000000..23a7c16 --- /dev/null +++ b/com.io7m.xstructural.xml/src/main/xsd/com/io7m/xstructural/xml/xstructural-8-index.xsd @@ -0,0 +1,61 @@ + + + + + + + + + + A UUID value. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-index.xsl b/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-index.xsl new file mode 100644 index 0000000..ae76918 --- /dev/null +++ b/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-index.xsl @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-web-multi.xsl b/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-web-multi.xsl index 3e260c0..fd1d654 100644 --- a/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-web-multi.xsl +++ b/com.io7m.xstructural.xml/src/main/xsl/com/io7m/xstructural/xml/s8/xstructural8-web-multi.xsl @@ -19,6 +19,7 @@