diff --git a/monticore-runtime/src/main/java/de/monticore/utils/MCThread2Instance.java b/monticore-runtime/src/main/java/de/monticore/utils/MCThread2Instance.java new file mode 100644 index 0000000000..f62c0eaf1d --- /dev/null +++ b/monticore-runtime/src/main/java/de/monticore/utils/MCThread2Instance.java @@ -0,0 +1,75 @@ +// (c) https://github.com/MontiCore/monticore +package de.monticore.utils; + +import de.se_rwth.commons.logging.Log; + +import java.util.function.Supplier; + +/** + * A Map which maps every thread to a different instance. + *

+ * This class is to be used for static delegates, + * to allow usage of MontiCore using multiple threads. + *

+ * Accessing of the instance of another thread is deliberately not possible. + */ +public class MCThread2Instance { + + protected static final String LOG_NAME = MCThread2Instance.class.getName(); + + // As of writing, this cannot do anything that ThreadLocal cannot do + // without a simple wrapper; + // Thus, currently this simply wraps ThreadLocal. + protected ThreadLocal threadLocal; + + public MCThread2Instance() { + this.threadLocal = new ThreadLocal(); + } + + public MCThread2Instance(T initialValue) { + this(() -> initialValue); + } + + public MCThread2Instance(Supplier initialValueSupplier) { + this.threadLocal = ThreadLocal.withInitial(initialValueSupplier); + } + + /** + * @param newInstance the new instance specific to this thread. + */ + public void set(T newInstance) { + Log.errorIfNull(newInstance); + getThreadLocal().set(newInstance); + } + + /** + * @return the instance specific to this thread. + */ + public T get() { + T instance = _internal_get(); + if (instance == null) { + Log.error( + "0x72100 internal error: " + + "Tried to get the thread-specific instance out of " + + MCThread2Instance.class.getName() + + ", but no thread-specific instance has been set yet." + + System.lineSeparator() + "Thread: " + Thread.currentThread() + ); + } + return instance; + } + + // internal + + /** + * @return thread local instance (can be null) + */ + protected T _internal_get() { + return getThreadLocal().get(); + } + + protected ThreadLocal getThreadLocal() { + return this.threadLocal; + } + +} diff --git a/monticore-runtime/src/test/java/de/monticore/utils/MCThread2InstanceTest.java b/monticore-runtime/src/test/java/de/monticore/utils/MCThread2InstanceTest.java new file mode 100644 index 0000000000..682769fe5a --- /dev/null +++ b/monticore-runtime/src/test/java/de/monticore/utils/MCThread2InstanceTest.java @@ -0,0 +1,109 @@ +// (c) https://github.com/MontiCore/monticore +package de.monticore.utils; + +import de.se_rwth.commons.logging.Finding; +import de.se_rwth.commons.logging.Log; +import de.se_rwth.commons.logging.LogStub; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class MCThread2InstanceTest { + + @BeforeEach + public void setupLog() { + LogStub.init(); + Log.enableFailQuick(false); + } + + @Test + public void testDefaultConstructor() { + MCThread2Instance mcThreadLocal = new MCThread2Instance<>(); + assertNull(mcThreadLocal.get()); + assertEquals(1, Log.getErrorCount()); + assertTrue(Log.getFindings().get(0).getMsg().startsWith("0x72100")); + runInThread(() -> assertNull(mcThreadLocal.get())); + // deliberately not checking Log again + } + + @Test + public void testConstructorWithInitialValue() { + Box box = new Box(1); + MCThread2Instance mcThreadLocal = new MCThread2Instance<>(box); + assertEquals(1, mcThreadLocal.get().value); + runInThread(() -> assertEquals(1, mcThreadLocal.get().value)); + mcThreadLocal.get().value = 2; + assertEquals(2, mcThreadLocal.get().value); + runInThread(() -> assertEquals(2, mcThreadLocal.get().value)); + assertNoFindings(); + } + + @Test + public void testConstructorWithSupplier() { + AtomicInteger i = new AtomicInteger(0); + MCThread2Instance mcThreadLocal = new MCThread2Instance<>( + () -> new Box(i.getAndIncrement()) + ); + assertEquals(0, mcThreadLocal.get().value); + runInThread(() -> assertEquals(1, mcThreadLocal.get().value)); + runInThread(() -> assertEquals(2, mcThreadLocal.get().value)); + } + + @Test + public void testSet() { + MCThread2Instance mcThreadLocal = new MCThread2Instance<>(); + Box box = new Box(4); + mcThreadLocal.set(box); + assertEquals(4, mcThreadLocal.get().value); + runInThread(() -> { + mcThreadLocal.set(new Box(5)); + }); + runInThread(() -> { + assertNull(mcThreadLocal.get()); + assertEquals(1, Log.getErrorCount()); + }); + assertEquals(4, mcThreadLocal.get().value); + } + + // internals + + protected void runInThread(Runnable task) { + Thread thread = new Thread(task); + thread.start(); + try { + thread.join(); + } + catch (InterruptedException e) { + fail(e); + } + } + + protected static void assertNoFindings() { + assertTrue(Log.getFindings().isEmpty(), + "Expected no Log findings, but got:" + + System.lineSeparator() + getAllFindingsAsString() + ); + } + + protected static String getAllFindingsAsString() { + return Log.getFindings().stream() + .map(Finding::buildMsg) + .collect(Collectors.joining(System.lineSeparator())); + } + + protected static class Box { + public int value; + + public Box(int value) { + this.value = value; + } + } + +}