diff --git a/eo-parser/pom.xml b/eo-parser/pom.xml index 5a1f522cb4..8703496710 100644 --- a/eo-parser/pom.xml +++ b/eo-parser/pom.xml @@ -158,6 +158,11 @@ SOFTWARE. jping + + com.yegor256 + together + + org.eolang xax diff --git a/eo-parser/src/main/java/org/eolang/parser/StrictXmir.java b/eo-parser/src/main/java/org/eolang/parser/StrictXmir.java index b860cd4cd0..5d0eaca558 100644 --- a/eo-parser/src/main/java/org/eolang/parser/StrictXmir.java +++ b/eo-parser/src/main/java/org/eolang/parser/StrictXmir.java @@ -27,7 +27,6 @@ import com.jcabi.manifests.Manifests; import com.jcabi.xml.StrictXML; import com.jcabi.xml.XML; -import com.jcabi.xml.XMLDocument; import java.io.File; import java.io.IOException; import java.net.URI; @@ -61,6 +60,13 @@ */ @SuppressWarnings("PMD.TooManyMethods") public final class StrictXmir implements XML { + /** + * XSD for current EO version. + */ + private static final String MINE = String.format( + "https://www.eolang.org/xsd/XMIR-%s.xsd", + Manifests.read("EO-Version") + ); /** * The XML. @@ -77,11 +83,19 @@ public StrictXmir(final XML src) { /** * Ctor. - * @param src The source + * Synchronization by XML is necessary in case we're trying to validate the same + * {@link XML} in multiple threads. In such case the path to XSD scheme inside XML should + * be updated only once. + * @param before The XML source * @param tmp The directory with cached XSD files */ - public StrictXmir(final XML src, final Path tmp) { - this.xml = new StrictXML(StrictXmir.reset(src, tmp)); + @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors") + public StrictXmir(final XML before, final Path tmp) { + synchronized (before) { + this.xml = new StrictXML( + StrictXmir.reset(before, tmp) + ); + } } @Override @@ -143,44 +157,46 @@ public Collection validate(final XML schema) { * @return New XML with the same node */ private static XML reset(final XML xml, final Path tmp) { - final Node node = xml.inner(); final List location = xml.xpath("/program/@xsi:noNamespaceSchemaLocation"); if (!location.isEmpty()) { - String uri = location.get(0); - if (uri.startsWith("http")) { - uri = String.format( + final String before = location.get(0); + final String after; + if (before.startsWith("http")) { + after = String.format( "file:///%s", StrictXmir.fetch( - uri, + before, tmp.resolve( - uri.substring(uri.lastIndexOf('/') + 1) - ) + before.substring(before.lastIndexOf('/') + 1) + ), + tmp ).getAbsoluteFile().toString().replace("\\", "/") ); + } else { + after = before; + } + if (!after.equals(before)) { + new Xembler( + new Directives().xpath("/program").attr( + "noNamespaceSchemaLocation xsi http://www.w3.org/2001/XMLSchema-instance", + after + ) + ).applyQuietly(xml.inner()); } - new Xembler( - new Directives().xpath("/program").attr( - "noNamespaceSchemaLocation xsi http://www.w3.org/2001/XMLSchema-instance", - uri - ) - ).applyQuietly(node); } - return new XMLDocument(node); + return xml; } /** * Fetch the XSD and place into the path. * @param uri The URI * @param path The file + * @param tmp Original directory * @return Where it was saved */ - private static File fetch(final String uri, final Path path) { + private static File fetch(final String uri, final Path path, final Path tmp) { final File ret; - final String mine = String.format( - "https://www.eolang.org/xsd/XMIR-%s.xsd", - Manifests.read("EO-Version") - ); - if (uri.equals(mine)) { + if (StrictXmir.MINE.equals(uri)) { if (path.toFile().getParentFile().mkdirs()) { Logger.debug(StrictXmir.class, "Directory for %[file]s created", path); } @@ -200,7 +216,7 @@ private static File fetch(final String uri, final Path path) { } ret = path.toFile(); } else { - ret = StrictXmir.download(uri, path); + ret = StrictXmir.download(uri, path, tmp); } return ret; } @@ -209,52 +225,55 @@ private static File fetch(final String uri, final Path path) { * Download URI from Internet and save to file. * @param uri The URI * @param path The file + * @param tmp Directory to synchronize by * @return Where it was saved */ @SuppressWarnings("PMD.CognitiveComplexity") - private static File download(final String uri, final Path path) { + private static File download(final String uri, final Path path, final Path tmp) { final File abs = path.toFile().getAbsoluteFile(); - if (!abs.exists()) { - if (abs.getParentFile().mkdirs()) { - Logger.debug(StrictXmir.class, "Directory for %[file]s created", path); - } - int attempt = 0; - while (true) { - ++attempt; - try { - Files.write( - path, - new IoCheckedBytes( - new BytesOf(new InputOf(new URI(uri))) - ).asBytes() - ); - Logger.debug( - StrictXmir.class, - "XSD downloaded from %s and copied to %[file]s", - uri, path - ); - break; - } catch (final IOException ex) { - if (attempt < 3) { - Logger.warn( - StrictXmir.class, - "Attempt #%d failed to download %s to %s: %[exception]s", - attempt, - uri, + synchronized (tmp) { + if (!abs.exists()) { + if (abs.getParentFile().mkdirs()) { + Logger.debug(StrictXmir.class, "Directory for %[file]s created", path); + } + int attempt = 0; + while (true) { + ++attempt; + try { + Files.write( path, + new IoCheckedBytes( + new BytesOf(new InputOf(new URI(uri))) + ).asBytes() + ); + Logger.debug( + StrictXmir.class, + "XSD downloaded from %s and copied to %[file]s", + uri, path + ); + break; + } catch (final IOException ex) { + if (attempt < 3) { + Logger.warn( + StrictXmir.class, + "Attempt #%d failed to download %s to %s: %[exception]s", + attempt, + uri, + path, + ex + ); + continue; + } + throw new IllegalArgumentException( + String.format("Failed to download %s to %s", uri, path), + ex + ); + } catch (final URISyntaxException ex) { + throw new IllegalArgumentException( + String.format("Wrong URI: %s", uri), ex ); - continue; } - throw new IllegalArgumentException( - String.format("Failed to download %s to %s", uri, path), - ex - ); - } catch (final URISyntaxException ex) { - throw new IllegalArgumentException( - String.format("Wrong URI: %s", uri), - ex - ); } } } diff --git a/eo-parser/src/test/java/org/eolang/parser/StrictXmirTest.java b/eo-parser/src/test/java/org/eolang/parser/StrictXmirTest.java index d1535c73d2..155edb6db9 100644 --- a/eo-parser/src/test/java/org/eolang/parser/StrictXmirTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/StrictXmirTest.java @@ -28,12 +28,14 @@ import com.jcabi.xml.XMLDocument; import com.yegor256.Mktmp; import com.yegor256.MktmpResolver; +import com.yegor256.Together; import com.yegor256.WeAreOnline; import java.nio.file.Path; import java.nio.file.Paths; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xembly.Directives; @@ -65,6 +67,34 @@ void validatesXmir(@Mktmp final Path tmp) { ); } + @RepeatedTest(20) + @ExtendWith(WeAreOnline.class) + @ExtendWith(MktmpResolver.class) + void doesNotFailWithDifferentXmlInMultipleThreads(@Mktmp final Path tmp) { + Assertions.assertDoesNotThrow( + new Together<>( + thread -> { + final XML xml = StrictXmirTest.xmir("https://www.eolang.org/XMIR.xsd"); + return new StrictXmir(xml, tmp); + } + )::asList, + "StrictXmir should not fail in different threads with different xmls" + ); + } + + @RepeatedTest(20) + @ExtendWith(WeAreOnline.class) + @ExtendWith(MktmpResolver.class) + void doesNotFailWithSameXmlInMultipleThreads(@Mktmp final Path tmp) { + final XML xml = StrictXmirTest.xmir("https://www.eolang.org/XMIR.xsd"); + Assertions.assertDoesNotThrow( + new Together<>( + thread -> new StrictXmir(xml, tmp) + )::asList, + "StrictXmir should not fail in different threads with the same xml" + ); + } + @Test @ExtendWith(MktmpResolver.class) @ExtendWith(WeAreOnline.class)