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)