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:56 UTC

[uima-uimaj] branch refactoring/UIMA-6431-Use-lambda-functions-as-CAS-processors created (now 16ce603dc)

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

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


      at 16ce603dc [UIMA-6431] Use lambda functions as CAS processors

This branch includes the following new commits:

     new 16ce603dc [UIMA-6431] Use lambda functions as CAS processors

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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

Posted by re...@apache.org.
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()));
+  }
+}