You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@uima.apache.org by re...@apache.org on 2022/04/08 13:41:57 UTC

[uima-uimaj] 01/01: [UIMA-6431] Use lambda functions as CAS processors

This is an automated email from the ASF dual-hosted git repository.

rec pushed a commit to branch refactoring/UIMA-6431-Use-lambda-functions-as-CAS-processors
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git

commit 16ce603dc965be9475c0b415d63e4cc5f634ecaf
Author: Richard Eckart de Castilho <re...@apache.org>
AuthorDate: Fri Apr 8 15:41:50 2022 +0200

    [UIMA-6431] Use lambda functions as CAS processors
    
    - Add default implementatins to AnalysisEngineServiceStub and ResourceServiceStub to reduce verbosity
    - Add analysis engine implementations using CAS- or JCas-accepting methods
    - Added tests
---
 .../uima/analysis_component/CasProcessor.java      | 60 +++++++++++++++
 .../analysis_component/CasProcessorAnnotator.java  | 77 +++++++++++++++++++
 .../analysis_component/JCasAnnotator_ImplBase.java |  1 -
 .../uima/analysis_component/JCasProcessor.java     | 60 +++++++++++++++
 .../analysis_component/JCasProcessorAnnotator.java | 87 ++++++++++++++++++++++
 .../analysis_engine/AnalysisEngineServiceStub.java |  8 +-
 .../AnalysisEngineProcessorAdapter.java}           | 70 +++++------------
 .../impl/AnalysisEngineProcessorStub.java}         | 41 +++++++---
 .../service/impl/AnalysisEngineServiceAdapter.java |  5 +-
 .../apache/uima/resource/ResourceServiceStub.java  |  7 +-
 .../CasProcessorAnnotatorTest.java                 | 82 ++++++++++++++++++++
 .../JCasProcessorAnnotatorTest.java                | 82 ++++++++++++++++++++
 12 files changed, 509 insertions(+), 71 deletions(-)

diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_component/CasProcessor.java b/uimaj-core/src/main/java/org/apache/uima/analysis_component/CasProcessor.java
new file mode 100644
index 000000000..96b4c7e65
--- /dev/null
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_component/CasProcessor.java
@@ -0,0 +1,60 @@
+/*
+ * 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.analysis_component;
+
+import java.util.Objects;
+
+import org.apache.uima.cas.CAS;
+
+/**
+ * A functional interface for a CAS processor.
+ *
+ * @param <E>
+ *          Thrown exception.
+ */
+@FunctionalInterface
+public interface CasProcessor<E extends Throwable> {
+
+  /**
+   * Accepts the processor.
+   *
+   * @param aCas
+   *          the CAS to process.
+   * @throws E
+   *           Thrown when the processor fails.
+   */
+  void process(CAS aCas) throws E;
+
+  /**
+   * Returns a composed {@code CasProcessor} like {@link CasProcessor#andThen(CasProcessor)}.
+   *
+   * @param after
+   *          the operation to perform after this operation
+   * @return a composed {@code CasProcessor} like {@link CasProcessor#andThen(CasProcessor)}.
+   * @throws NullPointerException
+   *           when {@code after} is null
+   */
+  default CasProcessor<E> andThen(final CasProcessor<E> after) {
+    Objects.requireNonNull(after);
+    return (final CAS t) -> {
+      process(t);
+      after.process(t);
+    };
+  }
+}
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_component/CasProcessorAnnotator.java b/uimaj-core/src/main/java/org/apache/uima/analysis_component/CasProcessorAnnotator.java
new file mode 100644
index 000000000..eeef72abb
--- /dev/null
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_component/CasProcessorAnnotator.java
@@ -0,0 +1,77 @@
+/*
+ * 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.analysis_component;
+
+import java.util.Map;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
+import org.apache.uima.analysis_engine.impl.AnalysisEngineProcessorAdapter;
+import org.apache.uima.analysis_engine.impl.AnalysisEngineProcessorStub;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.resource.ResourceInitializationException;
+import org.apache.uima.resource.ResourceSpecifier;
+import org.apache.uima.resource.metadata.ResourceMetaData;
+
+public class CasProcessorAnnotator extends AnalysisEngineProcessorAdapter {
+  private ResourceMetaData metaData;
+  private CasProcessor<? extends Exception> delegate;
+
+  public CasProcessorAnnotator(CasProcessor<? extends Exception> aCasAnnotator) {
+    metaData = UIMAFramework.getResourceSpecifierFactory().createAnalysisEngineMetaData();
+    delegate = aCasAnnotator;
+  }
+
+  @Override
+  public boolean initialize(ResourceSpecifier aSpecifier, Map<String, Object> aAdditionalParams)
+          throws ResourceInitializationException {
+    setStub(makeDelegate());
+    return super.initialize(aSpecifier, aAdditionalParams);
+  }
+
+  private AnalysisEngineProcessorStub makeDelegate() {
+    return new AnalysisEngineProcessorStub() {
+
+      @Override
+      public ResourceMetaData getMetaData() {
+        return metaData;
+      }
+
+      @Override
+      public void process(CAS aCAS) throws AnalysisEngineProcessException {
+        try {
+          delegate.process(aCAS);
+        } catch (Exception e) {
+          if (e instanceof AnalysisEngineProcessException) {
+            throw (AnalysisEngineProcessException) e;
+          } else {
+            throw new AnalysisEngineProcessException(e);
+          }
+        }
+      }
+    };
+  }
+
+  public static CasProcessorAnnotator of(CasProcessor<? extends Exception> aCasAnnotator)
+          throws ResourceInitializationException {
+    CasProcessorAnnotator engine = new CasProcessorAnnotator(aCasAnnotator);
+    engine.initialize(null, null);
+    return engine;
+  }
+}
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasAnnotator_ImplBase.java b/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasAnnotator_ImplBase.java
index 598edf3c5..9c450d166 100644
--- a/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasAnnotator_ImplBase.java
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasAnnotator_ImplBase.java
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.uima.analysis_component;
 
 import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasProcessor.java b/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasProcessor.java
new file mode 100644
index 000000000..4bcc3160d
--- /dev/null
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasProcessor.java
@@ -0,0 +1,60 @@
+/*
+ * 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.analysis_component;
+
+import java.util.Objects;
+
+import org.apache.uima.jcas.JCas;
+
+/**
+ * A functional interface for a JCas processor.
+ *
+ * @param <E>
+ *          Thrown exception.
+ */
+@FunctionalInterface
+public interface JCasProcessor<E extends Throwable> {
+
+  /**
+   * Accepts the processor.
+   *
+   * @param aJCas
+   *          the JCas to process.
+   * @throws E
+   *           Thrown when the processor fails.
+   */
+  void process(JCas aJCas) throws E;
+
+  /**
+   * Returns a composed {@code JCasProcessor} like {@link JCasProcessor#andThen(JCasProcessor)}.
+   *
+   * @param after
+   *          the operation to perform after this operation
+   * @return a composed {@code JCasProcessor} like {@link JCasProcessor#andThen(JCasProcessor)}.
+   * @throws NullPointerException
+   *           when {@code after} is null
+   */
+  default JCasProcessor<E> andThen(final JCasProcessor<E> after) {
+    Objects.requireNonNull(after);
+    return (final JCas t) -> {
+      process(t);
+      after.process(t);
+    };
+  }
+}
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasProcessorAnnotator.java b/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasProcessorAnnotator.java
new file mode 100644
index 000000000..8cecdc506
--- /dev/null
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_component/JCasProcessorAnnotator.java
@@ -0,0 +1,87 @@
+/*
+ * 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.analysis_component;
+
+import java.util.Map;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
+import org.apache.uima.analysis_engine.impl.AnalysisEngineProcessorAdapter;
+import org.apache.uima.analysis_engine.impl.AnalysisEngineProcessorStub;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.CASException;
+import org.apache.uima.jcas.JCas;
+import org.apache.uima.resource.ResourceInitializationException;
+import org.apache.uima.resource.ResourceSpecifier;
+import org.apache.uima.resource.metadata.ResourceMetaData;
+
+public class JCasProcessorAnnotator extends AnalysisEngineProcessorAdapter {
+  private ResourceMetaData metaData;
+  private JCasProcessor<? extends Exception> delegate;
+
+  public JCasProcessorAnnotator(
+          JCasProcessor<? extends Exception> aJCasAnnotator) {
+    metaData = UIMAFramework.getResourceSpecifierFactory().createAnalysisEngineMetaData();
+    delegate = aJCasAnnotator;
+  }
+
+  @Override
+  public boolean initialize(ResourceSpecifier aSpecifier, Map<String, Object> aAdditionalParams)
+          throws ResourceInitializationException {
+    setStub(makeDelegate());
+    return super.initialize(aSpecifier, aAdditionalParams);
+  }
+
+  private AnalysisEngineProcessorStub makeDelegate() {
+    return new AnalysisEngineProcessorStub() {
+
+      @Override
+      public ResourceMetaData getMetaData() {
+        return metaData;
+      }
+
+      @Override
+      public void process(CAS aCAS) throws AnalysisEngineProcessException {
+        JCas jcas;
+        try {
+          jcas = aCAS.getJCas();
+        } catch (CASException e) {
+          throw new AnalysisEngineProcessException(e);
+        }
+        try {
+          delegate.process(jcas);
+        } catch (Exception e) {
+          if (e instanceof AnalysisEngineProcessException) {
+            throw (AnalysisEngineProcessException) e;
+          } else {
+            throw new AnalysisEngineProcessException(e);
+          }
+        }
+      }
+    };
+  }
+
+  public static JCasProcessorAnnotator of(
+          JCasProcessor<? extends AnalysisEngineProcessException> aJCasAnnotator)
+          throws ResourceInitializationException {
+    JCasProcessorAnnotator engine = new JCasProcessorAnnotator(aJCasAnnotator);
+    engine.initialize(null, null);
+    return engine;
+  }
+}
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_engine/AnalysisEngineServiceStub.java b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/AnalysisEngineServiceStub.java
index c83cfacb6..4f287d8df 100644
--- a/uimaj-core/src/main/java/org/apache/uima/analysis_engine/AnalysisEngineServiceStub.java
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/AnalysisEngineServiceStub.java
@@ -43,7 +43,9 @@ public interface AnalysisEngineServiceStub extends ResourceServiceStub {
    * @throws ResourceServiceException
    *           tbd
    */
-  void callBatchProcessComplete() throws ResourceServiceException;
+  default void callBatchProcessComplete() throws ResourceServiceException {
+    // No action by default.
+  }
 
   /**
    * Performs service call to inform the AnalysisEngine that the processing of a collection has been
@@ -52,5 +54,7 @@ public interface AnalysisEngineServiceStub extends ResourceServiceStub {
    * @throws ResourceServiceException
    *           tbd
    */
-  void callCollectionProcessComplete() throws ResourceServiceException;
+  default void callCollectionProcessComplete() throws ResourceServiceException {
+    // No action by default.
+  }
 }
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/impl/AnalysisEngineProcessorAdapter.java
similarity index 69%
copy from uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java
copy to uimaj-core/src/main/java/org/apache/uima/analysis_engine/impl/AnalysisEngineProcessorAdapter.java
index 197f57cef..c99ac1fa2 100644
--- a/uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/impl/AnalysisEngineProcessorAdapter.java
@@ -17,53 +17,35 @@
  * under the License.
  */
 
-package org.apache.uima.analysis_engine.service.impl;
-
-import java.util.Map;
+package org.apache.uima.analysis_engine.impl;
 
 import org.apache.uima.UIMAFramework;
-import org.apache.uima.UIMARuntimeException;
 import org.apache.uima.UIMA_UnsupportedOperationException;
-import org.apache.uima.analysis_engine.AnalysisEngine;
 import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
-import org.apache.uima.analysis_engine.AnalysisEngineServiceStub;
 import org.apache.uima.analysis_engine.CasIterator;
 import org.apache.uima.analysis_engine.TextAnalysisEngine;
-import org.apache.uima.analysis_engine.impl.AnalysisEngineImplBase;
-import org.apache.uima.analysis_engine.impl.EmptyCasIterator;
 import org.apache.uima.cas.CAS;
 import org.apache.uima.collection.CasConsumer;
 import org.apache.uima.resource.ResourceConfigurationException;
-import org.apache.uima.resource.ResourceServiceException;
-import org.apache.uima.resource.ResourceSpecifier;
 import org.apache.uima.resource.metadata.ResourceMetaData;
 import org.apache.uima.util.Level;
 import org.apache.uima.util.UimaTimer;
 
 /**
- * Base class for analysis engine service adapters. Implements the {@link AnalysisEngine} interface
- * by communicating with an Analysis Engine service. This insulates the application from having to
- * know whether it is calling a local AnalysisEngine or a remote service.
- * <p>
- * Subclasses must provide an implementation of the {@link #initialize(ResourceSpecifier,Map)}
- * method, which must create an {@link AnalysisEngineServiceStub} object that can communicate with
- * the remote service. The stub must be passed to the {@link #setStub(AnalysisEngineServiceStub)}
- * method of this class.
- * 
- * 
+ * Base class for analysis engine processor adapters. Used for embedded (functional) processors.
  */
-public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBase
+public abstract class AnalysisEngineProcessorAdapter extends AnalysisEngineImplBase
         implements TextAnalysisEngine, CasConsumer {
 
   /**
    * current class
    */
-  private static final Class<AnalysisEngineServiceAdapter> CLASS_NAME = AnalysisEngineServiceAdapter.class;
+  private static final Class<AnalysisEngineProcessorAdapter> CLASS_NAME = AnalysisEngineProcessorAdapter.class;
 
   /**
-   * The stub that communicates with the remote service.
+   * The stub that talks to the actual implementation.
    */
-  private AnalysisEngineServiceStub mStub;
+  private AnalysisEngineProcessorStub mStub;
 
   /**
    * The resource metadata, cached so that service does not have to be called each time metadata is
@@ -77,13 +59,13 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
   private UimaTimer mTimer = UIMAFramework.newTimer();
 
   /**
-   * Sets the stub to be used to communicate with the remote service. Subclasses must call this from
-   * their <code>initialize</code> method.
+   * Sets the stub to be used to actual implementation. Subclasses must call this from their
+   * <code>initialize</code> method.
    * 
    * @param aStub
    *          the stub for the remote service
    */
-  protected void setStub(AnalysisEngineServiceStub aStub) {
+  protected void setStub(AnalysisEngineProcessorStub aStub) {
     mStub = aStub;
   }
 
@@ -92,7 +74,7 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
    * 
    * @return the stub for the remote service
    */
-  protected AnalysisEngineServiceStub getStub() {
+  protected AnalysisEngineProcessorStub getStub() {
     return mStub;
   }
 
@@ -101,14 +83,8 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
    */
   @Override
   public ResourceMetaData getMetaData() {
-    try {
-      if (mCachedMetaData == null && getStub() != null) {
-        mCachedMetaData = getStub().callGetMetaData();
-      }
-      return mCachedMetaData;
-    } catch (ResourceServiceException e) {
-      throw new UIMARuntimeException(e);
-    }
+
+    return getStub() != null ? getStub().getMetaData() : null;
   }
 
   /**
@@ -116,8 +92,9 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
    */
   @Override
   public void destroy() {
-    if (getStub() != null)
+    if (getStub() != null) {
       getStub().destroy();
+    }
     super.destroy();
   }
 
@@ -129,7 +106,7 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
             LOG_RESOURCE_BUNDLE, "UIMA_analysis_engine_process_begin__FINE", getResourceName());
     try {
       // invoke service
-      getStub().callProcess(aCAS);
+      getStub().process(aCAS);
 
       // log end of event
       UIMAFramework.getLogger(CLASS_NAME).logrb(Level.FINE, CLASS_NAME.getName(), "process",
@@ -138,10 +115,9 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
       // we don't support CasMultiplier services yet, so this always returns
       // an empty iterator
       return new EmptyCasIterator();
+    } catch (AnalysisEngineProcessException e) {
+      throw e;
     } catch (Exception e) {
-      // log exception
-      UIMAFramework.getLogger(CLASS_NAME).log(Level.SEVERE, "", e);
-      // rethrow as AnalysisEngineProcessException
       throw new AnalysisEngineProcessException(e);
     } finally {
       mTimer.stopIt();
@@ -199,20 +175,12 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
 
   @Override
   public void batchProcessComplete() throws AnalysisEngineProcessException {
-    try {
-      getStub().callBatchProcessComplete();
-    } catch (ResourceServiceException e) {
-      throw new AnalysisEngineProcessException(e);
-    }
+    getStub().batchProcessComplete();
   }
 
   @Override
   public void collectionProcessComplete() throws AnalysisEngineProcessException {
-    try {
-      getStub().callCollectionProcessComplete();
-    } catch (ResourceServiceException e) {
-      throw new AnalysisEngineProcessException(e);
-    }
+    getStub().collectionProcessComplete();
   }
 
   /**
diff --git a/uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/impl/AnalysisEngineProcessorStub.java
similarity index 52%
copy from uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java
copy to uimaj-core/src/main/java/org/apache/uima/analysis_engine/impl/AnalysisEngineProcessorStub.java
index 3b7028437..28e390a97 100644
--- a/uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/impl/AnalysisEngineProcessorStub.java
@@ -16,28 +16,49 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.uima.analysis_engine.impl;
 
-package org.apache.uima.resource;
-
+import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
+import org.apache.uima.cas.CAS;
 import org.apache.uima.resource.metadata.ResourceMetaData;
 
 /**
  * A stub that calls a remote AnalysisEngine service.
- * 
- * 
  */
-public interface ResourceServiceStub {
+public interface AnalysisEngineProcessorStub {
   /**
    * Performs service call to retrieve resource meta data.
    * 
    * @return metadata for the Resource
-   * @throws ResourceServiceException
-   *           passthru
    */
-  ResourceMetaData callGetMetaData() throws ResourceServiceException;
+  ResourceMetaData getMetaData();
+
+  /**
+   * Performs service call to process an entity.
+   * 
+   * @param aCAS
+   *          the CAS to process
+   */
+  void process(CAS aCAS) throws AnalysisEngineProcessException;
+
+  /**
+   * Notify the stub that all items in the batch have been processed.
+   */
+  default void batchProcessComplete() throws AnalysisEngineProcessException {
+    // No action by default.
+  }
+
+  /**
+   * Notify the stub that all items in the collection have been processed.
+   */
+  default void collectionProcessComplete() throws AnalysisEngineProcessException {
+    // No action by default.
+  }
 
   /**
-   * Called when this stub is no longer needed, so any open connections can be closed.
+   * Called when this stub is no longer needed, so resources can be cleaned up.
    */
-  void destroy();
+  default void destroy() {
+    // No action by default
+  }
 }
diff --git a/uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java
index 197f57cef..3c440ed98 100644
--- a/uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java
+++ b/uimaj-core/src/main/java/org/apache/uima/analysis_engine/service/impl/AnalysisEngineServiceAdapter.java
@@ -49,8 +49,6 @@ import org.apache.uima.util.UimaTimer;
  * method, which must create an {@link AnalysisEngineServiceStub} object that can communicate with
  * the remote service. The stub must be passed to the {@link #setStub(AnalysisEngineServiceStub)}
  * method of this class.
- * 
- * 
  */
 public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBase
         implements TextAnalysisEngine, CasConsumer {
@@ -116,8 +114,9 @@ public abstract class AnalysisEngineServiceAdapter extends AnalysisEngineImplBas
    */
   @Override
   public void destroy() {
-    if (getStub() != null)
+    if (getStub() != null) {
       getStub().destroy();
+    }
     super.destroy();
   }
 
diff --git a/uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java b/uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java
index 3b7028437..e81ba14ca 100644
--- a/uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java
+++ b/uimaj-core/src/main/java/org/apache/uima/resource/ResourceServiceStub.java
@@ -16,15 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.uima.resource;
 
 import org.apache.uima.resource.metadata.ResourceMetaData;
 
 /**
  * A stub that calls a remote AnalysisEngine service.
- * 
- * 
  */
 public interface ResourceServiceStub {
   /**
@@ -39,5 +36,7 @@ public interface ResourceServiceStub {
   /**
    * Called when this stub is no longer needed, so any open connections can be closed.
    */
-  void destroy();
+  default void destroy() {
+    // No action by default
+  }
 }
diff --git a/uimaj-core/src/test/java/org/apache/uima/analysis_component/CasProcessorAnnotatorTest.java b/uimaj-core/src/test/java/org/apache/uima/analysis_component/CasProcessorAnnotatorTest.java
new file mode 100644
index 000000000..3f66c514b
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/analysis_component/CasProcessorAnnotatorTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.analysis_component;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import org.apache.uima.analysis_engine.AnalysisEngine;
+import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.util.CasCreationUtils;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class CasProcessorAnnotatorTest {
+
+  private static final String NEEDLE = "needle";
+
+  private CAS cas;
+
+  @BeforeEach
+  public void setup() throws Exception {
+    cas = CasCreationUtils.createCas();
+  }
+
+  @Test
+  void thatProcessingWithJCasInterfaceWorks() throws Exception {
+    AnalysisEngine engine = CasProcessorAnnotator.of(_cas -> _cas.setDocumentText(NEEDLE));
+    engine.process(cas.getJCas());
+
+    assertThat(cas.getDocumentText()).isEqualTo(NEEDLE);
+  }
+
+  @Test
+  void thatProcessingWithCasInterfaceWorks() throws Exception {
+    AnalysisEngine engine = CasProcessorAnnotator.of(_cas -> _cas.setDocumentText(NEEDLE));
+    engine.process(cas);
+
+    assertThat(cas.getDocumentText()).isEqualTo(NEEDLE);
+  }
+
+  @Test
+  void thatAnalysisEngineProcessExceptionMakesItThrough() throws Exception {
+    AnalysisEngine engine = CasProcessorAnnotator.of(_cas -> {
+      throw new AnalysisEngineProcessException(NEEDLE, null);
+    });
+
+    assertThatExceptionOfType(AnalysisEngineProcessException.class)
+            .isThrownBy(() -> engine.process(cas)) //
+            .matches(ex -> NEEDLE.equals(ex.getMessageKey()));
+  }
+
+  @Test
+  void thatGenericExceptionIsWrapped() throws Exception {
+    AnalysisEngine engine = CasProcessorAnnotator.of(_cas -> {
+      throw new RuntimeException(NEEDLE);
+    });
+
+    Assertions.setMaxStackTraceElementsDisplayed(1000);
+    assertThatExceptionOfType(AnalysisEngineProcessException.class)
+            .isThrownBy(() -> engine.process(cas)) //
+            .matches(ex -> ex.getCause() instanceof RuntimeException)
+            .matches(ex -> NEEDLE.equals(ex.getCause().getMessage()));
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/analysis_component/JCasProcessorAnnotatorTest.java b/uimaj-core/src/test/java/org/apache/uima/analysis_component/JCasProcessorAnnotatorTest.java
new file mode 100644
index 000000000..11d67e7e5
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/analysis_component/JCasProcessorAnnotatorTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.analysis_component;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import org.apache.uima.analysis_engine.AnalysisEngine;
+import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.util.CasCreationUtils;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class JCasProcessorAnnotatorTest {
+
+  private static final String NEEDLE = "needle";
+
+  private CAS cas;
+
+  @BeforeEach
+  public void setup() throws Exception {
+    cas = CasCreationUtils.createCas();
+  }
+
+  @Test
+  void thatProcessingWithJCasInterfaceWorks() throws Exception {
+    AnalysisEngine engine = JCasProcessorAnnotator.of(_cas -> _cas.setDocumentText(NEEDLE));
+    engine.process(cas.getJCas());
+
+    assertThat(cas.getDocumentText()).isEqualTo(NEEDLE);
+  }
+
+  @Test
+  void thatProcessingWithCasInterfaceWorks() throws Exception {
+    AnalysisEngine engine = JCasProcessorAnnotator.of(_cas -> _cas.setDocumentText(NEEDLE));
+    engine.process(cas);
+
+    assertThat(cas.getDocumentText()).isEqualTo(NEEDLE);
+  }
+
+  @Test
+  void thatAnalysisEngineProcessExceptionMakesItThrough() throws Exception {
+    AnalysisEngine engine = JCasProcessorAnnotator.of(_cas -> {
+      throw new AnalysisEngineProcessException(NEEDLE, null);
+    });
+
+    assertThatExceptionOfType(AnalysisEngineProcessException.class)
+            .isThrownBy(() -> engine.process(cas)) //
+            .matches(ex -> NEEDLE.equals(ex.getMessageKey()));
+  }
+
+  @Test
+  void thatGenericExceptionIsWrapped() throws Exception {
+    AnalysisEngine engine = JCasProcessorAnnotator.of(_cas -> {
+      throw new RuntimeException(NEEDLE);
+    });
+
+    Assertions.setMaxStackTraceElementsDisplayed(1000);
+    assertThatExceptionOfType(AnalysisEngineProcessException.class)
+            .isThrownBy(() -> engine.process(cas)) //
+            .matches(ex -> ex.getCause() instanceof RuntimeException)
+            .matches(ex -> NEEDLE.equals(ex.getCause().getMessage()));
+  }
+}