Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.classpath
.project
.settings
.vscode
.vscode/
target
checkpoint_synchPoint.xml
checkpoint_synchPoint.xml.prev
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,23 @@ private static void getGeneratorsForTypeAndSubtypes(TypeImpl aTypeInfo,
jcci = t2jcci.get(typeInfo.getJCasClassName());
}

// PEAR-only: if the JCas class found for this type is not actually overridden by the PEAR
// (the PEAR classloader delegated to a parent), keep walking up the type hierarchy for an
// ancestor whose JCas class *is* PEAR-overridden. Without this, subtype FSes would be wrapped
// with the parent-loaded JCas class and would not be assignable to the PEAR's copy of the
// super-type, causing ClassCastException in idiomatic PEAR code such as
Comment on lines +1592 to +1596
// `for (T t : cas.select(T.class))`. See https://github.com/apache/uima-uimaj/issues/384.
if (isPear && !jcci.isPearOverride(tsi)) {
var ancestor = typeInfo;
Comment on lines +1592 to +1599
while ((ancestor = ancestor.getSuperType()) != null) {
var ancestorJcci = t2jcci.get(ancestor.getJCasClassName());
if (ancestorJcci != null && ancestorJcci.isPearOverride(tsi)) {
jcci = ancestorJcci;
break;
}
}
}

// skip entering a generator in the result if
// in a PEAR setup, and this cl is not the cl that loaded the JCas class.
// See method comment getGeneratorsForClassLoader(...) in for why.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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.cas.test;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.URL;

import org.apache.uima.cas.admin.CASFactory;
import org.apache.uima.cas.admin.FSIndexRepositoryMgr;
import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.cas.impl.TypeImpl;
import org.apache.uima.cas.impl.TypeSystemImpl;
import org.apache.uima.internal.util.UIMAClassLoader;
import org.apache.uima.jcas.cas.TOP;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.test.IsolatingClassloader;
import org.apache.uima.util.CasCreationUtils;
import org.junit.jupiter.api.Test;

/**
* Regression test for
* <a href="https://github.com/apache/uima-uimaj/issues/384">issue #384</a>.
* <p>
* In a PEAR scenario, the PEAR's classloader may redefine the JCas wrapper for a super-type while
* the wrappers for sub-types remain visible only via the parent classloader. Those sub-type
* wrappers therefore do not extend the PEAR's copy of the super-type. When PEAR code calls
* {@code select(superType.class)}, the iterator must not return sub-type instances that cannot be
* cast to the PEAR's super-type class -- otherwise idiomatic UIMA code such as
* {@code for (T t : cas.select(T.class))} fails with a {@link ClassCastException}.
*/
class SelectByClassInPearContextTest {

@Test
void thatSelectByClassFiltersIncompatibleSubtypeInstancesInPearContext() throws Exception {
var rootCl = getClass().getClassLoader();

// The PEAR redefines only the super-type (Level_1). The sub-type (Level_2) is NOT redefined,
// so when looked up via the PEAR classloader it is delegated to the parent classloader -- and
// that root-classloader Level_2 extends the root-classloader Level_1, not the PEAR's Level_1.
var clForLevel1 = new IsolatingClassloader("Level_1", rootCl)
.redefining("org\\.apache\\.uima\\.cas\\.test\\.Level_1(_Type)?.*");

var casImpl = (CASImpl) CasCreationUtils.createCas(buildLevelsTypeSystem(), null, null, null);

var level2Type = casImpl.getTypeSystem().getType(Level_2.class.getName());
var level2 = casImpl.createAnnotation(level2Type, 0, 1);
level2.addToIndexes();

casImpl.switchClassLoaderLockCasCL(new UIMAClassLoader(new URL[0], clForLevel1));

@SuppressWarnings({ "rawtypes", "unchecked" })
Class<? extends TOP> pearLevel1Class = (Class) clForLevel1.loadClass(Level_1.class.getName());
assertThat(pearLevel1Class.getClassLoader())
.as("Sanity check: the PEAR's Level_1 class is loaded by the PEAR classloader")
.isSameAs(clForLevel1);
assertThat(pearLevel1Class)
.as("Sanity check: the PEAR's Level_1 class is a different Class object than the "
+ "one loaded by the root classloader")
.isNotSameAs(Level_1.class);

// Sanity check: the Level_2 instance is reachable in the CAS via a type-based selector.
assertThat(casImpl.select(level2Type).asList())
.as("The Level_2 FS must be present in the CAS")
.hasSize(1);

// The actual contract under test: select(SuperType.class) must only return FSes that are
// assignable to the requested class. The Level_2 instance's JCas wrapper is loaded by the
// parent classloader and is not assignable to the PEAR's Level_1, so it must be filtered out.
assertThat(casImpl.select(pearLevel1Class).asList())
.as("select(PEAR_Level_1.class) must only return instances assignable to "
+ "the PEAR's copy of Level_1")
.allMatch(pearLevel1Class::isInstance);
}
Comment on lines +83 to +90

/**
* Builds a minimal type system with a Level_1 super-type and a Level_2 sub-type. JCas wrappers
* for both already exist in this package; they extend each other with matching {@code _TypeName}
* fields, so they are picked up automatically when the type system is committed.
*/
private static TypeSystemImpl buildLevelsTypeSystem() throws ResourceInitializationException {
var casMgr = (CASImpl) CASFactory.createCAS();
var tsi = (TypeSystemImpl) casMgr.getTypeSystemMgr();
TypeImpl level1 = tsi.addType(Level_1.class.getName(), tsi.annotType);
tsi.addType(Level_2.class.getName(), level1);
casMgr.commitTypeSystem();
try {
FSIndexRepositoryMgr irm = casMgr.getIndexRepositoryMgr();
casMgr.initCASIndexes();
irm.commit();
} catch (Exception e) {
throw new ResourceInitializationException(e);
}
return tsi;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public class Scenario3TestAnnotator
{
private static final String TYPE_NAME_COMPLEX_ANNOTATION_SUBTYPE = "org.apache.uima.it.pear_with_typesystem.type.ComplexAnnotationSubtype";

@SuppressWarnings("unused")
@Override
public void process(JCas aJCas) throws AnalysisEngineProcessException
{
Expand All @@ -43,8 +42,14 @@ public void process(JCas aJCas) throws AnalysisEngineProcessException
// The unit test should have prepared the CAS with one of these
assertFalse(aJCas.select(TYPE_NAME_COMPLEX_ANNOTATION_SUBTYPE).isEmpty());

// Iterating over the ComplexAnnotation instances should also return a ComplexAnnotationSubtype
// and that will trigger a ClassCastException - we have the assertion for this in the unit test
// Iterating over ComplexAnnotation instances also returns the ComplexAnnotationSubtype.
// The PEAR does not have a JCas wrapper for ComplexAnnotationSubtype, so the framework
// wraps it with the nearest PEAR-loaded ancestor wrapper (ComplexAnnotation). This means
// the iteration must succeed without a ClassCastException and the returned FS must be an
// instance of the PEAR-local ComplexAnnotation class even though its UIMA type is the
// sub-type. See https://github.com/apache/uima-uimaj/issues/384.
var complexAnnotation = aJCas.select(ComplexAnnotation.class).get();
assertTrue(ComplexAnnotation.class.isInstance(complexAnnotation));
assertTrue(TYPE_NAME_COMPLEX_ANNOTATION_SUBTYPE.equals(complexAnnotation.getType().getName()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import static org.apache.uima.pear.tools.PackageInstaller.installPackage;
import static org.apache.uima.util.CasCreationUtils.createCas;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.io.ByteArrayOutputStream;
import java.io.File;
Expand All @@ -50,7 +49,6 @@
import org.apache.uima.analysis_component.Annotator_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.cas.CAS;
import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.internal.util.Class_TCCL;
Expand Down Expand Up @@ -172,8 +170,11 @@ void testScenario2(@TempDir File aTemp) throws Exception
* PEAR can use {@link ComplexAnnotation} directly - we assume it is in the PEAR's local
* classpath and available at compile time.
* <p>
* However, PEAR does not know about {@link ComplexAnnotationSubtype}. Yet, it will find that
* the CAS contains an annotation of that type.
* However, PEAR does not know about {@link ComplexAnnotationSubtype}. The CAS still contains an
* annotation of that sub-type. Inside the PEAR, that FS must be exposed via the PEAR's copy of
* the nearest known ancestor wrapper ({@link ComplexAnnotation}) so that iterating with
* {@code select(ComplexAnnotation.class)} does not throw {@link ClassCastException}. See
* <a href="https://github.com/apache/uima-uimaj/issues/384">issue #384</a>.
*/
@Test
void testScenario3(@TempDir File aTemp) throws Exception
Expand All @@ -192,9 +193,11 @@ void testScenario3(@TempDir File aTemp) throws Exception
// Types provided from the global/pipeline level
.delegating(ComplexAnnotationSubtype.class, globalCL));

assertThatExceptionOfType(AnalysisEngineProcessException.class) //
.isThrownBy(() -> pearAnnotator.process(cas)) //
.withRootCauseInstanceOf(ClassCastException.class);
pearAnnotator.process(cas);

// After leaving the PEAR context the FS is still in the CAS with its original sub-type.
assertThat(cas.select(ComplexAnnotationSubtype.class).asList()) //
.hasSize(1);

pearAnnotator.getUimaContextAdmin().getResourceManager().destroy();
}
Expand Down
Loading