diff --git a/eo-parser/src/main/java/org/eolang/parser/TrStepped.java b/eo-parser/src/main/java/org/eolang/parser/TrStepped.java index 20b1a337c87..01f4d70d365 100644 --- a/eo-parser/src/main/java/org/eolang/parser/TrStepped.java +++ b/eo-parser/src/main/java/org/eolang/parser/TrStepped.java @@ -31,13 +31,15 @@ import com.yegor256.xsline.TrEnvelope; import com.yegor256.xsline.TrLambda; import com.yegor256.xsline.Train; +import java.util.concurrent.CountDownLatch; import org.cactoos.Scalar; import org.cactoos.io.ResourceOf; import org.cactoos.scalar.Sticky; +import org.cactoos.scalar.Synced; import org.cactoos.text.TextOf; /** - * Trains that adds sheet names that were processed. + * Train that adds sheet names that were processed. * * @since 0.1 */ @@ -47,18 +49,31 @@ final class TrStepped extends TrEnvelope { * Apply changes to each XML after processing. */ private static final Scalar STEPPED = new Sticky<>( - () -> new XSLDocument( - new TextOf( - new ResourceOf("org/eolang/parser/_stepped.xsl") - ).asString() + new Once( + () -> new XSLDocument( + new TextOf( + new ResourceOf("org/eolang/parser/_stepped.xsl") + ).asString() + ) ) ); /** * Ctor. + * * @param train Original train */ TrStepped(final Train train) { + this(train, TrStepped.STEPPED); + } + + /** + * Ctor. + * + * @param train Original train + * @param stepped XSL to apply + */ + TrStepped(final Train train, final Scalar stepped) { super( new TrLambda( train, @@ -66,7 +81,7 @@ final class TrStepped extends TrEnvelope { shift, new StLambda( shift::uid, - (pos, xml) -> TrStepped.STEPPED.value() + (pos, xml) -> new Synced<>(stepped).value() .with("step", pos) .with("sheet", shift.uid()) .transform(xml) @@ -75,4 +90,54 @@ final class TrStepped extends TrEnvelope { ) ); } + + /** + * Scalar that loads the value only once. + * + * @param Type of the value + * @since 0.51 + */ + static final class Once implements Scalar { + + /** + * Origin scalar. + */ + private final Scalar origin; + + /** + * Latch to count down. + */ + private final CountDownLatch latch; + + /** + * Ctor. + * + * @param origin Origin scalar + */ + Once(final Scalar origin) { + this(origin, new CountDownLatch(1)); + } + + /** + * Ctor. + * + * @param origin Origin scalar + * @param latch Latch to count down + */ + private Once(final Scalar origin, final CountDownLatch latch) { + this.origin = origin; + this.latch = latch; + } + + @Override + public T value() throws Exception { + if (this.latch.getCount() < 1) { + throw new IllegalStateException( + String.format("Resource '%s' should be loaded only once", this.origin) + ); + } + this.latch.countDown(); + return this.origin.value(); + } + } } diff --git a/eo-parser/src/test/java/org/eolang/parser/TrSteppedTest.java b/eo-parser/src/test/java/org/eolang/parser/TrSteppedTest.java new file mode 100644 index 00000000000..85c3fb95b65 --- /dev/null +++ b/eo-parser/src/test/java/org/eolang/parser/TrSteppedTest.java @@ -0,0 +1,92 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2025 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.eolang.parser; + +import com.jcabi.matchers.XhtmlMatchers; +import com.jcabi.xml.XML; +import com.jcabi.xml.XMLDocument; +import com.jcabi.xml.XSL; +import com.jcabi.xml.XSLDocument; +import com.yegor256.Together; +import com.yegor256.xsline.Shift; +import com.yegor256.xsline.StClasspath; +import com.yegor256.xsline.TrDefault; +import com.yegor256.xsline.Xsline; +import org.cactoos.io.ResourceOf; +import org.cactoos.scalar.Sticky; +import org.cactoos.text.TextOf; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +/** + * Test cases for {@link TrStepped}. + * + * @since 0.51 + */ +final class TrSteppedTest { + + @Test + void addsSheetName() { + MatcherAssert.assertThat( + "We expect the sheet name to be added", + new Xsline( + new TrStepped( + new TrDefault().with( + new StClasspath("/org/eolang/parser/print/wrap-data.xsl") + ) + ) + ).pass(new XMLDocument("no")).toString(), + XhtmlMatchers.hasXPath("/program/sheets/sheet[text()='wrap-data']") + ); + } + + @RepeatedTest(10) + void addsSheetNameConcurrently() { + final XML doc = new XMLDocument("yes"); + final Sticky loading = new Sticky<>( + new TrStepped.Once( + () -> new XSLDocument( + new TextOf( + () -> new ResourceOf("org/eolang/parser/_stepped.xsl").stream() + ).asString() + ) + ) + ); + MatcherAssert.assertThat( + "We expect the sheet name to be added successfully in concurrent environment", + new Together<>( + i -> new Xsline( + new TrStepped( + new TrDefault().with( + new StClasspath("/org/eolang/parser/print/wrap-data.xsl") + ), + loading + ) + ).pass(doc).toString() + ).iterator().next(), + XhtmlMatchers.hasXPath("/program/sheets/sheet[text()='wrap-data']") + ); + } +}