Issue #3804 - Update Decorator integration for various CDI implementations (#3838)

* Jetty Issue #3804 WELD-2587

Support CDI integration:
 + cdi2 module exposes jetty APIs
 + cdi module uses DecorationListener

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Jetty Issue #3804 WELD-2587

Remove DecoratingListener tests from test-jetty-webapp

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* improve CDI test

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Jetty Issue #3804 WELD-2587

Reverted test to use released CDI and cdi2 module for now.
To test new mechanism, you need to build the weld snapshot locally,
rebuild and switch to cdi module

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* remove cdi2 webapp references

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* document attribute

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* improved documentation

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* logging

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* improved javadoc

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Fixed version

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Reverted to also provide the DecoratingListener in the decorate module.
Renamed cdi-demo to weld-cdi-demo

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* revert from Weld SNAPSHOT

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* test all 3 weld integrations

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* updated destory implementation to release creationalcontext

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* reverted to released Weld version

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Issue #3804 CDI integration

dispose and release context in destroy

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Improved CDI module documentation

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* WIP on OWB

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Updates from review
Parameterised CDITests

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* share webapp resources for cdi webapp test

Signed-off-by: olivier lamy <oliver.lamy@gmail.com>

* Initialize OWB with a SCI so that listeners can be decorated

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Added OwbDecorator so that cdi2 module can be tested with OWB

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Lookup attribute name

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Cleanups

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Cleanup from Review

Don't do lazy bindings

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Cleanup from Review

Treat partial CDI same as no CDI

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* fix maven it test no more need of weld-servlet

Signed-off-by: olivier lamy <oliver.lamy@gmail.com>

* cleanup it parent pom removing non needed weld servlet

Signed-off-by: olivier lamy <oliver.lamy@gmail.com>

* upgraded to Weld 3.1.2.Final

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Cleanup from Review

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Cleanup from Review

Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Greg Wilkins 2019-08-08 04:04:07 +02:00 committed by GitHub
parent 259ef7cf7a
commit dc939d753a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 885 additions and 141 deletions

View File

@ -175,11 +175,12 @@ public class AnnotationConfiguration extends AbstractConfiguration
/**
* ServletContainerInitializerOrdering
*
* A list of classnames of ServletContainerInitializers in the order in which
* they are to be called back. One name only in the list can be "*", which is a
* <p>Applies an ordering to the {@link ServletContainerInitializer}s for the context, using
* the value of the "org.eclipse.jetty.containerInitializerOrder" context attribute.
* The attribute value is a list of classnames of ServletContainerInitializers in the order in which
* they are to be called. One name only in the list can be "*", which is a
* wildcard which matches any other ServletContainerInitializer name not already
* matched.
* matched.</p>
*/
public class ServletContainerInitializerOrdering
{

View File

@ -5,14 +5,34 @@
<version>9.4.20-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.cdi</groupId>
<artifactId>cdi-2</artifactId>
<name>Jetty :: CDI 2</name>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-cdi</artifactId>
<name>Jetty :: CDI</name>
<url>http://www.eclipse.org/jetty</url>
<packaging>jar</packaging>
<properties>
<bundle-symbolic-name>${project.groupId}.cdi2</bundle-symbolic-name>
<bundle-symbolic-name>${project.groupId}.cdi</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>

View File

@ -7,7 +7,8 @@
<Call name="addLifeCycleBinding">
<Arg>
<New class="org.eclipse.jetty.deploy.bindings.GlobalWebappConfigBinding">
<Set name="jettyXml"><Property name="jetty.home" default="."/>/etc/cdi2/jetty-web-cdi2.xml</Set>
<Set name="jettyXml"><Property name="jetty.home" default="." />/etc/cdi/jetty-web-cdi2.xml
</Set>
</New>
</Arg>
</Call>

View File

@ -0,0 +1,24 @@
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
CDI SPI integration for CDI inside the webapp.
This module does not provide CDI, but configures jetty to look for the CDI SPI within
a webapp. If the CDI SPI is found, then a CdiDecorator will be registered to
decorate Listeners, Filters and Servlets using the standard CDI SPI.
The module indicates to the webapp that this mechanism is available by setting the
"org.eclipse.jetty.cdi" context attribute to "CdiDecorator".
This is the preferred integration for OWB.
[tag]
cdi
[depend]
deploy
[lib]
lib/jetty-cdi-${jetty.version}.jar
lib/apache-jsp/org.mortbay.jasper.apache-el-*.jar
[ini]
jetty.webapp.addSystemClasses+=,org.eclipse.jetty.cdi.CdiServletContainerInitializer
jetty.webapp.addServerClasses+=,-org.eclipse.jetty.cdi.CdiServletContainerInitializer

View File

@ -1,7 +1,10 @@
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Jetty setup to support Weld/CDI2 with WELD inside the webapp
Deprecated CDI module. Current depends on cdi-spi.
[tag]
cdi
[depend]
cdi2
cdi-spi

View File

@ -1,7 +1,14 @@
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Jetty setup to support Weld/CDI2 with WELD inside the webapp
Deprecated support for CDI integrations inside the webapp.
This module does not provide CDI, but configures jetty so that a CDI implementation
can enable itself as a decorator for Filters, Servlets and Listeners.
This modules uses the deprecated technique of exposing private Jetty decorate APIs to the CDI
implementation in the webapp.
[tag]
cdi
[depend]
deploy
@ -10,9 +17,4 @@ deploy
lib/apache-jsp/org.mortbay.jasper.apache-el-*.jar
[xml]
etc/cdi2/jetty-cdi2.xml
[license]
Weld is an open source project hosted on Github and released under the Apache 2.0 license.
http://weld.cdi-spec.org/
http://www.apache.org/licenses/LICENSE-2.0.html
etc/cdi/jetty-cdi2.xml

View File

@ -0,0 +1,165 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.cdi;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.Decorator;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* A Decorator invokes a CDI provider within
* a webapplication to decorate objects created by the contexts {@link org.eclipse.jetty.util.DecoratedObjectFactory}
* (typically Listeners, Filters and Servlets).
* The CDI provide is invoked using {@link MethodHandle}s to avoid any CDI instance or dependencies within the server scope.
* The code invoked is equivalent to:
* <pre>
* public &lt;T&gt; T decorate(T o)
* {
* BeanManager manager = CDI.current().getBeanManager();
* manager.createInjectionTarget(manager.createAnnotatedType((Class&lt;T&gt;)o.getClass()))
* .inject(o,manager.createCreationalContext(null));
* return o;
* }
* </pre>
*/
public class CdiDecorator implements Decorator
{
private static final Logger LOG = Log.getLogger(CdiServletContainerInitializer.class);
private final ServletContextHandler _context;
private final Map<Object, Decorated> _decorated = new HashMap<>();
private final MethodHandle _current;
private final MethodHandle _getBeanManager;
private final MethodHandle _createAnnotatedType;
private final MethodHandle _createInjectionTarget;
private final MethodHandle _createCreationalContext;
private final MethodHandle _inject;
private final MethodHandle _dispose;
private final MethodHandle _release;
public CdiDecorator(ServletContextHandler context) throws UnsupportedOperationException
{
_context = context;
ClassLoader classLoader = _context.getClassLoader();
try
{
Class<?> cdiClass = classLoader.loadClass("javax.enterprise.inject.spi.CDI");
Class<?> beanManagerClass = classLoader.loadClass("javax.enterprise.inject.spi.BeanManager");
Class<?> annotatedTypeClass = classLoader.loadClass("javax.enterprise.inject.spi.AnnotatedType");
Class<?> injectionTargetClass = classLoader.loadClass("javax.enterprise.inject.spi.InjectionTarget");
Class<?> creationalContextClass = classLoader.loadClass("javax.enterprise.context.spi.CreationalContext");
Class<?> contextualClass = classLoader.loadClass("javax.enterprise.context.spi.Contextual");
MethodHandles.Lookup lookup = MethodHandles.lookup();
_current = lookup.findStatic(cdiClass, "current", MethodType.methodType(cdiClass));
_getBeanManager = lookup.findVirtual(cdiClass, "getBeanManager", MethodType.methodType(beanManagerClass));
_createAnnotatedType = lookup.findVirtual(beanManagerClass, "createAnnotatedType", MethodType.methodType(annotatedTypeClass, Class.class));
_createInjectionTarget = lookup.findVirtual(beanManagerClass, "createInjectionTarget", MethodType.methodType(injectionTargetClass, annotatedTypeClass));
_createCreationalContext = lookup.findVirtual(beanManagerClass, "createCreationalContext", MethodType.methodType(creationalContextClass, contextualClass));
_inject = lookup.findVirtual(injectionTargetClass, "inject", MethodType.methodType(Void.TYPE, Object.class, creationalContextClass));
_dispose = lookup.findVirtual(injectionTargetClass, "dispose", MethodType.methodType(Void.TYPE, Object.class));
_release = lookup.findVirtual(creationalContextClass, "release", MethodType.methodType(Void.TYPE));
}
catch (Exception e)
{
throw new UnsupportedOperationException(e);
}
}
/**
* Decorate an object.
* <p>The signature of this method must match what is introspected for by the
* Jetty DecoratingListener class. It is invoked dynamically.</p>
*
* @param o The object to be decorated
* @param <T> The type of the object to be decorated
* @return The decorated object
*/
public <T> T decorate(T o)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("decorate {} in {}", o, _context);
_decorated.put(o, new Decorated(o));
}
catch (Throwable th)
{
LOG.warn("Unable to decorate " + o, th);
}
return o;
}
/**
* Destroy a decorated object.
* <p>The signature of this method must match what is introspected for by the
* Jetty DecoratingListener class. It is invoked dynamically.</p>
*
* @param o The object to be destroyed
*/
public void destroy(Object o)
{
try
{
Decorated decorated = _decorated.remove(o);
if (decorated != null)
decorated.destroy(o);
}
catch (Throwable th)
{
LOG.warn("Unable to destroy " + o, th);
}
}
private class Decorated
{
private final Object _injectionTarget;
private final Object _creationalContext;
Decorated(Object o) throws Throwable
{
// BeanManager manager = CDI.current().getBeanManager();
Object manager = _getBeanManager.invoke(_current.invoke());
// AnnotatedType annotatedType = manager.createAnnotatedType((Class<T>)o.getClass());
Object annotatedType = _createAnnotatedType.invoke(manager, o.getClass());
// CreationalContext creationalContext = manager.createCreationalContext(null);
_creationalContext = _createCreationalContext.invoke(manager, null);
// InjectionTarget injectionTarget = manager.createInjectionTarget();
_injectionTarget = _createInjectionTarget.invoke(manager, annotatedType);
// injectionTarget.inject(o, creationalContext);
_inject.invoke(_injectionTarget, o, _creationalContext);
}
public void destroy(Object o) throws Throwable
{
_dispose.invoke(_injectionTarget, o);
_release.invoke(_creationalContext);
}
}
}

View File

@ -0,0 +1,60 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.cdi;
import java.util.Objects;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* A {@link ServletContainerInitializer} that introspects for a CDI API
* implementation within a web application. If the CDI API is found, then
* a {@link CdiDecorator} is registered as a {@link org.eclipse.jetty.util.Decorator}
* for the context.
* @see AnnotationConfiguration.ServletContainerInitializerOrdering
*/
public class CdiServletContainerInitializer implements ServletContainerInitializer
{
public static final String CDI_INTEGRATION_ATTRIBUTE = "org.eclipse.jetty.cdi";
private static final Logger LOG = Log.getLogger(CdiServletContainerInitializer.class);
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx)
{
try
{
ServletContextHandler context = ServletContextHandler.getServletContextHandler(ctx);
Objects.requireNonNull(context);
context.getObjectFactory().addDecorator(new CdiDecorator(context));
context.setAttribute(CDI_INTEGRATION_ATTRIBUTE, "CdiDecorator");
LOG.info("CdiDecorator enabled in " + ctx);
}
catch (UnsupportedOperationException e)
{
if (LOG.isDebugEnabled())
LOG.debug("CDI not found in " + ctx, e);
}
}
}

View File

@ -0,0 +1 @@
org.eclipse.jetty.cdi.CdiServletContainerInitializer

View File

@ -0,0 +1,18 @@
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<!-- =============================================================== --><!-- Mixin the Weld / CDI classes to the class loader --><!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<!-- Bind the jetty-web-decorate.xml to every deployed webapp -->
<Ref refid="DeploymentManager">
<Call name="addLifeCycleBinding">
<Arg>
<New class="org.eclipse.jetty.deploy.bindings.GlobalWebappConfigBinding">
<Set name="jettyXml"><Property name="jetty.home" default="." />/etc/jetty-web-decorate.xml
</Set>
</New>
</Arg>
</Call>
</Ref>
</Configure>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext" id="context">
<Get class="org.eclipse.jetty.webapp.DecoratingListener" name="DECORATOR_ATTRIBUTE" id="decoratorAttribute"/>
<!-- Add the DecoratingListener to the webapp to look for dynamic decorators -->
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.webapp.DecoratingListener">
<Arg><Ref refid="context" /></Arg>
<Arg type="String"><Ref refid="decoratorAttribute"/></Arg>
</New>
</Arg>
</Call>
</Configure>

View File

@ -0,0 +1,19 @@
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Jetty setup to support Decoration of Listeners, Filters and Servlets within a deployed
webapp (as used by some CDI integrations).
This module uses the DecoratingListener to register an object set as a context attribute
as a dynamic decorator. This module sets the "org.eclipse.jetty.webapp.DecoratingListener"
context attribute with the name of the context attribute that will be listened to.
By default the attribute is "org.eclipse.jetty.webapp.decorator".
This is the preferred integration for Weld >= 3.1.2
[tag]
cdi
[depend]
deploy
[xml]
etc/jetty-decorate.xml

View File

@ -140,6 +140,16 @@
<outputDirectory>${assembly-directory}/demo-base/webapps</outputDirectory>
<destFileName>async-rest.war</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-weld-cdi-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
<overWrite>true</overWrite>
<includes>**</includes>
<outputDirectory>${assembly-directory}/demo-base/webapps</outputDirectory>
<destFileName>test-weld-cdi.war</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
@ -348,7 +358,7 @@
<arguments>
<argument>jetty.home=${assembly-directory}</argument>
<argument>jetty.base=${assembly-directory}/demo-base</argument>
<argument>--add-to-startd=server,continuation,deploy,websocket,ext,resources,client,annotations,jndi,servlets,jsp,jstl,http,https,threadpool</argument>
<argument>--add-to-startd=server,continuation,deploy,websocket,ext,resources,client,annotations,cdi2,decorate,jndi,servlets,jsp,jstl,http,https,threadpool</argument>
</arguments>
</configuration>
<goals>
@ -449,6 +459,13 @@
<type>war</type>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-weld-cdi-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-documentation</artifactId>

View File

@ -30,13 +30,14 @@
<table>
<tr>
<td>
<h2>examples ...</h2>
<h2>tests ...</h2>
<ul>
<li><a href="/test/">Test Jetty Webapp</a></li>
<li><a href="/async-rest/">Async Rest</a></li>
<li><a href="/test-spec/">Servlet 3.1 Test</a></li>
<li><a href="/test-jaas/">JAAS Test</a></li>
<li><a href="/test-jndi/">JNDI Test</a></li>
<li><a href="/test-spec/">Servlet 3.1 Test</a></li>
<li><a href="/test-weld-cdi/">Weld CDI Test</a></li>
<li><a href="/async-rest/">Async Rest</a></li>
<li><a href="/oldContextPath/">Redirected Context</a></li>
</ul>
</td>

View File

@ -677,8 +677,8 @@
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.cdi</groupId>
<artifactId>cdi-2</artifactId>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-cdi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -30,11 +30,6 @@
<version>@javax.servlet.api.version@</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet</artifactId>
<version>@weld.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-perf-helper</artifactId>

View File

@ -18,10 +18,6 @@
</properties>
<dependencies>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>

View File

@ -51,7 +51,13 @@ public class DecoratedObjectFactory implements Iterable<Decorator>
public void addDecorator(Decorator decorator)
{
LOG.debug("Adding Decorator: {}", decorator);
this.decorators.add(decorator);
decorators.add(decorator);
}
public boolean removeDecorator(Decorator decorator)
{
LOG.debug("Remove Decorator: {}", decorator);
return decorators.remove(decorator);
}
public void clear()

View File

@ -0,0 +1,196 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.webapp;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Objects;
import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.Decorator;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* A ServletContextAttributeListener that listens for a specific context
* attribute (default "org.eclipse.jetty.webapp.decorator") to obtain a
* decorator instance from the webapp. The instance is then either coerced
* to a Decorator or reflected for decorator compatible methods so it can
* be added to the {@link WebAppContext#getObjectFactory()} as a
* {@link Decorator}.
* The context attribute "org.eclipse.jetty.webapp.DecoratingListener" if
* not set, is set to the name of the attribute this listener listens for.
*/
public class DecoratingListener implements ServletContextAttributeListener
{
public static final String DECORATOR_ATTRIBUTE = "org.eclipse.jetty.webapp.decorator";
private static final Logger LOG = Log.getLogger(DecoratingListener.class);
private static final MethodType DECORATE_TYPE;
private static final MethodType DESTROY_TYPE;
static
{
try
{
DECORATE_TYPE = MethodType.methodType(Object.class, Object.class);
DESTROY_TYPE = MethodType.methodType(Void.TYPE, Object.class);
// Check we have the right MethodTypes for the current Decorator signatures
MethodHandles.Lookup lookup = MethodHandles.lookup();
lookup.findVirtual(Decorator.class, "decorate", DECORATE_TYPE);
lookup.findVirtual(Decorator.class, "destroy", DESTROY_TYPE);
}
catch (Exception e)
{
throw new IllegalStateException(e);
}
}
private final ServletContextHandler _context;
private final String _attributeName;
private Decorator _decorator;
public DecoratingListener()
{
this((String)null);
}
public DecoratingListener(String attributeName)
{
this(WebAppContext.getCurrentWebAppContext(), attributeName);
}
public DecoratingListener(ServletContextHandler context)
{
this(context, null);
}
public DecoratingListener(ServletContextHandler context, String attributeName)
{
_context = context;
Objects.requireNonNull(_context);
_attributeName = attributeName == null ? DECORATOR_ATTRIBUTE : attributeName;
checkAndSetAttribute();
Object decorator = _context.getAttribute(_attributeName);
if (decorator != null)
_context.getObjectFactory().addDecorator(asDecorator(decorator));
}
protected void checkAndSetAttribute()
{
// If not set (by another DecoratingListener), flag the attribute that are
// listening for. If more than one DecoratingListener is used then this
// attribute reflects only the first.
if (_context.getAttribute(getClass().getName()) != null)
throw new IllegalStateException("Multiple DecoratingListeners detected");
_context.setAttribute(getClass().getName(), _attributeName);
}
private Decorator asDecorator(Object object)
{
if (object == null)
return null;
if (object instanceof Decorator)
return (Decorator)object;
try
{
Class<?> clazz = object.getClass();
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle decorate = lookup.findVirtual(clazz, "decorate", DECORATE_TYPE);
MethodHandle destroy = lookup.findVirtual(clazz, "destroy", DESTROY_TYPE);
return new DynamicDecorator(decorate, destroy, object);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public void attributeAdded(ServletContextAttributeEvent event)
{
if (_attributeName.equals(event.getName()))
{
_decorator = asDecorator(event.getValue());
_context.getObjectFactory().addDecorator(_decorator);
}
}
@Override
public void attributeRemoved(ServletContextAttributeEvent event)
{
if (_attributeName.equals(event.getName()) && _decorator != null)
{
_context.getObjectFactory().removeDecorator(_decorator);
_decorator = null;
}
}
@Override
public void attributeReplaced(ServletContextAttributeEvent event)
{
attributeRemoved(event);
attributeAdded(event);
}
private static class DynamicDecorator implements Decorator
{
private final MethodHandle _decorate;
private final MethodHandle _destroy;
private final Object _object;
private DynamicDecorator(MethodHandle decorate, MethodHandle destroy, Object object)
{
_decorate = decorate;
_destroy = destroy;
_object = object;
}
@Override
public <T> T decorate(T o)
{
try
{
return (T)_decorate.invoke(_object, o);
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
@Override
public void destroy(Object o)
{
try
{
_destroy.invoke(_object, o);
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
}
}

View File

@ -1040,6 +1040,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
new DumpableCollection("Configurations " + name, _configurations),
new DumpableCollection("Handler attributes " + name, ((AttributesMap)getAttributes()).getAttributeEntrySet()),
new DumpableCollection("Context attributes " + name, getServletContext().getAttributeEntrySet()),
new DumpableCollection("EventListeners " + this, Arrays.asList(getEventListeners())),
new DumpableCollection("Initparams " + name, getInitParams().entrySet())
);
}

View File

@ -37,7 +37,7 @@
<maven.version>3.6.0</maven.version>
<maven.resolver.version>1.3.1</maven.resolver.version>
<javax.servlet.api.version>3.1.0</javax.servlet.api.version>
<weld.version>2.4.5.Final</weld.version>
<weld.version>3.1.2.Final</weld.version>
<jetty.perf-helper.version>1.0.5</jetty.perf-helper.version>
<unix.socket.tmp></unix.socket.tmp>
<!-- enable or not TestTracker junit5 extension i.e log message when test method is starting -->

View File

@ -76,7 +76,7 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-cdi2-webapp</artifactId>
<artifactId>test-weld-cdi-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
<scope>test</scope>

View File

@ -127,6 +127,16 @@ public class DistributionTester
return start(Arrays.asList(args));
}
public Path getJettyBase()
{
return config.jettyBase;
}
public Path getJettyHome()
{
return config.jettyHome;
}
/**
* Start the distribution with the arguments
*

View File

@ -19,11 +19,20 @@
package org.eclipse.jetty.tests.distribution;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -32,70 +41,40 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class CDITests extends AbstractDistributionTest
{
/**
* Tests a WAR file that is CDI complete as it includes the weld
* library in its WEB-INF/lib directory.
*/
@Test
public void testCDI2_IncludedInWebapp() throws Exception
// Tests from here use these parameters
public static Stream<Arguments> tests()
{
String jettyVersion = System.getProperty("jettyVersion");
DistributionTester distribution = DistributionTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
String[] args1 = {
"--create-startd",
"--approve-all-licenses",
"--add-to-start=http,deploy,annotations,jsp"
};
try (DistributionTester.Run run1 = distribution.start(args1))
Consumer<DistributionTester> removeJettyWebXml = d ->
{
assertTrue(run1.awaitFor(5, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-cdi2-webapp:war:" + jettyVersion);
distribution.installWarFile(war, "demo");
distribution.installBaseResource("cdi/demo_context.xml", "webapps/demo.xml");
int port = distribution.freePort();
try (DistributionTester.Run run2 = distribution.start("jetty.http.port=" + port))
try
{
assertTrue(run2.awaitConsoleLogsFor("Started @", 10, TimeUnit.SECONDS));
startHttpClient();
ContentResponse response = client.GET("http://localhost:" + port + "/demo/greetings");
assertEquals(HttpStatus.OK_200, response.getStatus());
// Confirm Servlet based CDI
assertThat(response.getContentAsString(), containsString("Hello GreetingsServlet"));
// Confirm Listener based CDI (this has been a problem in the past, keep this for regression testing!)
assertThat(response.getHeaders().get("Server"), containsString("CDI-Demo-org.eclipse.jetty.test"));
run2.stop();
assertTrue(run2.awaitFor(5, TimeUnit.SECONDS));
Path jettyWebXml = d.getJettyBase().resolve("webapps/demo/WEB-INF/jetty-web.xml");
Files.deleteIfExists(jettyWebXml);
}
}
catch(IOException e)
{
throw new RuntimeException(e);
}
};
List<Object[]> tests = new ArrayList<>();
tests.add(new Object[]{"weld", "cdi-spi", null}); // Requires Weld >= 3.1.2
tests.add(new Object[]{"weld", "cdi2", null});
tests.add(new Object[]{"weld", "decorate", null}); // Requires Weld >= 3.1.2
tests.add(new Object[]{"owb", "cdi-spi", removeJettyWebXml});
tests.add(new Object[]{"owb", "cdi2", null});
// tests.add(new Object[]{"owb", "decorate", null}); // Will not be supported
return tests.stream().map(Arguments::of);
}
/**
* Tests a WAR file that is expects CDI to be configured by the server.
*
* <p>
* This means the WAR has the weld libs in its
* WEB-INF/lib directory.
* </p>
*
* <p>
* The expectation is that a context xml deployable file is not
* required when using this `cdi2` module, and the appropriate
* server side libs are made available to allow weld to function.
* (the required server side javax.el support comes from the cdi2 module)
* </p>
* Tests a WAR file that includes the CDI
* library in its WEB-INF/lib directory.
*/
@Test
public void testCDI2_ConfiguredByServer() throws Exception
@ParameterizedTest
@MethodSource("tests")
public void testCDIIncludedInWebapp(String implementation, String integration, Consumer<DistributionTester> configure) throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
DistributionTester distribution = DistributionTester.Builder.newInstance()
@ -106,18 +85,17 @@ public class CDITests extends AbstractDistributionTest
String[] args1 = {
"--create-startd",
"--approve-all-licenses",
// standard entries
"--add-to-start=http,deploy,annotations",
// cdi2 specific entry (should transitively pull in what it needs, the user should not be expected to know the transitive entries)
"--add-to-start=cdi2"
"--add-to-start=http,deploy,annotations,jsp,"+integration
};
try (DistributionTester.Run run1 = distribution.start(args1))
{
assertTrue(run1.awaitFor(5, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-cdi2-webapp:war:" + jettyVersion);
File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-" + implementation + "-cdi-webapp:war:" + jettyVersion);
distribution.installWarFile(war, "demo");
if (configure != null)
configure.accept(distribution);
int port = distribution.freePort();
try (DistributionTester.Run run2 = distribution.start("jetty.http.port=" + port))

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Set name="contextPath">/demo</Set>
<Set name="war"><Property name="jetty.webapps"/>/demo/</Set>
<Get name="serverClasspathPattern">
<Call name="add">
<Arg>-org.eclipse.jetty.util.Decorator</Arg>
</Call>
<Call name="add">
<Arg>-org.eclipse.jetty.util.DecoratedObjectFactory</Arg>
</Call>
<Call name="add">
<Arg>-org.eclipse.jetty.server.handler.ContextHandler.</Arg>
</Call>
<Call name="add">
<Arg>-org.eclipse.jetty.server.handler.ContextHandler</Arg>
</Call>
<Call name="add">
<Arg>-org.eclipse.jetty.servlet.ServletContextHandler</Arg>
</Call>
</Get>
</Configure>

View File

@ -41,6 +41,8 @@
<module>test-http2-webapp</module>
<module>test-simple-webapp</module>
<module>test-felix-webapp</module>
<module>test-cdi2-webapp</module>
<module>test-cdi-common-webapp</module>
<module>test-weld-cdi-webapp</module>
<module>test-owb-cdi-webapp</module>
</modules>
</project>
</project>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
<version>9.4.20-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-cdi-common-webapp</artifactId>
<name>Test :: CDI :: Common Demo Webapp</name>
<packaging>war</packaging>
<properties>
<bundle-symbolic-name>${project.groupId}.cdi.common</bundle-symbolic-name>
</properties>
<dependencies>
<!-- provided by container -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -37,6 +37,8 @@ public class GreetingsServlet extends HttpServlet
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
resp.setContentType("text/plain");
resp.getWriter().println("[" + greetings.getGreeting() + "]");
resp.getWriter().print(greetings.getGreeting());
resp.getWriter().print(" from ");
resp.getWriter().println(getServletContext().getAttribute("ServerID"));
}
}

View File

@ -21,8 +21,11 @@ package org.eclipse.jetty.test;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Inject;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
@ -43,10 +46,12 @@ public class InfoServlet extends HttpServlet
PrintWriter out = resp.getWriter();
out.println("Bean Manager: " + beanManager);
Set<Bean<?>> beans = beanManager.getBeans("");
Set<Bean<?>> beans = beanManager.getBeans(Object.class, new AnnotationLiteral<Any>() {});
for (Bean<?> bean : beans)
{
out.println(" " + bean);
out.printf("%16s => %s%n", bean.getName(), bean);
for (InjectionPoint ij : bean.getInjectionPoints())
out.printf("%16s -> %s%n", "", ij);
}
}
}

View File

@ -32,6 +32,8 @@ public class MyContextListener implements ServletContextListener
@Override
public void contextInitialized(ServletContextEvent sce)
{
if (serverId == null)
throw new IllegalStateException("CDI did not inject!");
sce.getServletContext().setAttribute("ServerID", serverId.get());
}

View File

@ -0,0 +1,7 @@
<H1>OWB CDI Test Webapp</H1>
<H2>CDI Info</H2>
<iframe src="info" width="100%" height="60%"></iframe>
<H2>Greetings test</H2>
<iframe src="greetings" width="100%"></iframe>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
<version>9.4.20-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-owb-cdi-webapp</artifactId>
<name>Test :: CDI On Jetty :: OWB Demo Webapp</name>
<packaging>war</packaging>
<properties>
<bundle-symbolic-name>${project.groupId}.cdi.owb</bundle-symbolic-name>
</properties>
<build>
<finalName>weld-owb-demo</finalName>
</build>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-cdi-common-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<!-- included in webapp -->
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-annotation_1.3_spec</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jcdi_2.0_spec</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-atinject_1.0_spec</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-interceptor_1.2_spec</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-web</artifactId>
<!-- TODO remove OwbServletContainerInitializer when updated to a version with https://issues.apache.org/jira/browse/OWB-1296 -->
<version>2.0.11</version>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-jetty9</artifactId>
<version>2.0.11</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.cdi.owb;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
import org.apache.webbeans.servlet.WebBeansConfigurationListener;
/**
* @deprecated This class will not be required once https://issues.apache.org/jira/browse/OWB-1296 is available
*/
@Deprecated
public class OwbServletContainerInitializer implements ServletContainerInitializer
{
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException
{
Listener listener = new Listener();
listener.preInitialize(new ServletContextEvent(ctx));
ctx.addListener(listener);
}
public static class Listener extends WebBeansConfigurationListener
{
void preInitialize(ServletContextEvent event)
{
super.contextInitialized(event);
}
@Override
public void contextInitialized(ServletContextEvent event)
{
}
}
}

View File

@ -0,0 +1 @@
org.eclipse.jetty.cdi.owb.OwbServletContainerInitializer

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="webAppCtx" class="org.eclipse.jetty.webapp.WebAppContext">
<New id="BeanManager" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg>
<Ref refid="webAppCtx"/>
</Arg>
<Arg>BeanManager</Arg>
<Arg>
<New class="javax.naming.Reference">
<Arg>javax.enterprise.inject.spi.BeanManager</Arg>
<Arg>org.apache.webbeans.container.ManagerObjectFactory</Arg>
<Arg/>
</New>
</Arg>
</New>
</Configure>

View File

@ -0,0 +1,14 @@
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="wac" class="org.eclipse.jetty.webapp.WebAppContext">
<!-- This file is only needed for cdi2 integration and should be removed if using the cdi module -->
<Get id="wal" name="classLoader"/>
<Get id="objf" name="objectFactory">
<Call name="addDecorator">
<Arg>
<New class="org.apache.webbeans.web.jetty9.JettyDecorator">
<Arg><Ref refid="wal"/></Arg>
</New>
</Arg>
</Call>
</Get>
</Configure>

View File

@ -3,11 +3,9 @@
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/web-app_3_1.xsd"
version="3.1">
<display-name>CDI Integration Test WebApp</display-name>
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
<display-name>OWB CDI Integration Test WebApp</display-name>
<!-- OWB Listener is added by OwbServletContainerInitializer -->
<resource-env-ref>
<description>Object factory for the CDI Bean Manager</description>

View File

@ -7,29 +7,32 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-cdi2-webapp</artifactId>
<name>Test :: CDI2 On Jetty :: Included in WebApp</name>
<artifactId>test-weld-cdi-webapp</artifactId>
<name>Test :: CDI On Jetty :: Weld Demo Webapp</name>
<packaging>war</packaging>
<properties>
<bundle-symbolic-name>${project.groupId}.cdi2.webapp</bundle-symbolic-name>
<bundle-symbolic-name>${project.groupId}.cdi.weld</bundle-symbolic-name>
</properties>
<build>
<finalName>cdi2-demo</finalName>
<finalName>weld-cdi-demo</finalName>
</build>
<dependencies>
<!-- provided by container -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-cdi-common-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<!-- included in webapp -->
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet</artifactId>
<artifactId>weld-servlet-core</artifactId>
<version>${weld.version}</version>
</dependency>
</dependencies>

View File

@ -0,0 +1,17 @@
<?xml version="1.0"?>
<web-app 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/web-app_3_1.xsd"
version="3.1">
<display-name>Weld CDI Integration Test WebApp</display-name>
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
<resource-env-ref>
<description>Object factory for the CDI Bean Manager</description>
<resource-env-ref-name>BeanManager</resource-env-ref-name>
<resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type>
</resource-env-ref>
</web-app>