diff --git a/src/main/java/org/eolang/jeo/Assembling.java b/src/main/java/org/eolang/jeo/Assembling.java index 2b10c07cf..8da16bc34 100644 --- a/src/main/java/org/eolang/jeo/Assembling.java +++ b/src/main/java/org/eolang/jeo/Assembling.java @@ -44,6 +44,11 @@ public final class Assembling implements Transformation { */ private final Path from; + /** + * XMIR representation. + */ + private final XmirRepresentation repr; + /** * Constructor. * @param target Target folder. @@ -52,6 +57,7 @@ public final class Assembling implements Transformation { Assembling(final Path target, final Path representation) { this.folder = target; this.from = representation; + this.repr = new XmirRepresentation(this.from); } @Override @@ -61,8 +67,7 @@ public Path source() { @Override public Path target() { - final XmirRepresentation repr = new XmirRepresentation(this.from); - final String name = new PrefixedName(repr.name()).decode(); + final String name = new PrefixedName(this.repr.name()).decode(); final String[] subpath = name.split("\\."); subpath[subpath.length - 1] = String.format("%s.class", subpath[subpath.length - 1]); return Paths.get(this.folder.toString(), subpath); @@ -70,6 +75,6 @@ public Path target() { @Override public byte[] transform() { - return new XmirRepresentation(this.from).toBytecode().bytes(); + return this.repr.toBytecode().bytes(); } } diff --git a/src/main/java/org/eolang/jeo/representation/XmirRepresentation.java b/src/main/java/org/eolang/jeo/representation/XmirRepresentation.java index d0b5be2e1..a3ca212da 100644 --- a/src/main/java/org/eolang/jeo/representation/XmirRepresentation.java +++ b/src/main/java/org/eolang/jeo/representation/XmirRepresentation.java @@ -24,15 +24,27 @@ package org.eolang.jeo.representation; import com.jcabi.xml.XML; -import com.jcabi.xml.XMLDocument; import java.io.FileNotFoundException; +import java.io.IOException; import java.nio.file.Path; +import java.util.Optional; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.SchemaFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.cactoos.io.ResourceOf; +import org.cactoos.io.UncheckedInput; import org.cactoos.scalar.Sticky; import org.cactoos.scalar.Synced; import org.cactoos.scalar.Unchecked; import org.eolang.jeo.representation.bytecode.Bytecode; import org.eolang.jeo.representation.xmir.XmlProgram; -import org.eolang.parser.Schema; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; /** * Intermediate representation of a class files from XMIR. @@ -41,10 +53,20 @@ */ public final class XmirRepresentation { + /** + * XPath's factory. + */ + private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance(); + + /** + * XML document factory. + */ + private static final DocumentBuilderFactory DOC_FACTORY = DocumentBuilderFactory.newInstance(); + /** * XML. */ - private final Unchecked xml; + private final Unchecked xml; /** * Source of the XML. @@ -64,7 +86,7 @@ public XmirRepresentation(final Path path) { * @param xml XML. */ public XmirRepresentation(final XML xml) { - this(xml, "Unknown"); + this(xml.node().getFirstChild(), "Unknown"); } /** @@ -73,7 +95,7 @@ public XmirRepresentation(final XML xml) { * @param source Source of the XML. */ private XmirRepresentation( - final XML xml, + final Node xml, final String source ) { this(new Unchecked<>(() -> xml), source); @@ -85,7 +107,7 @@ private XmirRepresentation( * @param source Source of the XML. */ private XmirRepresentation( - final Unchecked xml, + final Unchecked xml, final String source ) { this.xml = xml; @@ -94,17 +116,36 @@ private XmirRepresentation( /** * Retrieves class name from XMIR. + * This method intentionally uses classes from `org.w3c.dom` instead of `com.jcabi.xml` + * by performance reasons. * @return Class name. */ public String name() { - return new ClassName( - this.xml.value() - .xpath("/program/metas/meta/tail/text()") - .stream() - .findFirst() - .orElse(""), - this.xml.value().xpath("/program/@name").get(0) - ).full(); + final Node node = this.xml.value(); + final XPath xpath = XmirRepresentation.XPATH_FACTORY.newXPath(); + try { + return new ClassName( + Optional.ofNullable( + ((Node) xpath.evaluate( + "/program/metas/meta/tail/text()", + node, + XPathConstants.NODE + )).getTextContent() + ).orElse(""), + String.valueOf( + xpath.evaluate( + "/program/@name", + node, + XPathConstants.STRING + ) + ) + ).full(); + } catch (final XPathExpressionException exception) { + throw new IllegalStateException( + String.format("Can't extract class name from the '%s' source", this.source), + exception + ); + } } /** @@ -112,9 +153,9 @@ public String name() { * @return Array of bytes. */ public Bytecode toBytecode() { - final XML xmir = this.xml.value(); + final Node xmir = this.xml.value(); try { - new Schema(xmir).check(); + new OptimizedSchema(xmir).check(); return new XmlProgram(xmir).bytecode().bytecode(); } catch (final IllegalArgumentException exception) { throw new IllegalArgumentException( @@ -134,7 +175,7 @@ public Bytecode toBytecode() { * @param path Path to an XML file. * @return Lazy XML. */ - private static Unchecked fromFile(final Path path) { + private static Unchecked fromFile(final Path path) { return new Unchecked<>(new Synced<>(new Sticky<>(() -> XmirRepresentation.open(path)))); } @@ -142,16 +183,21 @@ private static Unchecked fromFile(final Path path) { * Convert a path to XML. * @param path Path to XML file. * @return XML. + * @checkstyle IllegalCatchCheck (20 lines) */ - private static XML open(final Path path) { + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private static Node open(final Path path) { try { - return new XMLDocument(path.toFile()); + return XmirRepresentation.DOC_FACTORY + .newDocumentBuilder() + .parse(path.toFile()) + .getDocumentElement(); } catch (final FileNotFoundException exception) { throw new IllegalStateException( String.format("Can't find file '%s'", path), exception ); - } catch (final IllegalArgumentException broken) { + } catch (final Exception broken) { throw new IllegalStateException( String.format( "Can't parse XML from the file '%s'", @@ -161,4 +207,63 @@ private static XML open(final Path path) { ); } } + + /** + * Optimized schema for XMIR. + * It is an optimized version of {@link org.eolang.parser.Schema} class. + * @since 0.6 + * @todo #889:30min Use the `Schema` class instead of `OptimizedSchema`. + * The `OptimizedSchema` class is a temporary solution to avoid the performance + * issues with the `Schema` class. We will be able to remove this class after + * the following issue is resolved: + * https://github.com/jcabi/jcabi-xml/issues/277 + */ + private static class OptimizedSchema { + /** + * Node. + */ + private final Node node; + + /** + * Schema factory. + */ + private final SchemaFactory factory; + + /** + * Constructor. + * @param node Node. + */ + OptimizedSchema(final Node node) { + this(node, SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema")); + } + + /** + * Constructor. + * @param node Node. + * @param factory Schema factory. + */ + private OptimizedSchema(final Node node, final SchemaFactory factory) { + this.node = node; + this.factory = factory; + } + + /** + * Check the node. + */ + void check() { + try { + this.factory.newSchema( + new StreamSource(new UncheckedInput(new ResourceOf("XMIR.xsd")).stream()) + ).newValidator().validate(new DOMSource(this.node)); + } catch (final IOException | SAXException exception) { + throw new IllegalStateException( + String.format( + "There are XSD violations, see the log", + exception.getMessage() + ), + exception + ); + } + } + } } diff --git a/src/main/java/org/eolang/jeo/representation/directives/DirectivesSeq.java b/src/main/java/org/eolang/jeo/representation/directives/DirectivesSeq.java index acd00e918..09371752e 100644 --- a/src/main/java/org/eolang/jeo/representation/directives/DirectivesSeq.java +++ b/src/main/java/org/eolang/jeo/representation/directives/DirectivesSeq.java @@ -82,21 +82,16 @@ private DirectivesSeq(final String name, final Iterable... elements) @Override public Iterator iterator() { + final List all = this.stream() + .map(Directives::new) + .collect(Collectors.toList()); return new DirectivesJeoObject( - String.format("seq.of%d", this.size()), + String.format("seq.of%d", all.size()), this.name, - this.stream().map(Directives::new).collect(Collectors.toList()) + all ).iterator(); } - /** - * Size of the sequence. - * @return Size. - */ - private long size() { - return this.stream().count(); - } - /** * Stream of directives. * @return Stream of directives. diff --git a/src/main/java/org/eolang/jeo/representation/xmir/XmlInstruction.java b/src/main/java/org/eolang/jeo/representation/xmir/XmlInstruction.java index de9062bc5..f41b76489 100644 --- a/src/main/java/org/eolang/jeo/representation/xmir/XmlInstruction.java +++ b/src/main/java/org/eolang/jeo/representation/xmir/XmlInstruction.java @@ -62,14 +62,6 @@ public final class XmlInstruction implements XmlBytecodeEntry { ); } - /** - * Constructor. - * @param xml XML string that represents an instruction. - */ - XmlInstruction(final String xml) { - this(new XmlNode(xml)); - } - /** * Constructor. * @param xmlnode Instruction node. diff --git a/src/main/java/org/eolang/jeo/representation/xmir/XmlNode.java b/src/main/java/org/eolang/jeo/representation/xmir/XmlNode.java index db430ff7b..2ce640f66 100644 --- a/src/main/java/org/eolang/jeo/representation/xmir/XmlNode.java +++ b/src/main/java/org/eolang/jeo/representation/xmir/XmlNode.java @@ -23,7 +23,6 @@ */ package org.eolang.jeo.representation.xmir; -import com.jcabi.xml.XML; import com.jcabi.xml.XMLDocument; import java.util.ArrayList; import java.util.List; @@ -44,9 +43,15 @@ public final class XmlNode { /** - * Parent node. + * XML node. + * Attention! + * Here we use the {@link Node} class instead of the {@link com.jcabi.xml.XML} + * by performance reasons. + * In some cases {@link Node} 10 times faster than {@link com.jcabi.xml.XML}. + * You can read more about it here: + * Optimization */ - private final XML node; + private final Node node; /** * Constructor. @@ -58,17 +63,9 @@ public XmlNode(final String xml) { /** * Constructor. - * @param parent XML document + * @param parent Xml node. */ public XmlNode(final Node parent) { - this(new XMLDocument(parent)); - } - - /** - * Constructor. - * @param parent Parent node. - */ - public XmlNode(final XML parent) { this.node = parent; } @@ -76,7 +73,7 @@ public XmlNode(final XML parent) { public boolean equals(final Object obj) { final boolean res; if (obj instanceof XmlNode) { - res = this.node.equals(((XmlNode) obj).node); + res = new XMLDocument(this.node).equals(new XMLDocument(((XmlNode) obj).node)); } else { res = false; } @@ -106,7 +103,7 @@ public Stream children() { * @return Text content. */ public String text() { - return this.node.node().getTextContent(); + return this.node.getTextContent(); } /** @@ -116,7 +113,7 @@ public String text() { */ public Optional attribute(final String name) { final Optional result; - final NamedNodeMap attrs = this.node.node().getAttributes(); + final NamedNodeMap attrs = this.node.getAttributes(); if (attrs == null) { result = Optional.empty(); } else { @@ -140,7 +137,7 @@ XmlNode child(final String name) { * @return List of elements. */ List xpath(final String xpath) { - return this.node.xpath(xpath); + return new XMLDocument(this.node).xpath(xpath); } /** @@ -206,7 +203,7 @@ boolean hasAttribute(final String name, final String value) { * @return Class. */ XmlClass toClass() { - return new XmlClass(this.node.node()); + return new XmlClass(this.node); } /** @@ -233,9 +230,14 @@ XmlBytecodeEntry toEntry() { */ private Optional optchild(final String name) { Optional result = Optional.empty(); - final List nodes = this.node.nodes(name); - if (!nodes.isEmpty()) { - result = Optional.of(new XmlNode(nodes.get(0))); + final NodeList children = this.node.getChildNodes(); + final int length = children.getLength(); + for (int index = 0; index < length; ++index) { + final Node current = children.item(index); + if (current.getNodeName().equals(name)) { + result = Optional.of(new XmlNode(current)); + break; + } } return result; } @@ -260,7 +262,7 @@ private IllegalStateException notFound(final String name) { * @return Stream of class objects. */ private Stream objects() { - final NodeList children = this.node.node().getChildNodes(); + final NodeList children = this.node.getChildNodes(); final List res = new ArrayList<>(children.getLength()); for (int index = 0; index < children.getLength(); ++index) { final Node child = children.item(index); diff --git a/src/main/java/org/eolang/jeo/representation/xmir/XmlProgram.java b/src/main/java/org/eolang/jeo/representation/xmir/XmlProgram.java index 88b3aff14..30afab904 100644 --- a/src/main/java/org/eolang/jeo/representation/xmir/XmlProgram.java +++ b/src/main/java/org/eolang/jeo/representation/xmir/XmlProgram.java @@ -40,13 +40,14 @@ * @since 0.1 */ public final class XmlProgram { - /** - * Program node name. - */ - private static final String PROGRAM = "program"; /** * Root node. + * Here we use the {@link Node} class instead of the {@link com.jcabi.xml.XML} + * by performance reasons. + * In some cases {@link Node} 10 times faster than {@link com.jcabi.xml.XML}. + * You can read more about it here: + * Optimization */ private final Node root; @@ -64,7 +65,7 @@ public XmlProgram(final String... lines) { * @param xml Raw XMIR. */ public XmlProgram(final XML xml) { - this(xml.node()); + this(xml.node().getFirstChild()); } /** @@ -80,7 +81,7 @@ public XmlProgram(final XML xml) { new DirectivesClass(name), new DirectivesMetas(name) ) ).xmlQuietly() - ).node() + ) ); } @@ -89,7 +90,7 @@ public XmlProgram(final XML xml) { * * @param root Root node. */ - private XmlProgram(final Node root) { + public XmlProgram(final Node root) { this.root = root; } @@ -123,7 +124,6 @@ public BytecodeProgram bytecode() { */ private XmlClass top() { return new XmlNode(this.root) - .child(XmlProgram.PROGRAM) .child("objects") .child("o") .toClass(); diff --git a/src/test/java/org/eolang/jeo/representation/XmirRepresentationTest.java b/src/test/java/org/eolang/jeo/representation/XmirRepresentationTest.java index 9514646f1..a9ce9d879 100644 --- a/src/test/java/org/eolang/jeo/representation/XmirRepresentationTest.java +++ b/src/test/java/org/eolang/jeo/representation/XmirRepresentationTest.java @@ -23,7 +23,9 @@ */ package org.eolang.jeo.representation; +import com.jcabi.log.Logger; import com.jcabi.matchers.XhtmlMatchers; +import com.jcabi.xml.XMLDocument; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -34,6 +36,7 @@ import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -153,4 +156,58 @@ void failsToOpenBrokenXmirRepresentationFromFile(@TempDir final Path dir) throws ) ); } + + /** + * This is a performance test, which is disabled by default. + * It is used to measure the performance of the conversion of the EO object + * into the bytecode representation and back. + */ + @Test + @Disabled + @SuppressWarnings("PMD.GuardLogStatement") + void convertsToXmirAndBack() { + final Bytecode before = new BytecodeProgram( + new BytecodeClass("org/eolang/foo/Math") + .helloWorldMethod() + ).bytecode(); + final int attempts = 500; + final long start = System.currentTimeMillis(); + for (int current = 0; current < attempts; ++current) { + final Bytecode actual = new XmirRepresentation( + new BytecodeRepresentation( + before + ).toEO() + ).toBytecode(); + MatcherAssert.assertThat( + String.format(XmirRepresentationTest.MESSAGE, before, actual), + actual, + Matchers.equalTo(before) + ); + } + final long end = System.currentTimeMillis(); + Logger.info( + this, + "We made %d attempts to convert bytecode to xmir and back in %[ms]s", + attempts, + end - start + ); + } + + @Test + void throwsExceptionIfXmirIsInvalid() { + MatcherAssert.assertThat( + "We excpect that XSD violation message will be easily understandable by developers", + Assertions.assertThrows( + IllegalStateException.class, + () -> new XmirRepresentation( + new XMLDocument( + new BytecodeProgram( + new BytecodeClass("org/eolang/foo/Math") + ).xml().toString().replace("package", "") + ) + ).toBytecode() + ).getCause().getMessage(), + Matchers.containsString("There are XSD violations, see the log") + ); + } } diff --git a/src/test/java/org/eolang/jeo/representation/directives/DirectivesSeqTest.java b/src/test/java/org/eolang/jeo/representation/directives/DirectivesSeqTest.java index 7e30277eb..a70898102 100644 --- a/src/test/java/org/eolang/jeo/representation/directives/DirectivesSeqTest.java +++ b/src/test/java/org/eolang/jeo/representation/directives/DirectivesSeqTest.java @@ -24,8 +24,13 @@ package org.eolang.jeo.representation.directives; import com.jcabi.matchers.XhtmlMatchers; +import java.util.stream.Stream; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.xembly.Directives; import org.xembly.ImpossibleModificationException; import org.xembly.Xembler; @@ -52,4 +57,48 @@ void convertsToNumberedSeq() throws ImpossibleModificationException { ) ); } + + @ParameterizedTest + @MethodSource("sequences") + void computesSize( + final DirectivesSeq actual, final int expected + ) throws ImpossibleModificationException { + MatcherAssert.assertThat( + "The size of the sequence is not as expected", + new Xembler(actual).xml(), + XhtmlMatchers.hasXPath( + String.format( + "/o[contains(@base,'seq.of%d') and @name='@']", + expected + ) + ) + ); + } + + /** + * Sequences to test. + * @return Stream of arguments. + */ + static Stream sequences() { + return Stream.of( + Arguments.of( + new DirectivesSeq( + new DirectivesValue("1"), new DirectivesValue("2") + ), + 2 + ), + Arguments.of(new DirectivesSeq(), 0), + Arguments.of(new DirectivesSeq(new Directives(), new Directives()), 0), + Arguments.of( + new DirectivesSeq( + new DirectivesValue("1"), new Directives(), + new Directives(), new DirectivesValue("4") + ), + 2 + ), + Arguments.of( + new DirectivesSeq(), 0 + ) + ); + } }