This is an automated email from the ASF dual-hosted git repository. rec pushed a commit to branch bugfix/382-Warning-when-PEAR-contains-a-JCAS-class-that-is-used-as-a-feature-range-outside-the-PEAR in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git
commit 2e69c7576319483b7c7221da31d27859c53b2def Author: Richard Eckart de Castilho <[email protected]> AuthorDate: Wed Aug 21 17:48:45 2024 +0200 Issue #382: Warning when PEAR contains a JCAS class that is used as a feature range outside the PEAR - Added a "test" which reproduces the warning although there is no assert that checks if the warning is logged - Some cleaning up --- .../org/apache/uima/cas/CASRuntimeException.java | 5 +- .../org/apache/uima/cas/impl/FSClassRegistry.java | 30 +- .../apache/uima/UIMAException_Messages.properties | 2 +- .../apache/uima/cas/impl/FSClassRegistryTest.java | 36 +- .../uima/cas/test/FSCreatedInPearContextTest.java | 30 +- .../apache/uima/cas/test/JCasClassLoaderTest.java | 378 +++++++++------------ uimaj-parent/pom.xml | 5 + .../org/apache/uima/test/IsolatingClassloader.java | 157 +++++++++ 8 files changed, 368 insertions(+), 275 deletions(-) diff --git a/uimaj-core/src/main/java/org/apache/uima/cas/CASRuntimeException.java b/uimaj-core/src/main/java/org/apache/uima/cas/CASRuntimeException.java index 0668d61d3..c4c2a7051 100644 --- a/uimaj-core/src/main/java/org/apache/uima/cas/CASRuntimeException.java +++ b/uimaj-core/src/main/java/org/apache/uima/cas/CASRuntimeException.java @@ -128,8 +128,9 @@ public class CASRuntimeException extends UIMARuntimeException { public static final String JCAS_TYPE_NOT_IN_CAS = "JCAS_TYPE_NOT_IN_CAS"; /** - * CAS type system type "{0}" defines field "{1}" with range "{2}", but JCas class has range - * "{3}". + * CAS type system type "{0}" (loaded by {1}) defines field "{2}" with range "{3}" (loaded by + * {4}), but JCas getter method is returning "{5}" (loaded by {6}) which is not a subtype of the + * declared range. */ public static final String JCAS_TYPE_RANGE_MISMATCH = "JCAS_TYPE_RANGE_MISMATCH"; diff --git a/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java b/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java index 50246d6a2..1b882da2e 100644 --- a/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java +++ b/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java @@ -1366,26 +1366,22 @@ public abstract class FSClassRegistry { // abstract to prevent instantiating; th rangeClass = range.getComponentType().getJavaClass(); } } - if (!rangeClass.isAssignableFrom(returnClass)) { // can return subclass of TOP, OK if range is - // TOP - if (rangeClass.getName().equals("org.apache.uima.jcas.cas.Sofa") && // exception: for - // backwards compat - // reasons, sofaRef - // returns SofaFS, not - // Sofa. - returnClass.getName().equals("org.apache.uima.cas.SofaFS")) { + + // can return subclass of TOP, OK if range is TOP + if (!rangeClass.isAssignableFrom(returnClass)) { + // exception: for backwards compatibility reasons, sofaRef returns SofaFS, not Sofa. + if (rangeClass.getName().equals("org.apache.uima.jcas.cas.Sofa") + && returnClass.getName().equals("org.apache.uima.cas.SofaFS")) { // empty } else { - - /** - * CAS type system type "{0}" defines field "{1}" with range "{2}", but JCas getter method - * is returning "{3}" which is not a subtype of the declared range. - */ + // CAS type system type "{0}" (loaded by {1}) defines field "{2}" with range "{3}" (loaded + // by {4}), but JCas getter method is returning "{5}" (loaded by {6}) which is not a + // subtype of the declared range. + // + // should throw, but some code breaks! add2errors(errorSet, new CASRuntimeException(CASRuntimeException.JCAS_TYPE_RANGE_MISMATCH, - ti.getName(), fi.getShortName(), rangeClass, returnClass), false); // should - // throw, but - // some code - // breaks! + ti.getName(), ti.getJavaClass().getClassLoader(), fi.getShortName(), rangeClass, + rangeClass.getClassLoader(), returnClass, returnClass.getClassLoader()), false); } } } // end of checking methods diff --git a/uimaj-core/src/main/resources/org/apache/uima/UIMAException_Messages.properties b/uimaj-core/src/main/resources/org/apache/uima/UIMAException_Messages.properties index c079e6643..9b0a6ead6 100644 --- a/uimaj-core/src/main/resources/org/apache/uima/UIMAException_Messages.properties +++ b/uimaj-core/src/main/resources/org/apache/uima/UIMAException_Messages.properties @@ -562,7 +562,7 @@ JCAS_FIELD_MISSING_IN_TYPE_SYSTEM = JCAS class "{0}" defines a UIMA field "{1}" JCAS_FIELD_ADJ_OFFSET_CHANGED = In JCAS class "{0}", UIMA field "{1}" was set up when this class was previously loaded and initialized, to have an adjusted offset of "{2}" but now the feature has a different adjusted offset of "{3}"; this may be due to something else other than type system commit actions loading and initializing the JCas class, or to having a different non-compatible type system for this class, trying to use a common JCas cover class, which is not supported. JCAS_CAS_MISMATCH_SUPERTYPE = JCas class supertypes for "{0}", "{1}" and the corresponding UIMA supertypes for "{2}", "{3}" do not have an intersection. JCAS_MISMATCH_SUPERTYPE = The JCas class: "{0}" has supertypes: "{1}" which do not match the UIMA type "{2}"''s supertypes "{3}". -JCAS_TYPE_RANGE_MISMATCH = CAS type system type "{0}" defines field "{1}" with range "{2}", but JCas getter method is returning "{3}" which is not a subtype of the declared range. +JCAS_TYPE_RANGE_MISMATCH = CAS type system type "{0}" (loaded by {1}) defines field "{2}" with range "{3}" (loaded by {4}), but JCas getter method is returning "{5}" (loaded by {6}) which is not a subtype of the declared range. JCAS_GET_NTH_ON_EMPTY_LIST = JCas getNthElement method called via invalid object - an empty list: {0}. JCAS_GET_NTH_NEGATIVE_INDEX = JCas getNthElement method called with index "{0}" which is negative. JCAS_GET_NTH_PAST_END = JCas getNthElement method called with index "{0}" larger than the length of the list. diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/impl/FSClassRegistryTest.java b/uimaj-core/src/test/java/org/apache/uima/cas/impl/FSClassRegistryTest.java index 0fba3aa30..20490f020 100644 --- a/uimaj-core/src/test/java/org/apache/uima/cas/impl/FSClassRegistryTest.java +++ b/uimaj-core/src/test/java/org/apache/uima/cas/impl/FSClassRegistryTest.java @@ -21,12 +21,7 @@ package org.apache.uima.cas.impl; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; -import java.util.Map; - import org.apache.uima.UIMAFramework; -import org.apache.uima.jcas.JCas; -import org.apache.uima.jcas.cas.TOP; -import org.apache.uima.resource.ResourceManager; import org.apache.uima.spi.SpiSentence; import org.apache.uima.spi.SpiToken; import org.apache.uima.util.CasCreationUtils; @@ -34,9 +29,10 @@ import org.apache.uima.util.Level; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class FSClassRegistryTest { +class FSClassRegistryTest { + @BeforeEach - public void setup() { + void setup() { System.setProperty(FSClassRegistry.RECORD_JCAS_CLASSLOADERS, "true"); // Calls to FSClassRegistry will fail unless the static initializer block in TypeSystemImpl @@ -48,15 +44,15 @@ public class FSClassRegistryTest { } @Test - public void thatCreatingResourceManagersWithExtensionClassloaderDoesNotFillUpCache() - throws Exception { + void thatCreatingResourceManagersWithExtensionClassloaderDoesNotFillUpCache() throws Exception { int numberOfCachedClassloadersAtStart = FSClassRegistry.clToType2JCasSize(); for (int i = 0; i < 5; i++) { - ResourceManager resMgr = UIMAFramework.newDefaultResourceManager(); + var resMgr = UIMAFramework.newDefaultResourceManager(); resMgr.setExtensionClassLoader(getClass().getClassLoader(), true); - JCas jcas = CasCreationUtils.createCas(null, null, null, resMgr).getJCas(); - ClassLoader cl = jcas.getCasImpl().getJCasClassLoader(); + var jcas = CasCreationUtils.createCas(null, null, null, resMgr).getJCas(); + + var cl = jcas.getCasImpl().getJCasClassLoader(); assertThat(cl.getResource(FSClassRegistryTest.class.getName().replace(".", "/") + ".class")) // .isNotNull(); @@ -70,14 +66,15 @@ public class FSClassRegistryTest { } @Test - public void thatCreatingResourceManagersWithExtensionPathDoesNotFillUpCache() throws Exception { - int numberOfCachedClassloadersAtStart = FSClassRegistry.clToType2JCasSize(); + void thatCreatingResourceManagersWithExtensionPathDoesNotFillUpCache() throws Exception { + var numberOfCachedClassloadersAtStart = FSClassRegistry.clToType2JCasSize(); + for (int i = 0; i < 5; i++) { - ResourceManager resMgr = UIMAFramework.newDefaultResourceManager(); + var resMgr = UIMAFramework.newDefaultResourceManager(); resMgr.setExtensionClassPath("src/test/java", true); - JCas jcas = CasCreationUtils.createCas(null, null, null, resMgr).getJCas(); + var jcas = CasCreationUtils.createCas(null, null, null, resMgr).getJCas(); - ClassLoader cl = jcas.getCasImpl().getJCasClassLoader(); + var cl = jcas.getCasImpl().getJCasClassLoader(); assertThat(cl.getResource(FSClassRegistryTest.class.getName().replace(".", "/") + ".java")) // .isNotNull(); @@ -91,9 +88,8 @@ public class FSClassRegistryTest { } @Test - public void thatJCasClassesCanBeLoadedThroughSPI() throws Exception { - Map<String, Class<? extends TOP>> jcasClasses = FSClassRegistry - .loadJCasClassesFromSPI(getClass().getClassLoader()); + void thatJCasClassesCanBeLoadedThroughSPI() throws Exception { + var jcasClasses = FSClassRegistry.loadJCasClassesFromSPI(getClass().getClassLoader()); assertThat(jcasClasses).containsOnly( // entry(SpiToken.class.getName(), SpiToken.class), // diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/test/FSCreatedInPearContextTest.java b/uimaj-core/src/test/java/org/apache/uima/cas/test/FSCreatedInPearContextTest.java index a130d6a0d..ef372c774 100644 --- a/uimaj-core/src/test/java/org/apache/uima/cas/test/FSCreatedInPearContextTest.java +++ b/uimaj-core/src/test/java/org/apache/uima/cas/test/FSCreatedInPearContextTest.java @@ -27,12 +27,10 @@ import java.io.File; import java.io.IOException; import java.net.URL; -import org.apache.uima.cas.Type; import org.apache.uima.cas.impl.CASImpl; -import org.apache.uima.cas.test.JCasClassLoaderTest.IsolatingClassloader; import org.apache.uima.internal.util.UIMAClassLoader; -import org.apache.uima.jcas.tcas.Annotation; import org.apache.uima.resource.metadata.TypeSystemDescription; +import org.apache.uima.test.IsolatingClassloader; import org.apache.uima.util.InvalidXMLException; import org.apache.uima.util.XMLInputSource; import org.junit.jupiter.api.Test; @@ -42,17 +40,17 @@ public class FSCreatedInPearContextTest { @Test public void thatOneTrampolineIsUsedWhenClassLoaderIsSwitched() throws Exception, IOException { - ClassLoader rootCl = getClass().getClassLoader(); + var rootCl = getClass().getClassLoader(); - IsolatingClassloader clForToken = new IsolatingClassloader("Token", rootCl) + var clForToken = new IsolatingClassloader("Token", rootCl) .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - CASImpl casImpl = (CASImpl) createCas(loadTokensAndSentencesTS(), null, null, null); + var casImpl = (CASImpl) createCas(loadTokensAndSentencesTS(), null, null, null); casImpl.switchClassLoaderLockCasCL(new UIMAClassLoader(new URL[0], clForToken)); casImpl.setDocumentText("Test"); - Type tokenType = casImpl.getTypeSystem().getType(Token.class.getName()); - Annotation token = casImpl.createAnnotation(tokenType, 0, 1); + var tokenType = casImpl.getTypeSystem().getType(Token.class.getName()); + var token = casImpl.createAnnotation(tokenType, 0, 1); token.addToIndexes(); assertThat(token.getClass().getClassLoader()) .as("Trampoline returned by createAnnotation after classloader switch") @@ -61,23 +59,25 @@ public class FSCreatedInPearContextTest { assertThat(casImpl.select(Token.type).asList()) // .as("Same trampoline returned by [select(Token.type)] after classloader switch") .usingElementComparator((a, b) -> a == b ? 0 : 1) // - .containsExactly(token).allMatch(t -> t.getClass().getClassLoader() == clForToken); + .containsExactly(token) // + .allMatch(t -> t.getClass().getClassLoader() == clForToken); casImpl.restoreClassLoaderUnlockCas(); assertThat(casImpl.select(Token.type).asList()) // .as("After switching back out of the the classloader context, we get the base FS") .usingElementComparator((a, b) -> a._id() == b._id() ? 0 : 1) // - .containsExactly(token).allMatch(t -> t.getClass().getClassLoader() == rootCl); + .containsExactly(token) // + .allMatch(t -> t.getClass().getClassLoader() == rootCl); } @Test public void thatResettingCasInPearContextWorks() throws Exception, IOException { - ClassLoader rootCl = getClass().getClassLoader(); + var rootCl = getClass().getClassLoader(); - IsolatingClassloader clForToken = new IsolatingClassloader("Token", rootCl) + var clForToken = new IsolatingClassloader("Token", rootCl) .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - CASImpl casImpl = (CASImpl) createCas(loadTokensAndSentencesTS(), null, null, null); + var casImpl = (CASImpl) createCas(loadTokensAndSentencesTS(), null, null, null); casImpl.switchClassLoaderLockCasCL(new UIMAClassLoader(new URL[0], clForToken)); casImpl.setDocumentText("Test"); @@ -86,8 +86,8 @@ public class FSCreatedInPearContextTest { casImpl.resetNoQuestions(); assertThatNoException().isThrownBy(() -> { - Type tokenType = casImpl.getTypeSystem().getType(Token.class.getName()); - Annotation token = casImpl.createAnnotation(tokenType, 0, 1); + var tokenType = casImpl.getTypeSystem().getType(Token.class.getName()); + var token = casImpl.createAnnotation(tokenType, 0, 1); token.addToIndexes(); }); } diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/test/JCasClassLoaderTest.java b/uimaj-core/src/test/java/org/apache/uima/cas/test/JCasClassLoaderTest.java index ce7fddfd9..d7c93c5a5 100644 --- a/uimaj-core/src/test/java/org/apache/uima/cas/test/JCasClassLoaderTest.java +++ b/uimaj-core/src/test/java/org/apache/uima/cas/test/JCasClassLoaderTest.java @@ -20,6 +20,7 @@ package org.apache.uima.cas.test; import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; +import static org.apache.commons.io.FileUtils.copyFile; import static org.apache.uima.UIMAFramework.getResourceSpecifierFactory; import static org.apache.uima.UIMAFramework.getXMLParser; import static org.apache.uima.UIMAFramework.newDefaultResourceManager; @@ -32,26 +33,16 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.File; import java.io.IOException; -import java.io.InputStream; +import java.lang.invoke.MethodHandles; import java.net.MalformedURLException; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; +import org.apache.uima.UIMAFramework; import org.apache.uima.analysis_component.Annotator_ImplBase; import org.apache.uima.analysis_component.JCasAnnotator_ImplBase; import org.apache.uima.analysis_engine.AnalysisEngine; -import org.apache.uima.analysis_engine.AnalysisEngineDescription; import org.apache.uima.analysis_engine.AnalysisEngineProcessException; import org.apache.uima.analysis_engine.impl.PrimitiveAnalysisEngine_impl; import org.apache.uima.cas.CAS; @@ -66,27 +57,36 @@ import org.apache.uima.resource.ResourceInitializationException; import org.apache.uima.resource.ResourceManager; import org.apache.uima.resource.metadata.TypeDescription; import org.apache.uima.resource.metadata.TypeSystemDescription; +import org.apache.uima.spi.JCasClassProviderForTesting; +import org.apache.uima.test.IsolatingClassloader; import org.apache.uima.util.InvalidXMLException; import org.apache.uima.util.XMLInputSource; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import x.y.z.Token; +import x.y.z.TokenType; public class JCasClassLoaderTest { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public static final String TYPE_NAME_TOKEN = Token.class.getName(); public static final String TYPE_NAME_ARRAY_HOST = "uima.testing.ArrayHost"; public static final String FEAT_NAME_ARRAY_HOST_VALUES = "values"; - public static Class casTokenClassViaClassloader; - public static Class casTokenClassViaCas; - public static Class addTokenAETokenClass; - public static Class fetchTokenAETokenClass; - public static Class indexedTokenClass; + public static Class<?> casTokenClassViaClassloader; + public static Class<?> casTokenClassViaCas; + public static Class<?> addTokenAETokenClass; + public static Class<?> fetchTokenAETokenClass; + public static Class<?> indexedTokenClass; public static boolean fetchThrowsClassCastException; - public static Class tokenClassAddedToArray; - public static Class tokenClassFetchedFromArray; + public static Class<?> tokenClassAddedToArray; + public static Class<?> tokenClassFetchedFromArray; @BeforeEach public void setup() { @@ -119,37 +119,36 @@ public class JCasClassLoaderTest { * engines should use its own version of the JCas wrappers to access the CAS. */ @Test - public void thatCASCanBeDefinedWithoutJCasWrappersAndTheyComeInWithAnnotatorsViaClasspath() + void thatCASCanBeDefinedWithoutJCasWrappersAndTheyComeInWithAnnotatorsViaClasspath() throws Exception { - ClassLoader rootCl = getClass().getClassLoader(); + var rootCl = getClass().getClassLoader(); // We do not want the CAS to know the Token JCas wrapper when it gets initialized - IsolatingClassloader clForCas = new IsolatingClassloader("CAS", rootCl) + var clForCas = new IsolatingClassloader("CAS", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - File cpBase = new File("target/test-output/JCasClassLoaderTest/classes"); - File cpPackageBase = new File(cpBase, "org/apache/uima/cas/test"); + var cpBase = new File("target/test-output/JCasClassLoaderTest/classes"); + var cpPackageBase = new File(cpBase, "org/apache/uima/cas/test"); cpPackageBase.mkdirs(); - FileUtils.copyFile(new File("target/test-classes/org/apache/uima/cas/test/Token.class"), + copyFile(new File("target/test-classes/org/apache/uima/cas/test/Token.class"), new File(cpPackageBase, "Token.class")); - FileUtils.copyFile(new File( + copyFile(new File( "target/test-classes/org/apache/uima/cas/test/JCasClassLoaderTest$AddATokenAnnotator.class"), new File(cpPackageBase, "JCasClassLoaderTest$AddATokenAnnotator.class")); - FileUtils.copyFile(new File( + copyFile(new File( "target/test-classes/org/apache/uima/cas/test/JCasClassLoaderTest$FetchTheTokenAnnotator.class"), new File(cpPackageBase, "JCasClassLoaderTest$FetchTheTokenAnnotator.class")); - JCas jcas = makeJCas(clForCas); - AnalysisEngine addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, cpBase); - AnalysisEngine fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, - cpBase); + var jcas = makeJCas(clForCas); + var addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, cpBase); + var fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, cpBase); jcas.setDocumentText("test"); addATokenAnnotator.process(jcas); fetchTheTokenAnnotator.process(jcas); - try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + try (var softly = new AutoCloseableSoftAssertions()) { softly.assertThat(casTokenClassViaClassloader).isNull(); softly.assertThat(casTokenClassViaCas).isSameAs(Annotation.class); softly.assertThat(addTokenAETokenClass).isNotNull(); @@ -183,26 +182,25 @@ public class JCasClassLoaderTest { * engines should use its own version of the JCas wrappers to access the CAS. */ @Test - public void thatCASCanBeDefinedWithoutJCasWrappersAndTheyComeInWithAnnotatorsViaClassloader() + void thatCASCanBeDefinedWithoutJCasWrappersAndTheyComeInWithAnnotatorsViaClassloader() throws Exception { - ClassLoader rootCl = getClass().getClassLoader(); + var rootCl = getClass().getClassLoader(); // We do not want the CAS to know the Token JCas wrapper when it gets initialized - IsolatingClassloader clForCas = new IsolatingClassloader("CAS", rootCl) + var clForCas = new IsolatingClassloader("CAS", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForAddATokenAnnotator = new IsolatingClassloader("AddATokenAnnotator", rootCl) + var clForAddATokenAnnotator = new IsolatingClassloader("AddATokenAnnotator", rootCl) .redefining("^.*AddATokenAnnotator$") .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForFetchTheTokenAnnotator = new IsolatingClassloader("FetchTheTokenAnnotator", - rootCl).redefining("^.*FetchTheTokenAnnotator$") - .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); + var clForFetchTheTokenAnnotator = new IsolatingClassloader("FetchTheTokenAnnotator", rootCl) + .redefining("^.*FetchTheTokenAnnotator$") + .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - JCas jcas = makeJCas(clForCas); - AnalysisEngine addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, - clForAddATokenAnnotator); - AnalysisEngine fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, + var jcas = makeJCas(clForCas); + var addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, clForAddATokenAnnotator); + var fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, clForFetchTheTokenAnnotator); jcas.setDocumentText("test"); @@ -210,7 +208,7 @@ public class JCasClassLoaderTest { addATokenAnnotator.process(jcas); fetchTheTokenAnnotator.process(jcas); - try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + try (var softly = new AutoCloseableSoftAssertions()) { softly.assertThat(casTokenClassViaClassloader).isNull(); softly.assertThat(casTokenClassViaCas).isSameAs(Annotation.class); softly.assertThat(addTokenAETokenClass).isNotNull(); @@ -245,23 +243,22 @@ public class JCasClassLoaderTest { * was first initialized. */ @Test - public void thatAnnotatorsCanLocallyUseDifferentJCasWrappers() throws Exception { - ClassLoader rootCl = getClass().getClassLoader(); + void thatAnnotatorsCanLocallyUseDifferentJCasWrappers() throws Exception { + var rootCl = getClass().getClassLoader(); - IsolatingClassloader clForCas = new IsolatingClassloader("CAS", rootCl); + var clForCas = new IsolatingClassloader("CAS", rootCl); - ClassLoader clForAddATokenAnnotator = new IsolatingClassloader("AddATokenAnnotator", rootCl) + var clForAddATokenAnnotator = new IsolatingClassloader("AddATokenAnnotator", rootCl) .redefining("^.*AddATokenAnnotator$") .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForFetchTheTokenAnnotator = new IsolatingClassloader("FetchTheTokenAnnotator", - rootCl).redefining("^.*FetchTheTokenAnnotator$") - .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); + var clForFetchTheTokenAnnotator = new IsolatingClassloader("FetchTheTokenAnnotator", rootCl) + .redefining("^.*FetchTheTokenAnnotator$") + .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - JCas jcas = makeJCas(clForCas); - AnalysisEngine addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, - clForAddATokenAnnotator); - AnalysisEngine fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, + var jcas = makeJCas(clForCas); + var addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, clForAddATokenAnnotator); + var fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, clForFetchTheTokenAnnotator); jcas.setDocumentText("test"); @@ -269,7 +266,7 @@ public class JCasClassLoaderTest { addATokenAnnotator.process(jcas); fetchTheTokenAnnotator.process(jcas); - try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + try (var softly = new AutoCloseableSoftAssertions()) { softly.assertThat(casTokenClassViaClassloader).isNotNull(); softly.assertThat(casTokenClassViaCas) .as("System-level Token wrapper loader and Token wrapper in the CAS are the same") @@ -314,27 +311,26 @@ public class JCasClassLoaderTest { * type in both annotators. */ @Test - public void thatTypeSystemCanComeFromItsOwnClassLoader() throws Exception { - ClassLoader rootCl = getClass().getClassLoader(); + void thatTypeSystemCanComeFromItsOwnClassLoader() throws Exception { + var rootCl = getClass().getClassLoader(); - IsolatingClassloader clForCas = new IsolatingClassloader("CAS", rootCl) + var clForCas = new IsolatingClassloader("CAS", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForTS = new IsolatingClassloader("TS", rootCl) + var clForTS = new IsolatingClassloader("TS", rootCl) .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForAddATokenAnnotator = new IsolatingClassloader("AddATokenAnnotator", rootCl) - .redefining("^.*AddATokenAnnotator$") + var clForAddATokenAnnotator = new IsolatingClassloader("AddATokenAnnotator", rootCl) + .redefining(AddATokenAnnotator.class) .delegating("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*", clForTS); - ClassLoader clForFetchTheTokenAnnotator = new IsolatingClassloader("FetchTheTokenAnnotator", - rootCl).redefining("^.*FetchTheTokenAnnotator$") - .delegating("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*", clForTS); + var clForFetchTheTokenAnnotator = new IsolatingClassloader("FetchTheTokenAnnotator", rootCl) + .redefining(FetchTheTokenAnnotator.class) + .delegating("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*", clForTS); - JCas jcas = makeJCas(clForCas); - AnalysisEngine addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, - clForAddATokenAnnotator); - AnalysisEngine fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, + var jcas = makeJCas(clForCas); + var addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotator.class, clForAddATokenAnnotator); + var fetchTheTokenAnnotator = makeAnalysisEngine(FetchTheTokenAnnotator.class, clForFetchTheTokenAnnotator); jcas.setDocumentText("test"); @@ -342,7 +338,7 @@ public class JCasClassLoaderTest { addATokenAnnotator.process(jcas); fetchTheTokenAnnotator.process(jcas); - try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + try (var softly = new AutoCloseableSoftAssertions()) { softly.assertThat(casTokenClassViaClassloader).isNull(); softly.assertThat(casTokenClassViaCas).isSameAs(Annotation.class); softly.assertThat(addTokenAETokenClass).isNotNull(); @@ -365,30 +361,28 @@ public class JCasClassLoaderTest { } @Test - public void thatFSArraySpliteratorReturnsProperJCasWrapper() throws Exception { - ClassLoader rootCl = getClass().getClassLoader(); + void thatFSArraySpliteratorReturnsProperJCasWrapper() throws Exception { + var rootCl = getClass().getClassLoader(); - IsolatingClassloader clForCas = new IsolatingClassloader("CAS", rootCl) + var clForCas = new IsolatingClassloader("CAS", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForCreators = new IsolatingClassloader("Creators", rootCl) + var clForCreators = new IsolatingClassloader("Creators", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*") - .redefining("^.*AddATokenAnnotatorNoJCas$") - .redefining("^.*AddTokenToArrayAnnotatorNoJCas$"); + .redefining(AddATokenAnnotatorNoJCas.class) + .redefining(AddTokenToArrayAnnotatorNoJCas.class); - ClassLoader clForAccessors = new IsolatingClassloader("Accessors", rootCl) + var clForAccessors = new IsolatingClassloader("Accessors", rootCl) .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*") - .redefining("^.*FetchTokenFromArrayViaSpliteratorAnnotator$"); + .redefining(FetchTokenFromArrayViaSpliteratorAnnotator.class); - TypeSystemDescription tsd = mergeTypeSystems( - asList(loadTokensAndSentencesTS(), makeArrayTestTS())); + var tsd = mergeTypeSystems(asList(loadTokensAndSentencesTS(), makeArrayTestTS())); - JCas jcas = makeJCas(clForCas, tsd); - AnalysisEngine addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotatorNoJCas.class, + var jcas = makeJCas(clForCas, tsd); + var addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotatorNoJCas.class, clForCreators); + var addTokenToArrayAnnotator = makeAnalysisEngine(AddTokenToArrayAnnotatorNoJCas.class, clForCreators); - AnalysisEngine addTokenToArrayAnnotator = makeAnalysisEngine( - AddTokenToArrayAnnotatorNoJCas.class, clForCreators); - AnalysisEngine fetchTokenFromArrayViaSpliteratorAnnotator = makeAnalysisEngine( + var fetchTokenFromArrayViaSpliteratorAnnotator = makeAnalysisEngine( FetchTokenFromArrayViaSpliteratorAnnotator.class, clForAccessors); jcas.setDocumentText("test"); @@ -397,7 +391,7 @@ public class JCasClassLoaderTest { addTokenToArrayAnnotator.process(jcas); fetchTokenFromArrayViaSpliteratorAnnotator.process(jcas); - try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + try (var softly = new AutoCloseableSoftAssertions()) { softly.assertThat(casTokenClassViaClassloader).isNull(); softly.assertThat(casTokenClassViaCas).isSameAs(Annotation.class); softly.assertThat(addTokenAETokenClass).isNotNull(); @@ -410,30 +404,28 @@ public class JCasClassLoaderTest { } @Test - public void thatFSArrayToArrayReturnsProperJCasWrapper() throws Exception { - ClassLoader rootCl = getClass().getClassLoader(); + void thatFSArrayToArrayReturnsProperJCasWrapper() throws Exception { + var rootCl = getClass().getClassLoader(); - IsolatingClassloader clForCas = new IsolatingClassloader("CAS", rootCl) + var clForCas = new IsolatingClassloader("CAS", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*"); - ClassLoader clForCreators = new IsolatingClassloader("Creators", rootCl) + var clForCreators = new IsolatingClassloader("Creators", rootCl) .hiding("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*") - .redefining("^.*AddATokenAnnotatorNoJCas$") - .redefining("^.*AddTokenToArrayAnnotatorNoJCas$"); + .redefining(AddATokenAnnotatorNoJCas.class) + .redefining(AddTokenToArrayAnnotatorNoJCas.class); - ClassLoader clForAccessors = new IsolatingClassloader("Accessors", rootCl) + var clForAccessors = new IsolatingClassloader("Accessors", rootCl) .redefining("org\\.apache\\.uima\\.cas\\.test\\.Token(_Type)?.*") - .redefining("^.*FetchTokenFromArrayViaToArrayAnnotator$"); + .redefining(FetchTokenFromArrayViaToArrayAnnotator.class); - TypeSystemDescription tsd = mergeTypeSystems( - asList(loadTokensAndSentencesTS(), makeArrayTestTS())); + var tsd = mergeTypeSystems(asList(loadTokensAndSentencesTS(), makeArrayTestTS())); - JCas jcas = makeJCas(clForCas, tsd); - AnalysisEngine addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotatorNoJCas.class, + var jcas = makeJCas(clForCas, tsd); + var addATokenAnnotator = makeAnalysisEngine(AddATokenAnnotatorNoJCas.class, clForCreators); + var addTokenToArrayAnnotator = makeAnalysisEngine(AddTokenToArrayAnnotatorNoJCas.class, clForCreators); - AnalysisEngine addTokenToArrayAnnotator = makeAnalysisEngine( - AddTokenToArrayAnnotatorNoJCas.class, clForCreators); - AnalysisEngine fetchTokenFromArrayViaSpliteratorAnnotator = makeAnalysisEngine( + var fetchTokenFromArrayViaSpliteratorAnnotator = makeAnalysisEngine( FetchTokenFromArrayViaToArrayAnnotator.class, clForAccessors); jcas.setDocumentText("test"); @@ -442,7 +434,7 @@ public class JCasClassLoaderTest { addTokenToArrayAnnotator.process(jcas); fetchTokenFromArrayViaSpliteratorAnnotator.process(jcas); - try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + try (var softly = new AutoCloseableSoftAssertions()) { softly.assertThat(casTokenClassViaClassloader).isNull(); softly.assertThat(casTokenClassViaCas).isSameAs(Annotation.class); softly.assertThat(addTokenAETokenClass).isNotNull(); @@ -454,6 +446,41 @@ public class JCasClassLoaderTest { } } + @Test + void thatFeatureRangeClassRedefinedInPearDoesNotCauseProblems(@TempDir File aTemp) + throws Exception { + LOG.info("-- Base runtime context --------------------------------------------------"); + LOG.info("{} loaded using {}", Token.class, Token.class.getClassLoader()); + LOG.info("{} loaded using {}", TokenType.class, TokenType.class.getClassLoader()); + + var tsd = UIMAFramework.getXMLParser().parseTypeSystemDescription( + new XMLInputSource("src/test/java/org/apache/uima/jcas/test/generatedx.xml")); + + LOG.info("-- JCas classloader context ----------------------------------------------"); + var rootCl = getClass().getClassLoader(); + var clForCas = new IsolatingClassloader("CAS Classloader", rootCl) // + .redefining(TokenType.class) // + .redefining(JCasClassProviderForTesting.class) // + .redefining(JCasCreator.class); + + var jcasCreatorClass = clForCas.loadClass(JCasCreatorImpl.class.getName()); + var creator = (JCasCreator) jcasCreatorClass.getDeclaredConstructor().newInstance(); + var jcas = creator.createJCas(clForCas, tsd); + var cas = jcas.getCas(); + + var t = cas.createFS(cas.getTypeSystem().getType(Token.class.getName())); + var tt = cas.createFS(cas.getTypeSystem().getType(TokenType.class.getName())); + + LOG.info("{} loaded using {}", t.getClass(), t.getClass().getClassLoader()); + LOG.info("{} loaded using {}", tt.getClass(), tt.getClass().getClassLoader()); + + assertThat(t.getClass().getClassLoader()) // + .isSameAs(Token.class.getClassLoader()); + + assertThat(tt.getClass().getClassLoader()) // + .isSameAs(clForCas); + } + public static Class<?> loadTokenClass(ClassLoader cl) { try { return cl.loadClass(TYPE_NAME_TOKEN); @@ -493,9 +520,11 @@ public class JCasClassLoaderTest { } private JCas makeJCas(IsolatingClassloader cl, TypeSystemDescription tsd) throws Exception { - cl.redefining("^.*JCasCreatorImpl$"); - Class jcasCreatorClass = cl.loadClass(JCasCreatorImpl.class.getName()); - JCasCreator creator = (JCasCreator) jcasCreatorClass.newInstance(); + cl.redefining(JCasCreatorImpl.class); + var jcasCreatorClass = cl.loadClass(JCasCreatorImpl.class.getName()); + var declaredConstructor = jcasCreatorClass.getDeclaredConstructor(); + declaredConstructor.setAccessible(true); + var creator = (JCasCreator) declaredConstructor.newInstance(); return creator.createJCas(cl, tsd); } @@ -505,12 +534,13 @@ public class JCasClassLoaderTest { */ private AnalysisEngine makeAnalysisEngine(Class<? extends Annotator_ImplBase> aeClass, ClassLoader cl) throws ResourceInitializationException, MalformedURLException { - ResourceManager resMgr = newDefaultResourceManager(); + var resMgr = newDefaultResourceManager(); resMgr.setExtensionClassLoader(cl, false); + printTokenClassLoaderInfo("AE creation: " + aeClass.getSimpleName(), resMgr.getExtensionClassLoader()); - AnalysisEngineDescription desc = getResourceSpecifierFactory() - .createAnalysisEngineDescription(); + + var desc = getResourceSpecifierFactory().createAnalysisEngineDescription(); desc.setAnnotatorImplementationName(aeClass.getName()); desc.setPrimitive(true); return produceAnalysisEngine(desc, resMgr, null); @@ -522,13 +552,14 @@ public class JCasClassLoaderTest { */ private AnalysisEngine makeAnalysisEngine(Class<? extends Annotator_ImplBase> aeClass, File... classPath) throws ResourceInitializationException, MalformedURLException { - String cp = Stream.of(classPath).map(Object::toString).collect(joining(File.pathSeparator)); - ResourceManager resMgr = newDefaultResourceManager(); + var cp = Stream.of(classPath).map(Object::toString).collect(joining(File.pathSeparator)); + var resMgr = newDefaultResourceManager(); resMgr.setExtensionClassPath(cp, false); + printTokenClassLoaderInfo("AE creation: " + aeClass.getSimpleName(), resMgr.getExtensionClassLoader()); - AnalysisEngineDescription desc = getResourceSpecifierFactory() - .createAnalysisEngineDescription(); + + var desc = getResourceSpecifierFactory().createAnalysisEngineDescription(); desc.setAnnotatorImplementationName(aeClass.getName()); desc.setPrimitive(true); return produceAnalysisEngine(desc, resMgr, null); @@ -575,8 +606,8 @@ public class JCasClassLoaderTest { public static class AddATokenAnnotatorNoJCas extends JCasAnnotator_ImplBase { @Override public void process(JCas aJCas) throws AnalysisEngineProcessException { - AnnotationFS token = aJCas.getCas().createAnnotation( - aJCas.getTypeSystem().getType(TYPE_NAME_TOKEN), 0, aJCas.getDocumentText().length()); + var token = aJCas.getCas().createAnnotation(aJCas.getTypeSystem().getType(TYPE_NAME_TOKEN), 0, + aJCas.getDocumentText().length()); addTokenAETokenClass = token.getClass(); System.out.printf("[AE runtime: %s] CAS class loader: %s%n", getClass().getName(), aJCas.getCasImpl().getJCasClassLoader()); @@ -593,13 +624,13 @@ public class JCasClassLoaderTest { @Override public void process(JCas aJCas) throws AnalysisEngineProcessException { - AnnotationFS token = (AnnotationFS) aJCas - .select(aJCas.getTypeSystem().getType(TYPE_NAME_TOKEN)).single(); + var token = (AnnotationFS) aJCas.select(aJCas.getTypeSystem().getType(TYPE_NAME_TOKEN)) + .single(); tokenClassAddedToArray = token.getClass(); - AnnotationFS arrayHost = aJCas.getCas() + var arrayHost = aJCas.getCas() .createAnnotation(aJCas.getTypeSystem().getType(TYPE_NAME_ARRAY_HOST), 0, 0); - FSArray array = new FSArray<>(aJCas, 1); + var array = new FSArray<>(aJCas, 1); array.set(0, token); arrayHost.setFeatureValue( arrayHost.getType().getFeatureByBaseName(FEAT_NAME_ARRAY_HOST_VALUES), array); @@ -608,12 +639,13 @@ public class JCasClassLoaderTest { } public static class FetchTokenFromArrayViaSpliteratorAnnotator extends JCasAnnotator_ImplBase { + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void process(JCas aJCas) throws AnalysisEngineProcessException { FeatureStructure arrayHost = aJCas.select(aJCas.getTypeSystem().getType(TYPE_NAME_ARRAY_HOST)) .single(); - FSArray array = (FSArray) arrayHost.getFeatureValue( + var array = (FSArray) arrayHost.getFeatureValue( arrayHost.getType().getFeatureByBaseName(FEAT_NAME_ARRAY_HOST_VALUES)); tokenClassFetchedFromArray = StreamSupport.stream(array.spliterator(), false).findFirst() @@ -622,15 +654,15 @@ public class JCasClassLoaderTest { } public static class FetchTokenFromArrayViaToArrayAnnotator extends JCasAnnotator_ImplBase { + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void process(JCas aJCas) throws AnalysisEngineProcessException { - FeatureStructure arrayHost = aJCas.select(aJCas.getTypeSystem().getType(TYPE_NAME_ARRAY_HOST)) - .single(); + var arrayHost = aJCas.select(aJCas.getTypeSystem().getType(TYPE_NAME_ARRAY_HOST)).single(); - FSArray array = (FSArray) arrayHost.getFeatureValue( + var array = (FSArray) arrayHost.getFeatureValue( arrayHost.getType().getFeatureByBaseName(FEAT_NAME_ARRAY_HOST_VALUES)); - Class withEmptyTemplate = array.toArray(new TOP[0])[0].getClass(); + var withEmptyTemplate = array.toArray(new TOP[0])[0].getClass(); tokenClassFetchedFromArray = array.toArray(new TOP[1])[0].getClass(); assertThat(tokenClassFetchedFromArray).isSameAs(withEmptyTemplate); } @@ -643,13 +675,13 @@ public class JCasClassLoaderTest { System.out.printf("%s class loader: %s%n", getClass().getName(), getClass().getClassLoader()); System.out.printf("[AE runtime: %s] %s %d %n", getClass().getName(), Token.class.getName(), Token.class.hashCode()); - Object casToken = aJCas.getAllIndexedFS(Token.class).get(); + var casToken = aJCas.getAllIndexedFS(Token.class).get(); System.out.printf("[AE runtime CAS: %s] %s %d %n", getClass().getName(), casToken.getClass().getName(), casToken.getClass().hashCode()); indexedTokenClass = casToken.getClass(); try { - List<Token> tokens = new ArrayList<>(); + var tokens = new ArrayList<Token>(); aJCas.getAllIndexedFS(Token.class).forEachRemaining(tokens::add); } catch (ClassCastException e) { fetchThrowsClassCastException = true; @@ -657,98 +689,4 @@ public class JCasClassLoaderTest { } } } - - /** - * Special ClassLoader that helps us modeling different class loader topologies. - */ - public static class IsolatingClassloader extends ClassLoader { - - private final Set<String> hideClassesPatterns = new HashSet<>(); - private final Set<String> redefineClassesPatterns = new HashSet<>(); - private final Map<String, ClassLoader> delegates = new LinkedHashMap<>(); - private final String id; - - private Map<String, Class<?>> loadedClasses = new HashMap<>(); - - public IsolatingClassloader(String name, ClassLoader parent) { - super(parent); - - id = name; - } - - public IsolatingClassloader hiding(String... patterns) { - hideClassesPatterns.addAll(asList(patterns)); - return this; - } - - public IsolatingClassloader redefining(String... patterns) { - redefineClassesPatterns.addAll(asList(patterns)); - return this; - } - - public IsolatingClassloader delegating(String pattern, ClassLoader delegate) { - delegates.put(pattern, delegate); - return this; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("["); - sb.append(id); - sb.append(", loaded="); - sb.append(loadedClasses.size()); - sb.append("]"); - return sb.toString(); - } - - @Override - protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - Optional<ClassLoader> delegate = delegates.entrySet().stream() - .filter(e -> name.matches(e.getKey())).map(Entry::getValue).findFirst(); - if (delegate.isPresent()) { - return delegate.get().loadClass(name); - } - - if (hideClassesPatterns.stream().anyMatch(name::matches)) { - System.out.printf("[%s] prevented access to hidden class: %s%n", id, name); - throw new ClassNotFoundException(name); - } - - if (redefineClassesPatterns.stream().anyMatch(name::matches)) { - Class<?> loadedClass = loadedClasses.get(name); - if (loadedClass != null) { - return loadedClass; - } - - System.out.printf("[%s] redefining class: %s%n", id, name); - - String internalName = name.replace(".", "/") + ".class"; - InputStream is = getParent().getResourceAsStream(internalName); - if (is == null) { - throw new ClassNotFoundException(name); - } - - try { - byte[] bytes = IOUtils.toByteArray(is); - Class<?> cls = defineClass(name, bytes, 0, bytes.length); - if (cls.getPackage() == null) { - int packageSeparator = name.lastIndexOf('.'); - if (packageSeparator != -1) { - String packageName = name.substring(0, packageSeparator); - definePackage(packageName, null, null, null, null, null, null, null); - } - } - loadedClasses.put(name, cls); - return cls; - } catch (IOException ex) { - throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); - } - } - - return super.loadClass(name, resolve); - } - } - } -} \ No newline at end of file +} diff --git a/uimaj-parent/pom.xml b/uimaj-parent/pom.xml index 2419a8d5d..7ae42db3a 100644 --- a/uimaj-parent/pom.xml +++ b/uimaj-parent/pom.xml @@ -225,6 +225,11 @@ <artifactId>slf4j-api</artifactId> <version>${slf4j-version}</version> </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>${slf4j-version}</version> + </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> diff --git a/uimaj-test-util/src/main/java/org/apache/uima/test/IsolatingClassloader.java b/uimaj-test-util/src/main/java/org/apache/uima/test/IsolatingClassloader.java new file mode 100644 index 000000000..4abcb4dc2 --- /dev/null +++ b/uimaj-test-util/src/main/java/org/apache/uima/test/IsolatingClassloader.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.uima.test; + +import static java.util.Arrays.asList; +import static java.util.regex.Pattern.quote; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; + +/** + * Special ClassLoader that helps us modeling different class loader topologies. + */ +public class IsolatingClassloader extends ClassLoader { + + private final Set<String> hideClassesPatterns = new HashSet<>(); + private final Set<String> redefineClassesPatterns = new HashSet<>(); + private final Map<String, ClassLoader> delegates = new LinkedHashMap<>(); + private final String id; + + private Map<String, Class<?>> loadedClasses = new HashMap<>(); + + public IsolatingClassloader(String name, ClassLoader parent) { + super(parent); + + id = name; + } + + public IsolatingClassloader hiding(Package... packages) { + for (var pack : packages) { + hideClassesPatterns.add(quote(pack.getName()) + "\\..*"); + } + return this; + } + + public IsolatingClassloader hiding(Class<?>... classes) { + for (var clazz : classes) { + hideClassesPatterns.add(quote(clazz.getName())); + } + return this; + } + + public IsolatingClassloader hiding(String... patterns) { + hideClassesPatterns.addAll(asList(patterns)); + return this; + } + + public IsolatingClassloader redefining(Package... packages) { + for (var pack : packages) { + redefineClassesPatterns.add(quote(pack.getName()) + "\\..*"); + } + return this; + } + + public IsolatingClassloader redefining(Class<?>... classes) { + for (var clazz : classes) { + redefineClassesPatterns.add(quote(clazz.getName())); + } + return this; + } + + public IsolatingClassloader redefining(String... patterns) { + redefineClassesPatterns.addAll(asList(patterns)); + return this; + } + + public IsolatingClassloader delegating(String pattern, ClassLoader delegate) { + delegates.put(pattern, delegate); + return this; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(id); + sb.append(", loaded="); + sb.append(loadedClasses.size()); + sb.append("]"); + return sb.toString(); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Optional<ClassLoader> delegate = delegates.entrySet().stream() + .filter(e -> name.matches(e.getKey())).map(Entry::getValue).findFirst(); + if (delegate.isPresent()) { + return delegate.get().loadClass(name); + } + + if (hideClassesPatterns.stream().anyMatch(name::matches)) { + System.out.printf("[%s] prevented access to hidden class: %s%n", id, name); + throw new ClassNotFoundException(name); + } + + if (redefineClassesPatterns.stream().anyMatch(name::matches)) { + Class<?> loadedClass = loadedClasses.get(name); + if (loadedClass != null) { + return loadedClass; + } + + System.out.printf("[%s] redefining class: %s%n", id, name); + + String internalName = name.replace(".", "/") + ".class"; + InputStream is = getParent().getResourceAsStream(internalName); + if (is == null) { + throw new ClassNotFoundException(name); + } + + try { + var buffer = new ByteArrayOutputStream(); + is.transferTo(buffer); + byte[] bytes = buffer.toByteArray(); + Class<?> cls = defineClass(name, bytes, 0, bytes.length); + if (cls.getPackage() == null) { + int packageSeparator = name.lastIndexOf('.'); + if (packageSeparator != -1) { + String packageName = name.substring(0, packageSeparator); + definePackage(packageName, null, null, null, null, null, null, null); + } + } + loadedClasses.put(name, cls); + return cls; + } catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + return super.loadClass(name, resolve); + } + } +}
