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

rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/main by this push:
     new b9271cfa21 TOMEE-2369 add the missing README.adoc files to the examples
b9271cfa21 is described below

commit b9271cfa215107717d731886bf257f5cc2b58460
Author: Richard Zowalla <[email protected]>
AuthorDate: Fri Sep 25 17:57:49 2026 +0200

    TOMEE-2369 add the missing README.adoc files to the examples
    
    TOMEE-2371 cdi-ejbcontext-jaas
    TOMEE-2374 client-resource-lookup-preview
    TOMEE-2376 connector-ear
    TOMEE-2378 datasource-definition
    TOMEE-2382 groovy-cdi
    TOMEE-2389 multiple-arquillian-adapters
    TOMEE-2394 rest-applicationcomposer
    TOMEE-2395 rest-jaas
    TOMEE-2396 scala-basic
    TOMEE-2397 server-events
    TOMEE-2398 simple-ear
---
 examples/cdi-ejbcontext-jaas/README.adoc           | 157 +++++++
 .../client-resource-lookup-preview/README.adoc     | 116 +++++
 examples/connector-ear/README.adoc                 | 438 +++++++++++++++++++
 examples/datasource-definition/README.adoc         | 197 +++++++++
 examples/groovy-cdi/README.adoc                    | 107 +++++
 examples/multiple-arquillian-adapters/README.adoc  | 348 +++++++++++++++
 examples/rest-applicationcomposer/README.adoc      | 133 ++++++
 examples/rest-jaas/README.adoc                     | 200 +++++++++
 examples/scala-basic/README.adoc                   | 211 +++++++++
 examples/server-events/README.adoc                 | 148 +++++++
 examples/simple-ear/README.adoc                    | 471 +++++++++++++++++++++
 11 files changed, 2526 insertions(+)

diff --git a/examples/cdi-ejbcontext-jaas/README.adoc 
b/examples/cdi-ejbcontext-jaas/README.adoc
new file mode 100644
index 0000000000..3b37856405
--- /dev/null
+++ b/examples/cdi-ejbcontext-jaas/README.adoc
@@ -0,0 +1,157 @@
+:index-group: Security
+:jbake-type: page
+:jbake-status: status=published
+= CDI, EJBContext and JAAS
+
+This example shows how a user who logs in programmatically in a servlet
+through a JAAS realm is seen as the caller principal of an EJB. The servlet
+calls `HttpServletRequest.login(...)`, and a CDI-injected stateless bean then
+reads the caller name from its `EJBContext`.
+
+The example is a plain WAR. It has no tests. Instead, it uses the
+`tomee-maven-plugin` to start a TomEE with its own `server.xml` and
+JAAS configuration.
+
+== The EJB: reading the caller from EJBContext
+
+`PrinciaplEjb` is a `@Stateless` bean. It gets its `EJBContext` injected with
+`@Resource` and returns the name of the current caller principal:
+
+[source,java]
+----
+@Stateless
+public class PrinciaplEjb {
+
+    @Resource
+    private EJBContext context;
+
+    public String info() {
+        return context.getCallerPrincipal().getName();
+    }
+}
+----
+
+The bean contains no security code of its own. It only reports the identity
+that the container passes to it.
+
+== The servlet: programmatic login
+
+`LogginServlet` is mapped to `/ejbcontext`. It gets the EJB injected with
+CDI's `@Inject` (not `@EJB`). It logs the user in with the `myUser` and
+`myPass` request parameters, then writes the caller name returned by the EJB
+and the result of `isUserInRole("admin")`:
+
+[source,java]
+----
+@WebServlet(urlPatterns = "/ejbcontext")
+public class LogginServlet extends HttpServlet {
+
+    @Inject
+    private PrinciaplEjb bean;
+
+    @Override
+    protected void service(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+        req.login(req.getParameter("myUser"), req.getParameter("myPass"));
+        // think to persist the information in the session if you need it later
+        resp.getWriter().write("logged user ==> " + bean.info() + "; 
isUserInRole(admin)? " + req.isUserInRole("admin"));
+    }
+}
+----
+
+The servlet overrides `service(...)`, so it handles every HTTP method.
+`req.login(...)` only authenticates the current request. As the comment in the
+code says, you have to store the information in the session yourself if you
+need it in later requests.
+
+== The TomEE configuration
+
+The files in `src/main/tomee/conf` are copied into the `conf` folder of the
+TomEE that the `tomee-maven-plugin` runs.
+
+=== The JAAS realm in server.xml
+
+The `Engine` uses Tomcat's `JAASRealm`. The realm points to the
+`PropertiesLoginModule` login context and tells Tomcat which principal classes
+stand for users and which stand for roles:
+
+[source,xml]
+----
+<Realm className="org.apache.catalina.realm.JAASRealm" 
appName="PropertiesLoginModule"
+       userClassNames="org.apache.openejb.core.security.jaas.UserPrincipal"
+       roleClassNames="org.apache.openejb.core.security.jaas.GroupPrincipal">
+</Realm>
+----
+
+=== The JAAS login module
+
+`login.config` defines the `PropertiesLoginModule` entry that the realm
+references. It uses OpenEJB's `PropertiesLoginModule`, which reads users and
+groups from two property files:
+
+[source]
+----
+PropertiesLoginModule {
+    org.apache.openejb.core.security.jaas.PropertiesLoginModule required
+    Debug=false
+    UsersFile="users.properties"
+    GroupsFile="groups.properties";
+};
+----
+
+`users.properties` defines a user `tomee` with the password `tomee`:
+
+[source,properties]
+----
+tomee=tomee
+----
+
+`groups.properties` puts that user in the `admin` group:
+
+[source,properties]
+----
+admin=tomee
+----
+
+=== The plugin configuration
+
+The JVM has to know where `login.config` is. The `pom.xml` sets the
+`java.security.auth.login.config` system property to the copy of the file
+inside the TomEE that the plugin builds under `target/apache-tomee`:
+
+[source,xml]
+----
+<plugin>
+  <groupId>org.apache.tomee.maven</groupId>
+  <artifactId>tomee-maven-plugin</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <configuration>
+    <systemVariables>
+      
<java.security.auth.login.config>${project.build.directory}/apache-tomee/conf/login.config</java.security.auth.login.config>
+      
<org.apache.catalina.STRICT_SERVLET_COMPLIANCE>false</org.apache.catalina.STRICT_SERVLET_COMPLIANCE>
+    </systemVariables>
+  </configuration>
+</plugin>
+----
+
+== Running the example
+
+The example has no automated tests. Build the WAR and start TomEE with the
+plugin:
+
+[source,bash]
+----
+mvn clean install tomee:run
+----
+
+Then call the servlet with the credentials from `users.properties`:
+
+[source,bash]
+----
+curl 
"http://localhost:8080/cdi-ejbcontext-jaas/ejbcontext?myUser=tomee&myPass=tomee";
+----
+
+The response shows the name that the EJB reads from its `EJBContext`, followed
+by the result of `isUserInRole("admin")`. For the user `tomee` this should be
+`tomee` and `true`, because `groups.properties` puts `tomee` in the `admin`
+group. If the credentials are wrong, `req.login(...)` throws a
+`ServletException`.
diff --git a/examples/client-resource-lookup-preview/README.adoc 
b/examples/client-resource-lookup-preview/README.adoc
new file mode 100644
index 0000000000..7bd3e8b33f
--- /dev/null
+++ b/examples/client-resource-lookup-preview/README.adoc
@@ -0,0 +1,116 @@
+:index-group: JMS and MDBs
+:jbake-type: page
+:jbake-status: status=published
+= Client Resource Lookup (Preview)
+
+This example shows how a standalone client using the remote OpenEJB client
+(`org.apache.openejb.client.RemoteInitialContextFactory`) can look up JMS
+resources, here an ActiveMQ `ConnectionFactory` and `Queue`, through JNDI.
+The resources are not fetched from a server: the client builds them from
+system properties. In effect you declare a small JNDI tree on the client side.
+
+The example is only a test, `SenderTest`. There is no application code.
+
+== Dependencies
+
+The client needs the OpenEJB client, which provides the `InitialContext`
+factory, and the ActiveMQ classes it will instantiate:
+
+[source,xml]
+----
+<dependency>
+  <groupId>org.apache.tomee.bom</groupId>
+  <artifactId>tomee-plus-api</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <scope>provided</scope>
+</dependency>
+<!-- here what is needed to use client lookup with jms resources -->
+<dependency>
+  <groupId>org.apache.tomee</groupId>
+  <artifactId>openejb-client</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <scope>runtime</scope>
+</dependency>
+<dependency>
+  <groupId>org.apache.tomee</groupId>
+  <artifactId>openejb-ejbd</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+</dependency>
+<dependency>
+  <groupId>org.apache.activemq</groupId>
+  <artifactId>activemq-broker</artifactId>
+  <version>6.2.6</version>
+  <scope>runtime</scope>
+</dependency>
+----
+
+== Declaring the client resources
+
+The resources are declared as system properties. The key is the JNDI name
+used for the lookup. The value follows the pattern
+`<kind>:<implementation class>:<constructor argument>`:
+
+[source,java]
+----
+@BeforeClass
+public static void configureClientResources() {
+    // can be set this way or with the key Resource/<type>
+    // in fact we create on client side a mini jndi tree
+    // the key is the jndi name (the one used for the lookup)
+    System.setProperty("aConnectionFactory", 
"connectionfactory:org.apache.activemq.ActiveMQConnectionFactory:tcp://localhost:11616");
+    System.setProperty("aQueue", 
"queue:org.apache.activemq.command.ActiveMQQueue:LISTENER");
+}
+----
+
+When the client resolves a name, it first checks for a system property
+with that name, after dropping a `java:` prefix and any `comp/env/`.
+If one exists, the value is parsed and the object is created on the
+client:
+
+* `connectionfactory:`, `queue:` and `topic:` load the given class
+  through the thread context class loader and call its `String`
+  constructor with the rest of the value. So
+  `new ActiveMQConnectionFactory("tcp://localhost:11616")` and
+  `new ActiveMQQueue("LISTENER")` are created here.
+* The client also knows `datasource:`, `javamail:` and `link:` (the last
+  one points to another system property).
+
+As the comment in the test says, you can also key the value by resource
+type as `Resource/<type>`. The client uses that property when the server
+answers a lookup with a resource of that type.
+
+Creating the `ActiveMQConnectionFactory` does not open a connection, so
+nothing has to listen on `tcp://localhost:11616` for this test.
+
+== The test
+
+`SenderTest` creates an `InitialContext` with the remote client factory and
+looks up both resources with the `java:` prefix:
+
+[source,java]
+----
+@Test
+public void send() throws Exception {
+    final Properties properties = new Properties();
+    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, 
"org.apache.openejb.client.RemoteInitialContextFactory");
+    final Context context = new InitialContext(properties);
+
+    final Queue destination = (Queue) context.lookup("java:aQueue");
+    assertNotNull(destination);
+    assertEquals("LISTENER", destination.getQueueName());
+
+    final ConnectionFactory connectionFactory = (ConnectionFactory) 
context.lookup("java:aConnectionFactory");
+    assertNotNull(connectionFactory);
+}
+----
+
+It checks that the queue is found and is named `LISTENER`, and that the
+connection factory is found. Both are answered from the system properties,
+so no TomEE server has to be running.
+
+== Running
+
+[source,bash]
+----
+mvn clean test
+----
diff --git a/examples/connector-ear/README.adoc 
b/examples/connector-ear/README.adoc
new file mode 100644
index 0000000000..aa5caa906a
--- /dev/null
+++ b/examples/connector-ear/README.adoc
@@ -0,0 +1,438 @@
+:index-group: Java EE Connectors
+:jbake-type: page
+:jbake-status: status=published
+= Connectors in EAR Files
+
+This example shows a Jakarta Connectors (JCA) resource adapter packaged as a 
RAR inside an EAR, next to a WAR that uses it.
+The resource adapter supports both directions:
+
+* *outbound*: a REST endpoint in the WAR gets a `SampleConnectionFactory` 
injected and sends a message through a connection;
+* *inbound*: the resource adapter delivers that message to a message-driven 
bean in the WAR, which implements the adapter's `InboundListener` interface.
+
+The message is stored in a singleton and can be read back over HTTP, so a 
single POST followed by a GET exercises the full round trip.
+
+== Module layout
+
+The example is a Maven multi-module build. The parent `pom.xml` lists the 
modules in build order:
+
+[source,xml]
+----
+<modules>
+  <module>connector-sample-api</module>
+  <module>connector-sample-impl</module>
+  <module>connector-sample-rar</module>
+  <module>connector-sample-war</module>
+  <module>connector-sample-ear</module>
+  <module>connector-sample-functional-tests</module>
+</modules>
+----
+
+[options="header"]
+|===
+|Module |Packaging |Contents
+|`connector-sample-api` |jar |The interfaces shared by the adapter and the 
application: `SampleConnectionFactory`, `SampleConnection` and 
`InboundListener`.
+|`connector-sample-impl` |jar |The resource adapter implementation 
(`org.superbiz.connector.adapter`). Depends on the API with `provided` scope.
+|`connector-sample-rar` |rar |Wraps the impl jar and adds `META-INF/ra.xml`.
+|`connector-sample-war` |war |The application: a REST endpoint, a 
message-driven bean and a singleton. Depends on the API with `provided` scope.
+|`connector-sample-ear` |ear |Bundles the RAR, the WAR and the API jar into 
one enterprise archive.
+|`connector-sample-functional-tests` |jar |Arquillian tests that deploy an EAR 
to a remote TomEE Plus.
+|===
+
+The API jar is the one piece of code both sides need, so it is not packaged in 
the RAR or in the WAR. It goes into the EAR's `lib` directory, where the 
adapter and the web application can both see it.
+
+== The API
+
+The outbound contract is a connection factory and a connection:
+
+[source,java]
+----
+public interface SampleConnectionFactory extends Serializable, Referenceable {
+    public SampleConnection getConnection() throws ResourceException;
+}
+
+public interface SampleConnection {
+    public void sendMessage(final String message);
+    public void close();
+}
+----
+
+The inbound contract is the message listener interface that message-driven 
beans implement:
+
+[source,java]
+----
+public interface InboundListener {
+    public void receiveMessage(final String message);
+}
+----
+
+== The resource adapter
+
+`SampleResourceAdapter` keeps track of every activated endpoint. When a 
message-driven bean is deployed, the container calls `endpointActivation`, and 
the adapter creates a `MessageEndpoint` and stores it. `sendMessage` passes a 
message to every registered endpoint by casting it to `InboundListener`:
+
+[source,java]
+----
+@Connector(description = "Sample Resource Adapter", displayName = "Sample 
Resource Adapter", eisType = "Sample Resource Adapter", version = "1.0")
+public class SampleResourceAdapter implements ResourceAdapter {
+
+    final Map<SampleActivationSpec, EndpointTarget> targets = new 
ConcurrentHashMap<>();
+
+    public void endpointActivation(final MessageEndpointFactory 
messageEndpointFactory, final ActivationSpec activationSpec)
+            throws ResourceException
+    {
+        final SampleActivationSpec sampleActivationSpec = 
(SampleActivationSpec) activationSpec;
+
+        try {
+            final MessageEndpoint messageEndpoint = 
messageEndpointFactory.createEndpoint(null);
+            final EndpointTarget target = new EndpointTarget(messageEndpoint);
+            targets.put(sampleActivationSpec, target);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void endpointDeactivation(MessageEndpointFactory 
messageEndpointFactory, ActivationSpec activationSpec) {
+        final SampleActivationSpec sampleActivationSpec = 
(SampleActivationSpec) activationSpec;
+
+        final EndpointTarget endpointTarget = 
targets.get(sampleActivationSpec);
+        if (endpointTarget == null) {
+            throw new IllegalStateException("No EndpointTarget to undeploy for 
ActivationSpec " + activationSpec);
+        }
+
+        endpointTarget.messageEndpoint.release();
+    }
+
+    public void sendMessage(final String message) {
+        final Collection<EndpointTarget> endpoints = this.targets.values();
+        for (final EndpointTarget endpoint : endpoints) {
+            endpoint.invoke(message);
+        }
+    }
+
+    public static class EndpointTarget {
+        private final MessageEndpoint messageEndpoint;
+
+        public EndpointTarget(final MessageEndpoint messageEndpoint) {
+            this.messageEndpoint = messageEndpoint;
+        }
+
+        public void invoke(final String message) {
+            ((InboundListener)this.messageEndpoint).receiveMessage(message);
+        }
+    }
+
+    // start, stop and getXAResources omitted
+}
+----
+
+`SampleActivationSpec` links the `InboundListener` interface to this adapter 
through `@Activation`:
+
+[source,java]
+----
+@Activation(messageListeners = InboundListener.class)
+public class SampleActivationSpec implements ActivationSpec {
+    // holds the ResourceAdapter and the bean class
+}
+----
+
+The outbound side follows the usual JCA pattern. 
`SampleManagedConnectionFactory` is declared with `@ConnectionDefinition` and 
implements `ResourceAdapterAssociation`, so the container gives it a reference 
to the resource adapter:
+
+[source,java]
+----
+@ConnectionDefinition(connectionFactory = SampleConnectionFactory.class,
+        connectionFactoryImpl = SampleConnectionFactoryImpl.class,
+        connection = SampleConnection.class,
+        connectionImpl = SampleConnectionImpl.class)
+public class SampleManagedConnectionFactory implements 
ManagedConnectionFactory, ResourceAdapterAssociation {
+
+    public Object createConnectionFactory(ConnectionManager cxManager) throws 
ResourceException {
+        return new SampleConnectionFactoryImpl(this, cxManager);
+    }
+
+    public Object createConnectionFactory() throws ResourceException {
+        throw new ResourceException("This resource adapter doesn't support 
non-managed environments");
+    }
+
+    public ManagedConnection createManagedConnection(Subject subject,
+                                                     ConnectionRequestInfo 
cxRequestInfo) throws ResourceException {
+        return new SampleManagedConnection(this);
+    }
+
+    // matchManagedConnections, log writer and resource adapter accessors 
omitted
+}
+----
+
+`SampleConnectionFactoryImpl.getConnection()` asks the container's 
`ConnectionManager` for a connection. The `SampleConnectionImpl` handle it 
returns delegates `sendMessage` to its `SampleManagedConnection`, which hands 
the message to the resource adapter:
+
+[source,java]
+----
+void sendMessage(final String message) {
+    final SampleResourceAdapter resourceAdapter = (SampleResourceAdapter) 
mcf.getResourceAdapter();
+    resourceAdapter.sendMessage(message);
+}
+----
+
+This is where outbound and inbound meet. Sending a message on an outbound 
connection delivers it to every activated message-driven bean.
+
+== The RAR descriptor
+
+`connector-sample-rar/src/main/rar/META-INF/ra.xml` declares the resource 
adapter class, the outbound connection definition (without transaction support) 
and the inbound message listener with its activation spec:
+
+[source,xml]
+----
+<connector xmlns="http://xmlns.jcp.org/xml/ns/javaee";
+           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+           xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
http://xmlns.jcp.org/xml/ns/javaee/connector_1_7.xsd";
+           version="1.7">
+    <description>Sample Resource Adapter</description>
+    <display-name>Sample Resource Adapter</display-name>
+    <eis-type>Sample Resource Adapter</eis-type>
+    <resourceadapter-version>1.0</resourceadapter-version>
+    <license>
+        <license-required>false</license-required>
+    </license>
+    <resourceadapter>
+        
<resourceadapter-class>org.superbiz.connector.adapter.SampleResourceAdapter</resourceadapter-class>
+        <config-property>
+            <config-property-name>interval</config-property-name>
+            <config-property-type>int</config-property-type>
+            <config-property-value>15</config-property-value>
+        </config-property>
+        <outbound-resourceadapter>
+            <connection-definition>
+                
<managedconnectionfactory-class>org.superbiz.connector.adapter.SampleManagedConnectionFactory</managedconnectionfactory-class>
+                
<connectionfactory-interface>SampleConnectionFactory</connectionfactory-interface>
+                
<connectionfactory-impl-class>org.superbiz.connector.adapter.SampleConnectionFactoryImpl</connectionfactory-impl-class>
+                <connection-interface>SampleConnection</connection-interface>
+                
<connection-impl-class>org.superbiz.connector.adapter.SampleConnectionImpl</connection-impl-class>
+            </connection-definition>
+            <transaction-support>NoTransaction</transaction-support>
+            <reauthentication-support>false</reauthentication-support>
+        </outbound-resourceadapter>
+        <inbound-resourceadapter>
+            <messageadapter>
+                <messagelistener>
+                    
<messagelistener-type>org.superbiz.connector.api.InboundListener</messagelistener-type>
+                    <activationspec>
+                        
<activationspec-class>org.superbiz.connector.adapter.SampleActivationSpec</activationspec-class>
+                    </activationspec>
+                </messagelistener>
+            </messageadapter>
+        </inbound-resourceadapter>
+    </resourceadapter>
+</connector>
+----
+
+The `connector-sample-rar` module has `rar` packaging and a single dependency 
on `connector-sample-impl`, so the RAR contains the impl jar and this 
descriptor.
+
+== The web application
+
+The WAR also has a TomEE-specific `WEB-INF/resources.xml`. It declares the 
resource adapter, a message-driven container bound to it, and the connection 
factory:
+
+[source,xml]
+----
+<resources>
+    <Resource id="SampleResourceAdapter" 
class-name="org.superbiz.connector.adapter.SampleResourceAdapter">
+    </Resource>
+
+    <Container id="SampleMDBContainer" ctype="MESSAGE">
+        ResourceAdapter=SampleResourceAdapter
+        ActivationSpecClass=org.superbiz.connector.adapter.SampleActivationSpec
+        MessageListenerInterface=org.superbiz.connector.api.InboundListener
+    </Container>
+
+    <Resource id="SampleConnectionFactory" 
type="org.superbiz.connector.api.SampleConnectionFactory" 
class-name="org.superbiz.connector.adapter.SampleManagedConnectionFactory">
+        ResourceAdapter=SampleResourceAdapter
+        TransactionSupport=none
+    </Resource>
+</resources>
+----
+
+`Sender` is a singleton EJB that is also a JAX-RS resource. A POST takes a 
connection from the injected `SampleConnectionFactory` and sends the request 
body. A GET returns every message received so far, one per line:
+
+[source,java]
+----
+@Singleton
+@Lock(LockType.READ)
+@Path("")
+public class Sender {
+
+    @Resource
+    private SampleConnectionFactory cf;
+
+    @EJB
+    private MessagesReceived messagesReceived;
+
+    @POST
+    @Consumes(MediaType.TEXT_PLAIN)
+    public void sendMessage(final String message) {
+        try {
+            final SampleConnection connection = cf.getConnection();
+            connection.sendMessage(message);
+            connection.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public String getMessages() {
+        final StringBuilder sb = new StringBuilder();
+        final List<String> messages = 
this.messagesReceived.getMessagesReceived();
+        for (int i = 0; i < messages.size(); i++) {
+            if (i > 0) {
+                sb.append("\n");
+            }
+            sb.append(messages.get(i));
+        }
+
+        return sb.toString();
+    }
+}
+----
+
+`Receiver` is the inbound endpoint. Because it implements `InboundListener`, 
not a JMS `MessageListener`, it is driven by the resource adapter:
+
+[source,java]
+----
+@MessageDriven(name = "Receiver")
+public class Receiver implements InboundListener {
+
+    @EJB
+    private MessagesReceived messagesReceived;
+
+    @Resource
+    private MessageDrivenContext context;
+
+    @Override
+    public void receiveMessage(String message) {
+        messagesReceived.messageReceived(message);
+    }
+}
+----
+
+`MessagesReceived` is a `@Singleton` that stores the received messages in a 
list.
+
+== The EAR
+
+`connector-sample-ear` uses the `maven-ear-plugin` to generate 
`application.xml`. It maps the WAR to the `/sample` context root, registers the 
RAR as a connector module, and puts library dependencies (the API jar) in `lib`:
+
+[source,xml]
+----
+<plugin>
+  <artifactId>maven-ear-plugin</artifactId>
+  <version>3.4.0</version>
+  <configuration>
+    <version>6</version>
+    <generateApplicationXml>true</generateApplicationXml>
+    <defaultLibBundleDir>lib</defaultLibBundleDir>
+    <modules>
+      <webModule>
+        <groupId>org.superbiz</groupId>
+        <artifactId>connector-sample-war</artifactId>
+        <contextRoot>/sample</contextRoot>
+      </webModule>
+      <rarModule>
+        <groupId>org.superbiz</groupId>
+        <artifactId>connector-sample-rar</artifactId>
+      </rarModule>
+    </modules>
+  </configuration>
+</plugin>
+----
+
+== Testing
+
+The tests are in `connector-sample-functional-tests`. They do not deploy the 
EAR built by Maven. Instead, each test builds the same structure with 
ShrinkWrap: an API jar in `lib`, a RAR that contains the impl jar and the real 
`ra.xml`, and a WAR that contains the `org.superbiz.application` classes and 
the real `resources.xml`. The descriptors are read from the sibling modules 
through the `Basedir` helper. The generated `application.xml` matches the one 
from the EAR module:
+
+[source,java]
+----
+final EnterpriseArchive enterpriseArchive = 
ShrinkWrap.create(EnterpriseArchive.class, "connector-sample.ear");
+enterpriseArchive.addAsLibraries(apiJar);
+enterpriseArchive.addAsModule(rar);
+enterpriseArchive.addAsModule(webArchive);
+
+ApplicationDescriptor applicationXml = 
Descriptors.create(ApplicationDescriptor.class);
+applicationXml.displayName("connector-sample-ear");
+applicationXml.createModule()
+        .getOrCreateWeb()
+            .webUri("connector-sample-war.war")
+            .contextRoot("/sample")
+        .up().up()
+        .createModule().connector("connector-sample-ra.rar")
+        .up().libraryDirectory("lib");
+----
+
+=== EARTest
+
+`EARTest` runs with the Arquillian `arquillian-tomee-remote` adapter against 
TomEE Plus, configured in `src/test/resources/arquillian.xml`. The 
`${tomee.version}` placeholder is filled in by Maven resource filtering:
+
+[source,xml]
+----
+<container qualifier="tomee" default="true">
+  <configuration>
+    <property name="httpPort">-1</property>
+    <property name="stopPort">-1</property>
+    <property name="version">${tomee.version}</property>
+    <property name="classifier">plus</property>
+    <property name="openejbVersion">${tomee.version}</property>
+    <property name="dir">target/apache-tomee-remote</property>
+    <property 
name="appWorkingDir">target/arquillian-test-working-dir</property>
+  </configuration>
+</container>
+----
+
+The test runs as a client. It POSTs `Hello, world` to `/sample/` and expects 
`204`. Then it GETs the same URL and expects `Hello, world` back, which proves 
the message went out through the connection factory and came back in through 
the message-driven bean:
+
+[source,java]
+----
+@Test
+public void testShouldMakeSureWebappIsWorking() throws Exception {
+    final String url = "http://"; + deploymentUrl.getHost() + ":" + 
deploymentUrl.getPort() + "/sample/";
+    final WebClient webClient = WebClient.create(url);
+    final Response response = 
webClient.path("").type(MediaType.TEXT_PLAIN_TYPE).post("Hello, world");
+    assertEquals(204, response.getStatus());
+
+    final String result = 
webClient.path("").accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
+    assertEquals("Hello, world", result);
+}
+----
+
+=== DeployInWebAppsDirectoryTest
+
+`DeployInWebAppsDirectoryTest` uses `RemoteTomEEContainer` only to start and 
stop TomEE Plus. It deploys by copying the exported EAR into the server's 
`webapps` directory, which is the path fixed in TOMEE-2145. It retries the same 
POST/GET check for up to 30 attempts, one second apart, while TomEE picks up 
the archive. Then it stops the container and starts it again to make sure it 
still starts with the EAR in `webapps`:
+
+[source,java]
+----
+container.start();
+final File webapps = new File(configuration.getDir(), "apache-tomee-" + 
configuration.getClassifier() + "-" + configuration.getVersion() + "/webapps");
+webapps.mkdirs();
+
+final InputStream is = 
createDeployment().as(ZipExporter.class).exportAsInputStream();
+IO.copy(is, new File(webapps, "connector-sample.ear"));
+
+final String appUrl = "http://localhost:"; + configuration.getHttpPort() + 
"/sample/";
+attempt(new Callable<Void>() {
+    @Override
+    public Void call() throws Exception {
+        runTests(appUrl);
+        return null;
+    }
+}, 30);
+
+container.stop();
+
+// restart the container, make sure all is still well
+container.start();
+----
+
+=== Running
+
+The functional tests module depends on the other modules' artifacts, including 
the EAR and the WAR's `classes` jar. Build the whole example from the 
`connector-ear` directory:
+
+[source,bash]
+----
+mvn clean install
+----
+
+Both tests download and start a TomEE Plus distribution of the version set by 
`tomee.version` in the parent POM.
diff --git a/examples/datasource-definition/README.adoc 
b/examples/datasource-definition/README.adoc
new file mode 100644
index 0000000000..eab91b00dc
--- /dev/null
+++ b/examples/datasource-definition/README.adoc
@@ -0,0 +1,197 @@
+:index-group: DataSources
+:jbake-type: page
+:jbake-status: status=published
+= DataSource Definition
+
+This example shows how to declare a `DataSource` directly in application code 
with the
+`@DataSourceDefinition` annotation. You do not need an entry in `tomee.xml`,
+`resources.xml` or a system property. The container reads the annotation, 
creates
+a pooled data source backed by an in-memory H2 database, binds it in JNDI and
+injects it with `@Resource`.
+
+== Persister
+
+The whole definition lives on a single CDI bean:
+
+[source,java]
+----
+package org.superbiz.dsdef;
+
+import jakarta.annotation.Resource;
+import jakarta.annotation.sql.DataSourceDefinition;
+import jakarta.inject.Named;
+import javax.sql.DataSource;
+
+@DataSourceDefinition(transactional = true,
+        url = "jdbc:h2:mem:persister",
+        className = "org.h2.jdbcx.JdbcDataSource",
+        user = "sa",
+        password = "",
+        name = "java:app/jdbc/persister",
+        initialPoolSize = 1,
+        maxPoolSize = 3
+)
+@Named
+public class Persister {
+
+    @Resource(lookup = "java:app/jdbc/persister")
+    private DataSource ds;
+
+    public DataSource getDs() {
+        return ds;
+    }
+}
+----
+
+The annotation attributes describe the data source:
+
+* `className` is the `DataSource` implementation of the JDBC driver, here H2's 
`org.h2.jdbcx.JdbcDataSource`.
+* `url`, `user` and `password` are the connection settings. 
`jdbc:h2:mem:persister` is an in-memory H2 database.
+* `name` is the JNDI name the data source is bound to. The `java:app` 
namespace makes it visible to the whole application.
+* `initialPoolSize` and `maxPoolSize` configure the connection pool.
+* `transactional = true` makes the data source take part in container-managed 
transactions.
+
+The `ds` field uses `@Resource(lookup = "java:app/jdbc/persister")`, so it 
receives the
+data source declared on the same class. `@Named` makes `Persister` a CDI bean 
that the
+test can inject.
+
+== beans.xml
+
+`src/main/resources/META-INF/beans.xml` is an empty marker that turns on CDI 
for the module:
+
+[source,xml]
+----
+<beans xmlns="http://java.sun.com/xml/ns/javaee";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"/>
+----
+
+== Dependencies
+
+The example compiles against the Jakarta EE Web Profile API. It adds the H2 
driver as a
+regular dependency and the embedded TomEE container as a test dependency:
+
+[source,xml]
+----
+<dependency>
+  <groupId>org.apache.tomee.bom</groupId>
+  <artifactId>tomee-webprofile-api</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <scope>provided</scope>
+</dependency>
+<dependency>
+  <groupId>com.h2database</groupId>
+  <artifactId>h2</artifactId>
+  <version>2.4.240</version>
+</dependency>
+<dependency>
+  <groupId>junit</groupId>
+  <artifactId>junit</artifactId>
+  <version>4.13.2</version>
+  <scope>test</scope>
+</dependency>
+<dependency>
+  <groupId>org.apache.tomee.bom</groupId>
+  <artifactId>tomee-webprofile</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <scope>test</scope>
+</dependency>
+----
+
+== DataSourceDefinitionTest
+
+The test starts an embedded container with `EJBContainer.createEJBContainer()` 
and
+injects the `Persister` bean into the test instance by binding the test to the
+`inject` name:
+
+[source,java]
+----
+public class DataSourceDefinitionTest {
+
+    private static EJBContainer container;
+
+    @Inject
+    private Persister persister;
+
+    @BeforeClass
+    public static void start() {
+        container = EJBContainer.createEJBContainer();
+    }
+
+    @Before
+    public void inject() throws NamingException {
+        container.getContext().bind("inject", this);
+    }
+
+    @AfterClass
+    public static void stop() {
+        container.close();
+    }
+----
+
+`checkDs` makes sure the injected data source is a TomEE-managed pool
+(`DbcpManagedDataSource`) and that the pool uses the values from the 
annotation.
+It then opens a connection, creates a table, inserts a row, commits and reads 
the row back:
+
+[source,java]
+----
+    @Test
+    public void checkDs() throws SQLException {
+        final DataSource ds = persister.getDs();
+        assertNotNull(ds);
+        assertThat(ds, instanceOf(DbcpManagedDataSource.class));
+
+        final DbcpManagedDataSource castedDs = (DbcpManagedDataSource) ds;
+
+        final String driver = castedDs.getDriverClassName();
+        assertEquals("org.h2.jdbcx.JdbcDataSource", driver);
+
+        final String user = castedDs.getUserName();
+        assertEquals("sa", user);
+
+        final String url = castedDs.getUrl();
+        assertEquals("jdbc:h2:mem:persister", url);
+
+        final int initPoolSize = castedDs.getInitialSize();
+        assertEquals(1, initPoolSize);
+
+        final int maxIdle = castedDs.getMaxIdle();
+        assertEquals(3, maxIdle);
+
+        final Connection connection = ds.getConnection();
+        assertNotNull(connection);
+
+        execute(connection, "CREATE TABLE TEST(ID INT PRIMARY KEY, NAME 
VARCHAR(255))");
+        execute(connection, "INSERT INTO TEST(ID, NAME) VALUES(1, 'foo')");
+        connection.commit();
+
+        final PreparedStatement statement = 
ds.getConnection().prepareStatement("SELECT NAME FROM TEST");
+        statement.execute();
+        final ResultSet set = statement.getResultSet();
+
+        assertTrue(set.next());
+        assertEquals("foo", set.getString("NAME"));
+    }
+----
+
+`lookup` checks that the data source is bound under the name given in the 
annotation,
+and that the JNDI lookup returns the same object that was injected into 
`Persister`:
+
+[source,java]
+----
+    @Test
+    public void lookup() throws NamingException {
+        final Object o = 
container.getContext().lookup("java:app/jdbc/persister");
+        assertNotNull(o);
+        assertEquals(persister.getDs(), o);
+    }
+----
+
+== Running
+
+Run the tests from the example directory:
+
+[source,bash]
+----
+mvn clean test
+----
diff --git a/examples/groovy-cdi/README.adoc b/examples/groovy-cdi/README.adoc
new file mode 100644
index 0000000000..50a1ca5fd3
--- /dev/null
+++ b/examples/groovy-cdi/README.adoc
@@ -0,0 +1,107 @@
+:index-group: Unrevised
+:jbake-type: page
+:jbake-status: status=published
+= Groovy CDI
+
+This example shows that a class written in http://groovy-lang.org[Groovy] can 
be used as a CDI bean.
+A Groovy class is injected with `@Inject` into an Arquillian test, which is 
also written in Groovy,
+and runs in the embedded OpenEJB container.
+
+== Compiling the Groovy sources
+
+The Groovy code lives in `src/main/groovy` and `src/test/groovy`. It is 
compiled by the
+`gmavenplus-plugin`, which runs its `compile` and `compileTests` goals:
+
+[source,xml]
+----
+<plugin>
+  <groupId>org.codehaus.gmavenplus</groupId>
+  <artifactId>gmavenplus-plugin</artifactId>
+  <version>4.2.1</version>
+  <executions>
+    <execution>
+      <goals>
+        <goal>compile</goal>
+        <goal>compileTests</goal>
+      </goals>
+    </execution>
+  </executions>
+</plugin>
+----
+
+The Groovy runtime comes from the `org.codehaus.groovy:groovy-all` dependency. 
The test
+dependencies are JUnit 4, `arquillian-openejb-embedded`, `ziplock` and 
`arquillian-junit-container`.
+
+== The bean
+
+`Hello` is a plain Groovy class (`src/main/groovy/org/superbiz/Hello.groovy`, 
in package
+`org.superbiz.groovy`). It has no annotations. Its `hi()` method returns the 
string `"hi"`:
+
+[source,groovy]
+----
+package org.superbiz.groovy
+
+class Hello {
+
+    def hi() {
+        "hi"
+    }
+}
+----
+
+The module also contains an empty `src/main/resources/META-INF/beans.xml`:
+
+[source,xml]
+----
+<beans>
+</beans>
+----
+
+== The test
+
+`HelloTest` is a JUnit 4 test written in Groovy and run by the `Arquillian` 
runner.
+The `@Deployment` method builds a `WebArchive` that contains:
+
+* the jar that holds `GroovyObject`, found with ziplock's 
`JarLocation.jarLocation(...)` and added
+  as a library, so the Groovy runtime is available in the deployment;
+* an empty `WEB-INF/beans.xml`, which turns on CDI for the archive;
+* the `Hello` class.
+
+The `Hello` instance is injected with `@Inject`. The test checks that it was 
injected and
+that `hi()` returns `"hi"`:
+
+[source,groovy]
+----
+@RunWith(Arquillian.class)
+class HelloTest {
+
+    @Inject
+    private Hello hello
+
+    @Deployment
+    static WebArchive war() {
+        ShrinkWrap.create(WebArchive.class)
+                  .addAsLibraries(JarLocation.jarLocation(GroovyObject.class))
+                  .addAsWebInfResource(EmptyAsset.INSTANCE, 
ArchivePaths.create("beans.xml"))
+                  .addClasses(Hello.class)
+    }
+
+    @Test
+    void hello() {
+        assertNotNull hello
+        assertEquals "hi", hello.hi()
+    }
+}
+----
+
+== Running the example
+
+From the `examples/groovy-cdi` directory, run:
+
+[source,bash]
+----
+$ mvn clean test
+----
+
+Maven compiles the Groovy sources with GMavenPlus, and Surefire runs 
`HelloTest` in the embedded
+OpenEJB container.
diff --git a/examples/multiple-arquillian-adapters/README.adoc 
b/examples/multiple-arquillian-adapters/README.adoc
new file mode 100644
index 0000000000..04394ff1a6
--- /dev/null
+++ b/examples/multiple-arquillian-adapters/README.adoc
@@ -0,0 +1,348 @@
+:index-group: Testing Techniques
+:jbake-type: page
+:jbake-status: status=published
+= Multiple Arquillian Adapters
+
+This example shows how to run tests against several TomEE Arquillian adapters 
in one Maven module.
+The adapters are all on the test classpath:
+
+* `arquillian-openejb-embedded`, which runs OpenEJB embedded, with and without 
its remote (HTTP) layer
+* `arquillian-tomee-embedded`, which runs TomEE embedded in the test JVM
+* `arquillian-tomee-remote`, which runs TomEE in a separate process
+
+Each test class is tagged with a JUnit category.
+Each surefire execution runs only one category and tells Arquillian which 
adapter and which `arquillian.xml` container to use.
+
+== The application
+
+The application has two beans.
+The first is a stateless EJB:
+
+[source,java]
+----
+package org.superbiz;
+
+import jakarta.ejb.Stateless;
+
+@Stateless
+public class SomeEJB {
+
+    public String ok() {
+        return "ejb";
+    }
+}
+----
+
+The second is a stateless EJB that is also a JAX-RS resource:
+
+[source,java]
+----
+package org.superbiz;
+
+import jakarta.ejb.Stateless;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+@Path("/rest")
+@Stateless
+public class SomeRest {
+
+    @GET
+    @Path("/ok")
+    public String ok() {
+        return "rest";
+    }
+}
+----
+
+== Dependencies
+
+The pom declares all three TomEE Arquillian adapters.
+It also declares `ziplock`, which the embedded remote test uses to read the 
HTTP response, and the `tomee-webprofile` BOM, which provides the container 
parts the embedded JAX-RS test needs:
+
+[source,xml]
+----
+<!-- additional container part for embedded JAX-RS test -->
+<dependency>
+  <groupId>org.apache.tomee.bom</groupId>
+  <artifactId>tomee-webprofile</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <scope>test</scope>
+</dependency>
+<!-- arquillian adapters -->
+<dependency>
+  <groupId>org.apache.tomee</groupId>
+  <artifactId>arquillian-openejb-embedded</artifactId>
+  <version>${tomee.version}</version>
+  <scope>test</scope>
+</dependency>
+<dependency>
+  <groupId>org.apache.tomee</groupId>
+  <artifactId>arquillian-tomee-embedded</artifactId>
+  <version>${tomee.version}</version>
+  <scope>test</scope>
+</dependency>
+<dependency>
+  <groupId>org.apache.tomee</groupId>
+  <artifactId>arquillian-tomee-remote</artifactId>
+  <version>${tomee.version}</version>
+  <scope>test</scope>
+</dependency>
+----
+
+== Selecting the adapter with surefire executions
+
+With several adapters on the classpath, each test run has to say which one to 
use.
+The pom turns off surefire's default execution with `<skip>true</skip>` in the 
plugin-level configuration.
+It then defines four executions in the `test` phase.
+Each execution sets three things:
+
+* `groups`: the JUnit category to run
+* `arquillian.launch`: the container qualifier to use from `arquillian.xml`
+* `openejb.arquillian.adapter`: the TomEE adapter to activate (`openejb`, 
`tomee-embedded` or `tomee-remote`)
+
+[source,xml]
+----
+<plugin>
+  <!--
+  Redefining execution to:
+  1. set the right arquillian adapter
+  2. set the category to execute (include/exclude works too)
+  3. set the arquillian launcher to use
+  -->
+  <groupId>org.apache.maven.plugins</groupId>
+  <artifactId>maven-surefire-plugin</artifactId>
+  <version>3.5.3</version>
+  <executions>
+    <execution>
+      <id>test-embedded</id>
+      <phase>test</phase>
+      <goals>
+        <goal>test</goal>
+      </goals>
+      <configuration>
+        <skip>${maven.test.skip}</skip>
+        <argLine>-Djdk.attach.allowAttachSelf</argLine>
+        <groups>org.superbiz.embedded.standalone.Embedded</groups>
+        <systemPropertyVariables>
+          <arquillian.launch>embedded</arquillian.launch>
+          <openejb.arquillian.adapter>openejb</openejb.arquillian.adapter>
+        </systemPropertyVariables>
+      </configuration>
+    </execution>
+    <execution>
+      <id>test-embedded-remote</id>
+      <phase>test</phase>
+      <goals>
+        <goal>test</goal>
+      </goals>
+      <configuration>
+        <skip>${maven.test.skip}</skip>
+        <groups>org.superbiz.embedded.remote.EmbeddedRemote</groups>
+        <systemPropertyVariables>
+          <arquillian.launch>embedded-remote</arquillian.launch>
+          <openejb.arquillian.adapter>openejb</openejb.arquillian.adapter>
+        </systemPropertyVariables>
+      </configuration>
+    </execution>
+    <execution>
+      <id>test-tomee-embedded</id>
+      <phase>test</phase>
+      <goals>
+        <goal>test</goal>
+      </goals>
+      <configuration>
+        <skip>${maven.test.skip}</skip>
+        <groups>org.superbiz.tomee.embedded.TomEEEmbedded</groups>
+        <argLine>-Djdk.attach.allowAttachSelf</argLine>
+        <systemPropertyVariables>
+          <arquillian.launch>tomee-embedded</arquillian.launch>
+          
<openejb.arquillian.adapter>tomee-embedded</openejb.arquillian.adapter>
+        </systemPropertyVariables>
+      </configuration>
+    </execution>
+    <execution>
+      <id>test-tomee-remote</id>
+      <phase>test</phase>
+      <goals>
+        <goal>test</goal>
+      </goals>
+      <configuration>
+        <skip>${maven.test.skip}</skip>
+        <groups>org.superbiz.tomee.remote.TomEERemote</groups>
+        <systemPropertyVariables>
+          <arquillian.launch>tomee-remote</arquillian.launch>
+          <openejb.arquillian.adapter>tomee-remote</openejb.arquillian.adapter>
+        </systemPropertyVariables>
+      </configuration>
+    </execution>
+  </executions>
+  <configuration>
+    <skip>true</skip>
+    <parallel>none</parallel>
+    <threadCount>1</threadCount>
+  </configuration>
+</plugin>
+----
+
+The two OpenEJB executions use the same adapter (`openejb`).
+They differ only in the `arquillian.launch` qualifier, so each picks a 
different container configuration.
+
+== Container configurations in arquillian.xml
+
+`src/test/resources/arquillian.xml` defines one `<container>` for each 
qualifier used in the pom:
+
+[source,xml]
+----
+<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+    >
+  <container qualifier="embedded">
+    <configuration>
+      <property name="properties">
+        # to ensure we don't start remote container even if it is found at 
classpath
+        openejb.embedded.remotable = false
+      </property>
+    </configuration>
+  </container>
+  <container qualifier="embedded-remote">
+    <configuration>
+      <property name="properties">
+        # optional if found at classpath
+        openejb.embedded.remotable = true
+      </property>
+    </configuration>
+  </container>
+  <container qualifier="tomee-embedded">
+    <configuration>
+      <property name="httpPort">-1</property>
+      <property name="stopPort">-1</property>
+      <property name="dir">target/tomee-embedded</property>
+      <property name="appWorkingDir">target/work3</property>
+    </configuration>
+  </container>
+  <container qualifier="tomee-remote">
+    <configuration>
+      <property name="httpPort">-1</property>
+      <property name="stopPort">-1</property>
+      <property name="ajpPort">-1</property>
+      <property name="dir">target/tomee-remote</property>
+      <property name="appWorkingDir">target/work4</property>
+    </configuration>
+  </container>
+</arquillian>
+----
+
+* `embedded` sets `openejb.embedded.remotable = false`, so OpenEJB runs 
without its remote layer.
+* `embedded-remote` sets `openejb.embedded.remotable = true`, so the embedded 
container also serves HTTP. The JAX-RS test needs this.
+* `tomee-embedded` and `tomee-remote` set their ports to `-1`, which asks for 
random free ports. Each also gets its own `dir` and `appWorkingDir` under 
`target`, so the two containers don't share files.
+
+== The tests
+
+Each test class is annotated with `@Category`.
+The category is a marker interface in the same package, for example:
+
+[source,java]
+----
+package org.superbiz.embedded.standalone;
+
+public interface Embedded {
+
+}
+----
+
+The other marker interfaces are `EmbeddedRemote`, `TomEEEmbedded` and 
`TomEERemote`.
+
+=== OpenEJB embedded
+
+This test deploys a jar that contains `SomeEJB`.
+It checks that the injected EJB is not `null` and that it returns `"ejb"`:
+
+[source,java]
+----
+@Category(Embedded.class)
+@RunWith(Arquillian.class)
+public class OpenEJBEmbeddedTest {
+
+    @EJB
+    private SomeEJB ejb;
+
+    @Deployment
+    public static JavaArchive jar() {
+        return ShrinkWrap.create(JavaArchive.class).addClass(SomeEJB.class);
+    }
+
+    @Test
+    public void check() {
+        assertNotNull(ejb);
+        assertEquals("ejb", ejb.ok());
+    }
+}
+----
+
+=== OpenEJB embedded with HTTP
+
+This test deploys `SomeRest` in a jar named `my-webapp.jar`.
+It calls the REST endpoint over HTTP on port `4204` and expects `"rest"`:
+
+[source,java]
+----
+@Category(EmbeddedRemote.class)
+@RunWith(Arquillian.class)
+public class OpenEJBEmbeddedRemoteTest {
+
+    @Deployment
+    public static JavaArchive jar() {
+        return ShrinkWrap.create(JavaArchive.class, 
"my-webapp.jar").addClass(SomeRest.class);
+    }
+
+    @Test
+    public void check() throws IOException {
+        final String content = IO.slurp(new 
URL("http://localhost:4204/my-webapp/rest/ok";));
+        assertEquals("rest", content);
+    }
+}
+----
+
+=== TomEE embedded and TomEE remote
+
+`TomEEEmbeddedTest` and `TomEERemoteTest` deploy a `test.war` that contains 
`SomeEJB`.
+Both run the same check as the OpenEJB embedded test.
+The remote test also adds its category interface to the archive, because the 
test class is loaded inside the remote server:
+
+[source,java]
+----
+@Category(TomEERemote.class)
+@RunWith(Arquillian.class)
+public class TomEERemoteTest {
+
+    @EJB
+    private SomeEJB ejb;
+
+    @Deployment
+    public static WebArchive war() {
+        // use test name for the war otherwise arquillian ejb enricher doesn't 
work
+        // don't forget the category otherwise it will fail since the runner 
parse annotations
+        // in embedded mode it is not so important
+        return ShrinkWrap.create(WebArchive.class, 
"test.war").addClasses(SomeEJB.class, TomEERemote.class);
+    }
+
+    @Test
+    public void check() {
+        assertNotNull(ejb);
+        assertEquals("ejb", ejb.ok());
+    }
+}
+----
+
+== Running
+
+[source,bash]
+----
+mvn clean test
+----
+
+This command runs all four surefire executions in turn: `test-embedded`, 
`test-embedded-remote`, `test-tomee-embedded` and `test-tomee-remote`.
+Each execution runs only the tests in its category, against its own adapter.
+The pom has no profiles for choosing a single adapter.
+To add another adapter or configuration, add a category interface, an 
`arquillian.xml` container and a matching surefire execution.
+Passing `-Dmaven.test.skip=true` skips all four executions.
diff --git a/examples/rest-applicationcomposer/README.adoc 
b/examples/rest-applicationcomposer/README.adoc
new file mode 100644
index 0000000000..ddc448ad39
--- /dev/null
+++ b/examples/rest-applicationcomposer/README.adoc
@@ -0,0 +1,133 @@
+:index-group: Testing Techniques
+:jbake-type: page
+:jbake-status: status=published
+= REST with Application Composer
+
+This example shows how to test a JAX-RS service with the Application Composer, 
without building a WAR or starting a full server.
+Instead of listing classes, the test builds its deployment programmatically as 
an `EjbModule`: it declares the REST resource as a singleton EJB and registers 
a JAX-RS `ExceptionMapper` as a CXF provider through the `openejb-jar` 
configuration.
+
+== The REST service
+
+`GreetingService` exposes a single `GET` method on `/greeting`. It does not 
return a value: it always throws an `IllegalArgumentException`.
+
+[source,java]
+----
+package org.superbiz.composed.rest;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+@Path("/greeting")
+public class GreetingService {
+
+    @GET
+    public String message() {
+        throw new IllegalArgumentException("this exception is handled by an 
exception mapper");
+    }
+}
+----
+
+== The exception mapper
+
+`IllegalArgumentExceptionMapper` turns an `IllegalArgumentException` into a 
`200 OK` response whose body is the exception message.
+
+[source,java]
+----
+package org.superbiz.composed.rest;
+
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.Provider;
+
+@Provider
+public class IllegalArgumentExceptionMapper implements 
ExceptionMapper<java.lang.IllegalArgumentException> {
+
+    @Override
+    public Response toResponse(final java.lang.IllegalArgumentException 
throwable) {
+        return Response.ok(throwable.getMessage()).build();
+    }
+}
+----
+
+If the mapper is picked up, a client calling `GET /greeting` receives the 
exception message as the response body.
+
+== The test
+
+`GreetingServiceTest` runs with the `ApplicationComposer` JUnit runner, which 
starts an embedded TomEE container for the test class and deploys the module 
described by the test itself.
+
+[source,java]
+----
+@RunWith(ApplicationComposer.class)
+public class GreetingServiceTest {
+
+    @Configuration
+    public Properties configuration() {
+        return new Properties() {{
+            setProperty(OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE, 
Boolean.TRUE.toString());
+        }};
+    }
+
+    @Module
+    public EjbModule app() {
+        final SingletonBean bean = (SingletonBean) new 
SingletonBean(GreetingService.class).localBean();
+        bean.setRestService(true);
+
+        // now create an ejbjar and an openejb-jar to hold the provider config
+
+        final EjbJar ejbJar = new EjbJar();
+        ejbJar.addEnterpriseBean(bean);
+
+        final OpenejbJar openejbJar = new OpenejbJar();
+        openejbJar.addEjbDeployment(new 
EjbDeployment(ejbJar.getEnterpriseBeans()[0]));
+
+        final Properties properties = 
openejbJar.getEjbDeployment().iterator().next().getProperties();
+        properties.setProperty("cxf.jaxrs.providers", 
IllegalArgumentExceptionMapper.class.getName());
+
+        // link all and return this module
+
+        final EjbModule module = new EjbModule(ejbJar);
+        module.setOpenejbJar(openejbJar);
+
+        return module;
+    }
+
+    @Test
+    public void checkProviderIsUsed() throws IOException {
+        final String message = IO.slurp(new 
URL("http://localhost:4204/GreetingServiceTest/greeting/";));
+        assertEquals("this exception is handled by an exception mapper", 
message);
+    }
+}
+----
+
+=== `@RunWith(ApplicationComposer.class)`
+
+The JUnit runner from `org.apache.openejb.junit`. It reads the 
`@Configuration` and `@Module` methods of the test class, boots the container 
with that setup and deploys the module before the tests run.
+
+=== `@Configuration`
+
+Returns the `Properties` used to configure the container. Here 
`OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE` is set to `true`, which starts 
the embedded HTTP layer so the REST service can be reached over HTTP.
+
+=== `@Module`
+
+Describes what gets deployed. Instead of returning a list of classes, this 
method builds an `EjbModule` by hand, using the `org.apache.openejb.jee` 
descriptor model:
+
+* `new SingletonBean(GreetingService.class).localBean()` declares 
`GreetingService` as a singleton EJB with a no-interface view, and 
`setRestService(true)` exposes it as a JAX-RS resource.
+* The bean is added to an `EjbJar`, the programmatic equivalent of 
`ejb-jar.xml`.
+* An `OpenejbJar` (the equivalent of `openejb-jar.xml`) gets an 
`EjbDeployment` for that bean. The deployment property `cxf.jaxrs.providers` is 
set to `IllegalArgumentExceptionMapper`, which registers the mapper as a CXF 
JAX-RS provider for this service.
+* Both descriptors are linked in an `EjbModule` via 
`module.setOpenejbJar(openejbJar)` and returned.
+
+=== What the test checks
+
+`checkProviderIsUsed()` reads 
`http://localhost:4204/GreetingServiceTest/greeting/` with `IO.slurp` and 
asserts that the body is `this exception is handled by an exception mapper`.
+Because `GreetingService.message()` always throws, this value can only come 
from `IllegalArgumentExceptionMapper`, so the test proves that the provider 
configured through `cxf.jaxrs.providers` is used.
+
+== Running
+
+The example is a Maven project. The OpenEJB classes are only needed by the 
test, so `tomee-webprofile` is declared with `test` scope while 
`tomee-webprofile-api` is `provided`.
+
+Run the test with:
+
+[source,bash]
+----
+mvn clean test
+----
diff --git a/examples/rest-jaas/README.adoc b/examples/rest-jaas/README.adoc
new file mode 100644
index 0000000000..2705efd1f3
--- /dev/null
+++ b/examples/rest-jaas/README.adoc
@@ -0,0 +1,200 @@
+:index-group: Security
+:jbake-type: page
+:jbake-status: status=published
+= REST and JAAS
+
+This example shows how to secure a JAX-RS endpoint in TomEE using JAAS.
+Tomcat's `JAASRealm` delegates authentication to OpenEJB's
+`PropertiesLoginModule`, which reads users and groups from two properties
+files. The servlet container protects the REST URLs with BASIC authentication,
+and the EJB container checks the caller's role with `@RolesAllowed` before
+the resource method runs.
+
+The example has no Java code besides the REST resource. The rest is
+configuration in `src/main/tomee/conf` and `WEB-INF/web.xml`.
+
+== The REST resource
+
+`JAASSecuredRestEndpoint` is a JAX-RS resource that is also a `@Singleton`
+EJB, so EJB security annotations apply to it. The `jaas` method is restricted
+to the `superUser` role and returns the name of the authenticated caller,
+taken from the injected `SessionContext`:
+
+[source,java]
+----
+package org.superbiz.jaxrs.jaas;
+
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.ejb.Lock;
+import jakarta.ejb.LockType;
+import jakarta.ejb.SessionContext;
+import jakarta.ejb.Singleton;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+@Path("rest")
+@Singleton
+@Lock(LockType.READ)
+public class JAASSecuredRestEndpoint {
+
+    @Resource
+    private SessionContext ctx;
+
+    @GET
+    @Path("jaas")
+    @RolesAllowed("superUser")
+    public String getPrincipalName() {
+        return ctx.getCallerPrincipal().getName();
+    }
+}
+----
+
+`@Lock(LockType.READ)` allows concurrent requests to the singleton, since
+the method does not change any state.
+
+== The JAAS login module
+
+`src/main/tomee/conf/login.config` declares a JAAS application named
+`PropertiesLoginModule` backed by
+`org.apache.openejb.core.security.jaas.PropertiesLoginModule`:
+
+[source]
+----
+PropertiesLoginModule {
+    org.apache.openejb.core.security.jaas.PropertiesLoginModule required
+    Debug=false
+    UsersFile="users.properties"
+    GroupsFile="groups.properties";
+};
+----
+
+The login module looks up both files in the TomEE `conf` directory.
+`users.properties` maps each user name to its password:
+
+[source]
+----
+tomee=tomee
+adminOnly=adminOnly
+----
+
+`groups.properties` maps each group (role) to a comma-separated list of users:
+
+[source]
+----
+admin=tomee,adminOnly
+superUser=tomee
+----
+
+So `tomee` has the roles `admin` and `superUser`, while `adminOnly` only has
+`admin`.
+
+== The Tomcat realm
+
+`src/main/tomee/conf/server.xml` configures a `JAASRealm` on the engine. Its
+`appName` matches the entry in `login.config`. `userClassNames` and
+`roleClassNames` tell Tomcat which principals created by the login module
+represent the user and its roles:
+
+[source,xml]
+----
+<Server port="8005" shutdown="SHUTDOWN">
+  <Listener className="org.apache.tomee.catalina.ServerListener"/>
+  <Listener className="org.apache.catalina.security.SecurityListener"/>
+
+  <Service name="Catalina">
+    <Connector port="8080" protocol="HTTP/1.1"/>
+    <Engine name="Catalina" defaultHost="localhost">
+
+      <Realm className="org.apache.catalina.realm.JAASRealm" 
appName="PropertiesLoginModule"
+             
userClassNames="org.apache.openejb.core.security.jaas.UserPrincipal"
+             
roleClassNames="org.apache.openejb.core.security.jaas.GroupPrincipal">
+      </Realm>
+
+      <Host name="localhost" appBase="webapps"/>
+    </Engine>
+  </Service>
+</Server>
+----
+
+== The web.xml security constraint
+
+`WEB-INF/web.xml` enables BASIC authentication and protects every URL under
+`/rest/*`. The `<role-name>*</role-name>` auth constraint only requires the
+caller to be authenticated with one of the declared roles; the finer-grained
+check is left to `@RolesAllowed` on the resource. The transport guarantee
+`CONFIDENTIAL` requires the requests to use HTTPS.
+
+[source,xml]
+----
+<web-app xmlns="http://java.sun.com/xml/ns/javaee";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd";
+         version="3.0">
+  <login-config>
+    <auth-method>BASIC</auth-method>
+    <realm-name>PropertiesLoginModule</realm-name>
+  </login-config>
+
+  <security-constraint>
+    <web-resource-collection>
+      <web-resource-name>Secured REST Service with JAAS</web-resource-name>
+      <url-pattern>/rest/*</url-pattern>
+    </web-resource-collection>
+    <auth-constraint>
+      <role-name>*</role-name>
+      <!-- we'll use JAAS so don't filter too much here -->
+    </auth-constraint>
+    <user-data-constraint>
+      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
+    </user-data-constraint>
+  </security-constraint>
+
+  <security-role>
+    <role-name>admin</role-name>
+  </security-role>
+  <security-role>
+    <role-name>superUser</role-name>
+  </security-role>
+</web-app>
+----
+
+== Running the example
+
+The `tomee-maven-plugin` copies `src/main/tomee/conf` into the TomEE
+instance it builds, deploys the application as `ROOT` and points the JVM to
+the JAAS configuration through the `java.security.auth.login.config` system
+property:
+
+[source,xml]
+----
+<plugin>
+  <groupId>org.apache.tomee.maven</groupId>
+  <artifactId>tomee-maven-plugin</artifactId>
+  <version>11.0.0-SNAPSHOT</version>
+  <configuration>
+    <systemVariables>
+      
<java.security.auth.login.config>${project.build.directory}/apache-tomee/conf/login.config</java.security.auth.login.config>
+    </systemVariables>
+    <context>ROOT</context>
+  </configuration>
+</plugin>
+----
+
+The example does not contain automated tests, so `mvn clean test` only
+compiles it. To try it, build and start TomEE with:
+
+[source]
+----
+mvn clean package tomee:run
+----
+
+The endpoint is `rest/jaas` under the root context. Because of the
+`CONFIDENTIAL` transport guarantee and the fact that the provided `server.xml`
+only defines a plain HTTP connector on port 8080, you need to add an HTTPS
+connector (with a keystore placed in `src/main/tomee/conf`) before calling it.
+Then:
+
+* `tomee` / `tomee` has the `superUser` role and gets its principal name, 
`tomee`, back.
+* `adminOnly` / `adminOnly` is authenticated but only has `admin`, so 
`@RolesAllowed("superUser")` rejects the call.
+* Requests without credentials or with a wrong password are rejected by the 
BASIC authentication.
diff --git a/examples/scala-basic/README.adoc b/examples/scala-basic/README.adoc
new file mode 100644
index 0000000000..1e8a0ae404
--- /dev/null
+++ b/examples/scala-basic/README.adoc
@@ -0,0 +1,211 @@
+:index-group: Misc
+:jbake-type: page
+:jbake-status: status=published
+= Basic Scala EJB
+
+This example shows that an EJB does not have to be written in Java. The
+`GreetingService` singleton bean is written in Scala, compiled with Maven and
+tested with ScalaTest against the embedded OpenEJB container, which injects the
+bean into the Scala test class just as it would into a Java one.
+
+== The bean
+
+`GreetingService` is a plain Scala class carrying the usual Jakarta EE
+annotations: `@Singleton` makes it a singleton session bean, `@LocalBean` 
exposes
+a no-interface view, and `@Lock(LockType.READ)` lets concurrent callers invoke 
it
+without waiting on each other.
+
+The companion `object` holds the default name. The `hi` method uses a Scala
+default parameter value, so it can be called with or without an argument. A
+`null` name falls back to the default as well.
+
+[source,scala]
+----
+package org.superbiz
+
+import jakarta.ejb.{LocalBean, Lock, LockType, Singleton}
+
+object GreetingService {
+  val DefaultName = "Scala"
+}
+
+@LocalBean
+@Singleton
+@Lock(LockType.READ)
+class GreetingService {
+  def hi(name: String = GreetingService.DefaultName) = {
+    if (name != null) {
+      "hi " + name
+    } else {
+      "hi " + GreetingService.DefaultName
+    }
+  }
+}
+----
+
+An empty `src/main/resources/META-INF/beans.xml` is also part of the module:
+
+[source,xml]
+----
+<beans/>
+----
+
+== Building Scala sources with Maven
+
+The `pom.xml` sets the source directories to `src/main/scala` and
+`src/test/scala` and uses the `scala-maven-plugin` to compile them. Its
+`compile` and `testCompile` goals are bound to the build, and the Scala version
+comes from the `scala.full-version` property (`2.13.15`):
+
+[source,xml]
+----
+<properties>
+  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  <scala.version>2.13</scala.version>
+  <scala.full-version>${scala.version}.15</scala.full-version>
+</properties>
+
+<build>
+  <sourceDirectory>src/main/scala</sourceDirectory>
+  <testSourceDirectory>src/test/scala</testSourceDirectory>
+  <plugins>
+    <!-- compile scala sources -->
+    <plugin>
+      <groupId>net.alchim31.maven</groupId>
+      <artifactId>scala-maven-plugin</artifactId>
+      <version>4.9.7</version>
+      <executions>
+        <execution>
+          <goals>
+            <goal>compile</goal>
+            <goal>testCompile</goal>
+          </goals>
+        </execution>
+      </executions>
+      <configuration>
+        <scalaVersion>${scala.full-version}</scalaVersion>
+      </configuration>
+    </plugin>
+    ...
+  </plugins>
+</build>
+----
+
+The tests are ScalaTest suites rather than JUnit tests, so Surefire is skipped
+and the `scalatest-maven-plugin` runs them in the `test` phase instead. It
+writes its reports to `target/surefire-reports`:
+
+[source,xml]
+----
+<!-- disable surefire because we use scalatest -->
+<plugin>
+  <groupId>org.apache.maven.plugins</groupId>
+  <artifactId>maven-surefire-plugin</artifactId>
+  <version>3.5.3</version>
+  <configuration>
+    <skipTests>true</skipTests>
+  </configuration>
+</plugin>
+<!-- enable scalatest as a replacement for surefire -->
+<plugin>
+  <groupId>org.scalatest</groupId>
+  <artifactId>scalatest-maven-plugin</artifactId>
+  <version>2.2.0</version>
+  <configuration>
+    <forkMode>never</forkMode>
+    
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
+    <junitxml>.</junitxml>
+    <filereports>TestSuite.txt</filereports>
+  </configuration>
+  <executions>
+    <execution>
+      <id>test</id>
+      <goals>
+        <goal>test</goal>
+      </goals>
+    </execution>
+  </executions>
+</plugin>
+----
+
+The Jakarta EE API comes from the `tomee-webprofile-api` BOM (scope
+`provided`), and the `tomee-webprofile` BOM (scope `test`) supplies the
+embedded container for the tests. The module also depends on `scala-library`
+and on `scalatest_2.13` for testing.
+
+== Testing
+
+`GreetingServiceTest` is a ScalaTest `AnyFunSuite`. It mixes in
+`BeforeAndAfterAll` and `BeforeAndAfterEach` to manage the embedded container:
+
+* `beforeAll` starts the container with `EJBContainer.createEJBContainer()`,
+  which deploys `GreetingService`.
+* `beforeEach` binds the test instance under the name `inject`, so the
+  container fills the `@Inject` field `service` before each test.
+* `afterAll` closes the container.
+
+[source,scala]
+----
+package org.superbiz
+
+import jakarta.ejb.embeddable.EJBContainer
+import jakarta.inject.Inject
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+import org.scalatest.funsuite.AnyFunSuite
+
+class GreetingServiceTest extends AnyFunSuite with BeforeAndAfterAll with 
BeforeAndAfterEach {
+  @Inject
+  private var service: GreetingService = null
+
+  test("Hi scala") {
+    val message = service hi "scala"
+    assert(message == "hi scala")
+  }
+
+  test("Default Hi") {
+    val message = service.hi()
+    assert(message == "hi Scala")
+  }
+
+  test("Hi null") {
+    val message = service.hi(null)
+    assert(message == "hi Scala") // when called with null we use default name
+  }
+
+  private var container: EJBContainer = null
+
+  override def beforeAll() {
+    container = EJBContainer.createEJBContainer()
+  }
+
+  override def beforeEach() {
+    container.getContext().bind("inject", this)
+  }
+
+  override def afterAll() {
+    if (container != null) {
+      container.close()
+    }
+  }
+}
+----
+
+The three tests check each branch of `hi`:
+
+* `Hi scala` calls the method with an explicit name, using Scala infix
+  notation (`service hi "scala"`), and expects `hi scala`.
+* `Default Hi` calls `service.hi()` without an argument, so the default
+  parameter value applies, and expects `hi Scala`.
+* `Hi null` passes `null` and expects the default name, `hi Scala`.
+
+== Running
+
+Run the tests with Maven:
+
+[source,bash]
+----
+mvn clean test
+----
+
+The example also has a `build.sbt` file for sbt users. The Maven build
+described above is the one covered in this README.
diff --git a/examples/server-events/README.adoc 
b/examples/server-events/README.adoc
new file mode 100644
index 0000000000..d165e0ee3f
--- /dev/null
+++ b/examples/server-events/README.adoc
@@ -0,0 +1,148 @@
+:index-group: Other Features
+:jbake-type: page
+:jbake-status: status=published
+= Server Events
+
+TomEE (through OpenEJB) fires internal events during its lifecycle, for 
example when the assembler has created an application.
+Any plain Java class can observe these events with a method whose parameter is 
annotated with `org.apache.openejb.observer.Observes`.
+Note that this is the OpenEJB observer API, not CDI's 
`jakarta.enterprise.event.Observes`.
+
+This example shows two ways to register such an observer:
+
+* as a service declared in the container configuration (`MyListener`)
+* as an extension discovered automatically from 
`META-INF/org.apache.openejb.extension` (`AutoDiscoveredListener`)
+
+== An observer registered as a service
+
+`MyListener` observes `Object`, so it receives every event fired by the server 
and logs it.
+It also has a setter, which makes `logAllEvent` a property that can be set 
from the service configuration.
+
+[source,java]
+----
+package org.superbiz.event;
+
+import org.apache.openejb.observer.Observes;
+
+import java.util.logging.Logger;
+
+/**
+ * registered as service in openejb.xml, tomee.xml, resources.xml or openejb 
system properties:
+ *
+ * listener = new://Service?class-name=org.superbiz.event.MyListener
+ * listener.logAllEvent = true
+ */
+public class MyListener {
+
+    private static final Logger LOGGER = 
Logger.getLogger(MyListener.class.getName());
+
+    private static boolean logAllEvent = false; // static for testing
+
+    public void global(@Observes final Object event) {
+        LOGGER.info(">>> an event occurred -> " + event.toString());
+    }
+
+    // configurable
+    public void setLogAllEvent(boolean logAllEvent) {
+        this.logAllEvent = logAllEvent;
+    }
+
+    public static boolean isLogAllEvent() {
+        return logAllEvent;
+    }
+}
+----
+
+As the Javadoc says, the class is not picked up on its own: it has to be 
declared as a service in `openejb.xml`, `tomee.xml`, `resources.xml` or as 
OpenEJB system properties:
+
+[source,properties]
+----
+listener = new://Service?class-name=org.superbiz.event.MyListener
+listener.logAllEvent = true
+----
+
+== An auto-discovered observer
+
+`AutoDiscoveredListener` only observes `AssemblerAfterApplicationCreated`, the 
event fired once an application has been created, and stores the application id.
+
+[source,java]
+----
+package org.superbiz.event;
+
+import 
org.apache.openejb.assembler.classic.event.AssemblerAfterApplicationCreated;
+import org.apache.openejb.observer.Observes;
+
+public class AutoDiscoveredListener {
+
+    private static String appName;
+
+    public void appCreated(@Observes final AssemblerAfterApplicationCreated 
appCreatedEvent) {
+        appName = appCreatedEvent.getApp().appId;
+    }
+
+    public static String getAppName() {
+        return appName;
+    }
+}
+----
+
+It needs no service declaration. Instead, its fully qualified class name is 
listed in `src/main/resources/META-INF/org.apache.openejb.extension`:
+
+[source,properties]
+----
+org.superbiz.event.AutoDiscoveredListener
+----
+
+== Testing
+
+The test runs with the embedded OpenEJB Arquillian adapter 
(`arquillian-openejb-embedded`).
+`src/test/resources/arquillian.xml` registers `MyListener` as a service 
through the container properties:
+
+[source,xml]
+----
+<arquillian
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+    >
+  <container qualifier="openejb" default="true">
+    <configuration>
+      <property name="properties">
+        listener = new://Service?class-name=org.superbiz.event.MyListener
+        listener.logAllEvent = true
+      </property>
+    </configuration>
+  </container>
+</arquillian>
+----
+
+The deployment is a `listener-test.jar` that contains both listeners and an 
`org.apache.openejb.extension` manifest resource naming 
`AutoDiscoveredListener`:
+
+[source,java]
+----
+@RunWith(Arquillian.class)
+public class ListenerTest {
+
+    @Deployment
+    public static JavaArchive jar() {
+        return ShrinkWrap.create(JavaArchive.class, "listener-test.jar")
+                .addClasses(MyListener.class, AutoDiscoveredListener.class)
+                .addAsManifestResource(new 
StringAsset(AutoDiscoveredListener.class.getName()), 
ArchivePaths.create("org.apache.openejb.extension"));
+    }
+
+    @Test
+    public void check() {
+        assertEquals("listener-test", AutoDiscoveredListener.getAppName());
+        assertTrue(MyListener.isLogAllEvent());
+    }
+}
+----
+
+The test checks that:
+
+* `AutoDiscoveredListener` received the `AssemblerAfterApplicationCreated` 
event for the deployed application, whose id is `listener-test`
+* `MyListener` was created from the service configuration and its 
`logAllEvent` property was set to `true`
+
+Run it with:
+
+[source,bash]
+----
+mvn clean test
+----
diff --git a/examples/simple-ear/README.adoc b/examples/simple-ear/README.adoc
new file mode 100644
index 0000000000..d0f551f17a
--- /dev/null
+++ b/examples/simple-ear/README.adoc
@@ -0,0 +1,471 @@
+:index-group: Testing Techniques
+:jbake-type: page
+:jbake-status: status=published
+= Simple EAR
+
+This example shows how to package a small Jakarta EE application as an
+Enterprise Archive (EAR) with Maven and how to test it against TomEE. It
+uses the moviefun code found in many other examples and splits it into
+five Maven modules:
+
+* `moviefun-business-model` - the JPA entity, the `persistence.xml` and the
+  `Movies` business interface
+* `moviefun-business-logic` - the `MoviesBean` stateless session bean (EJB 
module)
+* `moviefun-webapp` - a servlet and JSPs that use the EJB (web module)
+* `moviefun-ear` - packages the EJB module and the web module into an EAR
+* `moviefun-functional-tests` - deploys the EAR to a remote TomEE with
+  Arquillian and checks the web pages with HtmlUnit
+
+The dependencies between the modules look like this:
+
+[source]
+----
+moviefun-business-model
+        ^         ^
+        |         | (provided)
+moviefun-business-logic   moviefun-webapp
+        ^  (ejb)            ^  (war)
+        |                   |
+        +---- moviefun-ear -+
+                  ^  (ear, test scope)
+                  |
+        moviefun-functional-tests
+----
+
+== Project layout
+
+The parent pom lists the modules in build order:
+
+[source,xml]
+----
+<groupId>org.superbiz</groupId>
+<artifactId>moviefun-ear-parent</artifactId>
+<version>11.0.0-SNAPSHOT</version>
+<packaging>pom</packaging>
+<name>TomEE :: Examples :: Simple EAR</name>
+<modules>
+  <module>moviefun-business-model</module>
+  <module>moviefun-business-logic</module>
+  <module>moviefun-webapp</module>
+  <module>moviefun-ear</module>
+  <module>moviefun-functional-tests</module>
+</modules>
+----
+
+It also turns on filtering for `src/test/resources` and passes the TomEE
+version to the tests as the `tomee.version` system property.
+
+== Business model
+
+The `moviefun-business-model` module is a plain jar holding the `Movie`
+entity:
+
+[source,java]
+----
+@Entity
+public class Movie implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    private long id;
+
+    private String director;
+    private String title;
+    private int year;
+    private String genre;
+    private int rating;
+
+    // constructors, getters and setters
+}
+----
+
+the persistence unit that maps it:
+
+[source,xml]
+----
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"; version="1.0">
+
+  <persistence-unit name="movie-unit">
+    <jta-data-source>movieDatabase</jta-data-source>
+    <non-jta-data-source>movieDatabaseUnmanaged</non-jta-data-source>
+    <class>org.superbiz.model.Movie</class>
+
+    <properties>
+      <property name="openjpa.jdbc.SynchronizeMappings" 
value="buildSchema(ForeignKeys=true)"/>
+    </properties>
+  </persistence-unit>
+</persistence>
+----
+
+and the `@Local` business interface that both the EJB module and the web
+module compile against:
+
+[source,java]
+----
+@Local
+public interface Movies {
+    Movie find(Long id);
+
+    void addMovie(Movie movie);
+
+    void editMovie(Movie movie);
+
+    void deleteMovieId(long id);
+
+    List<Movie> getMovies();
+
+    List<Movie> findAll(int firstResult, int maxResults);
+
+    int countAll();
+
+    int count(String field, String searchTerm);
+
+    List<Movie> findRange(String field, String searchTerm, int firstResult, 
int maxResults);
+
+    void clean();
+}
+----
+
+Because the interface lives in the model module, the webapp only needs the
+model jar to talk to the EJB and does not depend on the business logic
+module at all.
+
+== Business logic
+
+The `moviefun-business-logic` module depends on the model and contains the
+EJB. An empty `META-INF/ejb-jar.xml` marks the jar as an EJB module:
+
+[source,xml]
+----
+<ejb-jar/>
+----
+
+`MoviesBean` is a stateless session bean that implements `Movies` using an
+`EntityManager` for the `movie-unit` persistence unit:
+
+[source,java]
+----
+@Stateless
+public class MoviesBean implements Movies {
+
+    @PersistenceContext(unitName = "movie-unit")
+    private EntityManager entityManager;
+
+    @Override
+    public void addMovie(Movie movie) {
+        entityManager.persist(movie);
+    }
+
+    @Override
+    public void deleteMovieId(long id) {
+        Movie movie = entityManager.find(Movie.class, id);
+        entityManager.remove(movie);
+    }
+
+    @Override
+    public List<Movie> getMovies() {
+        CriteriaQuery<Movie> cq = 
entityManager.getCriteriaBuilder().createQuery(Movie.class);
+        cq.select(cq.from(Movie.class));
+        return entityManager.createQuery(cq).getResultList();
+    }
+
+    // find, editMovie, findAll, countAll, count, findRange and clean
+    // are implemented with the Criteria API in the same way
+}
+----
+
+== Webapp
+
+The `moviefun-webapp` module is packaged as a war. It depends on the
+business model with `provided` scope, so the model classes are not copied
+into `WEB-INF/lib`; inside the EAR they come from the EAR's `lib` directory
+instead (see below).
+
+[source,xml]
+----
+<dependency>
+  <groupId>org.superbiz</groupId>
+  <artifactId>moviefun-business-model</artifactId>
+  <version>${project.version}</version>
+  <scope>provided</scope>
+</dependency>
+----
+
+`ActionServlet` gets the EJB from the EJB module injected through its
+`Movies` interface and uses it to add, remove, list and search movies
+before forwarding to `WEB-INF/moviefun.jsp`:
+
+[source,java]
+----
+@WebServlet("/moviefun/*")
+public class ActionServlet extends HttpServlet {
+
+    public static int PAGE_SIZE = 5;
+
+    @EJB
+    private Movies moviesBean;
+
+    private void process(HttpServletRequest request, HttpServletResponse 
response) throws ServletException, IOException {
+        String action = request.getParameter("action");
+
+        if ("Add".equals(action)) {
+            // read the form fields and call moviesBean.addMovie(...)
+        } else if ("Remove".equals(action)) {
+            // call moviesBean.deleteMovieId(...) for every selected id
+        } else {
+            // page through moviesBean.findAll(...) or 
moviesBean.findRange(...)
+        }
+
+        request.getRequestDispatcher("WEB-INF/moviefun.jsp").forward(request, 
response);
+    }
+}
+----
+
+`index.jsp` links to two pages: `setup.jsp`, which seeds the database with
+sample data, and `moviefun`, the servlet above. `setup.jsp` looks up the
+bean through the `java:comp/env` entry created by the servlet's `@EJB`
+field:
+
+[source,java]
+----
+InitialContext initialContext = new InitialContext();
+Movies moviesBean = (Movies) 
initialContext.lookup("java:comp/env/org.superbiz.moviefun.ActionServlet/moviesBean");
+
+moviesBean.addMovie(new Movie("Wedding Crashers", "David Dobkin", "Comedy", 7, 
2005));
+moviesBean.addMovie(new Movie("Starsky & Hutch", "Todd Phillips", "Action", 6, 
2004));
+moviesBean.addMovie(new Movie("Shanghai Knights", "David Dobkin", "Action", 6, 
2003));
+moviesBean.addMovie(new Movie("I-Spy", "Betty Thomas", "Adventure", 5, 2002));
+moviesBean.addMovie(new Movie("The Royal Tenenbaums", "Wes Anderson", 
"Comedy", 8, 2001));
+moviesBean.addMovie(new Movie("Zoolander", "Ben Stiller", "Comedy", 6, 2001));
+moviesBean.addMovie(new Movie("Shanghai Noon", "Tom Dey", "Comedy", 7, 2000));
+----
+
+== EAR packaging
+
+The `moviefun-ear` module has `ear` packaging and depends on the EJB module
+(type `ejb`) and on the web module (type `war`):
+
+[source,xml]
+----
+<artifactId>moviefun-ear</artifactId>
+<packaging>ear</packaging>
+
+<dependencies>
+  <dependency>
+    <groupId>org.superbiz</groupId>
+    <artifactId>moviefun-business-logic</artifactId>
+    <version>${project.version}</version>
+    <type>ejb</type>
+  </dependency>
+  <dependency>
+    <groupId>org.superbiz</groupId>
+    <artifactId>moviefun-webapp</artifactId>
+    <type>war</type>
+    <version>${project.version}</version>
+  </dependency>
+</dependencies>
+----
+
+The `maven-ear-plugin` generates the `application.xml`, maps the web module
+to the `/moviefun` context root and puts library jars into `lib`:
+
+[source,xml]
+----
+<plugin>
+  <artifactId>maven-ear-plugin</artifactId>
+  <version>3.4.0</version>
+  <configuration>
+    <version>8</version>
+    <generateApplicationXml>true</generateApplicationXml>
+    <defaultLibBundleDir>lib</defaultLibBundleDir>
+    <modules>
+      <webModule>
+        <groupId>org.superbiz</groupId>
+        <artifactId>moviefun-webapp</artifactId>
+        <contextRoot>/moviefun</contextRoot>
+      </webModule>
+      <ejbModule>
+        <groupId>org.superbiz</groupId>
+        <artifactId>moviefun-business-logic</artifactId>
+      </ejbModule>
+    </modules>
+  </configuration>
+</plugin>
+----
+
+`moviefun-business-model` is not listed directly; it is a transitive
+dependency of the EJB module, so the plugin places it in the EAR's `lib`
+directory, where it is visible to both the EJB module and the web module.
+
+== Testing
+
+=== Business logic test
+
+`MoviesTest` in `moviefun-business-logic` tests the EJB without a server.
+It boots OpenEJB through `LocalInitialContextFactory`, sets
+`openejb.deployments.classpath.ear` to `true` so the modules found on the
+test classpath are deployed together as one application, and defines the
+two data sources required by `persistence.xml` as in-memory HSQLDB
+databases:
+
+[source,java]
+----
+public class MoviesTest extends TestCase {
+
+    public void test() throws Exception {
+        Properties p = new Properties();
+        p.put(Context.INITIAL_CONTEXT_FACTORY, 
"org.apache.openejb.core.LocalInitialContextFactory");
+
+        p.put("openejb.deployments.classpath.ear", "true");
+
+        p.put("movieDatabase", "new://Resource?type=DataSource");
+        p.put("movieDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("movieDatabase.JdbcUrl", "jdbc:hsqldb:mem:moviedb");
+
+        p.put("movieDatabaseUnmanaged", "new://Resource?type=DataSource");
+        p.put("movieDatabaseUnmanaged.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("movieDatabaseUnmanaged.JdbcUrl", "jdbc:hsqldb:mem:moviedb");
+        p.put("movieDatabaseUnmanaged.JtaManaged", "false");
+
+        Context context = new InitialContext(p);
+
+        Movies movies = (Movies) context.lookup("MoviesBeanLocal");
+
+        movies.addMovie(new Movie("Quentin Tarantino", "Reservoir Dogs", 
1992));
+        movies.addMovie(new Movie("Joel Coen", "Fargo", 1996));
+        movies.addMovie(new Movie("Joel Coen", "The Big Lebowski", 1998));
+
+        List<Movie> list = movies.getMovies();
+        assertEquals("List.size()", 3, list.size());
+
+        for (Movie movie : list) {
+            movies.deleteMovieId(movie.getId());
+        }
+
+        assertEquals("Movies.getMovies()", 0, movies.getMovies().size());
+    }
+}
+----
+
+It adds three movies, checks that `getMovies()` returns them, deletes them
+and checks that the list is empty again.
+
+=== Functional tests
+
+The `moviefun-functional-tests` module tests the packaged EAR end to end.
+It depends on the EAR produced by the sibling module:
+
+[source,xml]
+----
+<dependency>
+  <groupId>org.superbiz</groupId>
+  <artifactId>moviefun-ear</artifactId>
+  <version>${project.version}</version>
+  <type>ear</type>
+  <scope>test</scope>
+</dependency>
+----
+
+together with `arquillian-tomee-remote`, the ShrinkWrap Maven resolver and
+HtmlUnit. The Arquillian container is configured in
+`src/test/resources/arquillian.xml`; a port of `-1` makes the adapter pick
+a free port, and TomEE is unpacked below `target`:
+
+[source,xml]
+----
+<container qualifier="tomee" default="true">
+  <configuration>
+    <property name="httpPort">-1</property>
+    <property name="stopPort">-1</property>
+    <property name="tomcatVersion"></property>
+    <property name="dir">target/apache-tomee-remote</property>
+    <property 
name="appWorkingDir">target/arquillian-test-working-dir</property>
+  </configuration>
+</container>
+----
+
+`MoviesArquillianHtmlUnitTest` does not assemble the archive itself.
+Its `@Deployment` method resolves the `moviefun-ear` artifact with the
+ShrinkWrap Maven resolver. The version is read from `version.properties`,
+a filtered test resource containing `version=${tomee.version}`:
+
+[source,java]
+----
+@RunWith(Arquillian.class)
+@RunAsClient
+public class MoviesArquillianHtmlUnitTest {
+
+    @Deployment
+    public static EnterpriseArchive createDeployment() {
+        ResourceBundle mavenVersion = ResourceBundle.getBundle("version");
+
+        final EnterpriseArchive enterpriseArchive = 
Maven.resolver().resolve("org.superbiz:moviefun-ear:ear:" + 
mavenVersion.getString("version"))
+                .withoutTransitivity().asSingle(EnterpriseArchive.class);
+
+        System.out.println(enterpriseArchive.toString(true));
+
+        return enterpriseArchive;
+    }
+
+    @ArquillianResource
+    private URL deploymentUrl;
+
+    @Test
+    public void testShouldMakeSureWebappIsWorking() throws Exception {
+        final String url = "http://"; + deploymentUrl.getHost() + ":" + 
deploymentUrl.getPort() + "/moviefun";
+
+        WebClient webClient = new WebClient();
+        HtmlPage page = webClient.getPage(url + "/setup.jsp");
+
+        assertMoviesPresent(page);
+
+        page = webClient.getPage(url + "/moviefun");
+
+        assertMoviesPresent(page);
+        webClient.close();
+    }
+
+    private void assertMoviesPresent(HtmlPage page) {
+        String pageAsText = page.asNormalizedText();
+        assertTrue(pageAsText.contains("Wedding Crashers"));
+        assertTrue(pageAsText.contains("Starsky & Hutch"));
+        assertTrue(pageAsText.contains("Shanghai Knights"));
+        assertTrue(pageAsText.contains("I-Spy"));
+        assertTrue(pageAsText.contains("The Royal Tenenbaums"));
+    }
+}
+----
+
+The test runs as a client. After the EAR is deployed under the `/moviefun`
+context root, it opens `setup.jsp` to seed the database, then opens the
+servlet at `/moviefun/moviefun`, and checks on both pages that the seeded
+movies are listed. This covers the whole chain: web module, `@EJB`
+injection across modules, the stateless bean and JPA.
+
+The module also contains `DeployInWebAppsDirectoryTest`, which copies the
+EAR into the `webapps` directory of a TomEE started by the remote adapter
+instead of using the Arquillian deployer. It is currently marked
+`@Ignore("Not implemented yet")` and does not run.
+
+=== Running the tests
+
+The functional tests resolve the EAR as a Maven artifact, so it has to be
+installed in the local Maven repository first. Build the example from the
+`simple-ear` directory with:
+
+[source,bash]
+----
+mvn clean install
+----
+
+Running only `mvn test` or `mvn verify` does not install `moviefun-ear`,
+so the ShrinkWrap resolver cannot find it (or picks up an old copy from
+the local repository). Once the example has been installed, the
+business logic test can also be run on its own:
+
+[source,bash]
+----
+mvn -pl moviefun-business-logic test
+----

Reply via email to