+ * Task to executing scanning of a resource for annotations.
+ */
+ public static class ParserTask implements Callable
+ {
+ protected final AnnotationParser _parser;
+ protected final Set extends AnnotationParser.Handler> _handlers;
+ protected final Resource _resource;
+ protected TimeStatistic _stat;
+
+ public ParserTask(AnnotationParser parser, Set extends AnnotationParser.Handler> handlers, Resource resource)
+ {
+ _parser = parser;
+ _handlers = handlers;
+ _resource = resource;
+ }
+
+ public void setStatistic(TimeStatistic stat)
+ {
+ _stat = stat;
+ }
+
+ @Override
+ public Void call() throws Exception
+ {
+ if (_stat != null)
+ _stat.start();
+ if (_parser != null)
+ _parser.parse(_handlers, _resource);
+ if (_stat != null)
+ _stat.end();
+ return null;
+ }
+
+ public TimeStatistic getStatistic()
+ {
+ return _stat;
+ }
+
+ public Resource getResource()
+ {
+ return _resource;
+ }
+ }
+
+ /**
+ * ServletContainerInitializerOrdering
+ *
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.
+ */
+ public static class ServletContainerInitializerOrdering
+ {
+ private final Map _indexMap = new HashMap<>();
+ private Integer _star = null;
+ private String _ordering = null;
+
+ public ServletContainerInitializerOrdering(String ordering)
+ {
+ if (ordering != null)
+ {
+ _ordering = ordering;
+
+ String[] tmp = StringUtil.csvSplit(ordering);
+
+ for (int i = 0; i < tmp.length; i++)
+ {
+ String s = tmp[i].trim();
+ _indexMap.put(s, i);
+ if ("*".equals(s))
+ {
+ if (_star != null)
+ throw new IllegalArgumentException("Duplicate wildcards in ServletContainerInitializer ordering " + ordering);
+ _star = i;
+ }
+ }
+ }
+ }
+
+ /**
+ * @return true if "*" is one of the values.
+ */
+ public boolean hasWildcard()
+ {
+ return _star != null;
+ }
+
+ /**
+ * @return the index of the "*" element, if it is specified. -1 otherwise.
+ */
+ public int getWildcardIndex()
+ {
+ if (!hasWildcard())
+ return -1;
+ return _star;
+ }
+
+ /**
+ * @return true if the ordering contains a single value of "*"
+ */
+ public boolean isDefaultOrder()
+ {
+ return (getSize() == 1 && hasWildcard());
+ }
+
+ /**
+ * Get the order index of the given classname
+ *
+ * @param name the classname to look up
+ * @return the index of the class name (or -1 if not found)
+ */
+ public int getIndexOf(String name)
+ {
+ Integer i = _indexMap.get(name);
+ if (i == null)
+ return -1;
+ return i;
+ }
+
+ /**
+ * Get the number of elements of the ordering
+ *
+ * @return the size of the index
+ */
+ public int getSize()
+ {
+ return _indexMap.size();
+ }
+
+ @Override
+ public String toString()
+ {
+ if (_ordering == null)
+ return "";
+ return _ordering;
+ }
+ }
+
+ /**
+ * ServletContainerInitializerComparator
+ *
+ * Comparator impl that orders a set of ServletContainerInitializers according to the
+ * list of classnames (optionally containing a "*" wildcard character) established in a
+ * ServletContainerInitializerOrdering.
+ *
+ * @see ServletContainerInitializerOrdering
+ */
+ public static class ServletContainerInitializerComparator implements Comparator
+ {
+ private final ServletContainerInitializerOrdering _ordering;
+
+ public ServletContainerInitializerComparator(ServletContainerInitializerOrdering ordering)
+ {
+ _ordering = ordering;
+ }
+
+ @Override
+ public int compare(ServletContainerInitializer sci1, ServletContainerInitializer sci2)
+ {
+ String c1 = (sci1 != null ? sci1.getClass().getName() : null);
+ String c2 = (sci2 != null ? sci2.getClass().getName() : null);
+
+ if (c1 == null && c2 == null)
+ return 0;
+
+ int i1 = _ordering.getIndexOf(c1);
+ if (i1 < 0 && _ordering.hasWildcard())
+ i1 = _ordering.getWildcardIndex();
+ int i2 = _ordering.getIndexOf(c2);
+ if (i2 < 0 && _ordering.hasWildcard())
+ i2 = _ordering.getWildcardIndex();
+
+ return Integer.compare(i1, i2);
+ }
+ }
+
+ public static class DiscoveredServletContainerInitializerHolder extends ServletContainerInitializerHolder
+ {
+ private final Set> _handlesTypes = new HashSet<>();
+ private final Set _discoveredClassNames = new HashSet<>();
+
+ public DiscoveredServletContainerInitializerHolder(Source source, ServletContainerInitializer sci, Class>... startupClasses)
+ {
+ super(source, sci);
+ //take the classes and set them aside until we can calculate all of their
+ //subclasses as necessary
+ _handlesTypes.addAll(_startupClasses);
+ }
+
+ /**
+ * Classes that have annotations that are listed in @HandlesTypes
+ * are discovered by the ContainerInitializerAnnotationHandler
+ * and added here.
+ * @param names of classnames that have an annotation that is listed as a class in HandlesTypes
+ */
+ @Override
+ public void addStartupClasses(String... names)
+ {
+ _discoveredClassNames.addAll(Arrays.asList(names));
+ }
+
+ /**
+ * Classes that are listed in @HandlesTypes and found by
+ * the createServletContainerInitializerAnnotationHandlers method.
+ * @param clazzes classes listed in HandlesTypes
+ */
+ @Override
+ public void addStartupClasses(Class>... clazzes)
+ {
+ _handlesTypes.addAll(Arrays.asList(clazzes));
+ }
+
+ @Override
+ protected Set> resolveStartupClasses() throws Exception
+ {
+ final Set> classes = new HashSet<>();
+ WebAppClassLoader.runWithServerClassAccess(() ->
+ {
+ for (String name:_startupClassNames)
+ {
+ classes.add(Loader.loadClass(name));
+ }
+ return null;
+ });
+ return classes;
+ }
+
+ /**
+ * Process each of the classes that are not annotations from @HandlesTypes and
+ * find all of the subclasses/implementations.
+ * Also process all of the classes that were discovered to have an annotation
+ * that was listed in @HandlesTypes, and find all of their subclasses/implementations
+ * in order to generate a complete set of classnames that can be passed into the
+ * onStartup method.
+ *
+ * @param classMap complete inheritance tree of all classes in the webapp, can be
+ * null if @HandlesTypes did not specify any classes.
+ */
+ void resolveClasses(Map> classMap)
+ {
+ Set finalClassnames = new HashSet<>();
+
+ if (classMap != null)
+ {
+ for (Class> c : _handlesTypes)
+ {
+ //find all subclasses/implementations of the classes (not annotations) named in @HandlesTypes
+ if (!c.isAnnotation())
+ addInheritedTypes(finalClassnames, classMap, classMap.get(c.getName()));
+ }
+
+ for (String classname:_discoveredClassNames)
+ {
+ //add each of the classes that were discovered to have an annotation listed in @HandlesTypes
+ finalClassnames.add(classname);
+ //walk its hierarchy and find all types that extend or implement the class
+ addInheritedTypes(finalClassnames, classMap, classMap.get(classname));
+ }
+ }
+
+ //finally, add the complete set of startup classnames
+ super.addStartupClasses(finalClassnames.toArray(new String[0]));
+ }
+
+ /**
+ * Recursively walk the class hierarchy for the given set of classnames.
+ *
+ * @param results all classes related to the set of classnames in names
+ * @param classMap full inheritance tree for all classes in the webapp
+ * @param names the names of classes for which to walk the hierarchy
+ */
+ private void addInheritedTypes(Set results, Map> classMap, Set names)
+ {
+ if (names == null || names.isEmpty())
+ return;
+
+ for (String s : names)
+ {
+ results.add(s);
+
+ //walk the hierarchy and find all types that extend or implement the class
+ addInheritedTypes(results, classMap, classMap.get(s));
+ }
+ }
+ }
+
+ @Override
+ public void preConfigure(final WebAppContext context)
+ {
+ String tmp = (String)context.getAttribute(SERVLET_CONTAINER_INITIALIZER_EXCLUSION_PATTERN);
+ State state = new State(context);
+ context.setAttribute(STATE, state);
+ state._sciExcludePattern = (tmp == null ? null : Pattern.compile(tmp));
+ }
+
+ private State getState(WebAppContext context)
+ {
+ if (context.getAttribute(STATE) instanceof State state)
+ return state;
+ throw new IllegalStateException("No state");
+ }
+
+ @Override
+ public void configure(WebAppContext context) throws Exception
+ {
+ State state = getState(context);
+
+ //handle introspectable annotations (postconstruct,predestroy, multipart etc etc)
+ context.getObjectFactory().addDecorator(new AnnotationDecorator(context));
+
+ if (!context.getMetaData().isMetaDataComplete())
+ {
+ //If web.xml not metadata-complete, if this is a servlet 3 webapp or above
+ //or configDiscovered is true, we need to search for annotations
+ if (context.getServletContext().getEffectiveMajorVersion() >= 3 || context.isConfigurationDiscovered())
+ {
+ state._discoverableAnnotationHandlers.add(new WebServletAnnotationHandler(context));
+ state._discoverableAnnotationHandlers.add(new WebFilterAnnotationHandler(context));
+ state._discoverableAnnotationHandlers.add(new WebListenerAnnotationHandler(context));
+ }
+ }
+
+ //Regardless of metadata, if there are any ServletContainerInitializers with @HandlesTypes, then we need to scan all the
+ //classes so we can call their onStartup() methods correctly
+ createServletContainerInitializerAnnotationHandlers(context, getNonExcludedInitializers(state));
+
+ if (!state._discoverableAnnotationHandlers.isEmpty() || state._classInheritanceHandler != null || !state._containerInitializerAnnotationHandlers.isEmpty())
+ scanForAnnotations(context, state);
+
+ Map> map = (Map>)context.getAttribute(AnnotationConfiguration.CLASS_INHERITANCE_MAP);
+ for (DiscoveredServletContainerInitializerHolder holder: state._sciHolders)
+ {
+ holder.resolveClasses(map);
+ context.addServletContainerInitializer(holder); //only add the holder now all classes are fully available
+ }
+ }
+
+ @Override
+ public void postConfigure(WebAppContext context) throws Exception
+ {
+ Map> classMap = (ClassInheritanceMap)context.getAttribute(CLASS_INHERITANCE_MAP);
+ if (classMap != null)
+ classMap.clear();
+ context.removeAttribute(CLASS_INHERITANCE_MAP);
+
+ if (!(context.removeAttribute(STATE) instanceof State state))
+ throw new IllegalStateException("No state");
+ state._discoverableAnnotationHandlers.clear();
+ state._classInheritanceHandler = null;
+ state._containerInitializerAnnotationHandlers.clear();
+ state._sciHolders.clear();
+
+ if (state._parserTasks != null)
+ {
+ state._parserTasks.clear();
+ state._parserTasks = null;
+ }
+
+ super.postConfigure(context);
+ }
+
+ /**
+ * Perform scanning of classes for discoverable
+ * annotations such as WebServlet/WebFilter/WebListener
+ *
+ * @param context the context for the scan
+ * @throws Exception if unable to scan
+ */
+ protected void scanForAnnotations(WebAppContext context, State state)
+ throws Exception
+ {
+ int javaPlatform = 0;
+ Object target = context.getAttribute(JavaVersion.JAVA_TARGET_PLATFORM);
+ if (target != null)
+ javaPlatform = Integer.parseInt(target.toString());
+ AnnotationParser parser = createAnnotationParser(javaPlatform);
+ state._parserTasks = new ArrayList<>();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Annotation scanning commencing: webxml={}, metadatacomplete={}, configurationDiscovered={}, multiThreaded={}, maxScanWait={}",
+ context.getServletContext().getEffectiveMajorVersion(),
+ context.getMetaData().isMetaDataComplete(),
+ context.isConfigurationDiscovered(),
+ isUseMultiThreading(context),
+ getMaxScanWait(context));
+
+ //scan selected jars on the container classpath first
+ parseContainerPath(context, parser);
+ //email from Rajiv Mordani jsrs 315 7 April 2010
+ // If there is a then the ordering should be
+ // WEB-INF/classes the order of the declared elements + others.
+ // In case there is no others then it is
+ // WEB-INF/classes + order of the elements.
+ parseWebInfClasses(state, parser);
+ //scan non-excluded, non medatadata-complete jars in web-inf lib
+ parseWebInfLib(state, parser);
+
+ long start = NanoTime.now();
+
+ //execute scan, either effectively synchronously (1 thread only), or asynchronously (limited by number of processors available)
+ final Semaphore task_limit = (isUseMultiThreading(context) ? new Semaphore(ProcessorUtils.availableProcessors()) : new Semaphore(1));
+ final CountDownLatch latch = new CountDownLatch(state._parserTasks.size());
+ final ExceptionUtil.MultiException multiException = new ExceptionUtil.MultiException();
+
+ for (final ParserTask p : state._parserTasks)
+ {
+ task_limit.acquire();
+ context.getServer().getThreadPool().execute(() ->
+ {
+ multiException.callAndCatch(p::call);
+ task_limit.release();
+ latch.countDown();
+ });
+ }
+
+ boolean timeout = !latch.await(getMaxScanWait(context), TimeUnit.SECONDS);
+ long elapsedMs = NanoTime.millisSince(start);
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Annotation scanning elapsed time={}ms", elapsedMs);
+ for (ParserTask p : state._parserTasks)
+ {
+ LOG.debug("Scanned {} in {}ms", p.getResource(), TimeUnit.NANOSECONDS.toMillis(p.getStatistic().getElapsedNanos()));
+ }
+
+ LOG.debug("Scanned {} container path jars, {} WEB-INF/lib jars, {} WEB-INF/classes dirs in {}ms for context {}",
+ (state._containerPathStats == null ? -1 : state._containerPathStats.getTotal()),
+ (state._webInfLibStats == null ? -1 : state._webInfLibStats.getTotal()),
+ (state._webInfClassesStats == null ? -1 : state._webInfClassesStats.getTotal()),
+ elapsedMs,
+ context);
+ }
+
+ if (timeout)
+ multiException.add(new Exception("Timeout scanning annotations"));
+ multiException.ifExceptionThrow();
+ }
+
+ /**
+ * @param javaPlatform The java platform to scan for.
+ * @return a new AnnotationParser. This method can be overridden to use a different implementation of
+ * the AnnotationParser. Note that this is considered internal API.
+ */
+ protected AnnotationParser createAnnotationParser(int javaPlatform)
+ {
+ return new AnnotationParser(javaPlatform);
+ }
+
+ /**
+ * Check if we should use multiple threads to scan for annotations or not
+ *
+ * @param context the context of the multi threaded setting
+ * @return true if multi threading is enabled on the context, server, or via a System property.
+ * @see #MULTI_THREADED
+ */
+ protected boolean isUseMultiThreading(WebAppContext context)
+ {
+ //try context attribute to see if we should use multithreading
+ Object o = context.getAttribute(MULTI_THREADED);
+ if (o instanceof Boolean)
+ {
+ return (Boolean)o;
+ }
+ //try server attribute to see if we should use multithreading
+ o = context.getServer().getAttribute(MULTI_THREADED);
+ if (o instanceof Boolean)
+ {
+ return (Boolean)o;
+ }
+ //try system property to see if we should use multithreading
+ return Boolean.parseBoolean(System.getProperty(MULTI_THREADED, Boolean.toString(DEFAULT_MULTI_THREADED)));
+ }
+
+ /**
+ * Work out how long we should wait for the async scanning to occur.
+ *
+ * @param context the context of the max scan wait setting
+ * @return the max scan wait setting on the context, or server, or via a System property.
+ * @see #MAX_SCAN_WAIT
+ */
+ protected int getMaxScanWait(WebAppContext context)
+ {
+ //try context attribute to get max time in sec to wait for scan completion
+ Object o = context.getAttribute(MAX_SCAN_WAIT);
+ if (o instanceof Number)
+ {
+ return ((Number)o).intValue();
+ }
+ //try server attribute to get max time in sec to wait for scan completion
+ o = context.getServer().getAttribute(MAX_SCAN_WAIT);
+ if (o instanceof Number)
+ {
+ return ((Number)o).intValue();
+ }
+ //try system property to get max time in sec to wait for scan completion
+ return Integer.getInteger(MAX_SCAN_WAIT, DEFAULT_MAX_SCAN_WAIT);
+ }
+
+ public void createServletContainerInitializerAnnotationHandlers(WebAppContext context, List scis)
+ throws Exception
+ {
+ if (scis == null || scis.isEmpty())
+ return; // nothing to do
+
+ State state = getState(context);
+
+ for (ServletContainerInitializer sci : scis)
+ {
+ Class>[] classes = new Class>[0];
+ HandlesTypes annotation = sci.getClass().getAnnotation(HandlesTypes.class);
+ if (annotation != null)
+ classes = annotation.value();
+
+ DiscoveredServletContainerInitializerHolder holder = new DiscoveredServletContainerInitializerHolder(new Source(Origin.ANNOTATION, sci.getClass()), sci);
+ state._sciHolders.add(holder);
+
+ if (classes.length > 0)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("HandlesTypes {} on initializer {}", Arrays.asList(classes), sci.getClass());
+
+ //If we haven't already done so, we need to register a handler that will
+ //process the whole class hierarchy to satisfy the ServletContainerInitializer
+ if (context.getAttribute(CLASS_INHERITANCE_MAP) == null)
+ {
+ Map> map = new ClassInheritanceMap();
+ context.setAttribute(CLASS_INHERITANCE_MAP, map);
+ state._classInheritanceHandler = new ClassInheritanceHandler(map);
+ }
+
+ for (Class> c : classes)
+ {
+ //The value of one of the HandlesTypes classes is actually an Annotation itself so
+ //register a handler for it to discover all classes that contain this annotation
+ if (c.isAnnotation())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Registering annotation handler for {}", c.getName());
+ state._containerInitializerAnnotationHandlers.add(new ContainerInitializerAnnotationHandler(holder, c));
+ }
+
+ holder.addStartupClasses(c);
+ }
+ }
+ }
+ }
+
+ protected Resource getJarFor(WebAppContext context, ServletContainerInitializer service)
+ {
+ URI uri = TypeUtil.getLocationOfClass(service.getClass());
+ if (uri == null)
+ return null;
+ return ResourceFactory.of(context).newResource(uri);
+ }
+
+ /**
+ * Check to see if the ServletContainerIntializer loaded via the ServiceLoader came
+ * from a jar that is excluded by the fragment ordering. See ServletSpec 3.0 p.85.
+ *
+ * @param context the context for the jars
+ * @param sci the servlet container initializer
+ * @param sciResource the resource for the servlet container initializer
+ * @return true if excluded
+ */
+ public boolean isFromExcludedJar(WebAppContext context, ServletContainerInitializer sci, Resource sciResource)
+ {
+ if (sci == null)
+ throw new IllegalArgumentException("ServletContainerInitializer null");
+ if (context == null)
+ throw new IllegalArgumentException("WebAppContext null");
+
+ //if we don't know where its from it can't be excluded
+ if (sciResource == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("!Excluded {} null resource", sci);
+ return false;
+ }
+
+ //A ServletContainerInitialier that came from WEB-INF/classes or equivalent cannot be excluded by an ordering
+ if (isFromWebInfClasses(context, sciResource))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("!Excluded {} from web-inf/classes", sci);
+ return false;
+ }
+
+ //A ServletContainerInitializer that came from the container's classpath cannot be excluded by an ordering
+ //of WEB-INF/lib jars
+ if (isFromContainerClassPath(context, sci))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("!Excluded {} from container classpath", sci);
+ return false;
+ }
+
+ //If no ordering, nothing is excluded
+ if (!context.getMetaData().isOrdered())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("!Excluded {} no ordering", sci);
+ return false;
+ }
+
+ List orderedJars = context.getMetaData().getWebInfResources(true);
+
+ //there is an ordering, but there are no jars resulting from the ordering, everything excluded
+ if (orderedJars.isEmpty())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Excluded {} empty ordering", sci);
+ return true;
+ }
+
+ //Check if it is excluded by an ordering
+ boolean included = false;
+ for (Resource r : orderedJars)
+ {
+ included = r.equals(sciResource);
+ if (included)
+ break;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{}Excluded {} found={}", included ? "!" : "", sci, included);
+ return !included;
+ }
+
+ /**
+ * Test if the ServletContainerIntializer is excluded by the
+ * o.e.j.containerInitializerExclusionPattern
+ *
+ * @param state the state of the Annotation parsing
+ * @param sci the ServletContainerIntializer
+ * @return true if the ServletContainerIntializer is excluded
+ */
+ protected boolean matchesExclusionPattern(State state, ServletContainerInitializer sci)
+ {
+ //no exclusion pattern, no SCI is excluded by it
+ if (state._sciExcludePattern == null)
+ return false;
+
+ //test if name of class matches the regex
+ if (LOG.isDebugEnabled())
+ LOG.debug("Checking {} against containerInitializerExclusionPattern", sci.getClass().getName());
+ return state._sciExcludePattern.matcher(sci.getClass().getName()).matches();
+ }
+
+ /**
+ * Test if the ServletContainerInitializer is from the container classpath
+ *
+ * @param context the context for the webapp classpath
+ * @param sci the ServletContainerIntializer
+ * @return true if ServletContainerIntializer is from container classpath
+ */
+ public boolean isFromContainerClassPath(WebAppContext context, ServletContainerInitializer sci)
+ {
+ if (sci == null)
+ return false;
+
+ ClassLoader sciLoader = sci.getClass().getClassLoader();
+
+ //if loaded by bootstrap loader, then its the container classpath
+ if (sciLoader == null)
+ return true;
+
+ //if there is no context classloader, then its the container classpath
+ if (context.getClassLoader() == null)
+ return true;
+
+ ClassLoader loader = sciLoader;
+ while (loader != null)
+ {
+ if (loader == context.getClassLoader())
+ return false; //the webapp classloader is in the ancestry of the classloader for the sci
+ else
+ loader = loader.getParent();
+ }
+
+ return true;
+ }
+
+ /**
+ * Test if the ServletContainerInitializer is from WEB-INF/classes
+ *
+ * @param context the webapp to test
+ * @param sci a Resource representing the SCI
+ * @return true if the sci Resource is inside a WEB-INF/classes directory, false otherwise
+ */
+ public boolean isFromWebInfClasses(WebAppContext context, Resource sci)
+ {
+ for (Resource dir : context.getMetaData().getWebInfClassesResources())
+ {
+ if (dir.equals(sci))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get SCIs that are not excluded from consideration
+ *
+ * @param state the web app annotation parse state
+ * @return the list of non-excluded servlet container initializers
+ */
+ protected List getNonExcludedInitializers(State state)
+ {
+ WebAppContext context = state._context;
+ ArrayList nonExcludedInitializers = new ArrayList<>();
+
+ //We use the ServiceLoader mechanism to find the ServletContainerInitializer classes to inspect
+ long start = NanoTime.now();
+ List scis = TypeUtil.serviceProviderStream(ServiceLoader.load(ServletContainerInitializer.class)).flatMap(provider ->
+ {
+ try
+ {
+ return Stream.of(provider.get());
+ }
+ catch (Error e)
+ {
+ // Probably a SCI discovered on the system classpath that is hidden by the context classloader
+ if (LOG.isDebugEnabled())
+ LOG.debug("Error: {} for {}", e.getMessage(), state._context, e);
+ else
+ LOG.info("Error: {} for {}", e.getMessage(), state._context);
+ return Stream.of();
+ }
+ }).toList();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Service loaders found in {}ms", NanoTime.millisSince(start));
+
+ Map sciResourceMap = new HashMap<>();
+ ServletContainerInitializerOrdering initializerOrdering = getInitializerOrdering(context);
+
+ //Get initial set of SCIs that aren't from excluded jars or excluded by the containerExclusionPattern, or excluded
+ //because containerInitializerOrdering omits it
+ for (ServletContainerInitializer sci : scis)
+ {
+ if (matchesExclusionPattern(state, sci))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by pattern", sci);
+ continue;
+ }
+
+ Resource sciResource = getJarFor(context, sci);
+ if (isFromExcludedJar(context, sci, sciResource))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} is from excluded jar", sci);
+ continue;
+ }
+
+ //check containerInitializerOrdering doesn't exclude it
+ String name = sci.getClass().getName();
+ if (initializerOrdering != null && (!initializerOrdering.hasWildcard() && initializerOrdering.getIndexOf(name) < 0))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} is excluded by ordering", sci);
+ continue;
+ }
+
+ sciResourceMap.put(sci, sciResource);
+ }
+
+ //Order the SCIs that are included
+ if (initializerOrdering != null && !initializerOrdering.isDefaultOrder())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ordering ServletContainerInitializers with {}", initializerOrdering);
+
+ //There is an ordering that is not just "*".
+ //Arrange ServletContainerInitializers according to the ordering of classnames given, irrespective of coming from container or webapp classpaths
+ nonExcludedInitializers.addAll(sciResourceMap.keySet());
+ nonExcludedInitializers.sort(new ServletContainerInitializerComparator(initializerOrdering));
+ }
+ else
+ {
+ //No jetty-specific ordering specified, or just the wildcard value "*" specified.
+ //Fallback to ordering the ServletContainerInitializers according to:
+ //container classpath first, WEB-INF/classes then WEB-INF/lib (obeying any web.xml jar ordering)
+
+ //First add in all SCIs that can't be excluded
+ int lastContainerSCI = -1;
+ for (Map.Entry entry : sciResourceMap.entrySet())
+ {
+ if (entry.getKey().getClass().getClassLoader() == context.getClassLoader().getParent())
+ {
+ nonExcludedInitializers.add(++lastContainerSCI, entry.getKey()); //add all container SCIs before any webapp SCIs
+ }
+ else if (entry.getValue() == null) //can't work out provenance of SCI, so can't be ordered/excluded
+ {
+ nonExcludedInitializers.add(entry.getKey()); //add at end of list
+ }
+ else
+ {
+ for (Resource dir : context.getMetaData().getWebInfClassesResources())
+ {
+ if (dir.equals(entry.getValue()))//from WEB-INF/classes so can't be ordered/excluded
+ {
+ nonExcludedInitializers.add(entry.getKey());
+ }
+ }
+ }
+ }
+
+ //throw out the ones we've already accounted for
+ for (ServletContainerInitializer s : nonExcludedInitializers)
+ {
+ sciResourceMap.remove(s);
+ }
+
+ if (context.getMetaData().getOrdering() == null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No web.xml ordering, ServletContainerInitializers in random order");
+ //add the rest of the scis
+ nonExcludedInitializers.addAll(sciResourceMap.keySet());
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ordering ServletContainerInitializers with ordering {}", context.getMetaData().getOrdering());
+
+ //add SCIs according to the ordering of its containing jar
+ for (Resource webInfJar : context.getMetaData().getWebInfResources(true))
+ {
+ for (Map.Entry entry : sciResourceMap.entrySet())
+ {
+ if (webInfJar.equals(entry.getValue()))
+ nonExcludedInitializers.add(entry.getKey());
+ }
+ }
+ }
+ }
+
+ //final pass over the non-excluded SCIs if the webapp version is < 3, in which case
+ //we will only call SCIs that are on the server's classpath
+ if (context.getServletContext().getEffectiveMajorVersion() < 3 && !context.isConfigurationDiscovered())
+ {
+ ListIterator it = nonExcludedInitializers.listIterator();
+ while (it.hasNext())
+ {
+ ServletContainerInitializer sci = it.next();
+ if (!isFromContainerClassPath(context, sci))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Ignoring SCI {}: old web.xml version {}.{}", sci.getClass().getName(),
+ context.getServletContext().getEffectiveMajorVersion(),
+ context.getServletContext().getEffectiveMinorVersion());
+ it.remove();
+ }
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ {
+ int i = 0;
+ for (ServletContainerInitializer sci : nonExcludedInitializers)
+ {
+ LOG.debug("ServletContainerInitializer: {} {} from {}", (++i), sci.getClass().getName(), sciResourceMap.get(sci));
+ }
+ }
+
+ return nonExcludedInitializers;
+ }
+
+ /**
+ * Jetty-specific extension that allows an ordering to be applied across ALL ServletContainerInitializers.
+ *
+ * @param context the context for the initializer ordering configuration
+ * @return the ordering of the ServletContainerIntializer's
+ */
+ public ServletContainerInitializerOrdering getInitializerOrdering(WebAppContext context)
+ {
+ if (context == null)
+ return null;
+
+ String tmp = (String)context.getAttribute(SERVLET_CONTAINER_INITIALIZER_ORDER);
+ if (StringUtil.isBlank(tmp))
+ return null;
+
+ return new ServletContainerInitializerOrdering(tmp);
+ }
+
+ /**
+ * Scan jars on container path.
+ *
+ * @param context the context for the scan
+ * @param parser the parser to scan with
+ */
+ public void parseContainerPath(final WebAppContext context, final AnnotationParser parser)
+ {
+ State state = getState(context);
+ //always parse for discoverable annotations as well as class hierarchy and servletcontainerinitializer related annotations
+ final Set handlers = new HashSet<>();
+ handlers.addAll(state._discoverableAnnotationHandlers);
+ handlers.addAll(state._containerInitializerAnnotationHandlers);
+ if (state._classInheritanceHandler != null)
+ handlers.add(state._classInheritanceHandler);
+
+ if (LOG.isDebugEnabled())
+ state._containerPathStats = new CounterStatistic();
+
+ //scan the container classpath jars that were selected by
+ //filtering in MetaInfConfiguration
+ for (Resource r : context.getMetaData().getContainerResources())
+ {
+ if (state._parserTasks != null)
+ {
+ ParserTask task = new ParserTask(parser, handlers, r);
+ state._parserTasks.add(task);
+ if (LOG.isDebugEnabled())
+ {
+ state._containerPathStats.increment();
+ task.setStatistic(new TimeStatistic());
+ }
+ }
+ }
+ }
+
+ /**
+ * Scan jars in WEB-INF/lib.
+ *
+ * Only jars selected by MetaInfConfiguration, and that are not excluded
+ * by an ordering will be considered.
+ *
+ * @param state the state for the scan
+ * @param parser the annotation parser to use
+ * @throws Exception if unable to scan and/or parse
+ */
+ protected void parseWebInfLib(State state, final AnnotationParser parser) throws Exception
+ {
+ //email from Rajiv Mordani jsrs 315 7 April 2010
+ //jars that do not have a web-fragment.xml are still considered fragments
+ //they have to participate in the ordering
+
+ //if there is an ordering, the ordered jars should be used.
+ //If there is no ordering, jars will be unordered.
+ WebAppContext context = state._context;
+ List jars = context.getMetaData().getWebInfResources(context.getMetaData().isOrdered());
+
+ if (LOG.isDebugEnabled())
+ {
+ if (state._webInfLibStats == null)
+ state._webInfLibStats = new CounterStatistic();
+ }
+
+ for (Resource r : jars)
+ {
+ //for each jar, we decide which set of annotations we need to parse for
+ final Set handlers = new HashSet<>();
+
+ FragmentDescriptor f = context.getMetaData().getFragmentDescriptorForJar(r);
+
+ //if its from a fragment jar that is metadata complete, we should skip scanning for @webservlet etc
+ // but yet we still need to do the scanning for the classes on behalf of the servletcontainerinitializers
+ //if a jar has no web-fragment.xml we scan it (because it is not excluded by the ordering)
+ //or if it has a fragment we scan it if it is not metadata complete
+ if (!WebDescriptor.isMetaDataComplete(f) || state._classInheritanceHandler != null || !state._containerInitializerAnnotationHandlers.isEmpty())
+ {
+ //register the classinheritance handler if there is one
+ if (state._classInheritanceHandler != null)
+ handlers.add(state._classInheritanceHandler);
+
+ //register the handlers for the @HandlesTypes values that are themselves annotations if there are any
+ handlers.addAll(state._containerInitializerAnnotationHandlers);
+
+ //only register the discoverable annotation handlers if this fragment is not metadata complete, or has no fragment descriptor
+ if (!WebDescriptor.isMetaDataComplete(f))
+ handlers.addAll(state._discoverableAnnotationHandlers);
+
+ if (state._parserTasks != null)
+ {
+ ParserTask task = new ParserTask(parser, handlers, r);
+ state._parserTasks.add(task);
+ if (LOG.isDebugEnabled())
+ {
+ state._webInfLibStats.increment();
+ task.setStatistic(new TimeStatistic());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Scan classes in WEB-INF/classes.
+ *
+ * @param state the state for the scan
+ * @param parser the annotation parser to use
+ */
+ protected void parseWebInfClasses(State state, AnnotationParser parser)
+ {
+ WebAppContext context = state._context;
+ Set handlers = new HashSet<>(state._discoverableAnnotationHandlers);
+ if (state._classInheritanceHandler != null)
+ handlers.add(state._classInheritanceHandler);
+ handlers.addAll(state._containerInitializerAnnotationHandlers);
+
+ if (LOG.isDebugEnabled())
+ state._webInfClassesStats = new CounterStatistic();
+
+ for (Resource dir : context.getMetaData().getWebInfClassesResources())
+ {
+ if (state._parserTasks != null)
+ {
+ ParserTask task = new ParserTask(parser, handlers, dir);
+ state._parserTasks.add(task);
+ if (LOG.isDebugEnabled())
+ {
+ state._webInfClassesStats.increment();
+ task.setStatistic(new TimeStatistic());
+ }
+ }
+ }
+ }
+
+ public static class ClassInheritanceMap extends ConcurrentHashMap>
+ {
+ @Override
+ public String toString()
+ {
+ return String.format("ClassInheritanceMap@%x{size=%d}", hashCode(), size());
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationDecorator.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationDecorator.java
new file mode 100644
index 00000000000..e40f2fc228a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationDecorator.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.Objects;
+
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.util.DecoratedObjectFactory;
+import org.eclipse.jetty.util.Decorator;
+
+/**
+ * AnnotationDecorator
+ */
+public class AnnotationDecorator implements Decorator
+{
+ protected AnnotationIntrospector _introspector;
+ protected WebAppContext _context;
+
+ public AnnotationDecorator(WebAppContext context)
+ {
+ _context = Objects.requireNonNull(context);
+ _introspector = new AnnotationIntrospector(_context);
+ registerHandlers();
+ }
+
+ private void registerHandlers()
+ {
+ _introspector.registerHandler(new ResourceAnnotationHandler(_context));
+ _introspector.registerHandler(new ResourcesAnnotationHandler(_context));
+ _introspector.registerHandler(new RunAsAnnotationHandler(_context));
+ _introspector.registerHandler(new PostConstructAnnotationHandler(_context));
+ _introspector.registerHandler(new PreDestroyAnnotationHandler(_context));
+ _introspector.registerHandler(new DeclareRolesAnnotationHandler(_context));
+ _introspector.registerHandler(new MultiPartConfigAnnotationHandler(_context));
+ _introspector.registerHandler(new ServletSecurityAnnotationHandler(_context));
+ }
+
+ /**
+ * Look for annotations that can be discovered with introspection:
+ *
+ *
Resource
+ *
Resources
+ *
RunAs
+ *
PostConstruct
+ *
PreDestroy
+ *
DeclareRoles
+ *
MultiPart
+ *
ServletSecurity
+ *
+ *
+ * @param o the object to introspect
+ * @param metaInfo information about the object to introspect
+ */
+ protected void introspect(Object o, Object metaInfo)
+ {
+ if (o == null)
+ return;
+ _introspector.introspect(o, metaInfo);
+ }
+
+ @Override
+ public T decorate(T o)
+ {
+ introspect(o, DecoratedObjectFactory.getAssociatedInfo());
+ return o;
+ }
+
+ @Override
+ public void destroy(Object o)
+ {
+
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationIntrospector.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationIntrospector.java
new file mode 100644
index 00000000000..7d38a70d685
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationIntrospector.java
@@ -0,0 +1,229 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.jetty.ee11.servlet.BaseHolder;
+import org.eclipse.jetty.ee11.servlet.Source.Origin;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.ee11.webapp.WebDescriptor;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.thread.AutoLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Introspects a class to find various types of
+ * annotations as defined by the servlet specification.
+ */
+public class AnnotationIntrospector
+{
+ private static final Logger LOG = LoggerFactory.getLogger(AnnotationIntrospector.class);
+
+ private final AutoLock _lock = new AutoLock();
+ private final Set> _introspectedClasses = new HashSet<>();
+ private final List _handlers = new ArrayList<>();
+ private final WebAppContext _context;
+
+ /**
+ * Interface for all handlers that wish to introspect a class to find a particular annotation
+ */
+ public interface IntrospectableAnnotationHandler
+ {
+ void handle(Class> clazz);
+ }
+
+ /**
+ * Base class for handlers that introspect a class to find a particular annotation.
+ * A handler can optionally introspect the parent hierarchy of a class.
+ */
+ public abstract static class AbstractIntrospectableAnnotationHandler implements IntrospectableAnnotationHandler
+ {
+ protected boolean _introspectAncestors;
+ protected WebAppContext _context;
+
+ public abstract void doHandle(Class> clazz);
+
+ public AbstractIntrospectableAnnotationHandler(boolean introspectAncestors, WebAppContext context)
+ {
+ _context = Objects.requireNonNull(context);
+ _introspectAncestors = introspectAncestors;
+ }
+
+ /**
+ * Check if the given class is permitted to have Servlet annotation.
+ *
+ * @param c the class
+ * @return true if the spec permits the class to have Servlet annotations, false otherwise
+ */
+ protected static boolean isAnnotatableServletClass(Class> c)
+ {
+ return jakarta.servlet.Servlet.class.isAssignableFrom(c) ||
+ jakarta.servlet.Filter.class.isAssignableFrom(c) ||
+ jakarta.servlet.ServletContextListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.ServletContextAttributeListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.ServletRequestListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.ServletRequestAttributeListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.http.HttpSessionListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.http.HttpSessionAttributeListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.http.HttpSessionIdListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.AsyncListener.class.isAssignableFrom(c) ||
+ jakarta.servlet.http.HttpUpgradeHandler.class.isAssignableFrom(c);
+ }
+
+ @Override
+ public void handle(Class> clazz)
+ {
+ Class> c = clazz;
+
+ //process the whole inheritance hierarchy for the class
+ while (c != null && (!c.equals(Object.class)))
+ {
+ doHandle(c);
+ if (!_introspectAncestors)
+ break;
+
+ c = c.getSuperclass();
+ }
+ }
+
+ public WebAppContext getContext()
+ {
+ return _context;
+ }
+ }
+
+ public AnnotationIntrospector(WebAppContext context)
+ {
+ _context = Objects.requireNonNull(context);
+ }
+
+ public void registerHandler(IntrospectableAnnotationHandler handler)
+ {
+ _handlers.add(handler);
+ }
+
+ /**
+ * Test if an object should be introspected for some specific types of annotations
+ * like PostConstruct/PreDestroy/MultiPart etc etc.
+ *
+ * According to servlet 4.0, these types of annotations should only be evaluated iff any
+ * of the following are true:
+ *
+ *
the object was created by the jakarta.servlet.ServletContext.createServlet/Filter/Listener method
+ *
the object comes either from a discovered annotation (WebServlet/Filter/Listener) or a declaration
+ * in a descriptor AND web.xml is NOT metadata-complete AND any web-fragment.xml associated with the location of
+ * the class is NOT metadata-complete
+ *
+ *
+ * We also support evaluations of these types of annotations for objects that were created directly
+ * by the jetty api.
+ *
+ * @param o the object to check for its ability to be introspected for annotations
+ * @param metaInfo meta information about the object to be introspected
+ * @return true if it can be introspected according to servlet 4.0 rules
+ */
+ public boolean isIntrospectable(Object o, Object metaInfo)
+ {
+ if (o == null)
+ return false; //nothing to introspect
+
+ if (metaInfo == null)
+ return true; //no information about the object to introspect, assume introspectable
+
+ BaseHolder> holder;
+
+ try
+ {
+ holder = (BaseHolder>)metaInfo;
+ }
+ catch (ClassCastException e)
+ {
+ LOG.warn("Not introspectable {}", metaInfo.getClass().getName(), e);
+ return true; //not the type of information we were expecting, assume introspectable
+ }
+
+ Origin origin = (holder.getSource() == null ? null : holder.getSource().getOrigin());
+ if (origin == null)
+ return true; //assume introspectable
+
+ switch (origin)
+ {
+ case EMBEDDED:
+ case JAKARTA_API:
+ {
+ return true; //objects created from the jetty or servlet api are always introspectable
+ }
+ case ANNOTATION:
+ {
+ return true; //we will have discovered annotations only if metadata-complete==false
+ }
+ default:
+ {
+ //must be from a descriptor. Only introspect if the descriptor with which it was associated
+ //is not metadata-complete
+ if (_context.getMetaData().isMetaDataComplete())
+ return false;
+
+ Resource descriptorLocation = holder.getSource().getResource();
+ if (descriptorLocation == null)
+ return true; //no descriptor, can't be metadata-complete
+ return !WebDescriptor.isMetaDataComplete(_context.getMetaData().getFragmentDescriptor(descriptorLocation));
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ public void introspect(Object o, Object metaInfo)
+ {
+ if (!isIntrospectable(o, metaInfo))
+ return;
+
+ Class> clazz = o.getClass();
+
+ try (AutoLock ignored = _lock.lock())
+ {
+ // Lock to ensure that only 1 thread can be introspecting, and that
+ // thread must have fully finished generating the products of
+ // the introspection before another thread is allowed in.
+ // We remember the classes that we have introspected to avoid
+ // reprocessing the same class.
+ if (_introspectedClasses.add(clazz))
+ {
+ for (IntrospectableAnnotationHandler handler : _handlers)
+ {
+ try
+ {
+ handler.handle(clazz);
+ }
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationParser.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationParser.java
new file mode 100644
index 00000000000..eca238850d0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/AnnotationParser.java
@@ -0,0 +1,684 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jetty.util.ExceptionUtil;
+import org.eclipse.jetty.util.FileID;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AnnotationParser
+ *
+ * Use asm to scan classes for annotations. A SAX-style parsing is done.
+ * Handlers are registered which will be called back when various types of
+ * entity are encountered, eg a class, a method, a field.
+ *
+ * Handlers are not called back in any particular order and are assumed
+ * to be order-independent.
+ *
+ * As a registered Handler will be called back for each annotation discovered
+ * on a class, a method, a field, the Handler should test to see if the annotation
+ * is one that it is interested in.
+ *
+ * For the servlet spec, we are only interested in annotations on classes, methods and fields,
+ * so the callbacks for handling finding a class, a method a field are themselves
+ * not fully implemented.
+ */
+public class AnnotationParser
+{
+ private static final Logger LOG = LoggerFactory.getLogger(AnnotationParser.class);
+ private static final int ASM_VERSION = asmVersion();
+
+ /**
+ * Map of classnames scanned and the first location from which scan occurred
+ */
+ protected Map _parsedClassNames = new ConcurrentHashMap<>();
+ private final int _asmVersion;
+
+ /**
+ * Determine the runtime version of asm.
+ *
+ * @return the {@link org.objectweb.asm.Opcodes} ASM value matching the runtime version of asm.
+ * TODO: can this be a jetty-util utility method to allow reuse across ee#?
+ * TODO: we should probably keep ASM centralized, as it's not EE specific, but Java Runtime specific behavior to keep up to date
+ */
+ private static int asmVersion()
+ {
+ // Find the highest available ASM version on the runtime/classpath, because
+ // if we run with a lower than available ASM version, against a class with
+ // new language features we'll get an UnsupportedOperationException, even if
+ // the ASM version supports the new language features.
+ // Also, if we run with a higher than available ASM version, we'll get
+ // an IllegalArgumentException from org.objectweb.asm.ClassVisitor.
+ // So must find exactly the maximum ASM api version available.
+
+ Optional asmVersion = Arrays.stream(Opcodes.class.getFields()).sequential()
+ .filter((f) -> f.getName().matches("ASM[0-9]+"))
+ .map((f) -> f.getName().substring(3))
+ .map(Integer::parseInt)
+ .max(Integer::compareTo);
+
+ if (asmVersion.isEmpty())
+ throw new IllegalStateException("Invalid " + Opcodes.class.getName());
+
+ int asmFieldId = asmVersion.get();
+ try
+ {
+ String fieldName = "ASM" + asmFieldId;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Using ASM API from {}.{}", Opcodes.class.getName(), fieldName);
+ return (int)Opcodes.class.getField(fieldName).get(null);
+ }
+ catch (Throwable e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Convert internal name to simple name
+ *
+ * @param name the internal name
+ * @return the simple name
+ */
+ public static String normalize(String name)
+ {
+ if (name == null)
+ return null;
+
+ if (name.startsWith("L") && name.endsWith(";"))
+ name = name.substring(1, name.length() - 1);
+
+ if (name.endsWith(".class"))
+ name = name.substring(0, name.length() - ".class".length());
+
+ return StringUtil.replace(name, '/', '.');
+ }
+
+ /**
+ * Convert internal names to simple names.
+ *
+ * @param list the list of internal names
+ * @return the array of simple names
+ */
+ public static String[] normalize(String[] list)
+ {
+ if (list == null)
+ return null;
+ String[] normalList = new String[list.length];
+ int i = 0;
+ for (String s : list)
+ {
+ normalList[i++] = normalize(s);
+ }
+ return normalList;
+ }
+
+ /**
+ * Immutable information gathered by parsing class header.
+ */
+ public static class ClassInfo
+ {
+ final Resource _containingResource;
+ final String _className;
+ final int _version;
+ final int _access;
+ final String _signature;
+ final String _superName;
+ final String[] _interfaces;
+
+ public ClassInfo(Resource resource, String className, int version, int access, String signature, String superName, String[] interfaces)
+ {
+ super();
+ _containingResource = resource;
+ _className = className;
+ _version = version;
+ _access = access;
+ _signature = signature;
+ _superName = superName;
+ _interfaces = interfaces;
+ }
+
+ public String getClassName()
+ {
+ return _className;
+ }
+
+ public int getVersion()
+ {
+ return _version;
+ }
+
+ public int getAccess()
+ {
+ return _access;
+ }
+
+ public String getSignature()
+ {
+ return _signature;
+ }
+
+ public String getSuperName()
+ {
+ return _superName;
+ }
+
+ public String[] getInterfaces()
+ {
+ return _interfaces;
+ }
+
+ public Resource getContainingResource()
+ {
+ return _containingResource;
+ }
+ }
+
+ /**
+ * Immutable information gathered by parsing a method on a class.
+ */
+ public static class MethodInfo
+ {
+ final ClassInfo _classInfo;
+ final String _methodName;
+ final int _access;
+ final String _desc;
+ final String _signature;
+ final String[] _exceptions;
+
+ public MethodInfo(ClassInfo classInfo, String methodName, int access, String desc, String signature, String[] exceptions)
+ {
+ super();
+ _classInfo = classInfo;
+ _methodName = methodName;
+ _access = access;
+ _desc = desc;
+ _signature = signature;
+ _exceptions = exceptions;
+ }
+
+ public ClassInfo getClassInfo()
+ {
+ return _classInfo;
+ }
+
+ public String getMethodName()
+ {
+ return _methodName;
+ }
+
+ public int getAccess()
+ {
+ return _access;
+ }
+
+ public String getDesc()
+ {
+ return _desc;
+ }
+
+ public String getSignature()
+ {
+ return _signature;
+ }
+
+ public String[] getExceptions()
+ {
+ return _exceptions;
+ }
+ }
+
+ /**
+ * Immutable information gathered by parsing a field on a class.
+ */
+ public static class FieldInfo
+ {
+ final ClassInfo _classInfo;
+ final String _fieldName;
+ final int _access;
+ final String _fieldType;
+ final String _signature;
+ final Object _value;
+
+ public FieldInfo(ClassInfo classInfo, String fieldName, int access, String fieldType, String signature, Object value)
+ {
+ super();
+ _classInfo = classInfo;
+ _fieldName = fieldName;
+ _access = access;
+ _fieldType = fieldType;
+ _signature = signature;
+ _value = value;
+ }
+
+ public ClassInfo getClassInfo()
+ {
+ return _classInfo;
+ }
+
+ public String getFieldName()
+ {
+ return _fieldName;
+ }
+
+ public int getAccess()
+ {
+ return _access;
+ }
+
+ public String getFieldType()
+ {
+ return _fieldType;
+ }
+
+ public String getSignature()
+ {
+ return _signature;
+ }
+
+ public Object getValue()
+ {
+ return _value;
+ }
+ }
+
+ /**
+ * Signature for all handlers that respond to parsing class files.
+ */
+ public interface Handler
+ {
+ void handle(ClassInfo classInfo);
+
+ void handle(MethodInfo methodInfo);
+
+ void handle(FieldInfo fieldInfo);
+
+ void handle(ClassInfo info, String annotationName);
+
+ void handle(MethodInfo info, String annotationName);
+
+ void handle(FieldInfo info, String annotationName);
+ }
+
+ /**
+ * Convenience base class to provide no-ops for all Handler methods.
+ */
+ public abstract static class AbstractHandler implements Handler
+ {
+ @Override
+ public void handle(ClassInfo classInfo)
+ {
+ }
+
+ @Override
+ public void handle(MethodInfo methodInfo)
+ {
+ }
+
+ @Override
+ public void handle(FieldInfo fieldInfo)
+ {
+ }
+
+ @Override
+ public void handle(ClassInfo info, String annotationName)
+ {
+ }
+
+ @Override
+ public void handle(MethodInfo info, String annotationName)
+ {
+ }
+
+ @Override
+ public void handle(FieldInfo info, String annotationName)
+ {
+ }
+ }
+
+ /**
+ * ASM Visitor for parsing a method. We are only interested in the annotations on methods.
+ */
+ public static class MyMethodVisitor extends MethodVisitor
+ {
+ final MethodInfo _mi;
+ final Set extends Handler> _handlers;
+
+ public MyMethodVisitor(final Set extends Handler> handlers,
+ final ClassInfo classInfo,
+ final int access,
+ final String name,
+ final String methodDesc,
+ final String signature,
+ final String[] exceptions,
+ final int asmVersion)
+ {
+ super(asmVersion);
+ _handlers = handlers;
+ _mi = new MethodInfo(classInfo, name, access, methodDesc, signature, exceptions);
+ }
+
+ /**
+ * We are only interested in finding the annotations on methods.
+ */
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible)
+ {
+ String annotationName = normalize(desc);
+ for (Handler h : _handlers)
+ {
+ h.handle(_mi, annotationName);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * An ASM visitor for parsing Fields.
+ * We are only interested in visiting annotations on Fields.
+ */
+ public static class MyFieldVisitor extends FieldVisitor
+ {
+ final FieldInfo _fieldInfo;
+ final Set extends Handler> _handlers;
+
+ public MyFieldVisitor(final Set extends Handler> handlers,
+ final ClassInfo classInfo,
+ final int access,
+ final String fieldName,
+ final String fieldType,
+ final String signature,
+ final Object value,
+ final int asmVersion)
+ {
+ super(asmVersion);
+ _handlers = handlers;
+ _fieldInfo = new FieldInfo(classInfo, fieldName, access, fieldType, signature, value);
+ }
+
+ /**
+ * Parse an annotation found on a Field.
+ */
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible)
+ {
+ String annotationName = normalize(desc);
+ for (Handler h : _handlers)
+ {
+ h.handle(_fieldInfo, annotationName);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * ASM visitor for a class.
+ */
+ public static class MyClassVisitor extends ClassVisitor
+ {
+ final int _asmVersion;
+ final Resource _containingResource;
+ final Set extends Handler> _handlers;
+ ClassInfo _ci;
+
+ public MyClassVisitor(Set extends Handler> handlers, Resource containingResource, int asmVersion)
+ {
+ super(asmVersion);
+ _asmVersion = asmVersion;
+ _handlers = handlers;
+ _containingResource = containingResource;
+ }
+
+ @Override
+ public void visit(final int version,
+ final int access,
+ final String name,
+ final String signature,
+ final String superName,
+ final String[] interfaces)
+ {
+ _ci = new ClassInfo(_containingResource, normalize(name), version, access, signature, normalize(superName), normalize(interfaces));
+ for (Handler h : _handlers)
+ {
+ h.handle(_ci);
+ }
+ }
+
+ /**
+ * Visit an annotation on a Class
+ */
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible)
+ {
+ String annotationName = normalize(desc);
+ for (Handler h : _handlers)
+ {
+ h.handle(_ci, annotationName);
+ }
+ return null;
+ }
+
+ /**
+ * Visit a method to extract its annotations
+ */
+ @Override
+ public MethodVisitor visitMethod(final int access,
+ final String name,
+ final String methodDesc,
+ final String signature,
+ final String[] exceptions)
+ {
+ return new MyMethodVisitor(_handlers, _ci, access, name, methodDesc, signature, exceptions, _asmVersion);
+ }
+
+ /**
+ * Visit a field to extract its annotations
+ */
+ @Override
+ public FieldVisitor visitField(final int access,
+ final String fieldName,
+ final String fieldType,
+ final String signature,
+ final Object value)
+ {
+ return new MyFieldVisitor(_handlers, _ci, access, fieldName, fieldType, signature, value, _asmVersion);
+ }
+ }
+
+ public AnnotationParser()
+ {
+ this(ASM_VERSION);
+ }
+
+ /**
+ * @param asmVersion The target asm version or 0 for the internal version.
+ */
+ public AnnotationParser(int asmVersion)
+ {
+ if (asmVersion == 0)
+ asmVersion = ASM_VERSION;
+ _asmVersion = asmVersion;
+ }
+
+ /**
+ * Parse a resource
+ *
+ * @param handlers the handlers to look for classes in
+ * @param r the resource to parse
+ * @throws Exception if unable to parse
+ */
+ public void parse(final Set extends Handler> handlers, Resource r) throws Exception
+ {
+ if (r == null)
+ return;
+
+ if (!r.exists())
+ return;
+
+ if (FileID.isJavaArchive(r.getPath()))
+ {
+ parseJar(handlers, r);
+ return;
+ }
+
+ if (r.isDirectory())
+ {
+ parseDir(handlers, r);
+ return;
+ }
+
+ if (FileID.isClassFile(r.getPath()))
+ {
+ parseClass(handlers, null, r.getPath());
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.warn("Resource not able to be scanned for classes: {}", r);
+ }
+
+ /**
+ * Parse all classes in a directory
+ *
+ * @param handlers the set of handlers to look for classes in
+ * @param dirResource the resource representing the baseResource being scanned (jar, dir, etc)
+ * @throws Exception if unable to parse
+ */
+ protected void parseDir(Set extends Handler> handlers, Resource dirResource) throws Exception
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Scanning dir {}", dirResource);
+
+ assert dirResource.isDirectory();
+
+ ExceptionUtil.MultiException multiException = new ExceptionUtil.MultiException();
+
+ for (Resource candidate : dirResource.getAllResources())
+ {
+ // Skip directories
+ if (candidate.isDirectory())
+ continue;
+
+ // Get the path relative to the base resource
+ Path relative = dirResource.getPathTo(candidate);
+
+ // select only relative non-hidden class files that are not modules nor versions
+ if (relative == null ||
+ FileID.isHidden(relative) ||
+ FileID.isMetaInfVersions(relative) ||
+ FileID.isModuleInfoClass(relative) ||
+ !FileID.isClassFile(relative))
+ continue;
+
+ try
+ {
+ parseClass(handlers, dirResource, candidate.getPath());
+ }
+ catch (Exception ex)
+ {
+ multiException.add(new RuntimeException("Error scanning entry " + ex, ex));
+ }
+ }
+
+ multiException.ifExceptionThrow();
+ }
+
+ /**
+ * Parse a resource that is a jar file.
+ *
+ * @param handlers the handlers to look for classes in
+ * @param jarResource the jar resource to parse
+ * @throws Exception if unable to parse
+ */
+ protected void parseJar(Set extends Handler> handlers, Resource jarResource) throws Exception
+ {
+ if (jarResource == null)
+ return;
+
+ /* if (!FileID.isJavaArchive(jarResource.getPath()))
+ return;*/
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Scanning jar {}", jarResource);
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ Resource insideJarResource = resourceFactory.newJarFileResource(jarResource.getURI());
+ parseDir(handlers, insideJarResource);
+ }
+ }
+
+ /**
+ * Use ASM on a class
+ *
+ * @param handlers the handlers to look for classes in
+ * @param containingResource the dir or jar that the class is contained within, can be null if not known
+ * @param classFile the class file to parse
+ * @throws IOException if unable to parse
+ */
+ protected void parseClass(Set extends Handler> handlers, Resource containingResource, Path classFile) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Parse class from {}", classFile.toUri());
+
+ URI location = classFile.toUri();
+
+ try (InputStream in = Files.newInputStream(classFile))
+ {
+ ClassReader reader = new ClassReader(in);
+ reader.accept(new MyClassVisitor(handlers, containingResource, _asmVersion), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+
+ String classname = normalize(reader.getClassName());
+ URI existing = _parsedClassNames.putIfAbsent(classname, location);
+ if (existing != null)
+ LOG.warn("{} scanned from multiple locations: {}, {}", classname, existing, location);
+ }
+ catch (IllegalArgumentException | IOException e)
+ {
+ throw new IOException("Unable to parse class: " + classFile.toUri(), e);
+ }
+ }
+
+ /**
+ * Useful mostly for testing to expose the list of parsed classes.
+ * @return the map of classnames to their URIs
+ */
+ Map getParsedClassNames()
+ {
+ return Collections.unmodifiableMap(_parsedClassNames);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ClassInheritanceHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ClassInheritanceHandler.java
new file mode 100644
index 00000000000..bdff5b3ca9f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ClassInheritanceHandler.java
@@ -0,0 +1,79 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * As asm scans for classes, remember the type hierarchy.
+ */
+public class ClassInheritanceHandler extends AnnotationParser.AbstractHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ClassInheritanceHandler.class);
+
+ Map> _inheritanceMap;
+
+ public ClassInheritanceHandler(Map> map)
+ {
+ _inheritanceMap = map;
+ }
+
+ @Override
+ public void handle(AnnotationParser.ClassInfo classInfo)
+ {
+ try
+ {
+ //Don't scan Object
+ if ("java.lang.Object".equals(classInfo.getClassName()))
+ return;
+
+ for (int i = 0; classInfo.getInterfaces() != null && i < classInfo.getInterfaces().length; i++)
+ {
+ addToInheritanceMap(classInfo.getInterfaces()[i], classInfo.getClassName());
+ }
+ //To save memory, we don't record classes that only extend Object, as that can be assumed
+ if (!"java.lang.Object".equals(classInfo.getSuperName()))
+ {
+ addToInheritanceMap(classInfo.getSuperName(), classInfo.getClassName());
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Failed to handle {}", classInfo, e);
+ }
+ }
+
+ private void addToInheritanceMap(String interfaceOrSuperClassName, String implementingOrExtendingClassName)
+ {
+
+ //As it is likely that the interfaceOrSuperClassName is already in the map, try getting it first
+ Set implementingClasses = _inheritanceMap.get(interfaceOrSuperClassName);
+ //If it isn't in the map, then add it in, but test to make sure that someone else didn't get in
+ //first and add it
+ if (implementingClasses == null)
+ {
+ implementingClasses = ConcurrentHashMap.newKeySet();
+ Set tmp = _inheritanceMap.putIfAbsent(interfaceOrSuperClassName, implementingClasses);
+ if (tmp != null)
+ implementingClasses = tmp;
+ }
+
+ implementingClasses.add(implementingOrExtendingClassName);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ContainerInitializerAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ContainerInitializerAnnotationHandler.java
new file mode 100644
index 00000000000..7c8ba9e2c1a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ContainerInitializerAnnotationHandler.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.Objects;
+
+import org.eclipse.jetty.ee11.servlet.ServletContainerInitializerHolder;
+
+/**
+ * Discovers classes that contain the specified annotation, either at class or
+ * method level. The specified annotation is derived from an @HandlesTypes on
+ * a ServletContainerInitializer class.
+ */
+public class ContainerInitializerAnnotationHandler extends AnnotationParser.AbstractHandler
+{
+ final ServletContainerInitializerHolder _holder;
+ final Class> _annotation;
+
+ public ContainerInitializerAnnotationHandler(ServletContainerInitializerHolder holder, Class> annotation)
+ {
+ _holder = Objects.requireNonNull(holder);
+ _annotation = Objects.requireNonNull(annotation);
+ }
+
+ /**
+ * Handle finding a class that is annotated with the annotation we were constructed with.
+ *
+ * @see AnnotationParser.Handler#handle(AnnotationParser.ClassInfo, String)
+ */
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotationName)
+ {
+ if (!_annotation.getName().equals(annotationName))
+ return;
+
+ _holder.addStartupClasses(info.getClassName());
+ }
+
+ /**
+ * Handle finding a field that is annotated with the annotation we were constructed with.
+ *
+ * @see AnnotationParser.Handler#handle(AnnotationParser.FieldInfo, String)
+ */
+ @Override
+ public void handle(AnnotationParser.FieldInfo info, String annotationName)
+ {
+ if (!_annotation.getName().equals(annotationName))
+ return;
+
+ _holder.addStartupClasses(info.getClassInfo().getClassName());
+ }
+
+ /**
+ * Handle finding a method that is annotated with the annotation we were constructed with.
+ *
+ * @see AnnotationParser.Handler#handle(AnnotationParser.MethodInfo, String)
+ */
+ @Override
+ public void handle(AnnotationParser.MethodInfo info, String annotationName)
+ {
+ if (!_annotation.getName().equals(annotationName))
+ return;
+ _holder.addStartupClasses(info.getClassInfo().getClassName());
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/DeclareRolesAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/DeclareRolesAnnotationHandler.java
new file mode 100644
index 00000000000..bed1bc14dd0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/DeclareRolesAnnotationHandler.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import jakarta.annotation.security.DeclareRoles;
+import jakarta.servlet.Servlet;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintAware;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DeclaresRolesAnnotationHandler
+ */
+public class DeclareRolesAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(DeclareRolesAnnotationHandler.class);
+
+ public DeclareRolesAnnotationHandler(WebAppContext context)
+ {
+ super(false, context);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ if (!Servlet.class.isAssignableFrom(clazz))
+ return; //only applicable on jakarta.servlet.Servlet derivatives
+
+ if (!(_context.getSecurityHandler() instanceof ConstraintAware))
+ {
+ LOG.warn("SecurityHandler not ConstraintAware, skipping security annotation processing");
+ return;
+ }
+
+ DeclareRoles declareRoles = clazz.getAnnotation(DeclareRoles.class);
+ if (declareRoles == null)
+ return;
+
+ String[] roles = declareRoles.value();
+ if (roles != null)
+ {
+ for (String r : roles)
+ {
+ ((ConstraintSecurityHandler)_context.getSecurityHandler()).addKnownRole(r);
+ _context.getMetaData().setOrigin("security-role." + r, declareRoles, clazz);
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/MultiPartConfigAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/MultiPartConfigAnnotationHandler.java
new file mode 100644
index 00000000000..45007b8d763
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/MultiPartConfigAnnotationHandler.java
@@ -0,0 +1,64 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import jakarta.servlet.MultipartConfigElement;
+import jakarta.servlet.Servlet;
+import jakarta.servlet.annotation.MultipartConfig;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.webapp.Descriptor;
+import org.eclipse.jetty.ee11.webapp.MetaData;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+
+/**
+ * MultiPartConfigAnnotationHandler
+ */
+public class MultiPartConfigAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ public MultiPartConfigAnnotationHandler(WebAppContext context)
+ {
+ //TODO verify that MultipartConfig is not inheritable
+ super(false, context);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ if (!Servlet.class.isAssignableFrom(clazz))
+ return;
+
+ MultipartConfig multi = clazz.getAnnotation(MultipartConfig.class);
+ if (multi == null)
+ return;
+
+ MetaData metaData = _context.getMetaData();
+
+ //TODO: The MultipartConfigElement needs to be set on the ServletHolder's Registration.
+ //How to identify the correct Servlet? If the Servlet has no WebServlet annotation on it, does it mean that this MultipartConfig
+ //annotation applies to all declared instances in web.xml/programmatically?
+ //Assuming TRUE for now.
+ for (ServletHolder holder : _context.getServletHandler().getServlets(clazz))
+ {
+ Descriptor d = metaData.getOriginDescriptor(holder.getName() + ".servlet.multipart-config");
+ //if a descriptor has already set the value for multipart config, do not
+ //let the annotation override it
+ if (d == null)
+ {
+ metaData.setOrigin(holder.getName() + ".servlet.multipart-config", multi, clazz);
+ holder.getRegistration().setMultipartConfig(new MultipartConfigElement(multi));
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/PostConstructAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/PostConstructAnnotationHandler.java
new file mode 100644
index 00000000000..fdd6b5f07f5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/PostConstructAnnotationHandler.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import jakarta.annotation.PostConstruct;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.webapp.Origin;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.plus.annotation.LifeCycleCallbackCollection;
+import org.eclipse.jetty.plus.annotation.PostConstructCallback;
+
+public class PostConstructAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ public PostConstructAnnotationHandler(WebAppContext wac)
+ {
+ super(true, wac);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ //Check that the PostConstruct is on a class that we're interested in
+ if (supportsPostConstruct(clazz))
+ {
+ Method[] methods = clazz.getDeclaredMethods();
+ for (Method method : methods)
+ {
+ if (method.isAnnotationPresent(PostConstruct.class))
+ {
+ if (method.getParameterCount() != 0)
+ throw new IllegalStateException(method + " has parameters");
+ if (method.getReturnType() != Void.TYPE)
+ throw new IllegalStateException(method + " is not void");
+ if (method.getExceptionTypes().length != 0)
+ throw new IllegalStateException(method + " throws checked exceptions");
+ if (Modifier.isStatic(method.getModifiers()))
+ throw new IllegalStateException(method + " is static");
+
+ //ServletSpec 3.0 p80 If web.xml declares even one post-construct then all post-constructs
+ //in fragments must be ignored. Otherwise, they are additive.
+ Origin origin = _context.getMetaData().getOrigin("post-construct");
+ if ((origin == Origin.WebXml ||
+ origin == Origin.WebDefaults ||
+ origin == Origin.WebOverride))
+ return;
+
+ PostConstructCallback callback = new PostConstructCallback(clazz, method.getName());
+ LifeCycleCallbackCollection lifecycles = (LifeCycleCallbackCollection)_context.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+ if (lifecycles == null)
+ {
+ lifecycles = new LifeCycleCallbackCollection();
+ _context.setAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION, lifecycles);
+ }
+ lifecycles.add(callback);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if the given class is permitted to have PostConstruct annotation.
+ *
+ * @param c the class
+ * @return true if the spec permits the class to have PostConstruct, false otherwise
+ */
+ public boolean supportsPostConstruct(Class> c)
+ {
+ return isAnnotatableServletClass(c);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/PreDestroyAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/PreDestroyAnnotationHandler.java
new file mode 100644
index 00000000000..6fa60e20228
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/PreDestroyAnnotationHandler.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import jakarta.annotation.PreDestroy;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.webapp.Origin;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.plus.annotation.LifeCycleCallbackCollection;
+import org.eclipse.jetty.plus.annotation.PreDestroyCallback;
+
+public class PreDestroyAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ public PreDestroyAnnotationHandler(WebAppContext wac)
+ {
+ super(true, wac);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ //Check that the PreDestroy is on a class that we're interested in
+ if (supportsPreDestroy(clazz))
+ {
+ Method[] methods = clazz.getDeclaredMethods();
+ for (Method method : methods)
+ {
+ if (method.isAnnotationPresent(PreDestroy.class))
+ {
+ if (method.getParameterCount() != 0)
+ throw new IllegalStateException(method + " has parameters");
+ if (method.getReturnType() != Void.TYPE)
+ throw new IllegalStateException(method + " is not void");
+ if (method.getExceptionTypes().length != 0)
+ throw new IllegalStateException(method + " throws checked exceptions");
+ if (Modifier.isStatic(method.getModifiers()))
+ throw new IllegalStateException(method + " is static");
+
+ //ServletSpec 3.0 p80 If web.xml declares even one predestroy then all predestroys
+ //in fragments must be ignored. Otherwise, they are additive.
+ Origin origin = _context.getMetaData().getOrigin("pre-destroy");
+ if ((origin == Origin.WebXml ||
+ origin == Origin.WebDefaults ||
+ origin == Origin.WebOverride))
+ return;
+
+ PreDestroyCallback callback = new PreDestroyCallback(clazz, method.getName());
+
+ LifeCycleCallbackCollection lifecycles = (LifeCycleCallbackCollection)_context.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+ if (lifecycles == null)
+ {
+ lifecycles = new LifeCycleCallbackCollection();
+ _context.setAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION, lifecycles);
+ }
+
+ lifecycles.add(callback);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if the spec permits the given class to use the PreDestroy annotation.
+ *
+ * @param c the class
+ * @return true if permitted, false otherwise
+ */
+ public boolean supportsPreDestroy(Class> c)
+ {
+ return isAnnotatableServletClass(c);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ResourceAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ResourceAnnotationHandler.java
new file mode 100644
index 00000000000..bbecf31461f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ResourceAnnotationHandler.java
@@ -0,0 +1,401 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.Locale;
+import javax.naming.InitialContext;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+
+import jakarta.annotation.Resource;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.webapp.MetaData;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.plus.annotation.Injection;
+import org.eclipse.jetty.plus.annotation.InjectionCollection;
+import org.eclipse.jetty.plus.jndi.NamingEntryUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ResourceAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ResourceAnnotationHandler.class);
+
+ protected static final List> ENV_ENTRY_TYPES = List.of(
+ String.class,
+ Character.class,
+ Integer.class,
+ Boolean.class,
+ Double.class,
+ Byte.class,
+ Short.class,
+ Long.class,
+ Float.class
+ );
+
+ public ResourceAnnotationHandler(WebAppContext wac)
+ {
+ super(true, wac);
+ }
+
+ /**
+ * Class level Resource annotations declare a name in the
+ * environment that will be looked up at runtime. They do
+ * not specify an injection.
+ */
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ if (supportsResourceInjection(clazz))
+ {
+ handleClass(clazz);
+
+ Method[] methods = clazz.getDeclaredMethods();
+ for (Method method : methods)
+ {
+ handleMethod(clazz, method);
+ }
+ Field[] fields = clazz.getDeclaredFields();
+ //For each field, get all of it's annotations
+ for (Field field : fields)
+ {
+ handleField(clazz, field);
+ }
+ }
+ }
+
+ public void handleClass(Class> clazz)
+ {
+ Resource resource = clazz.getAnnotation(Resource.class);
+ if (resource != null)
+ {
+ String name = resource.name();
+ String mappedName = resource.mappedName();
+
+ if (name == null || name.trim().isEmpty())
+ throw new IllegalStateException("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");
+
+ try
+ {
+ if (!NamingEntryUtil.bindToENC(_context, name, mappedName))
+ if (!NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName))
+ throw new IllegalStateException("No resource at " + (mappedName == null ? name : mappedName));
+ }
+ catch (NamingException e)
+ {
+ LOG.warn("Unable to bind name {} to {} from class {}", name, mappedName, clazz, e);
+ }
+ }
+ }
+
+ public void handleField(Class> clazz, Field field)
+ {
+ Resource resource = field.getAnnotation(Resource.class);
+ if (resource != null)
+ {
+ //JavaEE Spec 5.2.3: Field cannot be static
+ if (Modifier.isStatic(field.getModifiers()))
+ {
+ LOG.warn("Skipping Resource annotation on {}.{}: cannot be static", clazz.getName(), field.getName());
+ return;
+ }
+
+ //JavaEE Spec 5.2.3: Field cannot be final
+ if (Modifier.isFinal(field.getModifiers()))
+ {
+ LOG.warn("Skipping Resource annotation on {}.{}: cannot be final", clazz.getName(), field.getName());
+ return;
+ }
+
+ //work out default name
+ String name = clazz.getName() + "/" + field.getName();
+
+ //allow @Resource name= to override the field name
+ name = (resource.name() != null && !resource.name().trim().isEmpty() ? resource.name() : name);
+ String mappedName = (resource.mappedName() != null && !resource.mappedName().trim().isEmpty() ? resource.mappedName() : null);
+ //get the type of the Field
+ Class> type = field.getType();
+
+ //Servlet Spec 3.0 p. 76
+ //If a descriptor has specified at least 1 injection target for this
+ //resource, then it overrides this annotation
+ MetaData metaData = _context.getMetaData();
+ if (metaData.getOriginDescriptor("resource-ref." + name + ".injection") != null)
+ {
+ //at least 1 injection was specified for this resource by a descriptor, so
+ //it overrides this annotation
+ return;
+ }
+
+ //No injections for this resource in any descriptors, so we can add it
+ //Does the injection already exist?
+ InjectionCollection injections = (InjectionCollection)_context.getAttribute(InjectionCollection.INJECTION_COLLECTION);
+ if (injections == null)
+ {
+ injections = new InjectionCollection();
+ _context.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
+ }
+ Injection injection = injections.getInjection(name, clazz, field);
+ if (injection == null)
+ {
+ //No injection has been specified, add it
+ try
+ {
+ //try webapp scope first
+ boolean bound = NamingEntryUtil.bindToENC(_context, name, mappedName);
+
+ //try environment scope next
+ if (!bound)
+ bound = NamingEntryUtil.bindToENC(ServletContextHandler.ENVIRONMENT.getName(), name, mappedName);
+
+ //try Server scope next
+ if (!bound)
+ bound = NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName);
+
+ //try jvm scope next
+ if (!bound)
+ bound = NamingEntryUtil.bindToENC(null, name, mappedName);
+ if (!bound)
+ {
+ //see if there is an env-entry value been bound
+ try
+ {
+ InitialContext ic = new InitialContext();
+ String nameInEnvironment = (mappedName != null ? mappedName : name);
+ ic.lookup("java:comp/env/" + nameInEnvironment);
+ bound = true;
+ }
+ catch (NameNotFoundException e)
+ {
+ if (LOG.isTraceEnabled())
+ LOG.trace("ignored", e);
+ }
+ }
+ //Check there is a JNDI entry for this annotation
+ if (bound)
+ {
+ LOG.debug("Bound {} as {}", (mappedName == null ? name : mappedName), name);
+ // Make the Injection for it if the binding succeeded
+ injection = new Injection(clazz, field, type, name, mappedName);
+ injections.add(injection);
+
+ //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination
+ metaData.setOrigin("resource-ref." + name + ".injection", resource, clazz);
+ }
+ else if (!isEnvEntryType(type))
+ {
+ //if this is an env-entry type resource and there is no value bound for it, it isn't
+ //an error, it just means that perhaps the code will use a default value instead
+ // JavaEE Spec. sec 5.4.1.3
+
+ throw new IllegalStateException("No resource at " + (mappedName == null ? name : mappedName));
+ }
+ }
+ catch (NamingException e)
+ {
+ //if this is an env-entry type resource and there is no value bound for it, it isn't
+ //an error, it just means that perhaps the code will use a default value instead
+ // JavaEE Spec. sec 5.4.1.3
+ if (!isEnvEntryType(type))
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Process a Resource annotation on a Method.
+ *
+ * This will generate a JNDI entry, and an Injection to be
+ * processed when an instance of the class is created.
+ *
+ * @param clazz the class to process
+ * @param method the method to process
+ */
+ public void handleMethod(Class> clazz, Method method)
+ {
+
+ Resource resource = method.getAnnotation(Resource.class);
+ if (resource != null)
+ {
+ /*
+ * Commons Annotations Spec 2.3
+ * " The Resource annotation is used to declare a reference to a resource.
+ * It can be specified on a class, methods or on fields. When the
+ * annotation is applied on a field or method, the container will
+ * inject an instance of the requested resource into the application
+ * when the application is initialized... Even though this annotation
+ * is not marked Inherited, if used all superclasses MUST be examined
+ * to discover all uses of this annotation. All such annotation instances
+ * specify resources that are needed by the application. Note that this
+ * annotation may appear on private fields and methods of the superclasses.
+ * Injection of the declared resources needs to happen in these cases as
+ * well, even if a method with such an annotation is overridden by a subclass."
+ *
+ * Which IMHO, put more succinctly means "If you find a @Resource on any method
+ * or field, inject it!".
+ */
+ //JavaEE Spec 5.2.3: Method cannot be static
+ if (Modifier.isStatic(method.getModifiers()))
+ {
+ LOG.warn("Skipping Resource annotation on {}.{}: cannot be static", clazz.getName(), method.getName());
+ return;
+ }
+
+ // Check it is a valid javabean: must be void return type, the name must start with "set" and it must have
+ // only 1 parameter
+ if (!method.getName().startsWith("set"))
+ {
+ LOG.warn("Skipping Resource annotation on {}.{}: invalid java bean, does not start with 'set'", clazz.getName(), method.getName());
+ return;
+ }
+
+ if (method.getParameterCount() != 1)
+ {
+ LOG.warn("Skipping Resource annotation on {}.{}: invalid java bean, not single argument to method", clazz.getName(), method.getName());
+ return;
+ }
+
+ if (Void.TYPE != method.getReturnType())
+ {
+ LOG.warn("Skipping Resource annotation on {}.{}: invalid java bean, not void", clazz.getName(), method.getName());
+ return;
+ }
+
+ //default name is the javabean property name
+ String name = method.getName().substring(3);
+ name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
+ name = clazz.getName() + "/" + name;
+
+ name = (resource.name() != null && !resource.name().trim().isEmpty() ? resource.name() : name);
+ String mappedName = (resource.mappedName() != null && !resource.mappedName().trim().isEmpty() ? resource.mappedName() : null);
+ Class> paramType = method.getParameterTypes()[0];
+
+ Class> resourceType = resource.type();
+
+ //Servlet Spec 3.0 p. 76
+ //If a descriptor has specified at least 1 injection target for this
+ //resource, then it overrides this annotation
+ MetaData metaData = _context.getMetaData();
+ if (metaData.getOriginDescriptor("resource-ref." + name + ".injection") != null)
+ {
+ //at least 1 injection was specified for this resource by a descriptor, so
+ //it overrides this annotation
+ return;
+ }
+
+ //check if an injection has already been setup for this target by web.xml
+ InjectionCollection injections = (InjectionCollection)_context.getAttribute(InjectionCollection.INJECTION_COLLECTION);
+ if (injections == null)
+ {
+ injections = new InjectionCollection();
+ _context.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
+ }
+ Injection injection = injections.getInjection(name, clazz, method, paramType);
+ if (injection == null)
+ {
+ try
+ {
+ //try binding name to environment
+ //try the webapp's scope first
+ boolean bound = NamingEntryUtil.bindToENC(_context, name, mappedName);
+
+ //try the environment's scope
+ if (!bound)
+ bound = NamingEntryUtil.bindToENC(ServletContextHandler.ENVIRONMENT.getName(), name, mappedName);
+
+ //try the server's scope
+ if (!bound)
+ bound = NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName);
+
+ //try the jvm's scope
+ if (!bound)
+ bound = NamingEntryUtil.bindToENC(null, name, mappedName);
+
+ //TODO if it is an env-entry from web.xml it can be injected, in which case there will be no
+ //NamingEntry, just a value bound in java:comp/env
+ if (!bound)
+ {
+ try
+ {
+ InitialContext ic = new InitialContext();
+ String nameInEnvironment = (mappedName != null ? mappedName : name);
+ ic.lookup("java:comp/env/" + nameInEnvironment);
+ bound = true;
+ }
+ catch (NameNotFoundException e)
+ {
+ if (LOG.isTraceEnabled())
+ LOG.trace("ignored", e);
+ }
+ }
+
+ if (bound)
+ {
+ LOG.debug("Bound {} as {}", (mappedName == null ? name : mappedName), name);
+ // Make the Injection for it
+ injection = new Injection(clazz, method, paramType, resourceType, name, mappedName);
+ injections.add(injection);
+ //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination
+ metaData.setOrigin("resource-ref." + name + ".injection", resource, clazz);
+ }
+ else if (!isEnvEntryType(paramType))
+ {
+
+ //if this is an env-entry type resource and there is no value bound for it, it isn't
+ //an error, it just means that perhaps the code will use a default value instead
+ // JavaEE Spec. sec 5.4.1.3
+ throw new IllegalStateException("No resource at " + (mappedName == null ? name : mappedName));
+ }
+ }
+ catch (NamingException e)
+ {
+ //if this is an env-entry type resource and there is no value bound for it, it isn't
+ //an error, it just means that perhaps the code will use a default value instead
+ // JavaEE Spec. sec 5.4.1.3
+ if (!isEnvEntryType(paramType))
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if the given Class is one that the specification allows to have a Resource annotation.
+ *
+ * @param c the class
+ * @return true if Resource annotation permitted, false otherwise
+ */
+ public boolean supportsResourceInjection(Class> c)
+ {
+ return isAnnotatableServletClass(c);
+ }
+
+ /**
+ * Check if the class is one of the basic java types permitted as
+ * env-entries.
+ *
+ * @param clazz the class to check
+ * @return true if class is permitted by the spec to be an env-entry value
+ */
+ public boolean isEnvEntryType(Class> clazz)
+ {
+ return ENV_ENTRY_TYPES.contains(clazz);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ResourcesAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ResourcesAnnotationHandler.java
new file mode 100644
index 00000000000..d00ab838046
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ResourcesAnnotationHandler.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import javax.naming.NamingException;
+
+import jakarta.annotation.Resource;
+import jakarta.annotation.Resources;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.plus.jndi.NamingEntryUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ResourcesAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ResourcesAnnotationHandler.class);
+
+ public ResourcesAnnotationHandler(WebAppContext wac)
+ {
+ super(true, wac);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ Resources resources = clazz.getAnnotation(Resources.class);
+ if (resources != null)
+ {
+ Resource[] resArray = resources.value();
+ if (resArray == null || resArray.length == 0)
+ {
+ LOG.warn("Skipping empty or incorrect Resources annotation on {}", clazz.getName());
+ return;
+ }
+
+ for (Resource resource : resArray)
+ {
+ String name = resource.name();
+ String mappedName = resource.mappedName();
+
+ if (name == null || name.trim().isEmpty())
+ throw new IllegalStateException("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");
+
+ try
+ {
+ //TODO don't ignore the shareable, auth etc etc
+
+ if (!NamingEntryUtil.bindToENC(_context, name, mappedName))
+ if (!NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName))
+ LOG.warn("Skipping Resources(Resource) annotation on {} for name {}: no resource bound at {}",
+ clazz.getName(), name, (mappedName == null ? name : mappedName));
+ }
+ catch (NamingException e)
+ {
+ LOG.warn("Unable to bind {} to {}", name, mappedName, e);
+ }
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/RunAsAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/RunAsAnnotationHandler.java
new file mode 100644
index 00000000000..adbe3bf1769
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/RunAsAnnotationHandler.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import jakarta.servlet.Servlet;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.webapp.Descriptor;
+import org.eclipse.jetty.ee11.webapp.MetaData;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RunAsAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(RunAsAnnotationHandler.class);
+
+ public RunAsAnnotationHandler(WebAppContext wac)
+ {
+ //Introspect only the given class for a RunAs annotation, as it is a class level annotation,
+ //and according to Common Annotation Spec p2-6 a class-level annotation is not inheritable.
+ super(false, wac);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ if (!Servlet.class.isAssignableFrom(clazz))
+ return;
+
+ jakarta.annotation.security.RunAs runAs = clazz.getAnnotation(jakarta.annotation.security.RunAs.class);
+ if (runAs != null)
+ {
+ String role = runAs.value();
+ if (role != null)
+ {
+ for (ServletHolder holder : _context.getServletHandler().getServlets(clazz))
+ {
+ MetaData metaData = _context.getMetaData();
+ Descriptor d = metaData.getOriginDescriptor(holder.getName() + ".servlet.run-as");
+ //if a descriptor has already set the value for run-as, do not
+ //let the annotation override it
+ if (d == null)
+ {
+ metaData.setOrigin(holder.getName() + ".servlet.run-as", runAs, clazz);
+ holder.setRunAsRole(role);
+ }
+ }
+ }
+ else
+ LOG.warn("Bad value for @RunAs annotation on class {}", clazz.getName());
+ }
+ }
+
+ public void handleField(String className, String fieldName, int access, String fieldType, String signature, Object value, String annotation)
+ {
+ LOG.warn("@RunAs annotation not applicable for fields: {}.{}", className, fieldName);
+ }
+
+ public void handleMethod(String className, String methodName, int access, String params, String signature, String[] exceptions, String annotation)
+ {
+ LOG.warn("@RunAs annotation ignored on method: {}.{} {}", className, methodName, signature);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ServletSecurityAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ServletSecurityAnnotationHandler.java
new file mode 100644
index 00000000000..cae390bed95
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/ServletSecurityAnnotationHandler.java
@@ -0,0 +1,159 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.servlet.ServletSecurityElement;
+import jakarta.servlet.annotation.ServletSecurity;
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.ServletMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintAware;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
Inspect a class to see if it has an @ServletSecurity annotation on it,
+ * setting up the <security-constraint>s.
+ *
+ *
A servlet can be defined in:
+ *
+ *
web.xml
+ *
web-fragment.xml
+ *
@WebServlet annotation discovered
+ *
ServletContext.createServlet
+ *
+ *
+ * The ServletSecurity annotation for a servlet should only be processed
+ * iff metadata-complete == false.
+ */
+public class ServletSecurityAnnotationHandler extends AbstractIntrospectableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ServletSecurityAnnotationHandler.class);
+
+ public ServletSecurityAnnotationHandler(WebAppContext wac)
+ {
+ super(false, wac);
+ }
+
+ @Override
+ public void doHandle(Class> clazz)
+ {
+ if (!(_context.getSecurityHandler() instanceof ConstraintAware securityHandler))
+ {
+ LOG.warn("SecurityHandler not ConstraintAware, skipping security annotation processing");
+ return;
+ }
+
+ ServletSecurity servletSecurity = clazz.getAnnotation(ServletSecurity.class);
+ if (servletSecurity == null)
+ return;
+
+ //If there are already constraints defined (ie from web.xml) that match any
+ //of the url patterns defined for this servlet, then skip the security annotation.
+
+ List servletMappings = getServletMappings(clazz.getCanonicalName());
+ List constraintMappings = ((ConstraintAware)_context.getSecurityHandler()).getConstraintMappings();
+
+ if (constraintsExist(servletMappings, constraintMappings))
+ {
+ LOG.warn("Constraints already defined for {}, skipping ServletSecurity annotation", clazz.getName());
+ return;
+ }
+
+ //Make a fresh list
+ constraintMappings = new ArrayList<>();
+
+ ServletSecurityElement securityElement = new ServletSecurityElement(servletSecurity);
+ for (ServletMapping sm : servletMappings)
+ {
+ for (String url : sm.getPathSpecs())
+ {
+ _context.getMetaData().setOrigin("constraint.url." + url, servletSecurity, clazz);
+ constraintMappings.addAll(ConstraintSecurityHandler.createConstraintsWithMappingsForPath(clazz.getName(), url, securityElement));
+ }
+ }
+
+ //set up the security constraints produced by the annotation
+ constraintMappings.forEach(securityHandler::addConstraintMapping);
+
+ //Servlet Spec 3.1 requires paths with uncovered http methods to be reported
+ securityHandler.checkPathsWithUncoveredHttpMethods();
+ }
+
+ /**
+ * Get the ServletMappings for the servlet's class.
+ *
+ * @param className the class name
+ * @return the servlet mappings for the class
+ */
+ protected List getServletMappings(String className)
+ {
+ List results = new ArrayList<>();
+ ServletMapping[] mappings = _context.getServletHandler().getServletMappings();
+ for (ServletMapping mapping : mappings)
+ {
+ //Check the name of the servlet that this mapping applies to, and then find the ServletHolder for it to find it's class
+ ServletHolder holder = _context.getServletHandler().getServlet(mapping.getServletName());
+ if (holder.getClassName() != null && holder.getClassName().equals(className))
+ results.add(mapping);
+ }
+ return results;
+ }
+
+ /**
+ * Check if there are already <security-constraint> elements defined that match the url-patterns for
+ * the servlet.
+ *
+ * @param servletMappings the servlet mappings
+ * @param constraintMappings the constraint mappings
+ * @return true if constraint exists
+ */
+ protected boolean constraintsExist(List servletMappings, List constraintMappings)
+ {
+ boolean exists = false;
+
+ //Check to see if the path spec on each constraint mapping matches a pathSpec in the servlet mappings.
+ //If it does, then we should ignore the security annotations.
+ for (ServletMapping mapping : servletMappings)
+ {
+ //Get its url mappings
+ String[] pathSpecs = mapping.getPathSpecs();
+ if (pathSpecs == null)
+ continue;
+
+ //Check through the constraints to see if there are any whose pathSpecs (url mappings)
+ //match the servlet. If so, then we already have constraints defined for this servlet,
+ //and we will not be processing the annotation (ie web.xml or programmatic override).
+ for (int i = 0; constraintMappings != null && i < constraintMappings.size() && !exists; i++)
+ {
+ for (String pathSpec : pathSpecs)
+ {
+ //TODO decide if we need to check the origin
+ if (pathSpec.equals(constraintMappings.get(i).getPathSpec()))
+ {
+ exists = true;
+ break;
+ }
+ }
+ }
+ }
+ return exists;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebFilterAnnotation.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebFilterAnnotation.java
new file mode 100644
index 00000000000..426d60392a1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebFilterAnnotation.java
@@ -0,0 +1,198 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.Filter;
+import jakarta.servlet.annotation.WebFilter;
+import jakarta.servlet.annotation.WebInitParam;
+import org.eclipse.jetty.ee11.servlet.FilterHolder;
+import org.eclipse.jetty.ee11.servlet.FilterMapping;
+import org.eclipse.jetty.ee11.servlet.Source;
+import org.eclipse.jetty.ee11.webapp.DiscoveredAnnotation;
+import org.eclipse.jetty.ee11.webapp.MetaData;
+import org.eclipse.jetty.ee11.webapp.Origin;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.http.pathmap.ServletPathSpec;
+import org.eclipse.jetty.util.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * WebFilterAnnotation
+ */
+public class WebFilterAnnotation extends DiscoveredAnnotation
+{
+ private static final Logger LOG = LoggerFactory.getLogger(WebFilterAnnotation.class);
+
+ public WebFilterAnnotation(WebAppContext context, String className)
+ {
+ super(context, className);
+ }
+
+ public WebFilterAnnotation(WebAppContext context, String className, Resource resource)
+ {
+ super(context, className, resource);
+ }
+
+ @Override
+ public void apply()
+ {
+ // TODO verify against rules for annotation v descriptor
+
+ Class clazz = getTargetClass();
+ if (clazz == null)
+ {
+ LOG.warn("{} cannot be loaded", _className);
+ return;
+ }
+
+ //Servlet Spec 8.1.2
+ if (!Filter.class.isAssignableFrom(clazz))
+ {
+ LOG.warn("{} is not assignable from jakarta.servlet.Filter", clazz.getName());
+ return;
+ }
+ MetaData metaData = _context.getMetaData();
+
+ WebFilter filterAnnotation = (WebFilter)clazz.getAnnotation(WebFilter.class);
+
+ if (filterAnnotation.value().length > 0 && filterAnnotation.urlPatterns().length > 0)
+ {
+ LOG.warn("{} defines both @WebFilter.value and @WebFilter.urlPatterns", clazz.getName());
+ return;
+ }
+
+ String name = (filterAnnotation.filterName().isEmpty() ? clazz.getName() : filterAnnotation.filterName());
+ String[] urlPatterns = filterAnnotation.value();
+ if (urlPatterns.length == 0)
+ urlPatterns = filterAnnotation.urlPatterns();
+
+ FilterHolder holder = _context.getServletHandler().getFilter(name);
+ if (holder == null)
+ {
+ //Filter with this name does not already exist, so add it
+ holder = _context.getServletHandler().newFilterHolder(new Source(Source.Origin.ANNOTATION, clazz));
+ holder.setName(name);
+
+ holder.setHeldClass(clazz);
+ metaData.setOrigin(name + ".filter.filter-class", filterAnnotation, clazz);
+
+ holder.setDisplayName(filterAnnotation.displayName());
+ metaData.setOrigin(name + ".filter.display-name", filterAnnotation, clazz);
+
+ for (WebInitParam ip : filterAnnotation.initParams())
+ {
+ holder.setInitParameter(ip.name(), ip.value());
+ metaData.setOrigin(name + ".filter.init-param." + ip.name(), ip, clazz);
+ }
+
+ FilterMapping mapping = new FilterMapping();
+ mapping.setFilterName(holder.getName());
+ metaData.setOrigin(name + ".filter.mapping." + Long.toHexString(mapping.hashCode()), filterAnnotation, clazz);
+ if (urlPatterns.length > 0)
+ {
+ ArrayList paths = new ArrayList<>();
+ for (String s : urlPatterns)
+ {
+ paths.add(ServletPathSpec.normalize(s));
+ }
+ mapping.setPathSpecs(paths.toArray(new String[0]));
+ }
+
+ if (filterAnnotation.servletNames().length > 0)
+ {
+ ArrayList names = new ArrayList<>();
+ Collections.addAll(names, filterAnnotation.servletNames());
+ mapping.setServletNames(names.toArray(new String[0]));
+ }
+
+ EnumSet dispatcherSet = EnumSet.noneOf(DispatcherType.class);
+ dispatcherSet.addAll(Arrays.asList(filterAnnotation.dispatcherTypes()));
+ mapping.setDispatcherTypes(dispatcherSet);
+ metaData.setOrigin(name + ".filter.mappings", filterAnnotation, clazz);
+
+ holder.setAsyncSupported(filterAnnotation.asyncSupported());
+ metaData.setOrigin(name + ".filter.async-supported", filterAnnotation, clazz);
+
+ _context.getServletHandler().addFilter(holder);
+ _context.getServletHandler().addFilterMapping(mapping);
+ }
+ else
+ {
+ //A Filter definition for the same name already exists from web.xml
+ //ServletSpec 3.0 p81 if the Filter is already defined and has mappings,
+ //they override the annotation. If it already has DispatcherType set, that
+ //also overrides the annotation. Init-params are additive, but web.xml overrides
+ //init-params of the same name.
+ for (WebInitParam ip : filterAnnotation.initParams())
+ {
+ //if (holder.getInitParameter(ip.name()) == null)
+ if (metaData.getOrigin(name + ".filter.init-param." + ip.name()) == Origin.NotSet)
+ {
+ holder.setInitParameter(ip.name(), ip.value());
+ metaData.setOrigin(name + ".filter.init-param." + ip.name(), ip, clazz);
+ }
+ }
+
+ FilterMapping[] mappings = _context.getServletHandler().getFilterMappings();
+ boolean mappingExists = false;
+ if (mappings != null)
+ {
+ for (FilterMapping m : mappings)
+ {
+ if (m.getFilterName().equals(name))
+ {
+ mappingExists = true;
+ break;
+ }
+ }
+ }
+ //if a descriptor didn't specify at least one mapping, use the mappings from the annotation and the DispatcherTypes
+ //from the annotation
+ if (!mappingExists)
+ {
+ FilterMapping mapping = new FilterMapping();
+ mapping.setFilterName(holder.getName());
+ metaData.setOrigin(holder.getName() + ".filter.mapping." + Long.toHexString(mapping.hashCode()), filterAnnotation, clazz);
+ if (urlPatterns.length > 0)
+ {
+ ArrayList paths = new ArrayList<>();
+ for (String s : urlPatterns)
+ {
+ paths.add(ServletPathSpec.normalize(s));
+ }
+ mapping.setPathSpecs(paths.toArray(new String[0]));
+ }
+ if (filterAnnotation.servletNames().length > 0)
+ {
+ ArrayList names = new ArrayList<>();
+ Collections.addAll(names, filterAnnotation.servletNames());
+ mapping.setServletNames(names.toArray(new String[0]));
+ }
+
+ EnumSet dispatcherSet = EnumSet.noneOf(DispatcherType.class);
+ Collections.addAll(dispatcherSet, filterAnnotation.dispatcherTypes());
+ mapping.setDispatcherTypes(dispatcherSet);
+ _context.getServletHandler().addFilterMapping(mapping);
+ metaData.setOrigin(name + ".filter.mappings", filterAnnotation, clazz);
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebFilterAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebFilterAnnotationHandler.java
new file mode 100644
index 00000000000..78a3ad7d7ea
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebFilterAnnotationHandler.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * WebFilterAnnotationHandler
+ */
+public class WebFilterAnnotationHandler extends AbstractDiscoverableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(WebFilterAnnotationHandler.class);
+
+ public WebFilterAnnotationHandler(WebAppContext context)
+ {
+ super(context);
+ }
+
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebFilter".equals(annotationName))
+ return;
+
+ WebFilterAnnotation wfAnnotation = new WebFilterAnnotation(_context, info.getClassName(), info.getContainingResource());
+ addAnnotation(wfAnnotation);
+ }
+
+ @Override
+ public void handle(AnnotationParser.FieldInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebFilter".equals(annotationName))
+ return;
+ LOG.warn("@WebFilter not applicable for fields: {}.{}", info.getClassInfo().getClassName(), info.getFieldName());
+ }
+
+ @Override
+ public void handle(AnnotationParser.MethodInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebFilter".equals(annotationName))
+ return;
+ LOG.warn("@WebFilter not applicable for methods: {}.{} {}", info.getClassInfo().getClassName(), info.getMethodName(), info.getSignature());
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebListenerAnnotation.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebListenerAnnotation.java
new file mode 100644
index 00000000000..c070dbab130
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebListenerAnnotation.java
@@ -0,0 +1,91 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.EventListener;
+
+import jakarta.servlet.ServletContextAttributeListener;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.ServletRequestAttributeListener;
+import jakarta.servlet.ServletRequestListener;
+import jakarta.servlet.annotation.WebListener;
+import jakarta.servlet.http.HttpSessionAttributeListener;
+import jakarta.servlet.http.HttpSessionIdListener;
+import jakarta.servlet.http.HttpSessionListener;
+import org.eclipse.jetty.ee11.servlet.ListenerHolder;
+import org.eclipse.jetty.ee11.servlet.Source;
+import org.eclipse.jetty.ee11.webapp.DiscoveredAnnotation;
+import org.eclipse.jetty.ee11.webapp.MetaData;
+import org.eclipse.jetty.ee11.webapp.Origin;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.util.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * WebListenerAnnotation
+ */
+public class WebListenerAnnotation extends DiscoveredAnnotation
+{
+ private static final Logger LOG = LoggerFactory.getLogger(WebListenerAnnotation.class);
+
+ public WebListenerAnnotation(WebAppContext context, String className)
+ {
+ super(context, className);
+ }
+
+ public WebListenerAnnotation(WebAppContext context, String className, Resource resource)
+ {
+ super(context, className, resource);
+ }
+
+ @Override
+ public void apply()
+ {
+ Class extends java.util.EventListener> clazz = (Class extends EventListener>)getTargetClass();
+
+ if (clazz == null)
+ {
+ LOG.warn("{} cannot be loaded", _className);
+ return;
+ }
+
+ try
+ {
+ if (ServletContextListener.class.isAssignableFrom(clazz) ||
+ ServletContextAttributeListener.class.isAssignableFrom(clazz) ||
+ ServletRequestListener.class.isAssignableFrom(clazz) ||
+ ServletRequestAttributeListener.class.isAssignableFrom(clazz) ||
+ HttpSessionListener.class.isAssignableFrom(clazz) ||
+ HttpSessionAttributeListener.class.isAssignableFrom(clazz) ||
+ HttpSessionIdListener.class.isAssignableFrom(clazz))
+ {
+ MetaData metaData = _context.getMetaData();
+ if (metaData.getOrigin(clazz.getName() + ".listener") == Origin.NotSet)
+ {
+ ListenerHolder h = _context.getServletHandler().newListenerHolder(new Source(Source.Origin.ANNOTATION, clazz));
+ h.setHeldClass(clazz);
+ _context.getServletHandler().addListener(h);
+ metaData.setOrigin(clazz.getName() + ".listener", clazz.getAnnotation(WebListener.class), clazz);
+ }
+ }
+ else
+ LOG.warn("{} does not implement one of the servlet listener interfaces", clazz.getName());
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to add listener {}", clazz, e);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebListenerAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebListenerAnnotationHandler.java
new file mode 100644
index 00000000000..3a43444a62f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebListenerAnnotationHandler.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WebListenerAnnotationHandler extends AbstractDiscoverableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(WebListenerAnnotationHandler.class);
+
+ public WebListenerAnnotationHandler(WebAppContext context)
+ {
+ super(context);
+ }
+
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebListener".equals(annotationName))
+ return;
+
+ WebListenerAnnotation wlAnnotation = new WebListenerAnnotation(_context, info.getClassName(), info.getContainingResource());
+ addAnnotation(wlAnnotation);
+ }
+
+ @Override
+ public void handle(AnnotationParser.FieldInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebListener".equals(annotationName))
+ return;
+ LOG.warn("@WebListener is not applicable to fields: {}.{}", info.getClassInfo().getClassName(), info.getFieldName());
+ }
+
+ @Override
+ public void handle(AnnotationParser.MethodInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebListener".equals(annotationName))
+ return;
+ LOG.warn("@WebListener is not applicable to methods: {}.{} {}", info.getClassInfo().getClassName(), info.getMethodName(), info.getSignature());
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebServletAnnotation.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebServletAnnotation.java
new file mode 100644
index 00000000000..9a9dfbae1b3
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebServletAnnotation.java
@@ -0,0 +1,269 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.annotation.WebInitParam;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.ServletMapping;
+import org.eclipse.jetty.ee11.servlet.Source;
+import org.eclipse.jetty.ee11.webapp.DiscoveredAnnotation;
+import org.eclipse.jetty.ee11.webapp.MetaData;
+import org.eclipse.jetty.ee11.webapp.Origin;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.http.pathmap.ServletPathSpec;
+import org.eclipse.jetty.util.ArrayUtil;
+import org.eclipse.jetty.util.LazyList;
+import org.eclipse.jetty.util.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * WebServletAnnotation
+ */
+public class WebServletAnnotation extends DiscoveredAnnotation
+{
+ private static final Logger LOG = LoggerFactory.getLogger(WebServletAnnotation.class);
+
+ public WebServletAnnotation(WebAppContext context, String className)
+ {
+ super(context, className);
+ }
+
+ public WebServletAnnotation(WebAppContext context, String className, Resource resource)
+ {
+ super(context, className, resource);
+ }
+
+ @Override
+ public void apply()
+ {
+ //TODO check this algorithm with new rules for applying descriptors and annotations in order
+ Class extends Servlet> clazz = (Class extends Servlet>)getTargetClass();
+
+ if (clazz == null)
+ {
+ LOG.warn("{} cannot be loaded", _className);
+ return;
+ }
+
+ //Servlet Spec 8.1.1
+ if (!HttpServlet.class.isAssignableFrom(clazz))
+ {
+ LOG.warn("{} is not assignable from jakarta.servlet.http.HttpServlet", clazz.getName());
+ return;
+ }
+
+ WebServlet annotation = clazz.getAnnotation(WebServlet.class);
+
+ if (annotation.urlPatterns().length > 0 && annotation.value().length > 0)
+ {
+ LOG.warn("{} defines both @WebServlet.value and @WebServlet.urlPatterns", clazz.getName());
+ return;
+ }
+
+ String[] urlPatterns = annotation.value();
+ if (urlPatterns.length == 0)
+ urlPatterns = annotation.urlPatterns();
+
+ if (urlPatterns.length == 0)
+ {
+ LOG.warn("{} defines neither @WebServlet.value nor @WebServlet.urlPatterns", clazz.getName());
+ return;
+ }
+
+ //canonicalize the patterns
+ ArrayList urlPatternList = new ArrayList<>();
+ for (String p : urlPatterns)
+ {
+ urlPatternList.add(ServletPathSpec.normalize(p));
+ }
+
+ String servletName = (annotation.name().isEmpty() ? clazz.getName() : annotation.name());
+
+ MetaData metaData = _context.getMetaData();
+ ServletMapping mapping = null; //the new mapping
+
+ //Find out if a already exists with this name
+ ServletHolder[] holders = _context.getServletHandler().getServlets();
+
+ ServletHolder holder = null;
+ if (holders != null)
+ {
+ for (ServletHolder h : holders)
+ {
+ if (h.getName() != null && servletName.equals(h.getName()))
+ {
+ holder = h;
+ break;
+ }
+ }
+ }
+
+ //handle creation/completion of a servlet
+ if (holder == null)
+ {
+ //No servlet of this name has already been defined, either by a descriptor
+ //or another annotation (which would be impossible).
+ Source source = new Source(Source.Origin.ANNOTATION, clazz);
+
+ holder = _context.getServletHandler().newServletHolder(source);
+ holder.setHeldClass(clazz);
+ metaData.setOrigin(servletName + ".servlet.servlet-class", annotation, clazz);
+
+ holder.setName(servletName);
+ holder.setDisplayName(annotation.displayName());
+ metaData.setOrigin(servletName + ".servlet.display-name", annotation, clazz);
+
+ holder.setInitOrder(annotation.loadOnStartup());
+ metaData.setOrigin(servletName + ".servlet.load-on-startup", annotation, clazz);
+
+ holder.setAsyncSupported(annotation.asyncSupported());
+ metaData.setOrigin(servletName + ".servlet.async-supported", annotation, clazz);
+
+ for (WebInitParam ip : annotation.initParams())
+ {
+ holder.setInitParameter(ip.name(), ip.value());
+ metaData.setOrigin(servletName + ".servlet.init-param." + ip.name(), ip, clazz);
+ }
+
+ _context.getServletHandler().addServlet(holder);
+
+ mapping = new ServletMapping(source);
+ mapping.setServletName(holder.getName());
+ mapping.setPathSpecs(LazyList.toStringArray(urlPatternList));
+ _context.getMetaData().setOrigin(servletName + ".servlet.mapping." + Long.toHexString(mapping.hashCode()), annotation, clazz);
+ }
+ else
+ {
+ //set the class according to the servlet that is annotated, if it wasn't already
+ //NOTE: this may be considered as "completing" an incomplete servlet registration, and it is
+ //not clear from servlet 3.0 spec whether this is intended, or if only a ServletContext.addServlet() call
+ //can complete it, see http://java.net/jira/browse/SERVLET_SPEC-42
+ if (holder.getClassName() == null)
+ holder.setClassName(clazz.getName());
+ if (holder.getHeldClass() == null)
+ holder.setHeldClass(clazz);
+
+ //check if the existing servlet has each init-param from the annotation
+ //if not, add it
+ for (WebInitParam ip : annotation.initParams())
+ {
+ if (metaData.getOrigin(servletName + ".servlet.init-param." + ip.name()) == Origin.NotSet)
+ {
+ holder.setInitParameter(ip.name(), ip.value());
+ metaData.setOrigin(servletName + ".servlet.init-param." + ip.name(), ip, clazz);
+ }
+ }
+
+ //check the url-patterns
+ //ServletSpec 3.0 p81 If a servlet already has url mappings from a
+ //webxml or fragment descriptor the annotation is ignored.
+ //However, we want to be able to replace mappings that were given in webdefault-ee11.xml
+ List existingMappings = getServletMappingsForServlet(servletName);
+
+ //if any mappings for this servlet already set by a descriptor that is not webdefault-ee11.xml forget
+ //about processing these url mappings
+ if (existingMappings.isEmpty() || !containsNonDefaultMappings(existingMappings))
+ {
+ mapping = new ServletMapping(new Source(Source.Origin.ANNOTATION, clazz));
+ mapping.setServletName(servletName);
+ mapping.setPathSpecs(LazyList.toStringArray(urlPatternList));
+ _context.getMetaData().setOrigin(servletName + ".servlet.mapping." + Long.toHexString(mapping.hashCode()), annotation, clazz);
+ }
+ }
+
+ //We also want to be able to replace mappings that were defined in webdefault-ee11.xml
+ //that were for a different servlet eg a mapping in webdefault-ee11.xml for / to the jetty
+ //default servlet should be able to be replaced by an annotation for / to a different
+ //servlet
+ if (mapping != null)
+ {
+ //url mapping was permitted by annotation processing rules
+
+ //take a copy of the existing servlet mappings that we can iterate over and remove from. This is
+ //because the ServletHandler interface does not support removal of individual mappings.
+ List allMappings = ArrayUtil.asMutableList(_context.getServletHandler().getServletMappings());
+
+ //for each of the urls in the annotation, check if a mapping to same/different servlet exists
+ // if mapping exists and is from a default descriptor, it can be replaced. NOTE: we do not
+ // guard against duplicate path mapping here: that is the job of the ServletHandler
+ for (String p : urlPatternList)
+ {
+ ServletMapping existingMapping = _context.getServletHandler().getServletMapping(p);
+ if (existingMapping != null && existingMapping.isFromDefaultDescriptor())
+ {
+ String[] updatedPaths = ArrayUtil.removeFromArray(existingMapping.getPathSpecs(), p);
+ //if we removed the last path from a servletmapping, delete the servletmapping
+ if (updatedPaths == null || updatedPaths.length == 0)
+ {
+ boolean success = allMappings.remove(existingMapping);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Removed empty mapping {} from defaults descriptor success:{}", existingMapping, success);
+ }
+ else
+ {
+ existingMapping.setPathSpecs(updatedPaths);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Removed path {} from mapping {} from defaults descriptor ", p, existingMapping);
+ }
+ }
+ _context.getMetaData().setOrigin(servletName + ".servlet.mapping.url" + p, annotation, clazz);
+ }
+ allMappings.add(mapping);
+ _context.getServletHandler().setServletMappings(allMappings.toArray(new ServletMapping[0]));
+ }
+ }
+
+ /**
+ *
+ */
+ private List getServletMappingsForServlet(String name)
+ {
+ ServletMapping[] allMappings = _context.getServletHandler().getServletMappings();
+ if (allMappings == null)
+ return Collections.emptyList();
+
+ List mappings = new ArrayList<>();
+ for (ServletMapping m : allMappings)
+ {
+ if (m.getServletName() != null && name.equals(m.getServletName()))
+ {
+ mappings.add(m);
+ }
+ }
+ return mappings;
+ }
+
+ /**
+ *
+ */
+ private boolean containsNonDefaultMappings(List mappings)
+ {
+ if (mappings == null)
+ return false;
+ for (ServletMapping m : mappings)
+ {
+ if (!m.isFromDefaultDescriptor())
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebServletAnnotationHandler.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebServletAnnotationHandler.java
new file mode 100644
index 00000000000..01bca11fd27
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/WebServletAnnotationHandler.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Process a WebServlet annotation on a class.
+ */
+public class WebServletAnnotationHandler extends AbstractDiscoverableAnnotationHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(WebServletAnnotationHandler.class);
+
+ public WebServletAnnotationHandler(WebAppContext context)
+ {
+ super(context);
+ }
+
+ /**
+ * Handle discovering a WebServlet annotation.
+ */
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebServlet".equals(annotationName))
+ return;
+
+ WebServletAnnotation annotation = new WebServletAnnotation(_context, info.getClassName(), info.getContainingResource());
+ addAnnotation(annotation);
+ }
+
+ @Override
+ public void handle(AnnotationParser.FieldInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebServlet".equals(annotationName))
+ return;
+
+ LOG.warn("@WebServlet annotation not supported for fields");
+ }
+
+ @Override
+ public void handle(AnnotationParser.MethodInfo info, String annotationName)
+ {
+ if (!"jakarta.servlet.annotation.WebServlet".equals(annotationName))
+ return;
+
+ LOG.warn("@WebServlet annotation not supported for methods");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/package-info.java b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/package-info.java
new file mode 100644
index 00000000000..36f3b2fcd3d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/java/org/eclipse/jetty/ee11/annotations/package-info.java
@@ -0,0 +1,17 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty Annotations : Support for Servlet Annotations
+ */
+package org.eclipse.jetty.ee11.annotations;
diff --git a/jetty-ee11/jetty-ee11-annotations/src/main/resources/META-INF/services/org.eclipse.jetty.ee11.webapp.Configuration b/jetty-ee11/jetty-ee11-annotations/src/main/resources/META-INF/services/org.eclipse.jetty.ee11.webapp.Configuration
new file mode 100644
index 00000000000..92e58b441ae
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/main/resources/META-INF/services/org.eclipse.jetty.ee11.webapp.Configuration
@@ -0,0 +1 @@
+org.eclipse.jetty.ee11.annotations.AnnotationConfiguration
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-for-container-path.jar b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-for-container-path.jar
new file mode 100644
index 00000000000..c66593efae1
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-for-container-path.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-for-webinf.jar b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-for-webinf.jar
new file mode 100644
index 00000000000..bffda88ae1a
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-for-webinf.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-with-ordering.jar b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-with-ordering.jar
new file mode 100644
index 00000000000..37c5a2b4886
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci-with-ordering.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci.jar b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci.jar
new file mode 100644
index 00000000000..8f35be4b125
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/jar/test-sci.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/acme/ClassOne.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/acme/ClassOne.java
new file mode 100644
index 00000000000..6debfc14440
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/acme/ClassOne.java
@@ -0,0 +1,25 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.acme;
+
+/**
+ * ClassOne
+ */
+public class ClassOne
+{
+
+ public void one()
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ClassA.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ClassA.java
new file mode 100644
index 00000000000..9189326126b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ClassA.java
@@ -0,0 +1,91 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+/**
+ * ClassA
+ */
+@Sample(1)
+public class ClassA
+{
+ private Integer e;
+ private Integer f;
+ private Integer g;
+ private Integer h;
+ private Integer j;
+ private Integer k;
+
+ public static class Foo
+ {
+
+ }
+
+ @Sample(7)
+ private Integer m;
+
+ @Sample(2)
+ public void a(Integer[] x)
+ {
+ System.err.println("ClassA.public");
+ }
+
+ @Sample(3)
+ protected void b(Foo[] f)
+ {
+ System.err.println("ClassA.protected");
+ }
+
+ @Sample(4)
+ void c(int[] x)
+ {
+ System.err.println("ClassA.package");
+ }
+
+ @Sample(5)
+ private void d(int x, String y)
+ {
+ System.err.println("ClassA.private");
+ }
+
+ @Sample(6)
+ protected void l()
+ {
+ System.err.println("ClassA.protected method l");
+ }
+
+ public Integer getE()
+ {
+ return this.e;
+ }
+
+ public Integer getF()
+ {
+ return this.f;
+ }
+
+ public Integer getG()
+ {
+ return this.g;
+ }
+
+ public Integer getJ()
+ {
+ return this.j;
+ }
+
+ public void x()
+ {
+ System.err.println("ClassA.x");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ClassB.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ClassB.java
new file mode 100644
index 00000000000..6605624f6e1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ClassB.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+/**
+ * ClassB
+ */
+@Sample(value = 50)
+@Multi({"do", "re", "mi"})
+public class ClassB extends ClassA implements InterfaceD
+{
+
+ //test override of public scope method
+ @Sample(value = 51)
+ @Multi({"fa", "so", "la"})
+ public void a()
+ {
+ System.err.println("ClassB.public");
+ }
+
+ //test override of package scope method
+ @Sample(value = 52)
+ void c()
+ {
+ System.err.println("ClassB.package");
+ }
+
+ @Override
+ public void l()
+ {
+ System.err.println("Overridden method l has no annotation");
+ }
+
+ //test no annotation
+ public void z()
+ {
+ System.err.println("ClassB.z");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/FilterC.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/FilterC.java
new file mode 100644
index 00000000000..00abe559c0f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/FilterC.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.io.IOException;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.RunAs;
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.annotation.WebFilter;
+import jakarta.servlet.annotation.WebInitParam;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+@WebFilter(filterName = "CFilter", dispatcherTypes = {DispatcherType.REQUEST}, urlPatterns = {"/*"}, initParams = {
+ @WebInitParam(name = "a", value = "99")
+ }, asyncSupported = false)
+@RunAs("admin")
+public class FilterC implements Filter
+{
+ @Resource(mappedName = "foo")
+ private Double foo;
+
+ @PreDestroy
+ public void pre()
+ {
+
+ }
+
+ @PostConstruct
+ public void post()
+ {
+
+ }
+
+ @Override
+ public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
+ throws IOException, ServletException
+ {
+ HttpServletRequest request = (HttpServletRequest)arg0;
+ HttpServletResponse response = (HttpServletResponse)arg1;
+ HttpSession session = request.getSession(true);
+ String val = request.getParameter("action");
+ if (val != null)
+ session.setAttribute("action", val);
+ arg2.doFilter(request, response);
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(FilterConfig arg0) throws ServletException
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/InterfaceD.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/InterfaceD.java
new file mode 100644
index 00000000000..5fa08213ae7
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/InterfaceD.java
@@ -0,0 +1,22 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+/**
+ * InterfaceD
+ */
+public interface InterfaceD
+{
+
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ListenerC.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ListenerC.java
new file mode 100644
index 00000000000..1ea6af0775d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ListenerC.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.annotation.WebListener;
+
+@WebListener
+public class ListenerC implements ServletContextListener
+{
+
+ @Override
+ public void contextDestroyed(ServletContextEvent arg0)
+ {
+
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent arg0)
+ {
+
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/Multi.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/Multi.java
new file mode 100644
index 00000000000..5cad85666bc
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/Multi.java
@@ -0,0 +1,26 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
+public @interface Multi
+{
+ String[] value();
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/Sample.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/Sample.java
new file mode 100644
index 00000000000..f0944b4a981
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/Sample.java
@@ -0,0 +1,26 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
+public @interface Sample
+{
+ int value();
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletC.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletC.java
new file mode 100644
index 00000000000..af710c58ff0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletC.java
@@ -0,0 +1,68 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.io.IOException;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.DeclareRoles;
+import jakarta.annotation.security.RunAs;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.HttpConstraint;
+import jakarta.servlet.annotation.HttpMethodConstraint;
+import jakarta.servlet.annotation.MultipartConfig;
+import jakarta.servlet.annotation.ServletSecurity;
+import jakarta.servlet.annotation.WebInitParam;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+@DeclareRoles({"alice"})
+@WebServlet(urlPatterns = {"/foo/*", "/bah/*"}, name = "CServlet", initParams = {
+ @WebInitParam(name = "x", value = "y")
+ }, loadOnStartup = 2, asyncSupported = false)
+@MultipartConfig(fileSizeThreshold = 1000, maxFileSize = 2000, maxRequestSize = 3000)
+@RunAs("admin")
+@ServletSecurity(value = @HttpConstraint(rolesAllowed = {"fred", "bill", "dorothy"}), httpMethodConstraints = {
+ @HttpMethodConstraint(value = "GET", rolesAllowed =
+ {"bob", "carol", "ted"})
+})
+public class ServletC extends HttpServlet
+{
+ @Resource(mappedName = "foo", type = Double.class)
+ private Double foo;
+
+ @PreDestroy
+ public void pre()
+ {
+
+ }
+
+ @PostConstruct
+ public void post()
+ {
+
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ response.setContentType("text/html");
+ response.getWriter().println("
Annotated Servlet
");
+ response.getWriter().println("An annotated Servlet.");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletD.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletD.java
new file mode 100644
index 00000000000..09babac7426
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletD.java
@@ -0,0 +1,26 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import jakarta.servlet.annotation.WebInitParam;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+
+@WebServlet(urlPatterns = {"/", "/bah/*"}, name = "DServlet", initParams = {
+ @WebInitParam(name = "x", value = "y")
+ }, loadOnStartup = 1, asyncSupported = false)
+public class ServletD extends HttpServlet
+{
+ // no op
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletE.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletE.java
new file mode 100644
index 00000000000..3942987927c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/ServletE.java
@@ -0,0 +1,25 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import jakarta.annotation.PreDestroy;
+import jakarta.servlet.http.HttpServlet;
+
+public class ServletE extends HttpServlet
+{
+ @PreDestroy
+ public void preDestroy()
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationConfiguration.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationConfiguration.java
new file mode 100644
index 00000000000..96bbb31443a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationConfiguration.java
@@ -0,0 +1,418 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import jakarta.servlet.ServletContainerInitializer;
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration.State;
+import org.eclipse.jetty.ee11.webapp.RelativeOrdering;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.ee11.webapp.WebDescriptor;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.JAR;
+import org.eclipse.jetty.toolchain.test.MavenPaths;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestAnnotationConfiguration
+{
+ public static class TestableAnnotationConfiguration extends AnnotationConfiguration
+ {
+ public void assertAnnotationDiscovery(WebAppContext context, boolean b)
+ {
+ State state = (State)context.getAttribute(STATE);
+ if (!b)
+ assertTrue(state._discoverableAnnotationHandlers.isEmpty());
+ else
+ assertFalse(state._discoverableAnnotationHandlers.isEmpty());
+ }
+ }
+
+ public Path web25;
+ public Path web31false;
+ public Path web31true;
+ public Path jarDir;
+ public Path testSciJar;
+ public Path testContainerSciJar;
+ public Path testWebInfClassesJar;
+ public WorkDir workDir;
+ public URLClassLoader containerLoader;
+ public URLClassLoader webAppLoader;
+ public List classes;
+ public Resource targetClasses;
+ public Resource webInfClasses;
+
+ @BeforeEach
+ public void setup() throws Exception
+ {
+ web25 = MavenTestingUtils.getTestResourcePathFile("web25.xml");
+ web31false = MavenTestingUtils.getTestResourcePathFile("web31false.xml");
+ web31true = MavenTestingUtils.getTestResourcePathFile("web31true.xml");
+
+ // prepare an sci that will be on the webapp's classpath
+ jarDir = MavenTestingUtils.getProjectDirPath("src/test/jar");
+ testSciJar = jarDir.resolve("test-sci.jar");
+ assertTrue(Files.exists(testSciJar));
+
+ testContainerSciJar = jarDir.resolve("test-sci-for-container-path.jar");
+ testWebInfClassesJar = jarDir.resolve("test-sci-for-webinf.jar");
+ Path unpacked = workDir.getEmptyPathDir();
+ // unpack some classes to pretend that are in WEB-INF/classes
+ FS.cleanDirectory(unpacked);
+ JAR.unpack(testWebInfClassesJar.toFile(), unpacked.toFile());
+ webInfClasses = ResourceFactory.root().newResource(unpacked);
+
+ containerLoader = new URLClassLoader(new URL[]{
+ testContainerSciJar.toUri().toURL()
+ }, Thread.currentThread().getContextClassLoader());
+
+ targetClasses = ResourceFactory.root().newResource(MavenPaths.targetDir().resolve("test-classes"));
+
+ classes = List.of(webInfClasses, targetClasses);
+
+ webAppLoader = new URLClassLoader(new URL[]{
+ testSciJar.toUri().toURL(), targetClasses.getURI().toURL(), webInfClasses.getURI().toURL()
+ },
+ containerLoader);
+ }
+
+ @Test
+ public void testAnnotationScanControl() throws Exception
+ {
+ //check that a 2.5 webapp with configurationDiscovered will discover annotations
+ TestableAnnotationConfiguration config25 = new TestableAnnotationConfiguration();
+ WebAppContext context25 = new WebAppContext();
+ config25.preConfigure(context25);
+ context25.setClassLoader(Thread.currentThread().getContextClassLoader());
+ context25.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE);
+ context25.setAttribute(AnnotationConfiguration.MAX_SCAN_WAIT, 0);
+ context25.setConfigurationDiscovered(false);
+ context25.getMetaData().setWebDescriptor(new WebDescriptor(context25.getResourceFactory().newResource(web25)));
+ context25.getContext().getServletContext().setEffectiveMajorVersion(2);
+ context25.getContext().getServletContext().setEffectiveMinorVersion(5);
+ config25.configure(context25);
+ config25.assertAnnotationDiscovery(context25, false);
+
+ //check that a 2.5 webapp discover annotations
+ TestableAnnotationConfiguration config25b = new TestableAnnotationConfiguration();
+ WebAppContext context25b = new WebAppContext();
+ config25b.preConfigure(context25b);
+ context25b.setClassLoader(Thread.currentThread().getContextClassLoader());
+ context25b.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE);
+ context25b.setAttribute(AnnotationConfiguration.MAX_SCAN_WAIT, 0);
+ context25b.getMetaData().setWebDescriptor(new WebDescriptor(context25b.getResourceFactory().newResource(web25)));
+ context25b.getContext().getServletContext().setEffectiveMajorVersion(2);
+ context25b.getContext().getServletContext().setEffectiveMinorVersion(5);
+ config25b.configure(context25b);
+ config25b.assertAnnotationDiscovery(context25b, true);
+
+ //check that a 3.x webapp with metadata true won't discover annotations
+ TestableAnnotationConfiguration config31 = new TestableAnnotationConfiguration();
+ WebAppContext context31 = new WebAppContext();
+ config31.preConfigure(context31);
+ context31.setClassLoader(Thread.currentThread().getContextClassLoader());
+ context31.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE);
+ context31.setAttribute(AnnotationConfiguration.MAX_SCAN_WAIT, 0);
+ context31.getMetaData().setWebDescriptor(new WebDescriptor(context31.getResourceFactory().newResource(web31true)));
+ context31.getContext().getServletContext().setEffectiveMajorVersion(3);
+ context31.getContext().getServletContext().setEffectiveMinorVersion(1);
+ config31.configure(context31);
+ config31.assertAnnotationDiscovery(context31, false);
+
+ //check that a 3.x webapp with metadata false will discover annotations
+ TestableAnnotationConfiguration config31b = new TestableAnnotationConfiguration();
+ WebAppContext context31b = new WebAppContext();
+ config31b.preConfigure(context31b);
+ context31b.setClassLoader(Thread.currentThread().getContextClassLoader());
+ context31b.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE);
+ context31b.setAttribute(AnnotationConfiguration.MAX_SCAN_WAIT, 0);
+ context31b.getMetaData().setWebDescriptor(new WebDescriptor(context31b.getResourceFactory().newResource(web31false)));
+ context31b.getContext().getServletContext().setEffectiveMajorVersion(3);
+ context31b.getContext().getServletContext().setEffectiveMinorVersion(1);
+ config31b.configure(context31b);
+ config31b.assertAnnotationDiscovery(context31b, true);
+ }
+
+ @Test
+ public void testServerAndWebappSCIs() throws Exception
+ {
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(webAppLoader);
+
+ try
+ {
+ AnnotationConfiguration config = new AnnotationConfiguration();
+ WebAppContext context = new WebAppContext();
+ config.preConfigure(context);
+ List scis;
+
+ //test 3.1 webapp loads both server and app scis
+ context.setClassLoader(webAppLoader);
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(testSciJar));
+ context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(web31true)));
+ context.getMetaData().setWebInfClassesResources(classes);
+ context.getContext().getServletContext().setEffectiveMajorVersion(3);
+ context.getContext().getServletContext().setEffectiveMinorVersion(1);
+ config.preConfigure(context);
+ State state = (State)context.getAttribute(AnnotationConfiguration.STATE);
+ scis = config.getNonExcludedInitializers(state);
+ assertNotNull(scis);
+ assertEquals(3, scis.size());
+ assertEquals("com.acme.ServerServletContainerInitializer", scis.get(0).getClass().getName()); //container path
+ assertEquals("com.acme.webinf.WebInfClassServletContainerInitializer", scis.get(1).getClass().getName()); // web-inf
+ assertEquals("com.acme.initializer.FooInitializer", scis.get(2).getClass().getName()); //web-inf jar no web-fragment
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+
+ @Test
+ public void testClassScanHandlersForSCIs() throws Exception
+ {
+ //test that SCIs with a @HandlesTypes that is an annotation registers
+ //handlers for the scanning phase that will capture the class hierarchy,
+ //and also capture all classes that contain the annotation
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(webAppLoader);
+
+ try
+ {
+ class MyAnnotationConfiguration extends AnnotationConfiguration
+ {
+
+ @Override
+ public void createServletContainerInitializerAnnotationHandlers(WebAppContext context, List scis) throws Exception
+ {
+ State state = (State)context.getAttribute(STATE);
+ super.createServletContainerInitializerAnnotationHandlers(context, scis);
+ //check class hierarchy scanner handler is registered
+ assertNotNull(state._classInheritanceHandler);
+ //check
+ assertEquals(1, state._containerInitializerAnnotationHandlers.size());
+ ContainerInitializerAnnotationHandler handler = state._containerInitializerAnnotationHandlers.get(0);
+ assertThat(handler._holder.toString(), containsString("com.acme.initializer.FooInitializer"));
+ assertEquals("com.acme.initializer.Foo", handler._annotation.getName());
+ }
+ }
+
+ MyAnnotationConfiguration config = new MyAnnotationConfiguration();
+
+ WebAppContext context = new WebAppContext();
+ config.preConfigure(context);
+ List scis;
+
+ context.setClassLoader(webAppLoader);
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(testSciJar));
+ context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(web31true)));
+ context.getMetaData().setWebInfClassesResources(classes);
+ context.getContext().getServletContext().setEffectiveMajorVersion(3);
+ context.getContext().getServletContext().setEffectiveMinorVersion(1);
+ config.preConfigure(context);
+ State state = (State)context.getAttribute(AnnotationConfiguration.STATE);
+ scis = config.getNonExcludedInitializers(state);
+ assertNotNull(scis);
+ assertEquals(3, scis.size());
+
+ config.createServletContainerInitializerAnnotationHandlers(context, scis);
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+
+ @Test
+ public void testMetaDataCompleteSCIs() throws Exception
+ {
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(webAppLoader);
+
+ try
+ {
+ AnnotationConfiguration config = new AnnotationConfiguration();
+ WebAppContext context = new WebAppContext();
+ config.preConfigure(context);
+ List scis;
+ // test a 3.1 webapp with metadata-complete=false loads both server
+ // and webapp scis
+ context.setClassLoader(webAppLoader);
+ context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(web31false)));
+ context.getMetaData().setWebInfClassesResources(classes);
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(testSciJar));
+ context.getContext().getServletContext().setEffectiveMajorVersion(3);
+ context.getContext().getServletContext().setEffectiveMinorVersion(1);
+ config.preConfigure(context);
+ State state = (State)context.getAttribute(AnnotationConfiguration.STATE);
+ scis = config.getNonExcludedInitializers(state);
+ assertNotNull(scis);
+ assertEquals(3, scis.size());
+ assertEquals("com.acme.ServerServletContainerInitializer", scis.get(0).getClass().getName()); // container
+ // path
+ assertEquals("com.acme.webinf.WebInfClassServletContainerInitializer", scis.get(1).getClass().getName()); // web-inf
+ assertEquals("com.acme.initializer.FooInitializer", scis.get(2).getClass().getName()); // web-inf
+ // jar
+ // no
+ // web-fragment
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+
+ @Test
+ public void testRelativeOrderingWithSCIs() throws Exception
+ {
+ // test a 3.1 webapp with RELATIVE ORDERING loads sci from
+ // equivalent of WEB-INF/classes first as well as container path
+
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+
+ Path orderedFragmentJar = jarDir.resolve("test-sci-with-ordering.jar");
+ assertTrue(Files.exists(orderedFragmentJar));
+ URLClassLoader orderedLoader = new URLClassLoader(new URL[]{
+ orderedFragmentJar.toUri().toURL(), testSciJar.toUri().toURL(),
+ targetClasses.getURI().toURL(), webInfClasses.getURI().toURL()
+ },
+ containerLoader);
+ Thread.currentThread().setContextClassLoader(orderedLoader);
+
+ try
+ {
+ AnnotationConfiguration config = new AnnotationConfiguration();
+ WebAppContext context = new WebAppContext();
+ config.preConfigure(context);
+ List scis;
+ context.setClassLoader(orderedLoader);
+ context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(web31true)));
+ RelativeOrdering ordering = new RelativeOrdering(context.getMetaData());
+ context.getMetaData().setOrdering(ordering);
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(orderedFragmentJar));
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(testSciJar));
+ context.getMetaData().setWebInfClassesResources(classes);
+ context.getMetaData().orderFragments();
+ context.getContext().getServletContext().setEffectiveMajorVersion(3);
+ context.getContext().getServletContext().setEffectiveMinorVersion(1);
+ config.preConfigure(context);
+ State state = (State)context.getAttribute(AnnotationConfiguration.STATE);
+ scis = config.getNonExcludedInitializers(state);
+ assertNotNull(scis);
+ assertEquals(4, scis.size());
+ assertEquals("com.acme.ServerServletContainerInitializer", scis.get(0).getClass().getName()); //container path
+ assertEquals("com.acme.webinf.WebInfClassServletContainerInitializer", scis.get(1).getClass().getName()); // web-inf
+ assertEquals("com.acme.ordering.AcmeServletContainerInitializer", scis.get(2).getClass().getName()); // first
+ assertEquals("com.acme.initializer.FooInitializer", scis.get(3).getClass().getName()); //other in ordering
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+
+ @Test
+ public void testDiscoveredFalseWithSCIs() throws Exception
+ {
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(webAppLoader);
+ try
+ {
+ //test 2.5 webapp with configurationDiscovered=false loads only server scis
+ AnnotationConfiguration config = new AnnotationConfiguration();
+ WebAppContext context = new WebAppContext();
+ config.preConfigure(context);
+ List scis;
+ context.setConfigurationDiscovered(false);
+ context.setClassLoader(webAppLoader);
+ context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(web25)));
+ context.getMetaData().setWebInfClassesResources(classes);
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(testSciJar));
+ context.getContext().getServletContext().setEffectiveMajorVersion(2);
+ context.getContext().getServletContext().setEffectiveMinorVersion(5);
+ config.preConfigure(context);
+ State state = (State)context.getAttribute(AnnotationConfiguration.STATE);
+ scis = config.getNonExcludedInitializers(state);
+ assertNotNull(scis);
+ for (ServletContainerInitializer s : scis)
+ {
+ //should not have any of the web-inf lib scis in here
+ assertNotEquals("com.acme.ordering.AcmeServletContainerInitializer", s.getClass().getName());
+ assertNotEquals("com.acme.initializer.FooInitializer", s.getClass().getName());
+ //NOTE: should also not have the web-inf classes scis in here either, but due to the
+ //way the test is set up, the sci we're pretending is in web-inf classes will actually
+ //NOT be loaded by the webapp's classloader, but rather by the junit classloader, so
+ //it looks as if it is a container class.
+ }
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+
+ @Test
+ public void testDiscoveredTrueWithSCIs() throws Exception
+ {
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(webAppLoader);
+ try
+ {
+ //test 2.5 webapp with configurationDiscovered=true loads both server and webapp scis
+ AnnotationConfiguration config = new AnnotationConfiguration();
+ WebAppContext context = new WebAppContext();
+ config.preConfigure(context);
+ List scis;
+ context.setConfigurationDiscovered(true);
+ context.setClassLoader(webAppLoader);
+ context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(web25)));
+ context.getMetaData().setWebInfClassesResources(classes);
+ context.getMetaData().addWebInfResource(ResourceFactory.root().newResource(testSciJar));
+ context.getContext().getServletContext().setEffectiveMajorVersion(2);
+ context.getContext().getServletContext().setEffectiveMinorVersion(5);
+ config.preConfigure(context);
+ State state = (State)context.getAttribute(AnnotationConfiguration.STATE);
+ scis = config.getNonExcludedInitializers(state);
+ assertNotNull(scis);
+ assertEquals(3, scis.size(), scis::toString);
+ assertEquals("com.acme.ServerServletContainerInitializer", scis.get(0).getClass().getName()); //container path
+ assertEquals("com.acme.webinf.WebInfClassServletContainerInitializer", scis.get(1).getClass().getName()); // web-inf
+ assertEquals("com.acme.initializer.FooInitializer", scis.get(2).getClass().getName()); //web-inf jar no web-fragment
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationDecorator.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationDecorator.java
new file mode 100644
index 00000000000..a66643b628f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationDecorator.java
@@ -0,0 +1,131 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.Source;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.ee11.webapp.WebDescriptor;
+import org.eclipse.jetty.plus.annotation.LifeCycleCallbackCollection;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.DecoratedObjectFactory;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.eclipse.jetty.xml.XmlParser;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestAnnotationDecorator
+{
+ public static class TestWebDescriptor extends WebDescriptor
+ {
+ public TestWebDescriptor(Resource xml, Boolean metadataComplete)
+ {
+ super(xml);
+ _metaDataComplete = metadataComplete;
+ }
+
+ @Override
+ public void parse(XmlParser parser) throws Exception
+ {
+ }
+
+ @Override
+ public void processVersion()
+ {
+ }
+
+ @Override
+ public void processOrdering()
+ {
+ }
+
+ @Override
+ public void processDistributable()
+ {
+ }
+
+ @Override
+ public int getMajorVersion()
+ {
+ return 4;
+ }
+
+ @Override
+ public int getMinorVersion()
+ {
+ return 0;
+ }
+ }
+
+ @Test
+ public void testAnnotationDecorator(WorkDir workDir) throws Exception
+ {
+ Path docroot = workDir.getEmptyPathDir();
+ Path dummyDescriptor = docroot.resolve("dummy.xml");
+ Files.createFile(dummyDescriptor);
+ Resource dummyResource = ResourceFactory.root().newResource(dummyDescriptor);
+
+ assertThrows(NullPointerException.class, () -> new AnnotationDecorator(null));
+
+ WebAppContext context = new WebAppContext();
+ AnnotationDecorator decorator = new AnnotationDecorator(context);
+ ServletE servlet = new ServletE();
+ //test without BaseHolder metadata
+ decorator.decorate(servlet);
+ LifeCycleCallbackCollection callbacks = (LifeCycleCallbackCollection)context.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+ assertNotNull(callbacks);
+ assertFalse(callbacks.getPreDestroyCallbacks().isEmpty());
+
+ //reset
+ context.removeAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+
+ //test with BaseHolder metadata, should not introspect with metadata-complete==true
+ context.getMetaData().setWebDescriptor(new TestWebDescriptor(dummyResource, true));
+ assertTrue(context.getMetaData().isMetaDataComplete());
+ ServletHolder holder = new ServletHolder(new Source(Source.Origin.DESCRIPTOR));
+ holder.setHeldClass(ServletE.class);
+ context.getServletHandler().addServlet(holder);
+ DecoratedObjectFactory.associateInfo(holder);
+ decorator = new AnnotationDecorator(context);
+ decorator.decorate(servlet);
+ DecoratedObjectFactory.disassociateInfo();
+ callbacks = (LifeCycleCallbackCollection)context.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+ assertNull(callbacks);
+
+ //reset
+ context.removeAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+
+ //test with BaseHolder metadata, should introspect with metadata-complete==false
+ context.getMetaData().setWebDescriptor(new TestWebDescriptor(dummyResource, false));
+ DecoratedObjectFactory.associateInfo(holder);
+ decorator = new AnnotationDecorator(context);
+ decorator.decorate(servlet);
+ DecoratedObjectFactory.disassociateInfo();
+ callbacks = (LifeCycleCallbackCollection)context.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
+ assertNotNull(callbacks);
+ assertFalse(callbacks.getPreDestroyCallbacks().isEmpty());
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationIntrospector.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationIntrospector.java
new file mode 100644
index 00000000000..a368bf002f5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationIntrospector.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.nio.file.Path;
+
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.Source;
+import org.eclipse.jetty.ee11.webapp.FragmentDescriptor;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.ee11.webapp.WebDescriptor;
+import org.eclipse.jetty.logging.StacklessLogging;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestAnnotationIntrospector
+{
+ @Test
+ public void testIsIntrospectable() throws Exception
+ {
+ try (StacklessLogging ignore = new StacklessLogging(AnnotationIntrospector.class))
+ {
+ WebAppContext wac = new WebAppContext();
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ //can't introspect nothing
+ assertFalse(introspector.isIntrospectable(null, null));
+
+ //can introspect if no metadata to say otherwise
+ assertTrue(introspector.isIntrospectable(new Object(), null));
+
+ //can introspect if metdata isn't a BaseHolder
+ assertTrue(introspector.isIntrospectable(new Object(), new Object()));
+
+ //an EMBEDDED sourced servlet can be introspected
+ ServletHolder holder = new ServletHolder();
+ holder.setHeldClass(ServletE.class);
+ assertTrue(introspector.isIntrospectable(new ServletE(), holder));
+
+ //a JAKARTA API sourced servlet can be introspected
+ holder = new ServletHolder(Source.JAKARTA_API);
+ holder.setHeldClass(ServletE.class);
+ assertTrue(introspector.isIntrospectable(new ServletE(), holder));
+
+ //an ANNOTATION sourced servlet can be introspected
+ holder = new ServletHolder(new Source(Source.Origin.ANNOTATION, ServletE.class));
+ holder.setHeldClass(ServletE.class);
+ assertTrue(introspector.isIntrospectable(new ServletE(), holder));
+
+ //a DESCRIPTOR sourced servlet can be introspected if web.xml metdata-complete==false
+ Path xml = MavenTestingUtils.getTestResourcePathFile("web31false.xml");
+ Resource xmlResource = wac.getResourceFactory().newResource(xml);
+ wac.getMetaData().setWebDescriptor(new WebDescriptor(xmlResource));
+ holder = new ServletHolder(new Source(Source.Origin.DESCRIPTOR, xmlResource));
+ assertTrue(introspector.isIntrospectable(new ServletE(), holder));
+
+ //a DESCRIPTOR sourced servlet can be introspected if web-fragment.xml medata-complete==false && web.xml metadata-complete==false
+ xml = MavenTestingUtils.getTestResourcePathFile("web-fragment4false.xml");
+ xmlResource = wac.getResourceFactory().newResource(xml);
+ Resource parent = wac.getResourceFactory().newResource(xml.getParent());
+ wac.getMetaData().addFragmentDescriptor(parent, new FragmentDescriptor(xmlResource));
+ holder = new ServletHolder(new Source(Source.Origin.DESCRIPTOR, xmlResource));
+ assertTrue(introspector.isIntrospectable(new ServletE(), holder));
+
+ //a DESCRIPTOR sourced servlet cannot be introspected if web-fragment.xml medata-complete==true (&& web.xml metadata-complete==false)
+ xml = MavenTestingUtils.getTestResourcePathFile("web-fragment4true.xml");
+ xmlResource = wac.getResourceFactory().newResource(xml);
+ parent = wac.getResourceFactory().newResource(xml.getParent());
+ wac.getMetaData().addFragmentDescriptor(parent, new FragmentDescriptor(xmlResource));
+ holder = new ServletHolder(new Source(Source.Origin.DESCRIPTOR, xmlResource));
+ assertFalse(introspector.isIntrospectable(new ServletE(), holder));
+
+ //a DESCRIPTOR sourced servlet cannot be introspected if web.xml medata-complete==true
+ xml = MavenTestingUtils.getTestResourcePathFile("web31true.xml");
+ xmlResource = wac.getResourceFactory().newResource(xml);
+ wac.getMetaData().setWebDescriptor(new WebDescriptor(xmlResource));
+ holder = new ServletHolder(new Source(Source.Origin.DESCRIPTOR, xmlResource));
+ assertFalse(introspector.isIntrospectable(new ServletE(), holder));
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationParser.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationParser.java
new file mode 100644
index 00000000000..3df623a98f5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestAnnotationParser.java
@@ -0,0 +1,333 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestAnnotationParser
+{
+ public static class TrackingAnnotationHandler extends AnnotationParser.AbstractHandler
+ {
+ private final String annotationName;
+ public final Set foundClasses;
+
+ public TrackingAnnotationHandler(String annotationName)
+ {
+ this.annotationName = annotationName;
+ this.foundClasses = new HashSet<>();
+ }
+
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotation)
+ {
+ if (!annotationName.equals(annotation))
+ return;
+ foundClasses.add(info.getClassName());
+ }
+ }
+
+ public static class DuplicateClassScanHandler extends AnnotationParser.AbstractHandler
+ {
+ private Map> _classMap = new ConcurrentHashMap<>();
+
+ @Override
+ public void handle(AnnotationParser.ClassInfo info)
+ {
+ List list = new CopyOnWriteArrayList<>();
+ Resource r = info.getContainingResource();
+ list.add((r == null ? "" : r.toString()));
+
+ List existing = _classMap.putIfAbsent(info.getClassName(), list);
+ if (existing != null)
+ {
+ existing.addAll(list);
+ }
+ }
+
+ public List getParsedList(String classname)
+ {
+ return _classMap.get(classname);
+ }
+ }
+
+ @Test
+ public void testSampleAnnotation(WorkDir workDir) throws Exception
+ {
+ Path root = workDir.getEmptyPathDir();
+ copyClass(ClassA.class, root);
+
+ AnnotationParser parser = new AnnotationParser();
+
+ class SampleAnnotationHandler extends AnnotationParser.AbstractHandler
+ {
+ private List methods = Arrays.asList("a", "b", "c", "d", "l");
+
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotation)
+ {
+ if (!Sample.class.getName().equals(annotation))
+ return;
+
+ assertEquals(ClassA.class.getName(), info.getClassName());
+ }
+
+ @Override
+ public void handle(AnnotationParser.FieldInfo info, String annotation)
+ {
+ if (!Sample.class.getName().equals(annotation))
+ return;
+ assertEquals("m", info.getFieldName());
+ assertEquals(org.objectweb.asm.Type.OBJECT, org.objectweb.asm.Type.getType(info.getFieldType()).getSort());
+ }
+
+ @Override
+ public void handle(AnnotationParser.MethodInfo info, String annotation)
+ {
+ if (!Sample.class.getName().equals(annotation))
+ return;
+ assertEquals(ClassA.class.getName(), info.getClassInfo().getClassName());
+ assertThat(info.getMethodName(), is(in(methods)));
+ assertEquals(Sample.class.getName(), annotation);
+ }
+ }
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ parser.parse(Collections.singleton(new SampleAnnotationHandler()), resourceFactory.newResource(root));
+ }
+ }
+
+ @Test
+ public void testMultiAnnotation(WorkDir workDir) throws Exception
+ {
+ Path root = workDir.getEmptyPathDir();
+ copyClass(ClassB.class, root);
+ AnnotationParser parser = new AnnotationParser();
+
+ class MultiAnnotationHandler extends AnnotationParser.AbstractHandler
+ {
+ @Override
+ public void handle(AnnotationParser.ClassInfo info, String annotation)
+ {
+ if (!Multi.class.getName().equals(annotation))
+ return;
+ assertEquals(ClassB.class.getName(), info.getClassName());
+ }
+
+ @Override
+ public void handle(AnnotationParser.FieldInfo info, String annotation)
+ {
+ assertNotEquals(Multi.class.getName(), annotation, "There should not be any");
+ }
+
+ @Override
+ public void handle(AnnotationParser.MethodInfo info, String annotation)
+ {
+ if (!Multi.class.getName().equals(annotation))
+ return;
+ assertEquals(ClassB.class.getName(), info.getClassInfo().getClassName());
+ assertEquals("a", info.getMethodName());
+ }
+ }
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ parser.parse(Collections.singleton(new MultiAnnotationHandler()), resourceFactory.newResource(root));
+ }
+ }
+
+ @Test
+ public void testHiddenFilesInJar() throws Exception
+ {
+ Path badClassesJar = MavenTestingUtils.getTestResourcePathFile("bad-classes.jar");
+ AnnotationParser parser = new AnnotationParser();
+ Set emptySet = Collections.emptySet();
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ parser.parse(emptySet, resourceFactory.newResource(badClassesJar));
+ // only the valid classes inside bad-classes.jar should be parsed. If any invalid classes are parsed and exception would be thrown here
+ }
+ }
+
+ @Test
+ public void testModuleInfoClassInJar() throws Exception
+ {
+ Path badClassesJar = MavenTestingUtils.getTestResourcePathFile("jdk9/slf4j-api-1.8.0-alpha2.jar");
+ AnnotationParser parser = new AnnotationParser();
+ Set emptySet = Collections.emptySet();
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ parser.parse(emptySet, resourceFactory.newResource(badClassesJar));
+ // Should throw no exceptions, and happily skip the module-info.class files
+ }
+ }
+
+ @Test
+ public void testJep238MultiReleaseInJar() throws Exception
+ {
+ Path badClassesJar = MavenTestingUtils.getTestResourcePathFile("jdk9/log4j-api-2.9.0.jar");
+ AnnotationParser parser = new AnnotationParser();
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ // Should throw no exceptions and work with the META-INF/versions without incident
+ parser.parse(Collections.emptySet(), resourceFactory.newResource(badClassesJar));
+
+ //check for a class that is only in versions 9
+ Map parsed = parser.getParsedClassNames();
+ URI processIdUtilURI = parsed.get("org.apache.logging.log4j.util.ProcessIdUtil");
+ assertNotNull(processIdUtilURI);
+ if (Runtime.version().feature() > 17)
+ assertThat(processIdUtilURI.toString(), containsString("META-INF/versions/9"));
+ }
+ }
+
+ @Test
+ public void testJep238MultiReleaseInJarJDK10() throws Exception
+ {
+ Path jdk10Jar = MavenTestingUtils.getTestResourcePathFile("jdk10/multirelease-10.jar");
+ AnnotationParser parser = new AnnotationParser();
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ // Should throw no exceptions
+ parser.parse(Collections.emptySet(), resourceFactory.newResource(jdk10Jar));
+
+ Map parsed = parser.getParsedClassNames();
+ assertEquals(3, parsed.size());
+ assertThat(parsed.keySet(), containsInAnyOrder("hello.DetailedVer", "hello.Greetings", "hello.Hello"));
+ if (Runtime.version().feature() > 17)
+ assertThat(parsed.get("hello.Greetings").toString(), containsString("META-INF/versions/10"));
+ }
+ }
+
+ @Test
+ public void testBasedirExclusion(WorkDir workDir) throws Exception
+ {
+ Path testdir = workDir.getEmptyPathDir();
+ // Build up basedir, which itself has a path segment that violates java package and classnaming.
+ // The basedir should have no effect on annotation scanning.
+ // Intentionally using a base directory name that starts with a "."
+ // This mimics what you see in jenkins, hudson, hadoop, solr, camel, and selenium for their
+ // installed and/or managed webapps
+ Path basedir = testdir.resolve(".base/workspace/classes");
+ FS.ensureEmpty(basedir);
+
+ // Copy in class that is known to have annotations.
+ copyClass(ClassA.class, basedir);
+
+ // Setup Tracker
+ TrackingAnnotationHandler tracker = new TrackingAnnotationHandler(Sample.class.getName());
+
+ // Setup annotation scanning
+ AnnotationParser parser = new AnnotationParser();
+
+ // Parse
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ parser.parse(Collections.singleton(tracker), resourceFactory.newResource(basedir));
+ }
+
+ // Validate
+ assertThat("Found Class", tracker.foundClasses, contains(ClassA.class.getName()));
+ }
+
+ @Test
+ public void testScanDuplicateClassesInJars() throws Exception
+ {
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ Resource testJar = resourceFactory.newResource(MavenTestingUtils.getTargetPath("test-classes/tinytest.jar"));
+ Resource testJar2 = resourceFactory.newResource(MavenTestingUtils.getTargetPath("test-classes/tinytest_copy.jar"));
+ AnnotationParser parser = new AnnotationParser();
+ DuplicateClassScanHandler handler = new DuplicateClassScanHandler();
+ Set handlers = Collections.singleton(handler);
+ parser.parse(handlers, testJar);
+ parser.parse(handlers, testJar2);
+ List locations = handler.getParsedList("org.acme.ClassOne");
+ assertNotNull(locations);
+ assertEquals(2, locations.size());
+ assertNotEquals(locations.get(0), locations.get(1));
+ }
+ }
+
+ @Test
+ public void testScanDuplicateClasses() throws Exception
+ {
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ Resource testJar = resourceFactory.newResource(MavenTestingUtils.getTargetFile("test-classes/tinytest.jar").toPath());
+ File testClasses = new File(MavenTestingUtils.getTargetDir(), "test-classes");
+ AnnotationParser parser = new AnnotationParser();
+ DuplicateClassScanHandler handler = new DuplicateClassScanHandler();
+ Set handlers = Collections.singleton(handler);
+ parser.parse(handlers, testJar);
+ parser.parse(handlers, resourceFactory.newResource(testClasses.toPath()));
+ List locations = handler.getParsedList("org.acme.ClassOne");
+ assertNotNull(locations);
+ assertEquals(2, locations.size());
+ assertNotEquals(locations.get(0), locations.get(1));
+ }
+ }
+
+ private void copyClass(Class> clazz, Path outputDir) throws IOException, URISyntaxException
+ {
+ String classRef = TypeUtil.toClassReference(clazz);
+ URL url = this.getClass().getResource('/' + classRef);
+ assertThat("URL for: " + classRef, url, notNullValue());
+
+ Path srcClass = Paths.get(url.toURI());
+ Path dest = outputDir.resolve(classRef);
+ FS.ensureDirExists(dest.getParent());
+ Files.copy(srcClass, dest);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestDiscoveredServletContainerInitializerHolder.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestDiscoveredServletContainerInitializerHolder.java
new file mode 100644
index 00000000000..34960d01bf2
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestDiscoveredServletContainerInitializerHolder.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.servlet.ServletContainerInitializer;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.HandlesTypes;
+import org.eclipse.jetty.ee11.servlet.Source;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+public class TestDiscoveredServletContainerInitializerHolder
+{
+ /**
+ * A marker type that is passed as an arg to @HandlesTypes
+ */
+ interface Ordinary
+ {
+
+ }
+
+ /**
+ * An class with an annotation (that is listed in @HandlesTypes)
+ */
+ @Sample(value = 1)
+ public static class ASample
+ {
+ }
+
+ /**
+ * A class that extends a class with an annotation
+ */
+ public static class BSample extends ASample
+ {
+ }
+
+ @HandlesTypes({Sample.class})
+ public static class SampleServletContainerInitializer implements ServletContainerInitializer
+ {
+ @Override
+ public void onStartup(Set> c, ServletContext ctx) throws ServletException
+ {
+ }
+ }
+
+ @Test
+ public void test() throws Exception
+ {
+ //SCI with @HandlesTypes[Ordinary, Sample]
+ SampleServletContainerInitializer sci = new SampleServletContainerInitializer();
+
+ AnnotationConfiguration.DiscoveredServletContainerInitializerHolder holder =
+ new AnnotationConfiguration.DiscoveredServletContainerInitializerHolder(new Source(Source.Origin.ANNOTATION, sci.getClass()),
+ sci);
+
+ //add the @HandlesTypes to the holder
+ holder.addStartupClasses(Ordinary.class, Sample.class);
+
+ //pretend scanned and discovered that ASample has the Sample annotation
+ holder.addStartupClasses(ASample.class.getName());
+
+ //pretend we scanned the entire class hierarchy and found:
+ // com.acme.tom and com.acme.dick both extend Ordinary
+ // ASample has subclass BSample
+ Map> classMap = new HashMap<>();
+ classMap.put(Ordinary.class.getName(), new HashSet(Arrays.asList("com.acme.tom", "com.acme.dick")));
+ classMap.put(ASample.class.getName(), new HashSet(Arrays.asList(BSample.class.getName())));
+ holder.resolveClasses(classMap);
+
+ //we should now have the following classes that will be passed to the SampleServletContainerInitializer.onStartup
+ String toString = holder.toString();
+ assertThat(toString, containsString("com.acme.tom"));
+ assertThat(toString, containsString("com.acme.dick"));
+ assertThat(toString, containsString(ASample.class.getName()));
+ assertThat(toString, containsString(BSample.class.getName()));
+ assertThat(toString, containsString("applicable=[],annotated=[]"));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestRunAsAnnotation.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestRunAsAnnotation.java
new file mode 100644
index 00000000000..5b13374f57a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestRunAsAnnotation.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.ee11.webapp.WebDescriptor;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestRunAsAnnotation
+{
+
+ @Test
+ public void testRunAsAnnotation(WorkDir workDir) throws Exception
+ {
+ Path tmpPath = workDir.getEmptyPathDir();
+ WebAppContext wac = new WebAppContext();
+
+ //pre-add a servlet but not by descriptor
+ ServletHolder holder = new ServletHolder();
+ holder.setName("foo1");
+ holder.setHeldClass(ServletC.class);
+ holder.setInitOrder(1); //load on startup
+ wac.getServletHandler().addServletWithMapping(holder, "/foo/*");
+
+ //add another servlet of the same class, but as if by descriptor
+ ServletHolder holder2 = new ServletHolder();
+ holder2.setName("foo2");
+ holder2.setHeldClass(ServletC.class);
+ holder2.setInitOrder(1);
+ wac.getServletHandler().addServletWithMapping(holder2, "/foo2/*");
+ Path fakeXml = tmpPath.resolve("fake.xml");
+ Files.createFile(fakeXml);
+ wac.getMetaData().setOrigin(holder2.getName() + ".servlet.run-as", new WebDescriptor(wac.getResourceFactory().newResource(fakeXml)));
+
+ AnnotationIntrospector parser = new AnnotationIntrospector(wac);
+ RunAsAnnotationHandler handler = new RunAsAnnotationHandler(wac);
+ parser.registerHandler(handler);
+ parser.introspect(new ServletC(), null);
+
+ assertEquals("admin", holder.getRunAsRole());
+ assertNull(holder2.getRunAsRole());
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestSecurityAnnotationConversions.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestSecurityAnnotationConversions.java
new file mode 100644
index 00000000000..a027bf11c70
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestSecurityAnnotationConversions.java
@@ -0,0 +1,340 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.util.List;
+
+import jakarta.servlet.annotation.HttpConstraint;
+import jakarta.servlet.annotation.HttpMethodConstraint;
+import jakarta.servlet.annotation.ServletSecurity;
+import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
+import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee;
+import jakarta.servlet.http.HttpServlet;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.ServletMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintAware;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.security.Constraint;
+import org.eclipse.jetty.security.Constraint.Transport;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TestSecurityAnnotationConversions
+{
+ @ServletSecurity(value = @HttpConstraint(value = EmptyRoleSemantic.DENY))
+ public static class DenyServlet extends HttpServlet
+ {
+ }
+
+ @ServletSecurity
+ public static class PermitServlet extends HttpServlet
+ {
+ }
+
+ @ServletSecurity(value = @HttpConstraint(value = EmptyRoleSemantic.PERMIT, transportGuarantee = TransportGuarantee.CONFIDENTIAL, rolesAllowed =
+ {
+ "tom", "dick", "harry"
+ }))
+ public static class RolesServlet extends HttpServlet
+ {
+ }
+
+ @ServletSecurity(value = @HttpConstraint(value = EmptyRoleSemantic.PERMIT, transportGuarantee = TransportGuarantee.CONFIDENTIAL, rolesAllowed =
+ {
+ "tom", "dick", "harry"
+ }), httpMethodConstraints = {@HttpMethodConstraint(value = "GET")})
+ public static class Method1Servlet extends HttpServlet
+ {
+ }
+
+ @ServletSecurity(
+ value = @HttpConstraint(
+ value = EmptyRoleSemantic.PERMIT,
+ transportGuarantee = TransportGuarantee.CONFIDENTIAL,
+ rolesAllowed = {
+ "tom", "dick", "harry"
+ }),
+ httpMethodConstraints = {
+ @HttpMethodConstraint(value = "GET", transportGuarantee = TransportGuarantee.CONFIDENTIAL)
+ })
+ public static class Method2Servlet extends HttpServlet
+ {
+ }
+
+ public void setUp()
+ {
+ }
+
+ @Test
+ public void testDenyAllOnClass() throws Exception
+ {
+ WebAppContext wac = makeWebAppContext(DenyServlet.class.getCanonicalName(), "denyServlet", new String[]{
+ "/foo/*", "*.foo"
+ });
+
+ //Assume we found 1 servlet with a @HttpConstraint with value=EmptyRoleSemantic.DENY security annotation
+ ServletSecurityAnnotationHandler annotationHandler = new ServletSecurityAnnotationHandler(wac);
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ introspector.registerHandler(annotationHandler);
+
+ //set up the expected outcomes:
+ //1 ConstraintMapping per ServletMapping pathSpec
+ Constraint expectedConstraint = new Constraint.Builder()
+ .authorization(Constraint.Authorization.FORBIDDEN)
+ .transport(Transport.ANY)
+ .build();
+
+ ConstraintMapping[] expectedMappings = new ConstraintMapping[2];
+
+ expectedMappings[0] = new ConstraintMapping();
+ expectedMappings[0].setConstraint(expectedConstraint);
+ expectedMappings[0].setPathSpec("/foo/*");
+
+ expectedMappings[1] = new ConstraintMapping();
+ expectedMappings[1].setConstraint(expectedConstraint);
+ expectedMappings[1].setPathSpec("*.foo");
+
+ introspector.introspect(new DenyServlet(), null);
+
+ compareResults(expectedMappings, ((ConstraintAware)wac.getSecurityHandler()).getConstraintMappings());
+ }
+
+ @Test
+ public void testPermitAll() throws Exception
+ {
+ //Assume we found 1 servlet with a @ServletSecurity security annotation
+ WebAppContext wac = makeWebAppContext(PermitServlet.class.getCanonicalName(), "permitServlet", new String[]{
+ "/foo/*", "*.foo"
+ });
+
+ ServletSecurityAnnotationHandler annotationHandler = new ServletSecurityAnnotationHandler(wac);
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ introspector.registerHandler(annotationHandler);
+
+ //set up the expected outcomes - no constraints at all as per Servlet Spec 3.1 pg 129
+ //1 ConstraintMapping per ServletMapping pathSpec
+
+ ConstraintMapping[] expectedMappings = new ConstraintMapping[]{};
+ PermitServlet permit = new PermitServlet();
+ introspector.introspect(permit, null);
+
+ compareResults(expectedMappings, ((ConstraintAware)wac.getSecurityHandler()).getConstraintMappings());
+ }
+
+ @Test
+ public void testRolesAllowedWithTransportGuarantee() throws Exception
+ {
+ //Assume we found 1 servlet with annotation with roles defined and
+ //and a TransportGuarantee
+
+ WebAppContext wac = makeWebAppContext(RolesServlet.class.getCanonicalName(), "rolesServlet", new String[]{
+ "/foo/*", "*.foo"
+ });
+
+ ServletSecurityAnnotationHandler annotationHandler = new ServletSecurityAnnotationHandler(wac);
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ introspector.registerHandler(annotationHandler);
+
+ //set up the expected outcomes:compareResults
+ //1 ConstraintMapping per ServletMapping
+ Constraint expectedConstraint = new Constraint.Builder()
+ .roles("tom", "dick", "harry")
+ .transport(Transport.SECURE)
+ .build();
+
+ ConstraintMapping[] expectedMappings = new ConstraintMapping[2];
+ expectedMappings[0] = new ConstraintMapping();
+ expectedMappings[0].setConstraint(expectedConstraint);
+ expectedMappings[0].setPathSpec("/foo/*");
+
+ expectedMappings[1] = new ConstraintMapping();
+ expectedMappings[1].setConstraint(expectedConstraint);
+ expectedMappings[1].setPathSpec("*.foo");
+ introspector.introspect(new RolesServlet(), null);
+ compareResults(expectedMappings, ((ConstraintAware)wac.getSecurityHandler()).getConstraintMappings());
+ }
+
+ @Test
+ public void testMethodAnnotation() throws Exception
+ {
+ //ServletSecurity annotation with HttpConstraint of TransportGuarantee.CONFIDENTIAL, and a list of rolesAllowed, and
+ //an HttpMethodConstraint for GET method that permits all and has TransportGuarantee.NONE (ie is default)
+
+ WebAppContext wac = makeWebAppContext(Method1Servlet.class.getCanonicalName(), "method1Servlet", new String[]{
+ "/foo/*", "*.foo"
+ });
+
+ //set up the expected outcomes: - a Constraint for the RolesAllowed on the class
+ //with userdata constraint of DC_CONFIDENTIAL
+ //and mappings for each of the pathSpecs
+ Constraint expectedConstraint1 = new Constraint.Builder()
+ .roles("tom", "dick", "harry")
+ .transport(Transport.SECURE)
+ .build();
+
+ //a Constraint for the PermitAll on the doGet method
+ Constraint expectedConstraint2 = Constraint.ALLOWED_ANY_TRANSPORT;
+
+ ConstraintMapping[] expectedMappings = new ConstraintMapping[4];
+ expectedMappings[0] = new ConstraintMapping();
+ expectedMappings[0].setConstraint(expectedConstraint1);
+ expectedMappings[0].setPathSpec("/foo/*");
+ expectedMappings[0].setMethodOmissions(new String[]{"GET"});
+ expectedMappings[1] = new ConstraintMapping();
+ expectedMappings[1].setConstraint(expectedConstraint1);
+ expectedMappings[1].setPathSpec("*.foo");
+ expectedMappings[1].setMethodOmissions(new String[]{"GET"});
+
+ expectedMappings[2] = new ConstraintMapping();
+ expectedMappings[2].setConstraint(expectedConstraint2);
+ expectedMappings[2].setPathSpec("/foo/*");
+ expectedMappings[2].setMethod("GET");
+ expectedMappings[3] = new ConstraintMapping();
+ expectedMappings[3].setConstraint(expectedConstraint2);
+ expectedMappings[3].setPathSpec("*.foo");
+ expectedMappings[3].setMethod("GET");
+
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ ServletSecurityAnnotationHandler annotationHandler = new ServletSecurityAnnotationHandler(wac);
+ introspector.registerHandler(annotationHandler);
+ introspector.introspect(new Method1Servlet(), null);
+ compareResults(expectedMappings, ((ConstraintAware)wac.getSecurityHandler()).getConstraintMappings());
+ }
+
+ @Test
+ public void testMethodAnnotation2() throws Exception
+ {
+ //A ServletSecurity annotation that has HttpConstraint of CONFIDENTIAL with defined roles, but a
+ //HttpMethodConstraint for GET that permits all, but also requires CONFIDENTIAL
+ WebAppContext wac = makeWebAppContext(Method2Servlet.class.getCanonicalName(), "method2Servlet", new String[]{
+ "/foo/*", "*.foo"
+ });
+
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ ServletSecurityAnnotationHandler annotationHandler = new ServletSecurityAnnotationHandler(wac);
+ introspector.registerHandler(annotationHandler);
+
+ //set up the expected outcomes: - a Constraint for the RolesAllowed on the class
+ //with userdata constraint of DC_CONFIDENTIAL
+ //and mappings for each of the pathSpecs
+ Constraint expectedConstraint1 = new Constraint.Builder()
+ .roles("tom", "dick", "harry")
+ .transport(Transport.SECURE).build();
+
+ //a Constraint for the Permit on the GET method with a userdata
+ //constraint of DC_CONFIDENTIAL
+ Constraint expectedConstraint2 = new Constraint.Builder()
+ .authorization(Constraint.Authorization.ALLOWED)
+ .transport(Transport.SECURE)
+ .build();
+
+ ConstraintMapping[] expectedMappings = new ConstraintMapping[4];
+ expectedMappings[0] = new ConstraintMapping();
+ expectedMappings[0].setConstraint(expectedConstraint1);
+ expectedMappings[0].setPathSpec("/foo/*");
+ expectedMappings[0].setMethodOmissions(new String[]{"GET"});
+ expectedMappings[1] = new ConstraintMapping();
+ expectedMappings[1].setConstraint(expectedConstraint1);
+ expectedMappings[1].setPathSpec("*.foo");
+ expectedMappings[1].setMethodOmissions(new String[]{"GET"});
+
+ expectedMappings[2] = new ConstraintMapping();
+ expectedMappings[2].setConstraint(expectedConstraint2);
+ expectedMappings[2].setPathSpec("/foo/*");
+ expectedMappings[2].setMethod("GET");
+ expectedMappings[3] = new ConstraintMapping();
+ expectedMappings[3].setConstraint(expectedConstraint2);
+ expectedMappings[3].setPathSpec("*.foo");
+ expectedMappings[3].setMethod("GET");
+
+ introspector.introspect(new Method2Servlet(), null);
+ compareResults(expectedMappings, ((ConstraintAware)wac.getSecurityHandler()).getConstraintMappings());
+ }
+
+ private void compareResults(ConstraintMapping[] expectedMappings, List actualMappings)
+ {
+ assertNotNull(actualMappings);
+ assertEquals(expectedMappings.length, actualMappings.size());
+
+ for (ConstraintMapping am : actualMappings)
+ {
+ boolean matched = false;
+
+ for (int i = 0; i < expectedMappings.length && !matched; i++)
+ {
+ ConstraintMapping em = expectedMappings[i];
+ if (em.getPathSpec().equals(am.getPathSpec()))
+ {
+ if ((em.getMethod() == null && am.getMethod() == null) || em.getMethod() != null && em.getMethod().equals(am.getMethod()))
+ {
+ matched = true;
+
+ assertEquals(em.getConstraint().getAuthorization(), am.getConstraint().getAuthorization());
+ assertEquals(em.getConstraint().getTransport(), am.getConstraint().getTransport());
+ if (em.getMethodOmissions() == null)
+ {
+ assertNull(am.getMethodOmissions());
+ }
+ else
+ {
+ assertArrayEquals(am.getMethodOmissions(), em.getMethodOmissions());
+ }
+
+ if (em.getConstraint().getRoles() == null)
+ {
+ assertNull(am.getConstraint().getRoles());
+ }
+ else
+ {
+ assertThat(am.getConstraint().getRoles(), Matchers.equalTo(em.getConstraint().getRoles()));
+ }
+ }
+ }
+ }
+
+ if (!matched)
+ fail("No expected ConstraintMapping matching method:" + am.getMethod() + " pathSpec: " + am.getPathSpec());
+ }
+ }
+
+ private WebAppContext makeWebAppContext(String className, String servletName, String[] paths)
+ {
+ WebAppContext wac = new WebAppContext();
+
+ ServletHolder[] holders = new ServletHolder[1];
+ holders[0] = new ServletHolder();
+ holders[0].setClassName(className);
+ holders[0].setName(servletName);
+ holders[0].setServletHandler(wac.getServletHandler());
+ wac.getServletHandler().setServlets(holders);
+ wac.setSecurityHandler(new ConstraintSecurityHandler());
+
+ ServletMapping[] servletMappings = new ServletMapping[1];
+ servletMappings[0] = new ServletMapping();
+
+ servletMappings[0].setPathSpecs(paths);
+ servletMappings[0].setServletName(servletName);
+ wac.getServletHandler().setServletMappings(servletMappings);
+ return wac;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestServletAnnotations.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestServletAnnotations.java
new file mode 100644
index 00000000000..63678d12f8a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/TestServletAnnotations.java
@@ -0,0 +1,318 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.ServletMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee11.webapp.DiscoveredAnnotation;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * TestServletAnnotations
+ */
+@ExtendWith(WorkDirExtension.class)
+public class TestServletAnnotations
+{
+ public class TestWebServletAnnotationHandler extends WebServletAnnotationHandler
+ {
+ List _list = null;
+
+ public TestWebServletAnnotationHandler(WebAppContext context, List list)
+ {
+ super(context);
+ _list = list;
+ }
+
+ @Override
+ public void addAnnotation(DiscoveredAnnotation a)
+ {
+ super.addAnnotation(a);
+ _list.add(a);
+ }
+ }
+
+ @Test
+ public void testServletAnnotation(WorkDir workDir) throws Exception
+ {
+ Path root = workDir.getEmptyPathDir();
+ copyClass(org.eclipse.jetty.ee11.annotations.ServletC.class, root);
+
+ AnnotationParser parser = new AnnotationParser();
+
+ WebAppContext wac = new WebAppContext();
+ List results = new ArrayList();
+
+ TestWebServletAnnotationHandler handler = new TestWebServletAnnotationHandler(wac, results);
+
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ parser.parse(Collections.singleton(handler), resourceFactory.newResource(root));
+ }
+
+ assertEquals(1, results.size());
+ assertTrue(results.get(0) instanceof WebServletAnnotation);
+
+ results.get(0).apply();
+
+ ServletHolder[] holders = wac.getServletHandler().getServlets();
+ assertNotNull(holders);
+ assertEquals(1, holders.length);
+
+ // Verify servlet annotations
+ ServletHolder cholder = holders[0];
+ assertThat("Servlet Name", cholder.getName(), is("CServlet"));
+ assertThat("InitParameter[x]", cholder.getInitParameter("x"), is("y"));
+ assertThat("Init Order", cholder.getInitOrder(), is(2));
+ assertThat("Async Supported", cholder.isAsyncSupported(), is(false));
+
+ // Verify mappings
+ ServletMapping[] mappings = wac.getServletHandler().getServletMappings();
+ assertNotNull(mappings);
+ assertEquals(1, mappings.length);
+ String[] paths = mappings[0].getPathSpecs();
+ assertNotNull(paths);
+ assertEquals(2, paths.length);
+ }
+
+ @Test
+ public void testWebServletAnnotationOverrideDefault()
+ {
+ //if the existing servlet mapping TO A DIFFERENT SERVLET IS from a default descriptor we
+ //DO allow the annotation to replace the mapping.
+
+ WebAppContext wac = new WebAppContext();
+ ServletHolder defaultServlet = new ServletHolder();
+ defaultServlet.setClassName("org.eclipse.jetty.ee11.servlet.DefaultServlet");
+ defaultServlet.setName("default");
+ wac.getServletHandler().addServlet(defaultServlet);
+
+ ServletMapping m = new ServletMapping();
+ m.setPathSpec("/");
+ m.setServletName("default");
+ m.setFromDefaultDescriptor(true); //this mapping will be from a default descriptor
+ wac.getServletHandler().addServletMapping(m);
+
+ WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.ee11.annotations.ServletD", null);
+ annotation.apply();
+
+ //test that as the original servlet mapping had only 1 pathspec, then the whole
+ //servlet mapping should be deleted as that pathspec will be remapped to the DServlet
+ ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
+ assertNotNull(resultMappings);
+ assertEquals(1, resultMappings.length);
+ assertEquals(2, resultMappings[0].getPathSpecs().length);
+ resultMappings[0].getServletName().equals("DServlet");
+ for (String s : resultMappings[0].getPathSpecs())
+ {
+ assertThat(s, anyOf(is("/"), is("/bah/*")));
+ }
+ }
+
+ @Test
+ public void testWebServletAnnotationReplaceDefault()
+ {
+ //if the existing servlet mapping TO A DIFFERENT SERVLET IS from a default descriptor we
+ //DO allow the annotation to replace the mapping.
+ WebAppContext wac = new WebAppContext();
+ ServletHolder defaultServlet = new ServletHolder();
+ defaultServlet.setClassName("org.eclipse.jetty.ee11.servlet.DefaultServlet");
+ defaultServlet.setName("default");
+ wac.getServletHandler().addServlet(defaultServlet);
+
+ ServletMapping m = new ServletMapping();
+ m.setPathSpec("/");
+ m.setServletName("default");
+ m.setFromDefaultDescriptor(true); //this mapping will be from a default descriptor
+ wac.getServletHandler().addServletMapping(m);
+
+ ServletMapping m2 = new ServletMapping();
+ m2.setPathSpec("/other");
+ m2.setServletName("default");
+ m2.setFromDefaultDescriptor(true); //this mapping will be from a default descriptor
+ wac.getServletHandler().addServletMapping(m2);
+
+ WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.ee11.annotations.ServletD", null);
+ annotation.apply();
+
+ //test that only the mapping for "/" was removed from the mappings to the default servlet
+ ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
+ assertNotNull(resultMappings);
+ assertEquals(2, resultMappings.length);
+ for (ServletMapping r : resultMappings)
+ {
+ if (r.getServletName().equals("default"))
+ {
+ assertEquals(1, r.getPathSpecs().length);
+ assertEquals("/other", r.getPathSpecs()[0]);
+ }
+ else if (r.getServletName().equals("DServlet"))
+ {
+ assertEquals(2, r.getPathSpecs().length);
+ for (String p : r.getPathSpecs())
+ {
+ if (!p.equals("/") && !p.equals("/bah/*"))
+ fail("Unexpected path");
+ }
+ }
+ else
+ fail("Unexpected servlet mapping: " + r);
+ }
+ }
+
+ @Test
+ public void testWebServletAnnotationNotOverride()
+ {
+ //if the existing servlet mapping TO A DIFFERENT SERVLET IS NOT from a default descriptor we
+ //DO NOT allow the annotation to replace the mapping
+ WebAppContext wac = new WebAppContext();
+ ServletHolder servlet = new ServletHolder();
+ servlet.setClassName("org.eclipse.jetty.ee11.servlet.FooServlet");
+ servlet.setName("foo");
+ wac.getServletHandler().addServlet(servlet);
+ ServletMapping m = new ServletMapping();
+ m.setPathSpec("/");
+ m.setServletName("foo");
+ wac.getServletHandler().addServletMapping(m);
+
+ WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.ee11.annotations.ServletD", null);
+ annotation.apply();
+
+ ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
+ assertEquals(2, resultMappings.length);
+ for (ServletMapping r : resultMappings)
+ {
+ if (r.getServletName().equals("DServlet"))
+ {
+ assertEquals(2, r.getPathSpecs().length);
+ }
+ else if (r.getServletName().equals("foo"))
+ {
+ assertEquals(1, r.getPathSpecs().length);
+ }
+ else
+ fail("Unexpected servlet name: " + r);
+ }
+ }
+
+ @Test
+ public void testWebServletAnnotationIgnore()
+ {
+ //an existing servlet OF THE SAME NAME has even 1 non-default mapping we can't use
+ //any of the url mappings in the annotation
+ WebAppContext wac = new WebAppContext();
+ ServletHolder servlet = new ServletHolder();
+ servlet.setClassName("org.eclipse.jetty.ee11.servlet.OtherDServlet");
+ servlet.setName("DServlet");
+ wac.getServletHandler().addServlet(servlet);
+
+ ServletMapping m = new ServletMapping();
+ m.setPathSpec("/default");
+ m.setFromDefaultDescriptor(true);
+ m.setServletName("DServlet");
+ wac.getServletHandler().addServletMapping(m);
+
+ ServletMapping m2 = new ServletMapping();
+ m2.setPathSpec("/other");
+ m2.setServletName("DServlet");
+ wac.getServletHandler().addServletMapping(m2);
+
+ WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.ee11.annotations.ServletD", null);
+ annotation.apply();
+
+ ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
+ assertEquals(2, resultMappings.length);
+
+ for (ServletMapping r : resultMappings)
+ {
+ assertEquals(1, r.getPathSpecs().length);
+ if (!r.getPathSpecs()[0].equals("/default") && !r.getPathSpecs()[0].equals("/other"))
+ fail("Unexpected path in mapping: " + r);
+ }
+ }
+
+ @Test
+ public void testWebServletAnnotationNoMappings()
+ {
+ //an existing servlet OF THE SAME NAME has no mappings, therefore all mappings in the annotation
+ //should be accepted
+ WebAppContext wac = new WebAppContext();
+ ServletHolder servlet = new ServletHolder();
+ servlet.setName("foo");
+ wac.getServletHandler().addServlet(servlet);
+
+ WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.ee11.annotations.ServletD", null);
+ annotation.apply();
+
+ ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
+ assertEquals(1, resultMappings.length);
+ assertEquals(2, resultMappings[0].getPathSpecs().length);
+ for (String s : resultMappings[0].getPathSpecs())
+ {
+ assertThat(s, anyOf(is("/"), is("/bah/*")));
+ }
+ }
+
+ @Test
+ public void testDeclareRoles()
+ {
+ WebAppContext wac = new WebAppContext();
+ ConstraintSecurityHandler sh = new ConstraintSecurityHandler();
+ wac.setSecurityHandler(sh);
+ sh.setRoles(Set.of("humpty", "dumpty"));
+ DeclareRolesAnnotationHandler handler = new DeclareRolesAnnotationHandler(wac);
+ handler.doHandle(ServletC.class);
+ assertThat(sh.getKnownRoles(), containsInAnyOrder("humpty", "alice", "dumpty"));
+ }
+
+ private void copyClass(Class> clazz, Path outputDir) throws IOException, URISyntaxException
+ {
+ String classRef = TypeUtil.toClassReference(clazz);
+ URL url = this.getClass().getResource('/' + classRef);
+ assertThat("URL for: " + classRef, url, notNullValue());
+
+ Path srcClass = Paths.get(url.toURI());
+ Path dest = outputDir.resolve(classRef);
+ FS.ensureDirExists(dest.getParent());
+ Files.copy(srcClass, dest);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/ResourceA.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/ResourceA.java
new file mode 100644
index 00000000000..8976ed248ee
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/ResourceA.java
@@ -0,0 +1,115 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations.resources;
+
+import java.io.IOException;
+
+import jakarta.annotation.Resource;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+/**
+ * ResourceA
+ */
+public class ResourceA implements jakarta.servlet.Servlet
+{
+ private Integer e;
+ private Integer h;
+ private Integer k;
+
+ @Resource(name = "myf", mappedName = "resB") //test giving both a name and mapped name from the environment
+ private Integer f; //test a non inherited field that needs injection
+
+ @Resource(mappedName = "resA") //test the default naming scheme but using a mapped name from the environment
+ private Integer g;
+
+ @Resource(name = "resA") //test using the given name as the name from the environment
+ private Integer j;
+
+ @Resource(mappedName = "resB") //test using the default name on an inherited field
+ protected Integer n; //TODO - if it's inherited, is it supposed to use the classname of the class it is inherited by?
+
+ @Resource(name = "mye", mappedName = "resA", type = Integer.class)
+ public void setE(Integer e)
+ {
+ this.e = e;
+ }
+
+ public Integer getE()
+ {
+ return this.e;
+ }
+
+ public Integer getF()
+ {
+ return this.f;
+ }
+
+ public Integer getG()
+ {
+ return this.g;
+ }
+
+ public Integer getJ()
+ {
+ return this.j;
+ }
+
+ @Resource(mappedName = "resA")
+ public void setH(Integer h)
+ {
+ this.h = h;
+ }
+
+ @Resource(name = "resA")
+ public void setK(Integer k)
+ {
+ this.k = k;
+ }
+
+ public void x()
+ {
+ System.err.println("ResourceA.x");
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public ServletConfig getServletConfig()
+ {
+ return null;
+ }
+
+ @Override
+ public String getServletInfo()
+ {
+ return null;
+ }
+
+ @Override
+ public void init(ServletConfig arg0) throws ServletException
+ {
+ }
+
+ @Override
+ public void service(ServletRequest arg0, ServletResponse arg1)
+ throws ServletException, IOException
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/ResourceB.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/ResourceB.java
new file mode 100644
index 00000000000..2121d8991ad
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/ResourceB.java
@@ -0,0 +1,39 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations.resources;
+
+import jakarta.annotation.Resource;
+import jakarta.annotation.Resources;
+
+/**
+ * ResourceB
+ */
+@Resources({
+ @Resource(name = "peach", mappedName = "resA"),
+ @Resource(name = "pear", mappedName = "resB")
+})
+public class ResourceB extends ResourceA
+{
+ @Resource(mappedName = "resB")
+ private Integer f; //test no inheritance of private fields
+
+ @Resource
+ private Integer p = 8; //test no injection because no value
+
+ //test no annotation
+ public void z()
+ {
+ System.err.println("ResourceB.z");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/TestResourceAnnotations.java b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/TestResourceAnnotations.java
new file mode 100644
index 00000000000..e9800c1fc78
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/java/org/eclipse/jetty/ee11/annotations/resources/TestResourceAnnotations.java
@@ -0,0 +1,168 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.annotations.resources;
+
+import java.lang.reflect.Field;
+import java.util.Set;
+import javax.naming.Context;
+import javax.naming.InitialContext;
+
+import org.eclipse.jetty.ee11.annotations.AnnotationIntrospector;
+import org.eclipse.jetty.ee11.annotations.ResourceAnnotationHandler;
+import org.eclipse.jetty.ee11.annotations.ResourcesAnnotationHandler;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.plus.annotation.Injection;
+import org.eclipse.jetty.plus.annotation.InjectionCollection;
+import org.eclipse.jetty.plus.jndi.EnvEntry;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class TestResourceAnnotations
+{
+ private Server server;
+ private WebAppContext wac;
+ private InjectionCollection injections;
+ private Context comp;
+ private Context env;
+ private Object objA = 1000;
+ private Object objB = 2000;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ server = new Server();
+ wac = new WebAppContext();
+ wac.setServer(server);
+ injections = new InjectionCollection();
+ wac.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
+ InitialContext ic = new InitialContext();
+ comp = (Context)ic.lookup("java:comp");
+ env = comp.createSubcontext("env");
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ comp.destroySubcontext("env");
+ }
+
+ @Test
+ public void testResourceAnnotations()
+ throws Exception
+ {
+ new EnvEntry(server, "resA", objA, false);
+ new EnvEntry(server, "resB", objB, false);
+
+ AnnotationIntrospector parser = new AnnotationIntrospector(wac);
+ ResourceAnnotationHandler handler = new ResourceAnnotationHandler(wac);
+ parser.registerHandler(handler);
+
+ ResourceA resourceA = new ResourceA();
+ ResourceB resourceB = new ResourceB();
+ parser.introspect(resourceA, null);
+ parser.introspect(resourceB, null);
+
+ //processing classA should give us these jndi name bindings:
+ // java:comp/env/myf
+ // java:comp/env/org.eclipse.jetty.annotations.resources.ResourceA/g
+ // java:comp/env/mye
+ // java:comp/env/org.eclipse.jetty.annotations.resources.ResourceA/h
+ // java:comp/env/resA
+ // java:comp/env/org.eclipse.jetty.annotations.resources.ResourceB/f
+ // java:comp/env/org.eclipse.jetty.annotations.resources.ResourceA/n
+ //
+ assertEquals(objB, env.lookup("myf"));
+ assertEquals(objA, env.lookup("mye"));
+ assertEquals(objA, env.lookup("resA"));
+ assertEquals(objA, env.lookup("org.eclipse.jetty.ee11.annotations.resources.ResourceA/g"));
+ assertEquals(objA, env.lookup("org.eclipse.jetty.ee11.annotations.resources.ResourceA/h"));
+ assertEquals(objB, env.lookup("org.eclipse.jetty.ee11.annotations.resources.ResourceB/f"));
+ assertEquals(objB, env.lookup("org.eclipse.jetty.ee11.annotations.resources.ResourceA/n"));
+
+ //we should have Injections
+ assertNotNull(injections);
+
+ Set resBInjections = injections.getInjections(ResourceB.class.getName());
+ assertNotNull(resBInjections);
+
+ //only 1 field injection because the other has no Resource mapping
+ assertEquals(1, resBInjections.size());
+ Injection fi = resBInjections.iterator().next();
+ assertEquals("f", fi.getTarget().getName());
+
+ //3 method injections on class ResourceA, 4 field injections
+ Set resAInjections = injections.getInjections(ResourceA.class.getName());
+ assertNotNull(resAInjections);
+ assertEquals(7, resAInjections.size());
+ int fieldCount = 0;
+ int methodCount = 0;
+ for (Injection x : resAInjections)
+ {
+ if (x.isField())
+ fieldCount++;
+ else
+ methodCount++;
+ }
+ assertEquals(4, fieldCount);
+ assertEquals(3, methodCount);
+
+ //test injection
+ ResourceB binst = new ResourceB();
+ injections.inject(binst);
+
+ //check injected values
+ Field f = ResourceB.class.getDeclaredField("f");
+ f.setAccessible(true);
+ assertEquals(objB, f.get(binst));
+
+ //@Resource(mappedName="resA") //test the default naming scheme but using a mapped name from the environment
+ f = ResourceA.class.getDeclaredField("g");
+ f.setAccessible(true);
+ assertEquals(objA, f.get(binst));
+
+ //@Resource(name="resA") //test using the given name as the name from the environment
+ f = ResourceA.class.getDeclaredField("j");
+ f.setAccessible(true);
+ assertEquals(objA, f.get(binst));
+
+ //@Resource(mappedName="resB") //test using the default name on an inherited field
+ f = ResourceA.class.getDeclaredField("n");
+ f.setAccessible(true);
+ assertEquals(objB, f.get(binst));
+ }
+
+ @Test
+ public void testResourcesAnnotation()
+ throws Exception
+ {
+ new EnvEntry(server, "resA", objA, false);
+ new EnvEntry(server, "resB", objB, false);
+
+ AnnotationIntrospector introspector = new AnnotationIntrospector(wac);
+ ResourcesAnnotationHandler handler = new ResourcesAnnotationHandler(wac);
+ introspector.registerHandler(handler);
+ ResourceA resourceA = new ResourceA();
+ ResourceB resourceB = new ResourceB();
+ introspector.introspect(resourceA, null);
+ introspector.introspect(resourceB, null);
+
+ assertEquals(objA, env.lookup("peach"));
+ assertEquals(objB, env.lookup("pear"));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/bad-classes.jar b/jetty-ee11/jetty-ee11-annotations/src/test/resources/bad-classes.jar
new file mode 100644
index 00000000000..5538c18b552
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/resources/bad-classes.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk10/multirelease-10.jar b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk10/multirelease-10.jar
new file mode 100644
index 00000000000..a824bc10f1c
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk10/multirelease-10.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk9/log4j-api-2.9.0.jar b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk9/log4j-api-2.9.0.jar
new file mode 100644
index 00000000000..ab98d40e9a2
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk9/log4j-api-2.9.0.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk9/slf4j-api-1.8.0-alpha2.jar b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk9/slf4j-api-1.8.0-alpha2.jar
new file mode 100644
index 00000000000..7a2a9b2d8e4
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jdk9/slf4j-api-1.8.0-alpha2.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/jetty-logging.properties b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..0c65bb4ec58
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/resources/jetty-logging.properties
@@ -0,0 +1,6 @@
+# Jetty Logging using jetty-slf4j-impl
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.annotations.LEVEL=DEBUG
+#org.eclipse.jetty.util.MultiReleaseJarFile.LEVEL=DEBUG
+#org.eclipse.jetty.util.resources.LEVEL=DEBUG
+#org.eclipse.jetty.ee11.annotations.AnnotationParser.LEVEL=DEBUG
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/tinytest.jar b/jetty-ee11/jetty-ee11-annotations/src/test/resources/tinytest.jar
new file mode 100644
index 00000000000..4041f0e8a1b
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/resources/tinytest.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/tinytest_copy.jar b/jetty-ee11/jetty-ee11-annotations/src/test/resources/tinytest_copy.jar
new file mode 100644
index 00000000000..db720508d0d
Binary files /dev/null and b/jetty-ee11/jetty-ee11-annotations/src/test/resources/tinytest_copy.jar differ
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/web-fragment4false.xml b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web-fragment4false.xml
new file mode 100644
index 00000000000..4a8871e8c2c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web-fragment4false.xml
@@ -0,0 +1,11 @@
+
+
+
+ ardvaark
+
+
+
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/web-fragment4true.xml b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web-fragment4true.xml
new file mode 100644
index 00000000000..1738993b944
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web-fragment4true.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ badger
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/web25.xml b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web25.xml
new file mode 100644
index 00000000000..da2e65b6007
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web25.xml
@@ -0,0 +1,10 @@
+
+
+
+ Test 2.5 WebApp
+
+
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/web31false.xml b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web31false.xml
new file mode 100644
index 00000000000..81750df9dda
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web31false.xml
@@ -0,0 +1,11 @@
+
+
+
+ Test 31 WebApp
+
+
diff --git a/jetty-ee11/jetty-ee11-annotations/src/test/resources/web31true.xml b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web31true.xml
new file mode 100644
index 00000000000..768c5ea212f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-annotations/src/test/resources/web31true.xml
@@ -0,0 +1,11 @@
+
+
+
+ Test 31 WebApp
+
+
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/pom.xml b/jetty-ee11/jetty-ee11-apache-jsp/pom.xml
new file mode 100644
index 00000000000..ed33316aeb7
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/pom.xml
@@ -0,0 +1,95 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-apache-jsp
+ EE11 :: Apache JSP
+
+
+ ${project.groupId}.apache-jsp
+ true
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.mortbay.jasper
+ apache-jsp
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.eclipse.jetty
+ jetty-http-tools
+ test
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ test
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ test
+
+
+ org.eclipse.jetty.toolchain
+ jetty-test-helper
+ test
+
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ Jetty-specific ServletContainerInitializer for Jasper
+ org.eclipse.jetty.ee11.apache.jsp.*;version="${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}", org.eclipse.jetty.ee11.jsp.*;version="${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}"
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional
+ osgi.serviceloader;osgi.serviceloader=jakarta.servlet.ServletContainerInitializer,osgi.serviceloader;osgi.serviceloader=org.apache.juli.logging.Log
+ <_nouses>true
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ nolog-jar
+
+ jar
+
+
+ nolog
+
+ META-INF/services/org.apache.juli.logging.Log
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/config/modules/ee11-apache-jsp.mod b/jetty-ee11/jetty-ee11-apache-jsp/src/main/config/modules/ee11-apache-jsp.mod
new file mode 100644
index 00000000000..a475fe461d0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/config/modules/ee11-apache-jsp.mod
@@ -0,0 +1,26 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables use of the apache implementation of JSP.
+
+[environment]
+ee11
+
+[depend]
+ee11-servlet
+ee11-annotations
+
+[ini]
+ee11.jakarta.el.api.version?=@jakarta.el.api.version@
+ee11.jakarta.servlet.jsp.api.version?=@jakarta.servlet.jsp.api.version@
+eclipse.jdt.ecj.version?=@eclipse.jdt.ecj.version@
+ee11.jsp.impl.version?=@jsp.impl.version@
+
+[lib]
+lib/ee11-apache-jsp/jakarta.el.jakarta.el-api-${ee11.jakarta.el.api.version}.jar
+lib/ee11-apache-jsp/jakarta.servlet.jsp.jakarta.servlet.jsp-api-${ee11.jakarta.servlet.jsp.api.version}.jar
+lib/ee11-apache-jsp/org.eclipse.jdt.ecj-${eclipse.jdt.ecj.version}.jar
+lib/ee11-apache-jsp/org.mortbay.jasper.apache-el-${ee11.jsp.impl.version}.jar
+lib/ee11-apache-jsp/org.mortbay.jasper.apache-jsp-${ee11.jsp.impl.version}.jar
+lib/jetty-ee11-apache-jsp-${jetty.version}.jar
+
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/module-info.java b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/module-info.java
new file mode 100644
index 00000000000..5af260d128d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/module-info.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+module org.eclipse.jetty.ee11.apache.jsp
+{
+ requires java.xml;
+ requires jakarta.servlet;
+ requires org.eclipse.jetty.util;
+ requires org.mortbay.apache.jasper;
+ requires org.slf4j;
+
+ exports org.eclipse.jetty.ee11.apache.jsp;
+ exports org.eclipse.jetty.ee11.jsp;
+
+ provides org.apache.juli.logging.Log with
+ org.eclipse.jetty.ee11.apache.jsp.JuliLog;
+
+ provides jakarta.servlet.ServletContainerInitializer with
+ org.eclipse.jetty.ee11.apache.jsp.JettyJasperInitializer;
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JettyJasperInitializer.java b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JettyJasperInitializer.java
new file mode 100644
index 00000000000..d9622b5ab94
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JettyJasperInitializer.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.apache.jsp;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import jakarta.servlet.ServletContext;
+import org.apache.jasper.servlet.JasperInitializer;
+import org.apache.jasper.servlet.TldScanner;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.xml.sax.SAXException;
+
+/**
+ * JettyJasperInitializer
+ */
+public class JettyJasperInitializer extends JasperInitializer
+{
+ private static final Log LOG = LogFactory.getLog(JasperInitializer.class);
+
+ /**
+ * NullTldScanner
+ *
+ * Does nothing. Used when we can tell that all jsps have been precompiled, in which case
+ * the tlds are not needed.
+ */
+ private final class NullTldScanner extends TldScanner
+ {
+ /**
+ *
+ */
+ private NullTldScanner(ServletContext context, boolean namespaceAware, boolean validation, boolean blockExternal)
+ {
+ super(context, namespaceAware, validation, blockExternal);
+ }
+
+ @Override
+ public void scan() throws IOException, SAXException
+ {
+ return; //do nothing
+ }
+
+ @Override
+ public List getListeners()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void scanJars()
+ {
+ return; //do nothing
+ }
+ }
+
+ /**
+ * Make a TldScanner, and prefeed it the tlds that have already been discovered in jar files
+ * by the MetaInfConfiguration.
+ */
+ @Override
+ public TldScanner newTldScanner(ServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal)
+ {
+ String tmp = context.getInitParameter("org.eclipse.jetty.jsp.precompiled");
+ if (tmp != null && !tmp.isEmpty() && Boolean.valueOf(tmp))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Jsp precompilation detected");
+ return new NullTldScanner(context, namespaceAware, validate, blockExternal);
+ }
+
+ Collection tldUrls = (Collection)context.getAttribute("org.eclipse.jetty.tlds");
+ if (tldUrls != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Tld pre-scan detected");
+ return new JettyTldPreScanned(context, namespaceAware, validate, blockExternal, tldUrls);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Defaulting to jasper tld scanning");
+ return super.newTldScanner(context, namespaceAware, validate, blockExternal);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JettyTldPreScanned.java b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JettyTldPreScanned.java
new file mode 100644
index 00000000000..7e3808bab74
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JettyTldPreScanned.java
@@ -0,0 +1,87 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.apache.jsp;
+
+import java.net.URL;
+import java.util.Collection;
+
+import jakarta.servlet.ServletContext;
+import org.apache.jasper.servlet.TldPreScanned;
+import org.apache.tomcat.util.descriptor.tld.TldResourcePath;
+
+/**
+ * JettyTldPreScanned
+ *
+ * Change to TldPreScanned to not require that the tlds have been
+ * pre-scanned from a jar file, but rather may be files in the
+ * file system.
+ *
+ * This is important for running in the jetty maven plugin
+ * environment in multi-module builds, where modules that contain tlds
+ * may be in the reactor at the same time as a webapp being run with the
+ * plugin. That means that the tlds will be used from their location in
+ * the file system, rather than from their assembled jar.
+ */
+public class JettyTldPreScanned extends TldPreScanned
+{
+ private final Collection _jettyPreScannedURLs;
+
+ public JettyTldPreScanned(ServletContext context, boolean namespaceAware, boolean validation, boolean blockExternal, Collection preScannedTlds)
+ {
+ super(context, namespaceAware, validation, blockExternal, preScannedTlds);
+ _jettyPreScannedURLs = preScannedTlds;
+ }
+
+ @Override
+ public void scanJars()
+ {
+ if (_jettyPreScannedURLs != null)
+ {
+ for (URL url : _jettyPreScannedURLs)
+ {
+ String str = url.toExternalForm();
+ int a = str.indexOf("jar:");
+ int b = str.indexOf("META-INF");
+ if (b < 0)
+ throw new IllegalStateException("Bad tld url: " + str);
+
+ String path = str.substring(b);
+ if (a >= 0)
+ {
+ int c = str.indexOf("!/");
+ String fileUrl = str.substring(a + 4, c);
+ try
+ {
+ parseTld(new TldResourcePath(new URL(fileUrl), null, path));
+ }
+ catch (Exception e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+ else
+ {
+ try
+ {
+ parseTld(new TldResourcePath(url, null, null));
+ }
+ catch (Exception e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JuliLog.java b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JuliLog.java
new file mode 100644
index 00000000000..0e098090ce2
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/apache/jsp/JuliLog.java
@@ -0,0 +1,182 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.apache.jsp;
+
+import org.slf4j.LoggerFactory;
+
+public class JuliLog implements org.apache.juli.logging.Log
+{
+ public static org.apache.juli.logging.Log getInstance(String name)
+ {
+ return new JuliLog(name);
+ }
+
+ private final org.slf4j.Logger _logger;
+
+ public JuliLog()
+ {
+ _logger = LoggerFactory.getLogger("");
+ }
+
+ public JuliLog(String name)
+ {
+ _logger = LoggerFactory.getLogger(name);
+ }
+
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return _logger.isDebugEnabled();
+ }
+
+ @Override
+ public boolean isErrorEnabled()
+ {
+ return _logger.isErrorEnabled();
+ }
+
+ @Override
+ public boolean isFatalEnabled()
+ {
+ return _logger.isErrorEnabled();
+ }
+
+ @Override
+ public boolean isInfoEnabled()
+ {
+ return _logger.isInfoEnabled();
+ }
+
+ @Override
+ public boolean isTraceEnabled()
+ {
+ return _logger.isTraceEnabled();
+ }
+
+ @Override
+ public boolean isWarnEnabled()
+ {
+ return _logger.isWarnEnabled();
+ }
+
+ @Override
+ public void trace(Object message)
+ {
+ if (message instanceof String)
+ _logger.debug((String)message);
+ else
+ _logger.debug("{}", message);
+ }
+
+ @Override
+ public void trace(Object message, Throwable t)
+ {
+ if (message instanceof String)
+ _logger.debug((String)message, t);
+ else
+ _logger.debug("{}", message, t);
+ }
+
+ @Override
+ public void debug(Object message)
+ {
+ if (message instanceof String)
+ _logger.debug((String)message);
+ else
+ _logger.debug("{}", message);
+ }
+
+ @Override
+ public void debug(Object message, Throwable t)
+ {
+ if (message instanceof String)
+ _logger.debug((String)message, t);
+ else
+ _logger.debug("{}", message, t);
+ }
+
+ @Override
+ public void info(Object message)
+ {
+ if (message instanceof String)
+ _logger.info((String)message);
+ else
+ _logger.info("{}", message);
+ }
+
+ @Override
+ public void info(Object message, Throwable t)
+ {
+ if (message instanceof String)
+ _logger.info((String)message, t);
+ else
+ _logger.info("{}", message, t);
+ }
+
+ @Override
+ public void warn(Object message)
+ {
+ if (message instanceof String)
+ _logger.warn((String)message);
+ else
+ _logger.warn("{}", message);
+ }
+
+ @Override
+ public void warn(Object message, Throwable t)
+ {
+ if (message instanceof String)
+ _logger.warn((String)message, t);
+ else
+ _logger.warn("{}", message, t);
+ }
+
+ @Override
+ public void error(Object message)
+ {
+ if (message instanceof String)
+ _logger.warn((String)message);
+ else
+ _logger.warn("{}", message);
+ }
+
+ @Override
+ public void error(Object message, Throwable t)
+ {
+ if (message instanceof String)
+ _logger.warn((String)message, t);
+ else
+ _logger.warn("{}", message, t);
+ }
+
+ @Override
+ public void fatal(Object message)
+ {
+ if (message instanceof String)
+ _logger.warn((String)message);
+ else
+ _logger.warn("{}", message);
+ }
+
+ @Override
+ public void fatal(Object message, Throwable t)
+ {
+ if (message instanceof String)
+ _logger.warn((String)message, t);
+ else
+ _logger.warn("{}", message, t);
+ }
+}
+
+
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/jsp/JettyJspServlet.java b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/jsp/JettyJspServlet.java
new file mode 100644
index 00000000000..5835a52f004
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/java/org/eclipse/jetty/ee11/jsp/JettyJspServlet.java
@@ -0,0 +1,119 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jsp;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.jasper.servlet.JspServlet;
+
+/**
+ * JettyJspServlet
+ *
+ * Wrapper for the jsp servlet that handles receiving requests mapped from
+ * jsp-property-groups. Mappings could be wildcard urls like "/*", which would
+ * include welcome files, but we need those to be handled by the DefaultServlet.
+ */
+public class JettyJspServlet extends JspServlet
+{
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -5387857473125086791L;
+
+ @Override
+ public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ {
+ HttpServletRequest request = null;
+ if (req instanceof HttpServletRequest)
+ request = (HttpServletRequest)req;
+ else
+ throw new ServletException("Request not HttpServletRequest");
+
+ String servletPath = null;
+ String pathInfo = null;
+ if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null)
+ {
+ servletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
+ pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
+ if (servletPath == null)
+ {
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+ }
+ else
+ {
+ servletPath = request.getServletPath();
+ pathInfo = request.getPathInfo();
+ }
+
+ String pathInContext = addPaths(servletPath, pathInfo);
+
+ String jspFile = getInitParameter("jspFile");
+
+ //if this is a forced-path from a jsp-file, we want the jsp servlet to handle it,
+ //otherwise the default servlet might handle it
+ if (jspFile == null)
+ {
+ if (pathInContext != null && pathInContext.endsWith("/"))
+ {
+ //dispatch via forward to the default servlet
+ getServletContext().getNamedDispatcher("default").forward(req, resp);
+ return;
+ }
+ else
+ {
+ //check if it resolves to a directory
+ String realPath = getServletContext().getRealPath(pathInContext);
+ if (realPath != null)
+ {
+ Path asPath = Paths.get(realPath);
+ if (Files.exists(asPath) && Files.isDirectory(asPath))
+ {
+ //dispatch via forward to the default servlet
+ getServletContext().getNamedDispatcher("default").forward(req, resp);
+ return;
+ }
+ }
+ }
+ }
+
+ //fall through to the normal jsp servlet handling
+ super.service(req, resp);
+ }
+
+ /**
+ * @param servletPath the servletPath of the request
+ * @param pathInfo the pathInfo of the request
+ * @return servletPath with pathInfo appended
+ */
+ private String addPaths(String servletPath, String pathInfo)
+ {
+ if (servletPath.isEmpty())
+ return pathInfo;
+
+ if (pathInfo == null)
+ return servletPath;
+
+ return servletPath + pathInfo;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/jetty-ee11/jetty-ee11-apache-jsp/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
new file mode 100644
index 00000000000..b73014705ca
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
@@ -0,0 +1 @@
+org.eclipse.jetty.ee11.apache.jsp.JettyJasperInitializer
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/main/resources/META-INF/services/org.apache.juli.logging.Log b/jetty-ee11/jetty-ee11-apache-jsp/src/main/resources/META-INF/services/org.apache.juli.logging.Log
new file mode 100644
index 00000000000..e1ebb1e1cd7
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/main/resources/META-INF/services/org.apache.juli.logging.Log
@@ -0,0 +1 @@
+org.eclipse.jetty.ee11.apache.jsp.JuliLog
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJettyJspServlet.java b/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJettyJspServlet.java
new file mode 100644
index 00000000000..ef4f8979771
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJettyJspServlet.java
@@ -0,0 +1,121 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jsp;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.jsp.JspFactory;
+import org.apache.jasper.runtime.JspFactoryImpl;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.SimpleInstanceManager;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestJettyJspServlet
+{
+ public WorkDir workdir;
+ private Server _server;
+ private LocalConnector _connector;
+
+ public static class DfltServlet extends HttpServlet
+ {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
+ {
+ resp.setContentType("html/text");
+ resp.getOutputStream().println("This.Is.The.Default.");
+ }
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ JspFactory.setDefaultFactory(new JspFactoryImpl());
+ Path baseDir = MavenTestingUtils.getTestResourcePathDir("base");
+ _server = new Server();
+ _connector = new LocalConnector(_server);
+ _server.addConnector(_connector);
+ ServletContextHandler context = new ServletContextHandler("/context", true, false);
+ _server.setHandler(context);
+ context.setClassLoader(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()));
+ ServletHolder jspHolder = context.addServlet(JettyJspServlet.class, "/*");
+ jspHolder.setInitParameter("scratchdir", workdir.getEmptyPathDir().toString());
+ context.setBaseResourceAsPath(baseDir);
+ context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
+ ServletHolder dfltHolder = new ServletHolder();
+ dfltHolder.setName("default");
+ dfltHolder.setHeldClass(DfltServlet.class);
+ context.addServlet(dfltHolder, "/");
+ _server.start();
+ }
+
+ @AfterEach
+ public void tearDown()
+ {
+ LifeCycle.stop(_server);
+ }
+
+ @Test
+ public void testWithJsp() throws Exception
+ {
+ //test that an ordinary jsp is served by jsp servlet
+ String request =
+ "GET /context/foo.jsp HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+
+ String rawResponse = _connector.getResponse(request);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getContent(), not(containsString("This.Is.The.Default.")));
+ }
+
+ @Disabled //TODO
+ @Test
+ public void testWithDirectory() throws Exception
+ {
+ //test that a dir is served by the default servlet
+ String request =
+ "GET /context/dir HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ String rawResponse = _connector.getResponse(request);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getContent(), containsString("This.Is.The.Default."));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJettyTldPreScanned.java b/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJettyTldPreScanned.java
new file mode 100644
index 00000000000..6ddead0482d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJettyTldPreScanned.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jsp;
+
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tomcat.util.descriptor.tld.TaglibXml;
+import org.apache.tomcat.util.descriptor.tld.TldResourcePath;
+import org.eclipse.jetty.ee11.apache.jsp.JettyTldPreScanned;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * TestJettyTldPreScanned
+ */
+public class TestJettyTldPreScanned
+{
+
+ /**
+ * Test that a tld inside a jar can be scanned, as can a tld not inside a jar.
+ */
+ @Test
+ public void testIt()
+ throws Exception
+ {
+ File jar = MavenTestingUtils.getTestResourceFile("taglib.jar");
+ File tld = MavenTestingUtils.getTestResourceFile("META-INF/foo-taglib.tld");
+
+ List list = new ArrayList<>();
+ list.add(new URL("jar:" + jar.toURI().toURL().toString() + "!/META-INF/bar-taglib.tld"));
+ list.add(tld.toURI().toURL());
+
+ JettyTldPreScanned preScanned = new JettyTldPreScanned(new ServletContextHandler().getServletContext(), false, false, false, list);
+ preScanned.scanJars();
+ Map map = preScanned.getTldResourcePathTaglibXmlMap();
+ assertNotNull(map);
+ assertEquals(2, map.size());
+ for (TldResourcePath p : map.keySet())
+ {
+ URL u = p.getUrl();
+ TaglibXml tlx = map.get(p);
+ assertNotNull(tlx);
+ if (!"foo".equals(tlx.getShortName()) && !"bar".equals(tlx.getShortName()))
+ fail("unknown tag");
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJspFileNameToClass.java b/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJspFileNameToClass.java
new file mode 100644
index 00000000000..21c11dcdc79
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/test/java/org/eclipse/jetty/ee11/jsp/TestJspFileNameToClass.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jsp;
+
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestJspFileNameToClass
+{
+
+ @Test
+ public void testJspFileNameToClassName() throws Exception
+ {
+ ServletHolder h = new ServletHolder();
+ h.setName("test");
+
+ assertEquals(null, h.getClassNameForJsp(null));
+
+ assertEquals(null, h.getClassNameForJsp(""));
+
+ assertEquals(null, h.getClassNameForJsp("/blah/"));
+
+ assertEquals(null, h.getClassNameForJsp("//blah///"));
+
+ assertEquals(null, h.getClassNameForJsp("/a/b/c/blah/"));
+
+ assertEquals("org.apache.jsp.a.b.c.blah", h.getClassNameForJsp("/a/b/c/blah"));
+
+ assertEquals("org.apache.jsp.blah_jsp", h.getClassNameForJsp("/blah.jsp"));
+
+ assertEquals("org.apache.jsp.blah_jsp", h.getClassNameForJsp("//blah.jsp"));
+
+ assertEquals("org.apache.jsp.blah_jsp", h.getClassNameForJsp("blah.jsp"));
+
+ assertEquals("org.apache.jsp.a.b.c.blah_jsp", h.getClassNameForJsp("/a/b/c/blah.jsp"));
+
+ assertEquals("org.apache.jsp.a.b.c.blah_jsp", h.getClassNameForJsp("a/b/c/blah.jsp"));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/META-INF/foo-taglib.tld b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/META-INF/foo-taglib.tld
new file mode 100644
index 00000000000..f13333980ce
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/META-INF/foo-taglib.tld
@@ -0,0 +1,25 @@
+
+
+
+
+
+ 1.0
+ 1.2
+ foo
+ http://www.foo.com/taglib
+ foo example
+
+
+ foodate
+ com.foo.DateTag
+ TAGDEPENDENT
+ Display Date
+
+ tz
+ false
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/base/dir/empty.txt b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/base/dir/empty.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/base/foo.jsp b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/base/foo.jsp
new file mode 100644
index 00000000000..fb73b0b0002
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/base/foo.jsp
@@ -0,0 +1,23 @@
+
+<%@ page import="java.util.Enumeration" %>
+
+
JSP Dump
+
+
+
Request URI:
<%= request.getRequestURI() %>
+
ServletPath:
<%= request.getServletPath() %>
+
PathInfo:
<%= request.getPathInfo() %>
+
+<%
+ Enumeration e =request.getParameterNames();
+ while(e.hasMoreElements())
+ {
+ String name = (String)e.nextElement();
+%>
+
+
getParameter("<%= name %>")
+
<%= request.getParameter(name) %>
+<% } %>
+
+
+
diff --git a/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/taglib.jar b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/taglib.jar
new file mode 100644
index 00000000000..af1aa0c1186
Binary files /dev/null and b/jetty-ee11/jetty-ee11-apache-jsp/src/test/resources/taglib.jar differ
diff --git a/jetty-ee11/jetty-ee11-bom/pom.xml b/jetty-ee11/jetty-ee11-bom/pom.xml
new file mode 100644
index 00000000000..802fe82b47f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-bom/pom.xml
@@ -0,0 +1,176 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+
+ jetty-ee11-bom
+ pom
+ EE11 :: BOM
+ Jetty EE11 APIs BOM artifact
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-annotations
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-cdi
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-fcgi-proxy
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-glassfish-jstl
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jaspi
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jndi
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jspc-maven-plugin
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-plus
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-proxy
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-quickstart
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-runner
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlets
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-webapp
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi-alpn
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi-boot
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi-boot-jsp
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jakarta-client
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jakarta-client-webapp
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jakarta-common
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jakarta-server
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jetty-client-webapp
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jetty-server
+ 12.1.0-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-servlet
+ 12.1.0-SNAPSHOT
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+
+
+ flatten-clean
+
+ clean
+
+ clean
+
+
+ flatten
+
+ flatten
+
+ process-resources
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-cdi/pom.xml b/jetty-ee11/jetty-ee11-cdi/pom.xml
new file mode 100644
index 00000000000..2c9de77d3a5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-cdi
+ jar
+ EE11 :: CDI
+
+ ${project.groupId}.cdi
+
+
+
+ org.eclipse.jetty
+ jetty-util
+ compile
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-annotations
+ compile
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-webapp
+ compile
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"
+ osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.ee11.webapp.Configuration,
+ osgi.serviceloader; osgi.serviceloader=jakarta.servlet.ServletContainerInitializer
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven.surefire.plugin.version}
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/config/etc/cdi/jetty-ee11-cdi.xml b/jetty-ee11/jetty-ee11-cdi/src/main/config/etc/cdi/jetty-ee11-cdi.xml
new file mode 100644
index 00000000000..ce6c9112742
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/config/etc/cdi/jetty-ee11-cdi.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ org.eclipse.jetty.ee11.cdi
+
+
+
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi-decorate.mod b/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi-decorate.mod
new file mode 100644
index 00000000000..fbada7d3d9d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi-decorate.mod
@@ -0,0 +1,20 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Configures Jetty to use the "CdiDecoratingListener" as the default CDI mode.
+This mode that allows a webapp to register it's own CDI decorator.
+
+[environment]
+ee11
+
+[tag]
+cdi
+
+[provides]
+cdi-mode
+
+[depend]
+ee11-cdi
+
+[ini]
+jetty.cdi.mode=CdiDecoratingListener
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi-spi.mod b/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi-spi.mod
new file mode 100644
index 00000000000..86b16715575
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi-spi.mod
@@ -0,0 +1,20 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Configures Jetty to use the "CdiSpiDecorator" as the default CDI mode.
+This mode uses the CDI SPI to integrate an arbitrary CDI implementation.
+
+[environment]
+ee11
+
+[tag]
+cdi
+
+[provides]
+cdi-mode
+
+[depend]
+ee11-cdi
+
+[ini]
+jetty.cdi.mode=CdiSpiDecorator
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi.mod b/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi.mod
new file mode 100644
index 00000000000..7e6d18c6148
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/config/modules/ee11-cdi.mod
@@ -0,0 +1,32 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Provides integration of CDI within webapp to Jetty container object lifecycles.
+This module does not provide CDI, but configures jetty to support various
+integration modes with a CDI implementation on the webapp classpath.
+CDI integration modes can be selected per webapp with the "org.eclipse.jetty.ee11.cdi"
+init parameter or defaults to the mode set by the "org.eclipse.jetty.ee11.cdi" server
+attribute (which is initialised from the "jetty.cdi.mode" start property).
+Supported modes are:
+CdiSpiDecorator - Jetty will call the CDI SPI within the webapp to decorate
+ objects (default).
+CdiDecoratingLister - The webapp may register a decorator on the context attribute
+ "org.eclipse.jetty.ee11.cdi.decorator".
+
+[environment]
+ee11
+
+[tag]
+cdi
+
+[provides]
+cdi
+
+[depend]
+deploy
+
+[xml]
+etc/cdi/jetty-ee11-cdi.xml
+
+[lib]
+lib/jetty-ee11-cdi-${jetty.version}.jar
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/java/module-info.java b/jetty-ee11/jetty-ee11-cdi/src/main/java/module-info.java
new file mode 100644
index 00000000000..7b1bc0bf098
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/java/module-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+module org.eclipse.jetty.ee11.cdi
+{
+ requires org.eclipse.jetty.ee11.annotations;
+
+ requires transitive org.eclipse.jetty.ee11.servlet;
+ requires transitive org.eclipse.jetty.ee11.webapp;
+ requires static jakarta.cdi;
+
+ exports org.eclipse.jetty.ee11.cdi;
+}
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiConfiguration.java b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiConfiguration.java
new file mode 100644
index 00000000000..a42d6dd2afb
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiConfiguration.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.cdi;
+
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee11.plus.webapp.PlusConfiguration;
+import org.eclipse.jetty.ee11.webapp.AbstractConfiguration;
+
+/**
+ *
CDI Configuration
+ *
This configuration configures the WebAppContext server/system classes to
+ * be able to see the {@link CdiServletContainerInitializer}. Also hides the
+ * jakarta cdi classes that are on the environment/server classpath and allows
+ * the webapp to provide their own.
+ *
+ */
+public class CdiConfiguration extends AbstractConfiguration
+{
+ public CdiConfiguration()
+ {
+ super(new Builder()
+ .protectAndExpose("org.eclipse.jetty.ee11.cdi.CdiServletContainerInitializer")
+ .hide(getHiddenClasses())
+ .addDependents(AnnotationConfiguration.class, PlusConfiguration.class));
+ }
+
+ private static String[] getHiddenClasses()
+ {
+ //Only hide the cdi api classes if there is not also an impl on the
+ //environment classpath - vital for embedded uses.
+ if (CdiConfiguration.class.getClassLoader().getResource("META-INF/services/jakarta.enterprise.inject.spi.CDIProvider") == null)
+ return new String[]{"jakarta.enterprise.", "jakarta.decorator."};
+ return new String[0];
+ }
+}
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiDecoratingListener.java b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiDecoratingListener.java
new file mode 100644
index 00000000000..be04a0d9269
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiDecoratingListener.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.cdi;
+
+import org.eclipse.jetty.ee11.servlet.DecoratingListener;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+
+/**
+ * A DecoratingListener that listens for "org.eclipse.jetty.cdi.decorator"
+ */
+public class CdiDecoratingListener extends DecoratingListener
+{
+ public static final String MODE = "CdiDecoratingListener";
+ /**
+ * Attribute used by Weld to communicate to Jetty that it has created a WeldDecorator
+ */
+ public static final String ATTRIBUTE = "org.eclipse.jetty.cdi.decorator";
+
+ public CdiDecoratingListener(ServletContextHandler contextHandler)
+ {
+ super(contextHandler, ATTRIBUTE);
+ contextHandler.setAttribute(CdiServletContainerInitializer.CDI_INTEGRATION_ATTRIBUTE, MODE);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiServletContainerInitializer.java b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiServletContainerInitializer.java
new file mode 100644
index 00000000000..cc62cb7d2be
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiServletContainerInitializer.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.cdi;
+
+import java.util.Objects;
+import java.util.Set;
+
+import jakarta.servlet.ServletContainerInitializer;
+import jakarta.servlet.ServletContext;
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.Loader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
A {@link ServletContainerInitializer} that introspects for a CDI API
+ * implementation within a web application and applies an integration
+ * mode if CDI is found. CDI integration modes can be selected per webapp with
+ * the "org.eclipse.jetty.cdi" init parameter or default to the mode set by the
+ * "org.eclipse.jetty.cdi" server attribute. Supported modes are:
+ *
+ *
CdiSpiDecorator
+ *
Jetty will call the CDI SPI within the webapp to decorate objects (default).
+ *
CdiDecoratingLister
+ *
The webapp may register a decorator on the context attribute
+ * "org.eclipse.jetty.cdi.decorator".
+ *
+ *
+ * @see AnnotationConfiguration.ServletContainerInitializerOrdering
+ */
+public class CdiServletContainerInitializer implements ServletContainerInitializer
+{
+ public static final String CDI_INTEGRATION_ATTRIBUTE = "org.eclipse.jetty.cdi";
+ private static final Logger LOG = LoggerFactory.getLogger(CdiServletContainerInitializer.class);
+
+ @Override
+ public void onStartup(Set> c, ServletContext ctx)
+ {
+ try
+ {
+ ServletContextHandler context = ServletContextHandler.getServletContextHandler(ctx);
+ Objects.requireNonNull(context);
+
+ // Test if CDI is in the webapp by trying to load the CDI class.
+ ClassLoader loader = context.getClassLoader();
+ if (loader == null)
+ Loader.loadClass("jakarta.enterprise.inject.spi.CDI");
+ else
+ loader.loadClass("jakarta.enterprise.inject.spi.CDI");
+
+ String mode = ctx.getInitParameter(CDI_INTEGRATION_ATTRIBUTE);
+ if (mode == null)
+ {
+ mode = (String)context.getServer().getAttribute(CDI_INTEGRATION_ATTRIBUTE);
+ if (mode == null)
+ mode = CdiSpiDecorator.MODE;
+ }
+
+ switch (mode)
+ {
+ case CdiSpiDecorator.MODE:
+ context.getObjectFactory().addDecorator(new CdiSpiDecorator(context));
+ break;
+
+ case CdiDecoratingListener.MODE:
+ context.addEventListener(new CdiDecoratingListener(context));
+ break;
+
+ default:
+ throw new IllegalStateException(mode);
+ }
+ LOG.info("{} enabled in {}", mode, ctx);
+ }
+ catch (UnsupportedOperationException | ClassNotFoundException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("CDI not found in {}", ctx, e);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiSpiDecorator.java b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiSpiDecorator.java
new file mode 100644
index 00000000000..c67c6d9065c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-cdi/src/main/java/org/eclipse/jetty/ee11/cdi/CdiSpiDecorator.java
@@ -0,0 +1,228 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.cdi;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.Decorator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Decorator that invokes the CDI provider within a webapp to decorate objects created by
+ * the contexts {@link org.eclipse.jetty.util.DecoratedObjectFactory}
+ * (typically Listeners, Filters and Servlets).
+ * The CDI provider is invoked using reflection to avoid any CDI instance
+ * or dependencies within the server scope. The code invoked is equivalent to:
+ *
You have successfully authenticated. You can use this url in conjunction with any of the other urls that lead to a login form to test which urls are saved on entry to the login form.
+
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/logonError.html b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/logonError.html
new file mode 100644
index 00000000000..66a83869061
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/logonError.html
@@ -0,0 +1,4 @@
+
+
Authentication ERROR
+Username, password or role incorrect.
+
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/remote.html b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/remote.html
new file mode 100644
index 00000000000..705a6953fe1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/remote.html
@@ -0,0 +1,35 @@
+
+
+ Jetty Demo
+
+
+
+
+
+
+ This is a demo webapp for the Eclipse Jetty HTTP Server and Servlet Container.
+
+
+ This test context serves several demo filters and servlets that are not safe for deployment on the internet, since (by design) they contain cross domain scripting vulnerabilities and reveal private information. This page is displayed because you have accessed this context from a non local IP address.
+
+
+ You can disable the remote address checking by editing demo-base/webapps/demo-jetty.d/demo-jetty-override-web.xml, uncommenting the declaration of the TestFilter, and changing the "remote" init parameter to "true".
+
This demo redirects the request in a manner visible to the user agent, instead of doing an internal rewrite.
+
+
Cookie
+
All pages
+
This demo rule sets a "visited" cookie for each page you visit. The second time you go to any of the links above, you will see an additional line, "Previously visited: yes".
This demo shows how to modify the response code of a page to an error, based on its URL
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/small_powered_by.gif b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/small_powered_by.gif
new file mode 100644
index 00000000000..c5dd44319f0
Binary files /dev/null and b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp/small_powered_by.gif differ
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/ChatServletTest.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/ChatServletTest.java
new file mode 100644
index 00000000000..d1e21e70137
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/ChatServletTest.java
@@ -0,0 +1,88 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11;
+
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.example.ChatServlet;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class ChatServletTest
+{
+ private Server server;
+ private LocalConnector connector;
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ server = new Server();
+ connector = new LocalConnector(server);
+ server.addConnector(connector);
+ ServletContextHandler context = new ServletContextHandler("/");
+ server.setHandler(context);
+ ServletHolder dispatch = context.addServlet(ChatServlet.class, "/chat/*");
+ dispatch.setInitParameter("asyncTimeout", "500");
+ server.start();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testLogin() throws Exception
+ {
+ assertResponse("user=test&join=true&message=has%20joined!", "{\"from\":\"test\",\"chat\":\"has joined!\"}");
+ }
+
+ @Test
+ public void testChat() throws Exception
+ {
+ assertResponse("user=test&join=true&message=has%20joined!", "{\"from\":\"test\",\"chat\":\"has joined!\"}");
+ String response = connector.getResponse(createRequestString("user=test&message=message"));
+ assertThat(response.contains("{"), is(false)); // make sure we didn't get a json body
+ }
+
+ @Test
+ public void testPoll() throws Exception
+ {
+ assertResponse("user=test", "{action:\"poll\"}");
+ }
+
+ private void assertResponse(String requestBody, String expectedResponse) throws Exception
+ {
+ String response = connector.getResponse(createRequestString(requestBody));
+ assertThat(response.contains(expectedResponse), is(true));
+ }
+
+ private String createRequestString(String body)
+ {
+ return "POST /chat/ HTTP/1.1\r\n" +
+ "Host: tester\r\n" +
+ "Content-length: " + body.length() + "\r\n" +
+ "Content-type: application/x-www-form-urlencoded\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ body;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/DispatchServletTest.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/DispatchServletTest.java
new file mode 100644
index 00000000000..595ad056907
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/DispatchServletTest.java
@@ -0,0 +1,148 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11;
+
+import org.eclipse.jetty.ee11.servlet.DefaultServlet;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.example.DispatchServlet;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Simple tests against DispatchServlet.
+ */
+public class DispatchServletTest
+{
+ private Server server;
+ private LocalConnector connector;
+ private ServletContextHandler context;
+
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ server = new Server();
+ connector = new LocalConnector(server);
+ server.addConnector(connector);
+ context = new ServletContextHandler("/tests");
+ server.setHandler(context);
+ server.start();
+ }
+
+ @AfterEach
+ public void tearDown()
+ {
+ LifeCycle.stop(server);
+ }
+
+ /**
+ * As filed in JETTY-978.
+ *
+ * Security problems in demo dispatch servlet.
+ *
+ *
+ *
+ * The dispatcher servlet (org.example.DispatchServlet) is prone to a Denial of
+ * Service vulnerability.
+ *
+ *
+ * This example servlet is meant to be used as a resources dispatcher,
+ * however a malicious aggressor may abuse this functionality in order to
+ * cause a recursive inclusion. In details, it is possible to abuse the
+ * method org.example.DispatchServlet.doGet(DispatchServlet.java:203) forcing
+ * the application to recursively include the "Dispatch" servlet.
+ *
+ *
+ * Dispatch org.example.DispatchServlet 1 Dispatch /dispatch/* As a result, it
+ * is possible to trigger a "java.lang.StackOverflowError" and consequently
+ * an internal server error (500).
+ *
+ *
+ * Multiple requests may easily affect the availability of the servlet
+ * container. Since this attack can cause the server to consume resources in
+ * a non-linear relationship to the size of inputs, it should be considered
+ * as a server flaw.
+ *
+ *
+ * The vulnerability seems confined to the example servlet and it does not
+ * afflict the Jetty's core."
+ *
+ *
+ */
+ @Test
+ public void testSelfRefForwardDenialOfService() throws Exception
+ {
+ ServletHolder dispatch = context.addServlet(DispatchServlet.class, "/dispatch/*");
+ context.addServlet(DefaultServlet.class, "/");
+
+ String request = "GET /tests/dispatch/includeN/" + dispatch.getName() + " HTTP/1.1\n" +
+ "Host: tester\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = connector.getResponse(request);
+
+ String msg = "Response code on SelfRefDoS";
+
+ assertFalse(response.startsWith("HTTP/1.1 500 "), msg + " should not be code 500.");
+ assertTrue(response.startsWith("HTTP/1.1 403 "), msg + " should return error code 403 (Forbidden)");
+ }
+
+ @Test
+ public void testSelfRefDeep() throws Exception
+ {
+ context.addServlet(DispatchServlet.class, "/dispatch/*");
+ context.addServlet(DefaultServlet.class, "/");
+
+ String[] selfRefs =
+ {"/dispatch/forward", "/dispatch/includeS", "/dispatch/includeW", "/dispatch/includeN"};
+
+ /*
+ * Number of nested dispatch requests. 220 is a good value, as it won't
+ * trigger an Error 413 response (Entity too large). Anything larger
+ * than 220 will trigger a 413 response.
+ */
+ int nestedDepth = 220;
+
+ for (String selfRef : selfRefs)
+ {
+ String request = "GET /tests" +
+ selfRef.repeat(nestedDepth) +
+ "/ HTTP/1.1\n" +
+ "Host: tester\n" +
+ "Connection: close\n" +
+ "\n";
+ String response = connector.getResponse(request);
+
+ StringBuilder msg = new StringBuilder();
+ msg.append("Response code on nested \"").append(selfRef).append("\"");
+ msg.append(" (depth:").append(nestedDepth).append(")");
+
+ assertFalse(response.startsWith("HTTP/1.1 413 "),
+ msg + " should not be code 413 (Request Entity Too Large)," +
+ "the nestedDepth in the TestCase is too large (reduce it)");
+
+ assertFalse(response.startsWith("HTTP/1.1 500 "), msg + " should not be code 500.");
+ assertThat(response, Matchers.startsWith("HTTP/1.1 403 "));
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/TestServer.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/TestServer.java
new file mode 100644
index 00000000000..e8b82492cb0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee11/TestServer.java
@@ -0,0 +1,177 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11;
+
+import java.io.FileNotFoundException;
+import java.lang.management.ManagementFactory;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.ee11.webapp.Configurations;
+import org.eclipse.jetty.ee11.webapp.MetaInfConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.jmx.MBeanContainer;
+import org.eclipse.jetty.security.HashLoginService;
+import org.eclipse.jetty.server.CustomRequestLog;
+import org.eclipse.jetty.server.ForwardedRequestCustomizer;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.session.DefaultSessionCache;
+import org.eclipse.jetty.session.FileSessionDataStore;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.Disabled;
+
+@Disabled("Not a test case")
+public class TestServer
+{
+ public static void main(String[] args) throws Exception
+ {
+ Path webappProjectRoot = MavenTestingUtils.getBasePath();
+
+ // Setup Threadpool
+ QueuedThreadPool threadPool = new QueuedThreadPool();
+ threadPool.setMaxThreads(100);
+
+ // Setup server
+ Server server = new Server(threadPool);
+ Configurations.setServerDefault(server);
+ server.manage(threadPool);
+
+ // Setup JMX
+ MBeanContainer mbContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
+ server.addBean(mbContainer);
+
+ // Common HTTP configuration
+ HttpConfiguration config = new HttpConfiguration();
+ config.setSecurePort(8443);
+ config.addCustomizer(new ForwardedRequestCustomizer());
+ config.addCustomizer(new SecureRequestCustomizer());
+ config.setSendDateHeader(true);
+ config.setSendServerVersion(true);
+
+ // Http Connector
+ HttpConnectionFactory http = new HttpConnectionFactory(config);
+ ServerConnector httpConnector = new ServerConnector(server, http);
+ httpConnector.setPort(8080);
+ httpConnector.setIdleTimeout(30000);
+ server.addConnector(httpConnector);
+
+ // Handlers
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+
+ // Add restart handler to test the ability to save sessions and restart
+ /* TODO: figure out how to do this
+ RestartHandler restart = new RestartHandler();
+ restart.setHandler(handlers);
+ server.setHandler(restart);
+ */
+
+ // Setup context
+ HashLoginService login = new HashLoginService();
+ login.setName("Test Realm");
+ Path realmPropPath = webappProjectRoot.resolve("jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/test-realm.properties");
+ if (!Files.exists(realmPropPath))
+ throw new FileNotFoundException(realmPropPath.toString());
+ Resource realmResource = ResourceFactory.of(server).newResource(realmPropPath);
+ login.setConfig(realmResource);
+ server.addBean(login);
+
+ Path logPath = Files.createTempFile("jetty-yyyy_mm_dd", "log");
+ CustomRequestLog requestLog = new CustomRequestLog(logPath.toString());
+ server.setRequestLog(requestLog);
+
+ server.setStopAtShutdown(true);
+
+ WebAppContext webapp = new WebAppContext();
+ webapp.setContextPath("/test");
+ webapp.setParentLoaderPriority(true);
+ Path webappBase = webappProjectRoot.resolve("jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/main/webapp");
+ if (!Files.exists(webappBase))
+ throw new FileNotFoundException(webappBase.toString());
+ webapp.setBaseResource(webapp.getResourceFactory().newResource(webappBase));
+ webapp.setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN,
+ ".*/test-jetty-webapp/target/classes.*$|" +
+ ".*/jakarta.servlet.api-[^/]*\\.jar$|.*/jakarta.servlet.jsp.jstl-.*\\.jar$|.*/org.apache.taglibs.taglibs-standard.*\\.jar$"
+ );
+
+ webapp.setAttribute("testAttribute", "testValue");
+ Path sessionDir = Files.createTempDirectory("sessions");
+ DefaultSessionCache ss = new DefaultSessionCache(webapp.getSessionHandler());
+ FileSessionDataStore sds = new FileSessionDataStore();
+ ss.setSessionDataStore(sds);
+ sds.setStoreDir(sessionDir.toFile());
+ webapp.getSessionHandler().setSessionCache(ss);
+
+ contexts.addHandler(webapp);
+
+ ContextHandler srcroot = new ContextHandler();
+ Path srcRootPath = webappProjectRoot.resolve("jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src");
+ if (!Files.exists(srcRootPath))
+ throw new FileNotFoundException(srcRootPath.toString());
+ srcroot.setBaseResource(ResourceFactory.of(server).newResource(srcRootPath));
+ srcroot.setHandler(new ResourceHandler());
+ srcroot.setContextPath("/src");
+ contexts.addHandler(srcroot);
+
+ server.setHandler(contexts);
+ server.start();
+ server.dumpStdErr();
+
+ server.join();
+ }
+
+ //TODO how to restart server?
+ /*
+ private static class RestartHandler extends HandlerWrapper
+ {
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ super.handle(target, baseRequest, request, response);
+ if (Boolean.valueOf(request.getParameter("restart")))
+ {
+ final Server server = getServer();
+
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(100);
+ server.stop();
+ Thread.sleep(100);
+ server.start();
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to restart server", e);
+ }
+ }
+ }.start();
+ }
+ }
+ }*/
+}
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/jetty-logging.properties b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..28e85c0b13d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/jetty-logging.properties
@@ -0,0 +1,3 @@
+# Jetty Logging using jetty-slf4j-impl
+org.example.LEVEL=INFO
+# org.eclipse.jetty.ee11.annotations.LEVEL=DEBUG
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/test-realm.properties b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/test-realm.properties
new file mode 100644
index 00000000000..9d9bc368493
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jetty-webapp/src/test/resources/test-realm.properties
@@ -0,0 +1,20 @@
+#
+# This file defines users passwords and roles for a HashUserRealm
+#
+# The format is
+# : [, ...]
+#
+# Passwords may be clear text, obfuscated or checksummed. The class
+# org.eclipse.util.Password should be used to generate obfuscated
+# passwords or password checksums
+#
+# If DIGEST Authentication is used, the password must be in a recoverable
+# format, either plain text or OBF:.
+#
+jetty:MD5:164c88b302622e17050af52c89945d44,user
+admin:CRYPT:adpexzg3FUZAk,server-administrator,content-administrator,admin,user
+other:OBF:1xmk1w261u9r1w1c1xmq,user
+plain:plain,user
+user:password,user
+# This entry is for digest auth. The credential is a MD5 hash of username:realmname:password
+digest:MD5:6e120743ad67abfbc385bc2bb754e297,user
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/pom.xml b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/pom.xml
new file mode 100644
index 00000000000..1e42670f66d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demos
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-demo-jndi-webapp
+ war
+ EE11 :: Demo :: JNDI WebApp
+
+ ${project.groupId}.jndi
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ jakarta.transaction
+ jakarta.transaction-api
+ provided
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-mock-resources
+ provided
+
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ ${project.version}
+
+ src/main/templates/plugin-context.xml
+
+ src/main/webapp
+ src/main/webapp/WEB-INF/web.xml
+ /test-jndi
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jndi
+ ${project.version}
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-mock-resources
+ ${project.version}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ copy-dependencies
+
+ package
+
+ jakarta.transaction-api,ee11-demo-mock-resources
+ ${project.build.directory}/lib/jndi
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/demo.d/ee11-demo-jndi.properties b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/demo.d/ee11-demo-jndi.properties
new file mode 100644
index 00000000000..5c25b5bb012
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/demo.d/ee11-demo-jndi.properties
@@ -0,0 +1 @@
+environment: ee11
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/demo.d/ee11-demo-jndi.xml b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/demo.d/ee11-demo-jndi.xml
new file mode 100644
index 00000000000..562fb161826
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/demo.d/ee11-demo-jndi.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /ee11-test-jndi
+ /ee11-demo-jndi.war
+
+ true
+ false
+ true
+
+
+
+
+
+
+ woggle
+ 4000
+ false
+
+
+
+
+
+
+
+ wiggle
+ 100
+ true
+
+
+
+
+
+
+
+ mail/Session
+
+
+ CHANGE-ME
+ CHANGE-ME
+
+
+ false
+ CHANGE-ME
+ CHANGE-ME
+ false
+
+
+
+
+
+
+
+
+
+
+
+ jdbc/mydatasource
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/ee11-demo-jndi.mod b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/ee11-demo-jndi.mod
new file mode 100644
index 00000000000..a5a2da6957e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/config/modules/ee11-demo-jndi.mod
@@ -0,0 +1,29 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Demo JNDI Resources Webapp
+
+[environment]
+ee11
+
+[tags]
+demo
+webapp
+
+[depends]
+ee11-deploy
+ext
+jdbc
+ee11-plus
+ee11-jndi
+ee11-demo-mock-resources
+
+[files]
+basehome:modules/demo.d/ee11-demo-jndi.xml|webapps/ee11-demo-jndi.xml
+maven://org.eclipse.jetty.ee11.demos/jetty-ee11-demo-jndi-webapp/${jetty.version}/war|webapps/ee11-demo-jndi.war
+maven://jakarta.mail/jakarta.mail-api/@jakarta.mail.api.version@/jar|lib/ee11/jakarta.mail-api-@jakarta.mail.api.version@.jar
+maven://jakarta.activation/jakarta.activation-api/@jakarta.activation.api.version@/jar|lib/ee11/jakarta.activation-api-@jakarta.activation.api.version@.jar
+
+[lib]
+lib/ee11/jakarta.mail-api-@jakarta.mail.api.version@.jar
+lib/ee11/jakarta.activation-api-@jakarta.activation.api.version@.jar
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/java/org/example/JNDITest.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/java/org/example/JNDITest.java
new file mode 100644
index 00000000000..8c6bcce5ee3
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-jndi-webapp/src/main/java/org/example/JNDITest.java
@@ -0,0 +1,136 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.example;
+
+import java.io.IOException;
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.transaction.UserTransaction;
+
+/**
+ * JNDITest
+ *
+ * Use JNDI from within Jetty.
+ *
+ * Also, use servlet spec 2.5 resource injection and lifecycle callbacks from within the web.xml
+ * to set up some of the JNDI resources.
+ *
+ */
+public class JNDITest extends HttpServlet
+{
+ private DataSource myDS;
+ private Double wiggle;
+ private Integer woggle;
+ private Double gargle;
+ private String svr;
+
+ private String resourceNameMappingInjectionResult;
+ private String envEntryOverrideResult;
+ private String postConstructResult = "PostConstruct method called: FALSE";
+ private String preDestroyResult = "PreDestroy method called: NOT YET";
+ private String envEntryGlobalScopeResult;
+ private String envEntryWebAppScopeResult;
+ private String userTransactionResult;
+ private String svrResult;
+
+ public void setMyDatasource(DataSource ds)
+ {
+ myDS = ds;
+ }
+
+ private void postConstruct()
+ {
+ resourceNameMappingInjectionResult = "Injection of resource to locally mapped name (java:comp/env/mydatasource as java:comp/env/mydatasource1): " + (myDS != null ? "PASS" : "FAIL");
+ envEntryOverrideResult = "Override of EnvEntry in jetty-env.xml (java:comp/env/wiggle): " + (wiggle == 55.0 ? "PASS" : "FAIL(expected 55.0, got " + wiggle + ")") + "";
+ postConstructResult = "PostConstruct method called: PASS";
+ }
+
+ private void preDestroy()
+ {
+ preDestroyResult = "PreDestroy method called: PASS";
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException
+ {
+ super.init(config);
+ try
+ {
+ InitialContext ic = new InitialContext();
+ woggle = (Integer)ic.lookup("java:comp/env/woggle");
+ envEntryGlobalScopeResult = "EnvEntry defined in context xml lookup result (java:comp/env/woggle): " + (woggle == 4000 ? "PASS" : "FAIL(expected 4000, got " + woggle + ")") + "";
+ gargle = (Double)ic.lookup("java:comp/env/gargle");
+ svr = (String)ic.lookup("java:comp/env/svr");
+ svrResult = "Ref to Server in jetty-env.xml result: " + (svr != null ? "PASS" : "FAIL") + "";
+
+ envEntryWebAppScopeResult = "EnvEntry defined in jetty-env.xml lookup result (java:comp/env/gargle): " + (gargle == 100.0 ? "PASS" : "FAIL(expected 100, got " + gargle + ")") + "";
+ UserTransaction utx = (UserTransaction)ic.lookup("java:comp/UserTransaction");
+ userTransactionResult = "UserTransaction lookup result (java:comp/UserTransaction): " + (utx != null ? "PASS" : "FAIL") + "";
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ doGet(request, response);
+ }
+
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ try
+ {
+ response.setContentType("text/html");
+ ServletOutputStream out = response.getOutputStream();
+ out.println("");
+ out.println("");
+ out.println("
");
+ writer.println(" Result: " + (listener.isResourceInjected() ? " PASS" : " FAIL") + "");
+
+ writer.println("");
+ writer.println("");
+ writer.flush();
+ writer.close();
+
+ asyncContext.complete();
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/Bar.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/Bar.java
new file mode 100644
index 00000000000..172f5ad1fae
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/Bar.java
@@ -0,0 +1,22 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.example.test;
+
+public class Bar
+{
+ @org.example.initializer.Foo(2)
+ public void someMethod()
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/ClassLoaderServlet.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/ClassLoaderServlet.java
new file mode 100644
index 00000000000..9580653e288
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/ClassLoaderServlet.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.example.test;
+
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+@WebServlet(urlPatterns = "/classloader")
+public class ClassLoaderServlet extends HttpServlet
+{
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException
+ {
+ try
+ {
+ PrintWriter writer = resp.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println("");
+ writer.println("
ClassLoader Isolation Test
");
+
+ // TODO uncomment the following once 9.4.19 is released with a fix for #3726
+ /*
+ Class> webappIO = IO.class;
+ URI webappURI = getLocationOfClass(webappIO);
+ String webappVersion = webappIO.getPackage().getImplementationVersion();
+ Class> serverIO = req.getServletContext().getClass().getClassLoader().loadClass("org.eclipse.jetty.util.IO");
+ URI serverURI = getLocationOfClass(serverIO);
+ String serverVersion = serverIO.getPackage().getImplementationVersion();
+
+ writer.printf("
Webapp loaded org.eclipse.jetty.util.IO(%s) from %s%n",webappVersion,webappURI);
+ writer.printf(" Server loaded org.eclipse.jetty.util.IO(%s) from %s%n",serverVersion, serverURI);
+ if (webappVersion.equals(serverVersion))
+ writer.println(" Version Result: FAIL");
+ else
+ writer.println(" Version Result: PASS");
+ if (webappURI.equals(serverURI))
+ writer.println(" URI Result: FAIL
");
+ else
+ writer.println(" URI Result: PASS
");
+ */
+
+ writer.println("");
+ writer.println("");
+ writer.flush();
+ writer.close();
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+
+ public static URI getLocationOfClass(Class> clazz)
+ {
+ try
+ {
+ ProtectionDomain domain = clazz.getProtectionDomain();
+ if (domain != null)
+ {
+ CodeSource source = domain.getCodeSource();
+ if (source != null)
+ {
+ URL location = source.getLocation();
+
+ if (location != null)
+ return location.toURI();
+ }
+ }
+
+ String resourceName = clazz.getName().replace('.', '/') + ".class";
+ ClassLoader loader = clazz.getClassLoader();
+ URL url = (loader == null ? ClassLoader.getSystemClassLoader() : loader).getResource(resourceName);
+ if (url != null)
+ {
+ return getJarSource(url.toURI());
+ }
+ }
+ catch (URISyntaxException e)
+ {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+
+ public static URI getJarSource(URI uri)
+ {
+ try
+ {
+ if (!"jar".equals(uri.getScheme()))
+ return uri;
+ // Get SSP (retaining encoded form)
+ String s = uri.getRawSchemeSpecificPart();
+ int bangSlash = s.indexOf("!/");
+ if (bangSlash >= 0)
+ s = s.substring(0, bangSlash);
+ return new URI(s);
+ }
+ catch (URISyntaxException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/MultiPartTest.java b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/MultiPartTest.java
new file mode 100644
index 00000000000..1ede0103299
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-spec/jetty-ee11-demo-spec-webapp/src/main/java/org/example/test/MultiPartTest.java
@@ -0,0 +1,165 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.example.test;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.annotation.MultipartConfig;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.Part;
+
+/**
+ * MultiPartTest
+ *
+ * Test Servlet 3.0 MultiPart Mime handling.
+ */
+
+@MultipartConfig(location = "foo/bar", maxFileSize = 10240, maxRequestSize = -1, fileSizeThreshold = 2048)
+public class MultiPartTest extends HttpServlet
+{
+ private ServletConfig config;
+
+ @Override
+ public void init(ServletConfig config) throws ServletException
+ {
+ super.init(config);
+ this.config = config;
+ }
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ try
+ {
+ response.setContentType("text/html");
+ ServletOutputStream out = response.getOutputStream();
+ out.println("");
+ out.println("");
+ out.println("");
+ out.println("
+ Demo Web Application Only - Do NOT Deploy in Production
+
+
+
+
Jetty XXX Demo Webapp
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-template/src/main/resources/small_powered_by.gif b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-template/src/main/resources/small_powered_by.gif
new file mode 100644
index 00000000000..c5dd44319f0
Binary files /dev/null and b/jetty-ee11/jetty-ee11-demos/jetty-ee11-demo-template/src/main/resources/small_powered_by.gif differ
diff --git a/jetty-ee11/jetty-ee11-demos/pom.xml b/jetty-ee11/jetty-ee11-demos/pom.xml
new file mode 100644
index 00000000000..1c7333f9e04
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-demos/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demos
+ pom
+ EE11 :: Demos
+
+
+ jetty-ee11-demo-async-rest
+ jetty-ee11-demo-embedded
+ jetty-ee11-demo-jaas-webapp
+ jetty-ee11-demo-jetty-webapp
+ jetty-ee11-demo-jndi-webapp
+ jetty-ee11-demo-jsp-webapp
+ jetty-ee11-demo-mock-resources
+ jetty-ee11-demo-proxy-webapp
+ jetty-ee11-demo-simple-webapp
+ jetty-ee11-demo-spec
+ jetty-ee11-demo-template
+
+
+
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ bogus.*
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-examples/pom.xml b/jetty-ee11/jetty-ee11-examples/pom.xml
new file mode 100644
index 00000000000..c574e89a10f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-examples/pom.xml
@@ -0,0 +1,36 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-examples
+ EE11 :: Examples
+
+
+ true
+
+
+
+
+ org.eclipse.jetty
+ jetty-http
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ ${project.version}
+
+
+ org.hamcrest
+ hamcrest
+
+
+
diff --git a/jetty-ee11/jetty-ee11-examples/src/test/java/org/acme/MyServlet.java b/jetty-ee11/jetty-ee11-examples/src/test/java/org/acme/MyServlet.java
new file mode 100644
index 00000000000..57fa5d2e5f4
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-examples/src/test/java/org/acme/MyServlet.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.acme;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class MyServlet extends HttpServlet
+{
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ {
+ resp.getWriter().println("the servlet says Hello World");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-examples/src/test/java/org/eclipse/jetty/examples/Jetty12Example.java b/jetty-ee11/jetty-ee11-examples/src/test/java/org/eclipse/jetty/examples/Jetty12Example.java
new file mode 100644
index 00000000000..c9592c1a6fa
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-examples/src/test/java/org/eclipse/jetty/examples/Jetty12Example.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.examples;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.util.Callback;
+
+public class Jetty12Example
+{
+ public static void main(String[] args) throws Exception
+ {
+ org.eclipse.jetty.server.Server server = new org.eclipse.jetty.server.Server();
+
+ org.eclipse.jetty.server.ServerConnector connector = new org.eclipse.jetty.server.ServerConnector(server);
+ server.addConnector(connector);
+
+ // Declare server handler collection
+ org.eclipse.jetty.server.handler.ContextHandlerCollection contexts =
+ new org.eclipse.jetty.server.handler.ContextHandlerCollection();
+ server.setHandler(contexts);
+
+ // Add an embedded jetty-12 context
+ org.eclipse.jetty.server.handler.ContextHandler embedded =
+ new org.eclipse.jetty.server.handler.ContextHandler("/embedded");
+ embedded.setHandler(new Handler.Abstract.NonBlocking()
+ {
+ @Override
+ public boolean handle(org.eclipse.jetty.server.Request request,
+ org.eclipse.jetty.server.Response response,
+ Callback callback) throws Exception
+ {
+ response.setStatus(200);
+ response.getHeaders().put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_PLAIN_UTF_8.asString());
+ Content.Sink.write(response, true, "the handler says Hello World", callback);
+ return true;
+ }
+ });
+ contexts.addHandler(embedded);
+
+ // Add an EE11 ServletContext
+ URLClassLoader ee11Loader = new URLClassLoader(
+ new URL[] {new URL("file:lib/ee11/servlet-api-6.0.jar"),
+ new URL("file:lib/ee11/jetty-ee11-servlet.jar")},
+ Jetty12Example.class.getClassLoader());
+ org.eclipse.jetty.server.handler.ContextHandler ee11Context = (org.eclipse.jetty.server.handler.ContextHandler)
+ ee11Loader.loadClass("org.eclipse.jetty.ee11.servlet.ServletContextHandler")
+ .getDeclaredConstructor().newInstance();
+ org.eclipse.jetty.server.Handler ee11Servlet = (org.eclipse.jetty.server.Handler)
+ ee11Loader.loadClass("org.eclipse.jetty.ee11.servlet.ServletHandler")
+ .getDeclaredConstructor().newInstance();
+ ee11Context.setHandler(ee11Servlet);
+ contexts.addHandler(ee11Context);
+ ee11Servlet.getClass().getMethod("addServletWithMapping", String.class, String.class)
+ .invoke(ee11Servlet, "org.acme.MyServlet", "/");
+
+
+ server.start();
+ server.dumpStdErr();
+ server.join();
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/pom.xml b/jetty-ee11/jetty-ee11-fcgi-proxy/pom.xml
new file mode 100644
index 00000000000..5c024c15826
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/pom.xml
@@ -0,0 +1,48 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-fcgi-proxy
+ EE11 :: FCGI Proxy
+
+
+ ${project.groupId}.fcgi.proxy
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-proxy
+
+
+ org.eclipse.jetty.fcgi
+ jetty-fcgi-client
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ test
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ test
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/config/modules/ee11-fcgi-proxy.mod b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/config/modules/ee11-fcgi-proxy.mod
new file mode 100644
index 00000000000..637c514a191
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/config/modules/ee11-fcgi-proxy.mod
@@ -0,0 +1,16 @@
+[description]
+Enables support for EE11 FastCGI proxying.
+
+[environment]
+ee11
+
+[tags]
+fcgi
+proxy
+
+[depends]
+fcgi
+
+[lib]
+lib/jetty-ee11-fcgi-proxy-${jetty.version}.jar
+lib/jetty-ee11-proxy-${jetty.version}.jar
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/module-info.java b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/module-info.java
new file mode 100644
index 00000000000..785070a66ae
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/module-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+module org.eclipse.jetty.ee11.fcgi.proxy
+{
+ requires transitive org.eclipse.jetty.ee11.proxy;
+ requires transitive org.eclipse.jetty.fcgi.client;
+ requires transitive org.eclipse.jetty.server;
+
+ requires static jakarta.servlet;
+
+ exports org.eclipse.jetty.ee11.fcgi.proxy;
+}
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/org/eclipse/jetty/ee11/fcgi/proxy/FastCGIProxyServlet.java b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/org/eclipse/jetty/ee11/fcgi/proxy/FastCGIProxyServlet.java
new file mode 100644
index 00000000000..98c5f9cd2a0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/org/eclipse/jetty/ee11/fcgi/proxy/FastCGIProxyServlet.java
@@ -0,0 +1,306 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.fcgi.proxy;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.Request;
+import org.eclipse.jetty.ee11.proxy.AsyncProxyServlet;
+import org.eclipse.jetty.fcgi.FCGI;
+import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.io.Transport;
+import org.eclipse.jetty.util.ProcessorUtils;
+import org.eclipse.jetty.util.URIUtil;
+
+/**
+ * Specific implementation of {@link org.eclipse.jetty.ee11.proxy.AsyncProxyServlet.Transparent} for FastCGI.
+ *
+ * This servlet accepts an HTTP request and transforms it into a FastCGI request
+ * that is sent to the FastCGI server specified in the {@code proxyTo}
+ * init-param.
+ *
+ * This servlet accepts these additional {@code init-param}s:
+ *
+ *
{@code scriptRoot}, mandatory, that must be set to the directory where
+ * the application that must be served via FastCGI is installed and corresponds to
+ * the FastCGI DOCUMENT_ROOT parameter
+ *
{@code scriptPattern}, optional, defaults to {@code (.+?\.php)},
+ * that specifies a regular expression with at least 1 and at most 2 groups that specify
+ * respectively:
+ *
+ *
the FastCGI SCRIPT_NAME parameter
+ *
the FastCGI PATH_INFO parameter
+ *
+ *
{@code fastCGI.HTTPS}, optional, defaults to false, that specifies whether
+ * to force the FastCGI {@code HTTPS} parameter to the value {@code on}
+ *
{@code fastCGI.envNames}, optional, a comma separated list of environment variable
+ * names read via {@link System#getenv(String)} that are forwarded as FastCGI parameters.
+ *
{@code unixDomainPath}, optional, that specifies the Unix-Domain path the FastCGI
+ * server listens to.
+ *
+ *
+ * @see TryFilesFilter
+ */
+public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent
+{
+ public static final String SCRIPT_ROOT_INIT_PARAM = "scriptRoot";
+ public static final String SCRIPT_PATTERN_INIT_PARAM = "scriptPattern";
+ public static final String ORIGINAL_URI_ATTRIBUTE_INIT_PARAM = "originalURIAttribute";
+ public static final String ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM = "originalQueryAttribute";
+ public static final String FASTCGI_HTTPS_INIT_PARAM = "fastCGI.HTTPS";
+ public static final String FASTCGI_ENV_NAMES_INIT_PARAM = "fastCGI.envNames";
+
+ private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr";
+ private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remotePort";
+ private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverName";
+ private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverAddr";
+ private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverPort";
+ private static final String SCHEME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".scheme";
+ private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI";
+ private static final String REQUEST_QUERY_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestQuery";
+
+ private Pattern scriptPattern;
+ private String originalURIAttribute;
+ private String originalQueryAttribute;
+ private boolean fcgiHTTPS;
+ private Set fcgiEnvNames;
+ private Path unixDomainPath;
+
+ @Override
+ public void init() throws ServletException
+ {
+ super.init();
+
+ String value = getInitParameter(SCRIPT_PATTERN_INIT_PARAM);
+ if (value == null)
+ value = "(.+?\\.php)";
+ scriptPattern = Pattern.compile(value);
+
+ originalURIAttribute = getInitParameter(ORIGINAL_URI_ATTRIBUTE_INIT_PARAM);
+ originalQueryAttribute = getInitParameter(ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM);
+
+ fcgiHTTPS = Boolean.parseBoolean(getInitParameter(FASTCGI_HTTPS_INIT_PARAM));
+
+ fcgiEnvNames = Collections.emptySet();
+ String envNames = getInitParameter(FASTCGI_ENV_NAMES_INIT_PARAM);
+ if (envNames != null)
+ {
+ fcgiEnvNames = Stream.of(envNames.split(","))
+ .map(String::trim)
+ .collect(Collectors.toSet());
+ }
+
+ String path = getInitParameter("unixDomainPath");
+ if (path != null)
+ unixDomainPath = Path.of(path);
+ }
+
+ @Override
+ protected HttpClient newHttpClient()
+ {
+ ServletConfig config = getServletConfig();
+ String scriptRoot = config.getInitParameter(SCRIPT_ROOT_INIT_PARAM);
+ if (scriptRoot == null)
+ throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured");
+
+ int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2);
+ String value = config.getInitParameter("selectors");
+ if (value != null)
+ selectors = Integer.parseInt(value);
+ ClientConnector connector = new ClientConnector();
+ connector.setSelectors(selectors);
+ return new HttpClient(new ProxyHttpClientTransportOverFCGI(connector, scriptRoot));
+ }
+
+ @Override
+ protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyResponse, Request proxyRequest)
+ {
+ proxyRequest.attribute(REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr());
+ proxyRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(request.getRemotePort()));
+ proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName());
+ proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr());
+ proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort()));
+ proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme());
+
+ // Has the original URI been rewritten ?
+ String originalURI = null;
+ String originalQuery = null;
+ if (originalURIAttribute != null)
+ originalURI = (String)request.getAttribute(originalURIAttribute);
+ if (originalURI != null && originalQueryAttribute != null)
+ {
+ originalQuery = (String)request.getAttribute(originalQueryAttribute);
+ if (originalQuery != null)
+ originalURI += "?" + originalQuery;
+ }
+
+ if (originalURI == null)
+ {
+ // If we are forwarded or included, retain the original request URI.
+ String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
+ originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
+ if (originalPath == null)
+ {
+ originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
+ originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
+ }
+ if (originalPath != null)
+ {
+ originalURI = originalPath;
+ if (originalQuery != null)
+ originalURI += "?" + originalQuery;
+ }
+ }
+
+ if (originalURI != null)
+ proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
+ if (originalQuery != null)
+ proxyRequest.attribute(REQUEST_QUERY_ATTRIBUTE, originalQuery);
+
+ // If the Host header is missing, add it.
+ if (!proxyRequest.getHeaders().contains(HttpHeader.HOST))
+ {
+ String server = request.getServerName();
+ int port = request.getServerPort();
+ if (port != URIUtil.getDefaultPortForScheme(request.getScheme()))
+ server += ":" + port;
+ String host = server;
+ proxyRequest.headers(headers -> headers
+ .put(HttpHeader.HOST, host)
+ .put(HttpHeader.X_FORWARDED_HOST, host));
+ }
+
+ // PHP does not like multiple Cookie headers, coalesce into one.
+ List cookies = proxyRequest.getHeaders().getValuesList(HttpHeader.COOKIE);
+ if (cookies.size() > 1)
+ {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < cookies.size(); ++i)
+ {
+ if (i > 0)
+ builder.append("; ");
+ String cookie = cookies.get(i);
+ builder.append(cookie);
+ }
+ proxyRequest.headers(headers -> headers.put(HttpHeader.COOKIE, builder.toString()));
+ }
+
+ Path unixDomain = unixDomainPath;
+ if (unixDomain != null)
+ proxyRequest.transport(new Transport.TCPUnix(unixDomain));
+
+ super.sendProxyRequest(request, proxyResponse, proxyRequest);
+ }
+
+ protected void customizeFastCGIHeaders(Request proxyRequest, HttpFields.Mutable fastCGIHeaders)
+ {
+ for (String envName : fcgiEnvNames)
+ {
+ String envValue = System.getenv(envName);
+ if (envValue != null)
+ fastCGIHeaders.put(envName, envValue);
+ }
+
+ fastCGIHeaders.remove("HTTP_PROXY");
+
+ fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
+ fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
+ fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
+ fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
+ fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
+
+ if (fcgiHTTPS || HttpScheme.HTTPS.is((String)proxyRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
+ fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
+
+ URI proxyRequestURI = proxyRequest.getURI();
+ String rawPath = proxyRequestURI == null ? proxyRequest.getPath() : proxyRequestURI.getRawPath();
+ String rawQuery = proxyRequestURI == null ? null : proxyRequestURI.getRawQuery();
+
+ String requestURI = (String)proxyRequest.getAttributes().get(REQUEST_URI_ATTRIBUTE);
+ if (requestURI == null)
+ {
+ requestURI = rawPath;
+ if (rawQuery != null)
+ requestURI += "?" + rawQuery;
+ }
+ fastCGIHeaders.put(FCGI.Headers.REQUEST_URI, requestURI);
+
+ String requestQuery = (String)proxyRequest.getAttributes().get(REQUEST_QUERY_ATTRIBUTE);
+ if (requestQuery != null)
+ fastCGIHeaders.put(FCGI.Headers.QUERY_STRING, requestQuery);
+
+ String scriptName = rawPath;
+ Matcher matcher = scriptPattern.matcher(rawPath);
+ if (matcher.matches())
+ {
+ // Expect at least one group in the regular expression.
+ scriptName = matcher.group(1);
+
+ // If there is a second group, map it to PATH_INFO.
+ if (matcher.groupCount() > 1)
+ fastCGIHeaders.put(FCGI.Headers.PATH_INFO, matcher.group(2));
+ }
+ fastCGIHeaders.put(FCGI.Headers.SCRIPT_NAME, scriptName);
+
+ String root = fastCGIHeaders.get(FCGI.Headers.DOCUMENT_ROOT);
+ fastCGIHeaders.put(FCGI.Headers.SCRIPT_FILENAME, root + scriptName);
+ }
+
+ private class ProxyHttpClientTransportOverFCGI extends HttpClientTransportOverFCGI
+ {
+ private ProxyHttpClientTransportOverFCGI(ClientConnector connector, String scriptRoot)
+ {
+ super(connector, scriptRoot);
+ }
+
+ @Override
+ public void customize(Request request, HttpFields.Mutable fastCGIHeaders)
+ {
+ super.customize(request, fastCGIHeaders);
+ customizeFastCGIHeaders(request, fastCGIHeaders);
+ if (_log.isDebugEnabled())
+ {
+ TreeMap fcgi = new TreeMap<>();
+ for (HttpField field : fastCGIHeaders)
+ {
+ fcgi.put(field.getName(), field.getValue());
+ }
+ String eol = System.lineSeparator();
+ _log.debug("FastCGI variables {}{}", eol, fcgi.entrySet().stream()
+ .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue()))
+ .collect(Collectors.joining(eol)));
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/org/eclipse/jetty/ee11/fcgi/proxy/TryFilesFilter.java b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/org/eclipse/jetty/ee11/fcgi/proxy/TryFilesFilter.java
new file mode 100644
index 00000000000..f8f38a6af33
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/src/main/java/org/eclipse/jetty/ee11/fcgi/proxy/TryFilesFilter.java
@@ -0,0 +1,138 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.fcgi.proxy;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+ * Inspired by nginx's try_files functionality.
+ *
+ * This filter accepts the {@code files} init-param as a list of space-separated
+ * file URIs. The special token {@code $path} represents the current request URL's
+ * path (the portion after the context path).
+ *
+ * Typical example of how this filter can be configured is the following:
+ *
+ * For a request such as {@code /context/path/to/resource.ext}, this filter will
+ * try to serve the {@code /maintenance.html} file if it finds it; failing that,
+ * it will try to serve the {@code /path/to/resource.ext} file if it finds it;
+ * failing that it will forward the request to {@code /index.php?p=/path/to/resource.ext}.
+ * The last file URI specified in the list is therefore the "fallback" to which the request
+ * is forwarded to in case no previous files can be found.
+ *
+ * The files are resolved using {@link ServletContext#getResource(String)} to make sure
+ * that only files visible to the application are served.
+ *
+ * @see FastCGIProxyServlet
+ */
+public class TryFilesFilter implements Filter
+{
+ public static final String FILES_INIT_PARAM = "files";
+
+ private String[] files;
+
+ @Override
+ public void init(FilterConfig config) throws ServletException
+ {
+ String param = config.getInitParameter(FILES_INIT_PARAM);
+ if (param == null)
+ throw new ServletException(String.format("Missing mandatory parameter '%s'", FILES_INIT_PARAM));
+ files = param.split(" ");
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
+ {
+ HttpServletRequest httpRequest = (HttpServletRequest)request;
+ HttpServletResponse httpResponse = (HttpServletResponse)response;
+
+ for (int i = 0; i < files.length - 1; ++i)
+ {
+ String file = files[i];
+ String resolved = resolve(httpRequest, file);
+
+ URL url = request.getServletContext().getResource(resolved);
+ if (url == null)
+ continue;
+
+ if (Files.isReadable(toPath(url)))
+ {
+ chain.doFilter(httpRequest, httpResponse);
+ return;
+ }
+ }
+
+ // The last one is the fallback
+ fallback(httpRequest, httpResponse, chain, files[files.length - 1]);
+ }
+
+ private Path toPath(URL url) throws IOException
+ {
+ try
+ {
+ return Paths.get(url.toURI());
+ }
+ catch (URISyntaxException x)
+ {
+ throw new IOException(x);
+ }
+ }
+
+ protected void fallback(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String fallback) throws IOException, ServletException
+ {
+ String resolved = resolve(request, fallback);
+ request.getServletContext().getRequestDispatcher(resolved).forward(request, response);
+ }
+
+ private String resolve(HttpServletRequest request, String value)
+ {
+ String path = request.getServletPath();
+ String info = request.getPathInfo();
+ if (info != null)
+ path += info;
+ if (!path.startsWith("/"))
+ path = "/" + path;
+ return StringUtil.replace(value, "$path", path);
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/java/org/eclipse/jetty/ee11/fcgi/proxy/TryFilesFilterTest.java b/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/java/org/eclipse/jetty/ee11/fcgi/proxy/TryFilesFilterTest.java
new file mode 100644
index 00000000000..3602aa362ae
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/java/org/eclipse/jetty/ee11/fcgi/proxy/TryFilesFilterTest.java
@@ -0,0 +1,108 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.fcgi.proxy;
+
+import java.util.EnumSet;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
+import org.eclipse.jetty.ee11.servlet.FilterHolder;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TryFilesFilterTest
+{
+ private Server server;
+ private ServerConnector connector;
+ private ServerConnector sslConnector;
+ private HttpClient client;
+ private String forwardPath;
+
+ public void prepare(HttpServlet servlet) throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
+ serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
+ serverSslContextFactory.setKeyStorePassword("storepwd");
+ sslConnector = new ServerConnector(server, serverSslContextFactory);
+ server.addConnector(sslConnector);
+
+ ServletContextHandler context = new ServletContextHandler("/");
+ server.setHandler(context);
+
+ FilterHolder filterHolder = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+ forwardPath = "/index.php";
+ filterHolder.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path " + forwardPath + "?p=$path");
+
+ context.addServlet(new ServletHolder(servlet), "/*");
+
+ ClientConnector clientConnector = new ClientConnector();
+ SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(true);
+// clientSslContextFactory.setEndpointIdentificationAlgorithm(null);
+// clientSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
+// clientSslContextFactory.setKeyStorePassword("storepwd");
+ clientConnector.setSslContextFactory(clientSslContextFactory);
+ client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector));
+ server.addBean(client);
+
+ server.start();
+ }
+
+ @AfterEach
+ public void dispose() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testHTTPSRequestIsForwarded() throws Exception
+ {
+ final String path = "/one/";
+ prepare(new HttpServlet()
+ {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ {
+ assertTrue("https".equalsIgnoreCase(req.getScheme()));
+ assertTrue(req.isSecure());
+ assertEquals(forwardPath, req.getRequestURI());
+ assertTrue(req.getQueryString().endsWith(path));
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", sslConnector.getLocalPort())
+ .scheme("https")
+ .path(path)
+ .send();
+
+ assertEquals(200, response.getStatus());
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/resources/jetty-logging.properties b/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..37021aa1510
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/resources/jetty-logging.properties
@@ -0,0 +1,2 @@
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.fcgi.proxy.LEVEL=DEBUG
diff --git a/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/resources/keystore.p12 b/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/resources/keystore.p12
new file mode 100644
index 00000000000..8ab40f72afd
Binary files /dev/null and b/jetty-ee11/jetty-ee11-fcgi-proxy/src/test/resources/keystore.p12 differ
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/pom.xml b/jetty-ee11/jetty-ee11-glassfish-jstl/pom.xml
new file mode 100644
index 00000000000..2980bc9f8ba
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-glassfish-jstl
+ jar
+ EE11 :: Glassfish JSTL
+ https://projects.eclipse.org/projects/ee4j.glassfish
+
+ ${project.groupId}.glassfish.jstl
+ true
+
+
+
+
+
+ jakarta.servlet.jsp.jstl
+ jakarta.servlet.jsp.jstl-api
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+
+
+
+ org.glassfish.web
+ jakarta.servlet.jsp.jstl
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-annotations
+ test
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-webapp
+ test
+
+
+
+ org.eclipse.jetty.toolchain
+ jetty-test-helper
+ test
+
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/main/config/modules/ee11-glassfish-jstl.mod b/jetty-ee11/jetty-ee11-glassfish-jstl/src/main/config/modules/ee11-glassfish-jstl.mod
new file mode 100644
index 00000000000..9aa2b5871f9
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/main/config/modules/ee11-glassfish-jstl.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables the glassfish version of JSTL for all webapps.
+
+[environment]
+ee11
+
+[depends]
+ee11-apache-jsp
+
+[ini]
+ee11.jakarta.servlet.jsp.jstl.api.version?=@jakarta.servlet.jsp.jstl.api.version@
+ee11.jakarta.servlet.jsp.jstl.impl.version?=@jakarta.servlet.jsp.jstl.impl.version@
+
+[lib]
+lib/ee11-glassfish-jstl/jakarta.servlet.jsp.jstl.jakarta.servlet.jsp.jstl-api-${ee11.jakarta.servlet.jsp.jstl.api.version}.jar
+lib/ee11-glassfish-jstl/org.glassfish.web.jakarta.servlet.jsp.jstl-${ee11.jakarta.servlet.jsp.jstl.impl.version}.jar
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/main/resources/readme.txt b/jetty-ee11/jetty-ee11-glassfish-jstl/src/main/resources/readme.txt
new file mode 100644
index 00000000000..a516023c352
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/main/resources/readme.txt
@@ -0,0 +1,4 @@
+This empty jar file is purely to work around a problem with the Maven Dependency plugin.
+Several modules in jetty use the Dependency plugin to copy or unpack the dependencies of other modules.
+However, the Dependency plugin is not capable of unpacking or copying a dependency of type 'pom', which
+this module is, as it consists purely of external dependencies needed to run jsp.
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JspConfig.java b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JspConfig.java
new file mode 100644
index 00000000000..deb61b1abe1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JspConfig.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jstl;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+
+import jakarta.servlet.ServletContext;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+
+/**
+ * Attempt at collecting up all of the JSP specific configuration bits and pieces into a single place
+ * for WebAppContext users to utilize.
+ */
+public class JspConfig
+{
+ public static void init(WebAppContext context, URI baseUri, File scratchDir)
+ {
+ context.setAttribute(ServletContext.TEMPDIR, scratchDir);
+ context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
+ ".*/jetty-jakarta-servlet-api-[^/]*\\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\\.jar|.*taglibs-standard.*\\.jar");
+ context.setWar(baseUri.toASCIIString());
+ context.setBaseResourceAsPath(Path.of(baseUri));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JspIncludeTest.java b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JspIncludeTest.java
new file mode 100644
index 00000000000..f04da53deea
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JspIncludeTest.java
@@ -0,0 +1,178 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jstl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee11.webapp.Configurations;
+import org.eclipse.jetty.ee11.webapp.JettyWebXmlConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.IO;
+import org.eclipse.jetty.toolchain.test.JAR;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+@ExtendWith(WorkDirExtension.class)
+public class JspIncludeTest
+{
+ private static Server server;
+ private static URI baseUri;
+
+ @BeforeAll
+ public static void startServer(WorkDir workDir) throws Exception
+ {
+ Path tmpPath = workDir.getEmptyPathDir();
+ // Setup Server
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ //Base dir for test
+ File testDir = tmpPath.toFile();
+ File testLibDir = new File(testDir, "WEB-INF/lib");
+ FS.ensureDirExists(testLibDir);
+
+ //Make a taglib jar
+ File srcTagLibDir = MavenTestingUtils.getProjectDir("src/test/taglibjar");
+ File scratchTagLibDir = new File(testDir, JspIncludeTest.class.getSimpleName() + "-taglib-scratch");
+ IO.copy(srcTagLibDir, scratchTagLibDir);
+ File tagLibJar = new File(testLibDir, "testtaglib.jar");
+ JAR.create(scratchTagLibDir, tagLibJar);
+
+ //Copy content
+ File destWebAppDir = new File(testDir, "webapp");
+ FS.ensureDirExists(destWebAppDir);
+ File srcWebAppDir = MavenTestingUtils.getProjectDir("src/test/webapp");
+ IO.copyDir(srcWebAppDir, destWebAppDir);
+
+ // Configure WebAppContext
+ Configurations.setServerDefault(server).add(new JettyWebXmlConfiguration(), new AnnotationConfiguration());
+
+ WebAppContext context = new WebAppContext();
+ context.setContextPath("/");
+
+ File scratchDir = new File(testDir, JspIncludeTest.class.getSimpleName() + "-scratch");
+ FS.ensureEmpty(scratchDir);
+ JspConfig.init(context, destWebAppDir.toURI(), scratchDir);
+
+ server.setHandler(context);
+
+ // Start Server
+ server.start();
+
+ // Figure out Base URI
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ baseUri = new URI(String.format("http://%s:%d/", host, port));
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testTopWithIncluded() throws IOException
+ {
+ URI uri = baseUri.resolve("/top.jsp");
+
+ InputStream in = null;
+ InputStreamReader reader = null;
+ HttpURLConnection connection = null;
+
+ try
+ {
+ connection = (HttpURLConnection)uri.toURL().openConnection();
+ connection.connect();
+ if (HttpURLConnection.HTTP_OK != connection.getResponseCode())
+ {
+ String body = getPotentialBody(connection);
+ String err = String.format("GET request failed (%d %s) %s%n%s", connection.getResponseCode(), connection.getResponseMessage(),
+ uri.toASCIIString(), body);
+ throw new IOException(err);
+ }
+ in = connection.getInputStream();
+ reader = new InputStreamReader(in);
+ StringWriter writer = new StringWriter();
+ IO.copy(reader, writer);
+
+ String response = writer.toString();
+ // System.out.printf("Response%n%s",response);
+ assertThat("Response", response, containsString("
Hello, this is the top page."));
+ assertThat("Response", response, containsString("
This is the included page"));
+ assertThat("Response Header[main-page-key]", connection.getHeaderField("main-page-key"), is("main-page-value"));
+ assertThat("Response Header[included-page-key]", connection.getHeaderField("included-page-key"), is("included-page-value"));
+ }
+ finally
+ {
+ IO.close(reader);
+ IO.close(in);
+ }
+ }
+
+ /**
+ * Attempt to obtain the body text if available. Do not throw an exception if body is unable to be fetched.
+ *
+ * @param connection the connection to fetch the body content from.
+ * @return the body content, if present.
+ */
+ private String getPotentialBody(HttpURLConnection connection)
+ {
+ InputStream in = null;
+ InputStreamReader reader = null;
+ try
+ {
+ in = connection.getInputStream();
+ reader = new InputStreamReader(in);
+ StringWriter writer = new StringWriter();
+ IO.copy(reader, writer);
+ return writer.toString();
+ }
+ catch (IOException e)
+ {
+ return "";
+ }
+ finally
+ {
+ IO.close(reader);
+ IO.close(in);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JstlTest.java b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JstlTest.java
new file mode 100644
index 00000000000..dc4fe19721d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/java/org/eclipse/jetty/ee11/jstl/JstlTest.java
@@ -0,0 +1,144 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jstl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.jsp.JspException;
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.JAR;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+public class JstlTest
+{
+ private static Server server;
+ private static URI baseUri;
+
+ @BeforeAll
+ public static void startServer() throws Exception
+ {
+ // Setup Server
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ //Base dir for test
+ File testDir = MavenTestingUtils.getTargetTestingDir("jstl");
+ File testLibDir = new File(testDir, "WEB-INF/lib");
+ FS.ensureDirExists(testLibDir);
+
+ //Make a taglib jar
+ File srcTagLibDir = MavenTestingUtils.getProjectDir("src/test/taglibjar");
+ File scratchTagLibDir = MavenTestingUtils.getTargetFile("tests/" + JstlTest.class.getSimpleName() + "-taglib-scratch");
+ IO.copy(srcTagLibDir, scratchTagLibDir);
+ File tagLibJar = new File(testLibDir, "testtaglib.jar");
+ JAR.create(scratchTagLibDir, tagLibJar);
+
+ //Copy content
+ File srcWebAppDir = MavenTestingUtils.getProjectDir("src/test/webapp");
+ IO.copyDir(srcWebAppDir, testDir);
+
+ // Configure WebAppCont
+ WebAppContext context = new WebAppContext();
+ context.setContextPath("/");
+
+ File scratchDir = MavenTestingUtils.getTargetFile("tests/" + JstlTest.class.getSimpleName() + "-scratch");
+ FS.ensureEmpty(scratchDir);
+ JspConfig.init(context, testDir.toURI(), scratchDir);
+
+ context.addConfiguration(new AnnotationConfiguration());
+
+ server.setHandler(context);
+
+ // Start Server
+ server.start();
+
+ // Figure out Base URI
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ baseUri = new URI(String.format("http://%s:%d/", host, port));
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception
+ {
+ if (server != null)
+ server.stop();
+ }
+
+ @Test
+ public void testUrlsBasic() throws IOException
+ {
+ HttpURLConnection http = (HttpURLConnection)baseUri.resolve("/urls.jsp").toURL().openConnection();
+ assertThat("http response", http.getResponseCode(), is(200));
+ try (InputStream input = http.getInputStream())
+ {
+ String resp = IO.toString(input, StandardCharsets.UTF_8);
+ assertThat("Response should be JSP processed", resp, not(containsString("")));
+ assertThat("Response should be JSP processed", resp, not(containsString("")));
+ assertThat("Response", resp, not(containsString("[jtest:errorhandler] exception is null")));
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/resources/jetty-logging.properties b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..c1f44baf179
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/resources/jetty-logging.properties
@@ -0,0 +1,3 @@
+# Jetty Logging using jetty-slf4j-impl
+# org.eclipse.jetty.LEVEL=INFO
+# org.eclipse.jetty.util.LEVEL=DEBUG
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/taglibjar/META-INF/etag.tld b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/taglibjar/META-INF/etag.tld
new file mode 100644
index 00000000000..de59ff05a3a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/taglibjar/META-INF/etag.tld
@@ -0,0 +1,16 @@
+
+
+
+ eclipse jetty test taglib
+ 1.0
+
+ jtest
+ org.eclipse.jetty.jstl.jtest
+
+
+ errorhandler
+ /META-INF/tags/errorhandler.tag
+
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/taglibjar/META-INF/tags/errorhandler.tag b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/taglibjar/META-INF/tags/errorhandler.tag
new file mode 100644
index 00000000000..ddd44dd4910
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/taglibjar/META-INF/tags/errorhandler.tag
@@ -0,0 +1,12 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+
+
+
+[jtest:errorhandler] exception : ${tossable}
+[jtest:errorhandler] exception.message : ${tossable.message}
+
+
+[jtest:errorhandler] exception is null
+
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..1043ddea79f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+
+
+ Test webapp for JSTL
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/catch-basic.jsp b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/catch-basic.jsp
new file mode 100644
index 00000000000..06e2bcfd2bc
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/catch-basic.jsp
@@ -0,0 +1,16 @@
+<%@ page contentType="text/plain; charset=UTF-8" %>
+<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
+<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
+Title: JSTL c:catch test
+
+
+
+
+
+
+[c:catch] exception : ${catchException}
+[c:catch] exception.message : ${catchException.message}
+
+
+[c:catch] exception is null
+
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/catch-taglib.jsp b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/catch-taglib.jsp
new file mode 100644
index 00000000000..28f7488e2ab
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/catch-taglib.jsp
@@ -0,0 +1,11 @@
+<%@ page contentType="text/plain; charset=UTF-8" %>
+<%@ taglib uri="org.eclipse.jetty.jstl.jtest" prefix="jtest" %>
+<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
+<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
+Title: JSTL c:catch test
+
+
+
+
+
+parsedNum =
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/included.jsp b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/included.jsp
new file mode 100644
index 00000000000..dde5f29c45d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/included.jsp
@@ -0,0 +1,8 @@
+<%
+ String headerPrefix = "";
+ if(request.getDispatcherType() == DispatcherType.INCLUDE)
+ headerPrefix = "org.eclipse.jetty.server.include.";
+
+ response.setHeader(headerPrefix + "included-page-key","included-page-value");
+%>
+
This is the included page
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/ref.jsp b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/ref.jsp
new file mode 100644
index 00000000000..0debf754890
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/ref.jsp
@@ -0,0 +1,2 @@
+<%@ page contentType="text/plain; charset=UTF-8" %>
+Reference Page: No useful content here, just used for other tests
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/top.jsp b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/top.jsp
new file mode 100644
index 00000000000..6feebaea4eb
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/top.jsp
@@ -0,0 +1,5 @@
+<%
+ application.getRequestDispatcher("/included.jsp").include(request,response);
+ response.setHeader("main-page-key", "main-page-value");
+%>
+
Hello, this is the top page.
diff --git a/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/urls.jsp b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/urls.jsp
new file mode 100644
index 00000000000..bc18e9ac5f5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-glassfish-jstl/src/test/webapp/urls.jsp
@@ -0,0 +1,6 @@
+<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
+<%@ page contentType="text/plain; charset=UTF-8" %>
+Title: JSTL c:url Tests
+[c:url value] =
+
+[c:url param] =
diff --git a/jetty-ee11/jetty-ee11-home/pom.xml b/jetty-ee11/jetty-ee11-home/pom.xml
new file mode 100644
index 00000000000..af868381e8c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-home/pom.xml
@@ -0,0 +1,576 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+ ../pom.xml
+
+ jetty-ee11-home
+ pom
+ EE11 :: Home Assembly
+
+
+ ${basedir}/target/jetty-ee11-home
+ ${basedir}/target/jetty-ee11-home-sources
+ true
+
+
+
+
+ org.eclipse.jetty
+ jetty-openid
+ true
+
+
+ org.eclipse.jetty
+ jetty-security
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-annotations
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-cdi
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-fcgi-proxy
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-glassfish-jstl
+
+
+ jakarta.el
+ jakarta.el-api
+
+
+ javax.el
+ el-api
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jaspi
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jndi
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-plus
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-proxy
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-quickstart
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlets
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-async-rest-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-jaas-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-jetty-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ javax.el
+ el-api
+
+
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-jndi-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-jsp-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-mock-resources
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-proxy-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-simple-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.demos
+ jetty-ee11-demo-spec-webapp
+ ${project.version}
+ config
+ jar
+ true
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jakarta-server
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jetty-client-webapp
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jetty-server
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-servlet
+
+
+ org.ow2.asm
+ asm
+
+
+ org.ow2.asm
+ asm-analysis
+
+
+ org.ow2.asm
+ asm-commons
+
+
+ org.ow2.asm
+ asm-tree
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+ posix
+ false
+
+
+
+ binary
+
+ single
+
+ package
+
+
+ src/main/assembly/jetty-assembly.xml
+
+
+
+
+ sources
+
+ single
+
+ package
+
+
+ src/main/assembly/jetty-source-assembly.xml
+
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-ee11-annotations-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ jakarta.annotation,org.ow2.asm
+ jakarta.annotation-api,asm,asm-commons,asm-tree,asm-analysis
+ jar
+ ${assembly-directory}/lib/ee11-annotations
+
+
+
+ copy-ee11-annotations-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ jakarta.annotation,org.ow2.asm
+ jakarta.annotation-api,asm,asm-commons,asm-tree,asm-analysis
+ jar
+ sources
+ ${source-assembly-directory}/lib/ee11-annotations
+
+
+
+
+ copy-ee11-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.ee11
+ jetty-websocket-core-client,jetty-websocket-core-common,jetty-websocket-core-server,jetty-websocket-jetty-api,jetty-websocket-jetty-client,jetty-websocket-jetty-common,jetty-websocket-jetty-server
+ org.eclipse.jetty.ee11.demos,org.eclipse.jetty.ee11.websocket
+ jar
+ ${assembly-directory}/lib
+
+
+
+ copy-ee11-jaspi-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ jakarta.authentication
+ jakarta.authentication-api
+ jar
+ ${assembly-directory}/lib/ee11-jaspi
+
+
+
+ copy-ee11-jaspi-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ jakarta.authentication
+ jakarta.authentication-api
+ jar
+ sources
+ ${source-assembly-directory}/lib/ee11-jaspi
+
+
+
+ copy-ee11-jsp-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ true
+ jakarta.servlet.jsp,jakarta.el,org.mortbay.jasper,org.eclipse.jdt
+ jakarta.servlet.jsp-api,jakarta.el-api,apache-el,apache-jsp,ecj
+ jar
+ ${assembly-directory}/lib/ee11-apache-jsp
+
+
+
+ copy-ee11-jsp-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ true
+ jakarta.servlet.jsp,jakart.el,org.mortbay.jasper,org.eclipse.jdt
+ jakart.servlet.jsp-api,jakarta.el-api,apache-el,apache-jsp,ecj
+ jar
+ sources
+ ${source-assembly-directory}/lib/ee11-apache-jsp
+
+
+
+ copy-ee11-jstl-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ true
+ jakarta.servlet.jsp.jstl,org.glassfish.web
+ jakarta.servlet.jsp.jstl-api,jakarta.servlet.jsp.jstl
+ jar
+ ${assembly-directory}/lib/ee11-glassfish-jstl
+
+
+
+ copy-ee11-jstl-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ true
+ jakarta.servlet.jsp.jstl,org.glassfish.web
+ jakarta.servlet.jsp.jstl-api,jakarta.servlet.jsp.jstl
+ jar
+ sources
+ ${source-assembly-directory}/lib/ee11-glassfish-jstl
+
+
+
+ copy-ee11-lib-jakarta-websocket-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ jakarta.websocket
+ jar
+ ${assembly-directory}/lib/ee11-websocket
+
+
+
+ copy-ee11-lib-jakarta-websocket-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ jakarta.websocket
+ jar
+ sources
+ ${source-assembly-directory}/lib/ee11-websocket
+
+
+
+ copy-ee11-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.ee11
+ jar
+ org.eclipse.jetty.ee11.demos,org.eclipse.jetty.ee11.websocket
+ sources
+ ${source-assembly-directory}/lib
+
+
+
+ copy-lib-core-websocket-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.websocket
+ jar
+ ${assembly-directory}/lib
+
+
+
+ copy-lib-core-websocket-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.websocket
+ jar
+ sources
+ ${source-assembly-directory}/lib
+
+
+
+ copy-lib-ee11-websocket-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.ee11.websocket
+ jar
+ ${assembly-directory}/lib/ee11-websocket
+
+
+
+ copy-lib-ee11-websocket-src-deps
+
+ copy-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.ee11.websocket
+ jar
+ sources
+ ${source-assembly-directory}/lib/ee11-websocket
+
+
+
+ copy-lib-servlet-api-deps
+
+ copy
+
+ generate-resources
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${jakarta.servlet.api.version}
+
+
+ ${assembly-directory}/lib
+
+
+
+ copy-lib-servlet-api-src-deps
+
+ copy
+
+ generate-resources
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${jakarta.servlet.api.version}
+ sources
+
+
+ ${source-assembly-directory}/lib
+
+
+
+ copy-lib-transaction-api-deps
+
+ copy
+
+ generate-resources
+
+
+
+ jakarta.transaction
+ jakarta.transaction-api
+ ${jakarta.transaction-api.version}
+
+
+ jakarta.interceptor
+ jakarta.interceptor-api
+ ${jakarta.interceptor.api.version}
+
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+ ${jakarta.enterprise.cdi.api.version}
+
+
+ jakarta.inject
+ jakarta.inject-api
+ ${jakarta.inject.api.version}
+
+
+ jakarta.enterprise
+ jakarta.enterprise.lang-model
+ ${jakarta.enterprise.lang.model.version}
+
+
+ ${assembly-directory}/lib
+
+
+
+
+ copy-lib-transaction-api-src-deps
+
+ copy
+
+ generate-resources
+
+
+
+ jakarta.transaction
+ jakarta.transaction-api
+ ${jakarta.transaction-api.version}
+ sources
+
+
+ ${source-assembly-directory}/lib
+
+
+
+ unpack-config-deps
+
+ unpack-dependencies
+
+ generate-resources
+
+ org.eclipse.jetty.ee11, org.eclipse.jetty.ee11.demos
+
+ config
+ false
+ META-INF/**,webapps/**,start.d/**,start.ini
+ ${assembly-directory}
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-home/src/main/assembly/jetty-assembly.xml b/jetty-ee11/jetty-ee11-home/src/main/assembly/jetty-assembly.xml
new file mode 100644
index 00000000000..dc2f3d984d6
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-home/src/main/assembly/jetty-assembly.xml
@@ -0,0 +1,20 @@
+
+ binary-assembly
+
+ tar.gz
+ zip
+
+ false
+
+
+ ${assembly-directory}
+
+
+ **
+
+
+ **/META-INF/**
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-home/src/main/assembly/jetty-source-assembly.xml b/jetty-ee11/jetty-ee11-home/src/main/assembly/jetty-source-assembly.xml
new file mode 100644
index 00000000000..cf9b6f56ffe
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-home/src/main/assembly/jetty-source-assembly.xml
@@ -0,0 +1,17 @@
+
+ sources
+
+ tar.gz
+ zip
+
+ false
+
+
+ ${source-assembly-directory}
+
+
+ **
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/pom.xml b/jetty-ee11/jetty-ee11-jaspi/pom.xml
new file mode 100644
index 00000000000..9746107889d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-jaspi
+ EE11 :: JASPI
+ Jetty security infrastructure
+
+
+ ${project.groupId}.security.jaspi
+ org.eclipse.jetty.ee11.security.jaspi.*
+
+
+
+
+ jakarta.authentication
+ jakarta.authentication-api
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+
+
+ org.slf4j
+ slf4j-api
+
+
+ jakarta.activation
+ jakarta.activation-api
+ test
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ test
+
+
+ org.eclipse.jetty.toolchain
+ jetty-test-helper
+ test
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ manifest
+
+
+
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"
+ osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.ee11.servlet.security.Authenticator$Factory
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-authmoduleconfig.xml b/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-authmoduleconfig.xml
new file mode 100644
index 00000000000..189888d9d57
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-authmoduleconfig.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-default.xml b/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-default.xml
new file mode 100644
index 00000000000..e7060566cb9
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-default.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-demo.xml b/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-demo.xml
new file mode 100644
index 00000000000..2b5ac1534c1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/config/etc/jaspi/jetty-ee11-jaspi-demo.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+ org.eclipse.jetty.ee11.security.jaspi.provider.JaspiAuthConfigProvider
+
+
+
+
+
+
+
+ HttpServlet
+
+
+ server /test
+
+
+ A simple provider using HTTP BASIC authentication.
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi-default-auth-config-factory.mod b/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi-default-auth-config-factory.mod
new file mode 100644
index 00000000000..1fb25c739eb
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi-default-auth-config-factory.mod
@@ -0,0 +1,19 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Provides a DefaultAuthConfigFactory for jaspi
+
+[environment]
+ee11
+
+[tags]
+security
+
+[depend]
+ee11-security
+
+[provide]
+auth-config-factory
+
+[xml]
+etc/jaspi/jetty-ee11-jaspi-default.xml
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi-demo.mod b/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi-demo.mod
new file mode 100644
index 00000000000..d32296a3190
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi-demo.mod
@@ -0,0 +1,19 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables JASPI basic authentication the /test context path.
+
+[environment]
+ee11
+
+[tags]
+security
+
+[depend]
+jaspi
+
+[xml]
+etc/jaspi/jetty-ee11-jaspi-demo.xml
+
+[files]
+basehome:etc/jaspi/jetty-ee11-jaspi-demo.xml|etc/jaspi/jetty-ee11-jaspi-demo.xml
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi.mod b/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi.mod
new file mode 100644
index 00000000000..e18fcf6da8b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/config/modules/ee11-jaspi.mod
@@ -0,0 +1,28 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables JASPI authentication for deployed web applications.
+
+[environment]
+ee11
+
+[tags]
+security
+
+[depend]
+ee11-security
+auth-config-factory
+
+[ini]
+ee11.jakarta.authentication.api.version?=@jakarta.authentication.api.version@
+
+[lib]
+lib/jetty-ee11-jaspi-${jetty.version}.jar
+lib/ee11-jaspi/jakarta.authentication-api-${ee11.jakarta.authentication.api.version}.jar
+
+[xml]
+etc/jaspi/jetty-ee11-jaspi-authmoduleconfig.xml
+
+[files]
+basehome:etc/jaspi/jetty-ee11-jaspi-authmoduleconfig.xml|etc/jaspi/jetty-ee11-jaspi-authmoduleconfig.xml
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/module-info.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/module-info.java
new file mode 100644
index 00000000000..ece6a09d3ff
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/module-info.java
@@ -0,0 +1,29 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+module org.eclipse.jetty.ee11.security.jaspi
+{
+ requires jakarta.servlet;
+ requires org.slf4j;
+
+ requires transitive jakarta.security.auth.message;
+ requires transitive org.eclipse.jetty.ee11.servlet;
+
+ exports org.eclipse.jetty.ee11.security.jaspi;
+ exports org.eclipse.jetty.ee11.security.jaspi.callback;
+ exports org.eclipse.jetty.ee11.security.jaspi.modules;
+ exports org.eclipse.jetty.ee11.security.jaspi.provider;
+
+ provides org.eclipse.jetty.security.Authenticator.Factory with
+ org.eclipse.jetty.ee11.security.jaspi.JaspiAuthenticatorFactory;
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/DefaultAuthConfigFactory.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/DefaultAuthConfigFactory.java
new file mode 100644
index 00000000000..362f6527751
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/DefaultAuthConfigFactory.java
@@ -0,0 +1,260 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.security.SecurityPermission;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import jakarta.security.auth.message.config.AuthConfigFactory;
+import jakarta.security.auth.message.config.AuthConfigProvider;
+import jakarta.security.auth.message.config.RegistrationListener;
+import jakarta.security.auth.message.module.ServerAuthModule;
+import org.eclipse.jetty.util.security.SecurityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A very basic {@link AuthConfigFactory} that allows for registering providers programmatically.
+ */
+public class DefaultAuthConfigFactory extends AuthConfigFactory
+{
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultAuthConfigFactory.class);
+ private final Map _registrations = new ConcurrentHashMap<>();
+
+ public DefaultAuthConfigFactory()
+ {
+ }
+
+ @Override
+ public AuthConfigProvider getConfigProvider(String layer, String appContext, RegistrationListener listener)
+ {
+ DefaultRegistrationContext registrationContext = _registrations.get(getKey(layer, appContext));
+ if (registrationContext == null)
+ registrationContext = _registrations.get(getKey(null, appContext));
+ if (registrationContext == null)
+ registrationContext = _registrations.get(getKey(layer, null));
+ if (registrationContext == null)
+ registrationContext = _registrations.get(getKey(null, null));
+ if (registrationContext == null)
+ return null;
+
+ // TODO: according to the javadoc you're supposed to register listener even if there is no context available.
+ if (listener != null)
+ registrationContext.addListener(listener);
+ return registrationContext.getProvider();
+ }
+
+ @Override
+ public String registerConfigProvider(String className, Map properties, String layer, String appContext, String description)
+ {
+ checkPermission();
+
+ String key = getKey(layer, appContext);
+ AuthConfigProvider configProvider = createConfigProvider(className, properties);
+ DefaultRegistrationContext context = new DefaultRegistrationContext(configProvider, layer, appContext, description, true);
+ DefaultRegistrationContext oldContext = _registrations.put(key, context);
+ if (oldContext != null)
+ oldContext.notifyListeners();
+ return key;
+ }
+
+ @Override
+ public String registerConfigProvider(AuthConfigProvider provider, String layer, String appContext, String description)
+ {
+ checkPermission();
+
+ String key = getKey(layer, appContext);
+ DefaultRegistrationContext context = new DefaultRegistrationContext(provider, layer, appContext, description, false);
+ DefaultRegistrationContext oldContext = _registrations.put(key, context);
+ if (oldContext != null)
+ oldContext.notifyListeners();
+ return key;
+ }
+
+ @Override
+ public boolean removeRegistration(String registrationID)
+ {
+ checkPermission();
+
+ DefaultRegistrationContext registrationContext = _registrations.remove(registrationID);
+ if (registrationContext == null)
+ return false;
+
+ registrationContext.notifyListeners();
+ return true;
+ }
+
+ @Override
+ public String registerServerAuthModule(ServerAuthModule serverAuthModule, Object context)
+ {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeServerAuthModule(Object context)
+ {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String[] detachListener(RegistrationListener listener, String layer, String appContext)
+ {
+ checkPermission();
+
+ List registrationIds = new ArrayList<>();
+ for (DefaultRegistrationContext registration : _registrations.values())
+ {
+ if ((layer == null || layer.equals(registration.getMessageLayer())) && (appContext == null || appContext.equals(registration.getAppContext())))
+ {
+ if (registration.removeListener(listener))
+ registrationIds.add(getKey(registration.getMessageLayer(), registration.getAppContext()));
+ }
+ }
+
+ return registrationIds.toArray(new String[0]);
+ }
+
+ @Override
+ public String[] getRegistrationIDs(AuthConfigProvider provider)
+ {
+ List registrationIds = new ArrayList<>();
+ for (DefaultRegistrationContext registration : _registrations.values())
+ {
+ if (provider == registration.getProvider())
+ registrationIds.add(getKey(registration.getMessageLayer(), registration.getAppContext()));
+ }
+
+ return registrationIds.toArray(new String[0]);
+ }
+
+ @Override
+ public RegistrationContext getRegistrationContext(String registrationID)
+ {
+ return _registrations.get(registrationID);
+ }
+
+ @Override
+ public void refresh()
+ {
+ checkPermission();
+
+ // TODO: maybe we should re-construct providers created from classname.
+ }
+
+ private static void checkPermission()
+ {
+ SecurityUtils.checkPermission(new SecurityPermission(PROVIDER_REGISTRATION_PERMISSION_NAME));
+ }
+
+ private static String getKey(String layer, String appContext)
+ {
+ return layer + "/" + appContext;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private AuthConfigProvider createConfigProvider(String className, Map properties)
+ {
+ try
+ {
+ // Javadoc specifies all AuthConfigProvider implementations must have this constructor, and that
+ // to construct this we must pass a null value for the factory argument of the constructor.
+ return (AuthConfigProvider)Class.forName(className)
+ .getConstructor(Map.class, AuthConfigFactory.class)
+ .newInstance(properties, null);
+ }
+ catch (ReflectiveOperationException e)
+ {
+ throw new SecurityException(e);
+ }
+ }
+
+ private static class DefaultRegistrationContext implements RegistrationContext
+ {
+ private final String _layer;
+ private final String _appContext;
+ private final boolean _persistent;
+ private final AuthConfigProvider _provider;
+ private final String _description;
+ private final List _listeners = new CopyOnWriteArrayList<>();
+
+ public DefaultRegistrationContext(AuthConfigProvider provider, String layer, String appContext, String description, boolean persistent)
+ {
+ _provider = provider;
+ _layer = layer;
+ _appContext = appContext;
+ _description = description;
+ _persistent = persistent;
+ }
+
+ public AuthConfigProvider getProvider()
+ {
+ return _provider;
+ }
+
+ @Override
+ public String getMessageLayer()
+ {
+ return _layer;
+ }
+
+ @Override
+ public String getAppContext()
+ {
+ return _appContext;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return _description;
+ }
+
+ @Override
+ public boolean isPersistent()
+ {
+ return false;
+ }
+
+ public void addListener(RegistrationListener listener)
+ {
+ _listeners.add(listener);
+ }
+
+ public void notifyListeners()
+ {
+ for (RegistrationListener listener : _listeners)
+ {
+ try
+ {
+ listener.notify(_layer, _appContext);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Error from RegistrationListener", t);
+ }
+ }
+ }
+
+ public boolean removeListener(RegistrationListener listener)
+ {
+ return _listeners.remove(listener);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiAuthenticator.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiAuthenticator.java
new file mode 100644
index 00000000000..7e4974584a1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiAuthenticator.java
@@ -0,0 +1,294 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.security.auth.Subject;
+
+import jakarta.security.auth.message.AuthException;
+import jakarta.security.auth.message.AuthStatus;
+import jakarta.security.auth.message.callback.CallerPrincipalCallback;
+import jakarta.security.auth.message.callback.GroupPrincipalCallback;
+import jakarta.security.auth.message.config.AuthConfigFactory;
+import jakarta.security.auth.message.config.AuthConfigProvider;
+import jakarta.security.auth.message.config.RegistrationListener;
+import jakarta.security.auth.message.config.ServerAuthConfig;
+import jakarta.security.auth.message.config.ServerAuthContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.eclipse.jetty.ee11.servlet.ServletContextRequest;
+import org.eclipse.jetty.security.AuthenticationState;
+import org.eclipse.jetty.security.EmptyLoginService;
+import org.eclipse.jetty.security.IdentityService;
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.security.ServerAuthException;
+import org.eclipse.jetty.security.UserIdentity;
+import org.eclipse.jetty.security.UserPrincipal;
+import org.eclipse.jetty.security.authentication.LoginAuthenticator;
+import org.eclipse.jetty.security.authentication.SessionAuthentication;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+
+import static org.eclipse.jetty.ee11.security.jaspi.JaspiAuthenticatorFactory.MESSAGE_LAYER;
+
+/**
+ * Implementation of Jetty {@link LoginAuthenticator} that is a bridge from Jakarta Authentication to Jetty Security.
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class JaspiAuthenticator extends LoginAuthenticator
+{
+ private final Subject _serviceSubject;
+ private final String _appContext;
+ private final boolean _allowLazyAuthentication;
+ private final AuthConfigFactory _authConfigFactory = AuthConfigFactory.getFactory();
+ private Map _authProperties;
+ private IdentityService _identityService;
+ private ServletCallbackHandler _callbackHandler;
+ private ServerAuthConfig _authConfig;
+
+ public JaspiAuthenticator(Subject serviceSubject, String appContext, boolean allowLazyAuthentication)
+ {
+ _serviceSubject = serviceSubject;
+ _appContext = appContext;
+ _allowLazyAuthentication = allowLazyAuthentication;
+ }
+
+ @Deprecated
+ public JaspiAuthenticator(ServerAuthConfig authConfig, Map authProperties, ServletCallbackHandler callbackHandler, Subject serviceSubject, boolean allowLazyAuthentication, IdentityService identityService)
+ {
+ if (callbackHandler == null)
+ throw new NullPointerException("No CallbackHandler");
+ if (authConfig == null)
+ throw new NullPointerException("No AuthConfig");
+ this._authProperties = authProperties;
+ this._callbackHandler = callbackHandler;
+ this._serviceSubject = serviceSubject;
+ this._allowLazyAuthentication = allowLazyAuthentication;
+ this._identityService = identityService;
+ this._appContext = null;
+ this._authConfig = authConfig;
+ }
+
+ @Override
+ public void setConfiguration(Configuration configuration)
+ {
+ LoginService loginService = configuration.getLoginService();
+ if (loginService == null)
+ {
+ // Add an empty login service so we can use JASPI without tying into Jetty auth mechanisms.
+ configuration = new JaspiAuthenticatorConfiguration(configuration);
+ loginService = configuration.getLoginService();
+ }
+
+ super.setConfiguration(configuration);
+
+ // Only do this if the new constructor was used.
+ if (_authConfig == null)
+ {
+ _identityService = configuration.getIdentityService();
+ _callbackHandler = new ServletCallbackHandler(loginService);
+ _authProperties = new HashMap();
+ for (String key : configuration.getParameterNames())
+ {
+ _authProperties.put(key, configuration.getParameter(key));
+ }
+ }
+ }
+
+ private ServerAuthConfig getAuthConfig() throws AuthException
+ {
+ if (_authConfig != null)
+ return _authConfig;
+
+ RegistrationListener listener = (layer, appContext) -> _authConfig = null;
+ AuthConfigProvider authConfigProvider = _authConfigFactory.getConfigProvider(MESSAGE_LAYER, _appContext, listener);
+ if (authConfigProvider == null)
+ {
+ _authConfigFactory.detachListener(listener, MESSAGE_LAYER, _appContext);
+ return null;
+ }
+
+ _authConfig = authConfigProvider.getServerAuthConfig(MESSAGE_LAYER, _appContext, _callbackHandler);
+ return _authConfig;
+ }
+
+ @Override
+ public String getAuthenticationType()
+ {
+ return "JASPI";
+ }
+
+ @Override
+ public UserIdentity login(String username, Object password, Request request, Response response)
+ {
+ UserIdentity user = _loginService.login(username, password, request, request::getSession);
+ if (user != null)
+ {
+ updateSession(request, response);
+ HttpSession session = ((HttpServletRequest)request).getSession(true);
+ if (session != null)
+ {
+ SessionAuthentication sessionAuth = new SessionAuthentication(getAuthenticationType(), user, password);
+ session.setAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE, sessionAuth);
+ }
+ }
+ return user;
+ }
+
+ @Override
+ public AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException
+ {
+ JaspiMessageInfo info = new JaspiMessageInfo(request, response, callback);
+ request.setAttribute("org.eclipse.jetty.ee11.security.jaspi.info", info);
+
+ return validateRequest(info);
+ }
+
+ public AuthenticationState validateRequest(JaspiMessageInfo messageInfo) throws ServerAuthException
+ {
+ try
+ {
+ ServerAuthConfig authConfig = getAuthConfig();
+ if (authConfig == null)
+ throw new ServerAuthException("No ServerAuthConfig");
+
+ String authContextId = authConfig.getAuthContextID(messageInfo);
+ ServerAuthContext authContext = authConfig.getAuthContext(authContextId, _serviceSubject, _authProperties);
+ Subject clientSubject = new Subject();
+
+ AuthStatus authStatus = authContext.validateRequest(messageInfo, clientSubject, _serviceSubject);
+
+ if (authStatus == AuthStatus.SEND_CONTINUE)
+ return AuthenticationState.CHALLENGE;
+ if (authStatus == AuthStatus.SEND_FAILURE)
+ return AuthenticationState.SEND_FAILURE;
+
+ if (authStatus == AuthStatus.SUCCESS)
+ {
+ Set ids = clientSubject.getPrivateCredentials(UserIdentity.class);
+ UserIdentity userIdentity;
+ if (ids.size() > 0)
+ {
+ userIdentity = ids.iterator().next();
+ }
+ else
+ {
+ CallerPrincipalCallback principalCallback = _callbackHandler.getThreadCallerPrincipalCallback();
+ if (principalCallback == null)
+ {
+ return null;
+ }
+ Principal principal = principalCallback.getPrincipal();
+ if (principal == null)
+ {
+ String principalName = principalCallback.getName();
+
+ // TODO: if the Principal class is provided it doesn't need to be in subject, why do we enforce this here?
+ Set principals = principalCallback.getSubject().getPrincipals();
+ for (Principal p : principals)
+ {
+ if (p.getName().equals(principalName))
+ {
+ principal = p;
+ break;
+ }
+ }
+ if (principal == null)
+ {
+ principal = new UserPrincipal(principalName, null);
+ }
+ }
+ GroupPrincipalCallback groupPrincipalCallback = _callbackHandler.getThreadGroupPrincipalCallback();
+ String[] groups = groupPrincipalCallback == null ? null : groupPrincipalCallback.getGroups();
+ userIdentity = _identityService.newUserIdentity(clientSubject, principal, groups);
+ }
+
+ HttpSession session = ((HttpServletRequest)messageInfo.getRequestMessage()).getSession(false);
+ AuthenticationState cached = (session == null ? null : (SessionAuthentication)session.getAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE));
+ if (cached != null)
+ return cached;
+
+ return new UserAuthenticationSucceeded(getAuthenticationType(), userIdentity);
+ }
+ if (authStatus == AuthStatus.SEND_SUCCESS)
+ {
+ // we are processing a message in a secureResponse dialog.
+ return AuthenticationState.SEND_SUCCESS;
+ }
+ if (authStatus == AuthStatus.FAILURE)
+ {
+ Response.writeError(messageInfo.getBaseRequest(), messageInfo.getBaseResponse(), messageInfo.getCallback(), HttpServletResponse.SC_FORBIDDEN);
+ return AuthenticationState.SEND_FAILURE;
+ }
+ // should not happen
+ throw new IllegalStateException("No AuthStatus returned");
+ }
+ catch (AuthException e)
+ {
+ throw new ServerAuthException(e);
+ }
+ }
+
+ // TODO This is not longer supported by core security
+ public boolean secureResponse(Request request, Response response, Callback callback, boolean mandatory, AuthenticationState.Succeeded validatedSucceeded) throws ServerAuthException
+ {
+ ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
+ JaspiMessageInfo info = (JaspiMessageInfo)servletContextRequest.getServletApiRequest().getAttribute("org.eclipse.jetty.ee11.security.jaspi.info");
+ if (info == null)
+ throw new NullPointerException("MessageInfo from request missing: " + request);
+ return secureResponse(info, validatedSucceeded);
+ }
+
+ public boolean secureResponse(JaspiMessageInfo messageInfo, AuthenticationState validatedUser) throws ServerAuthException
+ {
+ try
+ {
+ ServerAuthConfig authConfig = getAuthConfig();
+ if (authConfig == null)
+ throw new NullPointerException("no ServerAuthConfig found for context");
+
+ String authContextId = authConfig.getAuthContextID(messageInfo);
+ ServerAuthContext authContext = authConfig.getAuthContext(authContextId, _serviceSubject, _authProperties);
+ if (validatedUser instanceof AuthenticationState.Succeeded userAuthenticated)
+ authContext.cleanSubject(messageInfo, userAuthenticated.getUserIdentity().getSubject());
+ AuthStatus status = authContext.secureResponse(messageInfo, _serviceSubject);
+ return (AuthStatus.SEND_SUCCESS.equals(status));
+ }
+ catch (AuthException e)
+ {
+ throw new ServerAuthException(e);
+ }
+ }
+
+ private static class JaspiAuthenticatorConfiguration extends Configuration.Wrapper
+ {
+ private final LoginService loginService = new EmptyLoginService();
+
+ public JaspiAuthenticatorConfiguration(Configuration configuration)
+ {
+ super(configuration);
+ }
+
+ @Override
+ public LoginService getLoginService()
+ {
+ return loginService;
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiAuthenticatorFactory.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiAuthenticatorFactory.java
new file mode 100644
index 00000000000..7d00399a637
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiAuthenticatorFactory.java
@@ -0,0 +1,153 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Set;
+import javax.security.auth.Subject;
+
+import jakarta.security.auth.message.config.AuthConfigFactory;
+import org.eclipse.jetty.security.Authenticator;
+import org.eclipse.jetty.security.DefaultAuthenticatorFactory;
+import org.eclipse.jetty.server.Context;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+ * Jakarta Authentication (JASPI) Authenticator Factory.
+ *
+ * This is used to link a jetty-security {@link Authenticator.Factory} to a Jakarta Authentication {@link AuthConfigFactory}.
+ *
+ * This should be initialized with the provided {@link DefaultAuthConfigFactory} to set up Jakarta Authentication {@link AuthConfigFactory} before use.
+ * (A different {@link AuthConfigFactory} may also be provided using the same steps below)
+ *
Alternatively: set {@link AuthConfigFactory#DEFAULT_FACTORY_SECURITY_PROPERTY}
+ *
+ *
+ */
+public class JaspiAuthenticatorFactory extends DefaultAuthenticatorFactory
+{
+ public static final String MESSAGE_LAYER = "HttpServlet";
+
+ private Subject _serviceSubject;
+ private String _serverName;
+
+ /**
+ * Get the serviceSubject.
+ * @return the serviceSubject
+ */
+ public Subject getServiceSubject()
+ {
+ return _serviceSubject;
+ }
+
+ /**
+ * Set the serviceSubject to set.
+ * @param serviceSubject the serviceSubject to set
+ */
+ public void setServiceSubject(Subject serviceSubject)
+ {
+ _serviceSubject = serviceSubject;
+ }
+
+ /**
+ * Get the serverName.
+ * @return the serverName
+ */
+ public String getServerName()
+ {
+ return _serverName;
+ }
+
+ /**
+ * Set the serverName to set.
+ * @param serverName the serverName to set
+ */
+ public void setServerName(String serverName)
+ {
+ _serverName = serverName;
+ }
+
+ @Override
+ public Authenticator getAuthenticator(Server server, Context context, Authenticator.Configuration configuration)
+ {
+ AuthConfigFactory factory = AuthConfigFactory.getFactory();
+ if (factory == null)
+ return null;
+
+ String serverName = findServerName(context, server);
+ Subject serviceSubject = findServiceSubject(server);
+ String contextPath = StringUtil.isEmpty(context.getContextPath()) ? "/" : context.getContextPath();
+ String appContext = serverName + " " + contextPath;
+
+ // We will only create the Authenticator if an AuthConfigProvider matches this context.
+ if (factory.getConfigProvider(MESSAGE_LAYER, appContext, null) == null)
+ return null;
+
+ return new JaspiAuthenticator(serviceSubject, appContext, true);
+ }
+
+ /**
+ * Find a service Subject. If {@link #setServiceSubject(Subject)} has not been
+ * used to set a subject, then the {@link Server#getBeans(Class)} method is used
+ * to look for a Subject.
+ *
+ * @param server the server to pull the Subject from
+ * @return the subject
+ */
+ protected Subject findServiceSubject(Server server)
+ {
+ if (_serviceSubject != null)
+ return _serviceSubject;
+ List subjects = (List)server.getBeans(Subject.class);
+ if (subjects.size() > 0)
+ return subjects.get(0);
+ return null;
+ }
+
+ /**
+ * Find a servername. If {@link #setServerName(String)} has not been called,
+ * then use the virtualServerName of the context.
+ * If this is also null, then use the name of the a principal in the service subject.
+ * If none are found, return "server".
+ * @param context the context
+ * @param server the server to find the name of
+ * @return the server name from the service Subject (or default value if not
+ * found in subject or principals)
+ */
+ protected String findServerName(Context context, Server server)
+ {
+ if (_serverName != null)
+ return _serverName;
+
+ List virtualHosts = context.getVirtualHosts();
+
+ if (virtualHosts != null && !virtualHosts.isEmpty())
+ return virtualHosts.get(0);
+
+ Subject subject = findServiceSubject(server);
+ if (subject != null)
+ {
+ Set principals = subject.getPrincipals();
+ if (principals != null && !principals.isEmpty())
+ return principals.iterator().next().getName();
+ }
+
+ return "server";
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiMessageInfo.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiMessageInfo.java
new file mode 100644
index 00000000000..bf0e0204190
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/JaspiMessageInfo.java
@@ -0,0 +1,238 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import org.eclipse.jetty.ee11.servlet.ServletContextRequest;
+import org.eclipse.jetty.ee11.servlet.ServletContextResponse;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Almost an implementation of jaspi MessageInfo.
+ */
+public class JaspiMessageInfo implements MessageInfo
+{
+ public static final String AUTHENTICATION_TYPE_KEY = "jakarta.servlet.http.authType";
+ private final Callback _callback;
+ private Request _request;
+ private Response _response;
+ private final MIMap _map;
+
+ public JaspiMessageInfo(Request request, Response response, Callback callback)
+ {
+ _request = request;
+ _response = response;
+ _callback = callback;
+ //JASPI 3.8.1
+ _map = new MIMap();
+ }
+
+ public Callback getCallback()
+ {
+ return _callback;
+ }
+
+ @Override
+ public Map getMap()
+ {
+ return _map;
+ }
+
+ public Request getBaseRequest()
+ {
+ return _request;
+ }
+
+ public Response getBaseResponse()
+ {
+ return _response;
+ }
+
+ @Override
+ public Object getRequestMessage()
+ {
+ if (_request == null)
+ return null;
+ return Request.as(_request, ServletContextRequest.class).getServletApiRequest();
+ }
+
+ @Override
+ public Object getResponseMessage()
+ {
+ if (_response == null)
+ return null;
+ return Response.as(_response, ServletContextResponse.class).getServletApiResponse();
+ }
+
+ @Override
+ public void setRequestMessage(Object request)
+ {
+ if (!(request instanceof ServletRequest))
+ throw new IllegalStateException("Not a ServletRequest");
+ _request = ServletContextRequest.getServletContextRequest((ServletRequest)request);
+ }
+
+ @Override
+ public void setResponseMessage(Object response)
+ {
+ if (!(response instanceof ServletResponse))
+ throw new IllegalStateException("Not a ServletResponse");
+ _response = ServletContextResponse.getServletContextResponse((ServletResponse)response);
+ }
+
+ //TODO this has bugs in the view implementations. Changing them will not affect the hardcoded values.
+ private static class MIMap implements Map
+ {
+ private String authenticationType;
+ private Map delegate;
+
+ private MIMap()
+ {
+ }
+
+ @Override
+ public int size()
+ {
+ return delegate.size();
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return delegate == null || delegate.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key)
+ {
+ if (AUTHENTICATION_TYPE_KEY.equals(key))
+ return authenticationType != null;
+ return delegate != null && delegate.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value)
+ {
+ if (authenticationType == value || (authenticationType != null && authenticationType.equals(value)))
+ return true;
+ return delegate != null && delegate.containsValue(value);
+ }
+
+ @Override
+ public Object get(Object key)
+ {
+ if (AUTHENTICATION_TYPE_KEY.equals(key))
+ return authenticationType;
+ if (delegate == null)
+ return null;
+ return delegate.get(key);
+ }
+
+ @Override
+ public Object put(Object key, Object value)
+ {
+ if (AUTHENTICATION_TYPE_KEY.equals(key))
+ {
+ String authenticationType = this.authenticationType;
+ this.authenticationType = (String)value;
+ if (delegate != null)
+ delegate.put(AUTHENTICATION_TYPE_KEY, value);
+ return authenticationType;
+ }
+
+ return getDelegate(true).put(key, value);
+ }
+
+ @Override
+ public Object remove(Object key)
+ {
+ if (AUTHENTICATION_TYPE_KEY.equals(key))
+ {
+ String authenticationType = this.authenticationType;
+ this.authenticationType = null;
+ if (delegate != null)
+ delegate.remove(AUTHENTICATION_TYPE_KEY);
+ return authenticationType;
+ }
+ if (delegate == null)
+ return null;
+ return delegate.remove(key);
+ }
+
+ @Override
+ public void putAll(Map map)
+ {
+ if (map != null)
+ {
+ for (Object o : map.entrySet())
+ {
+ Map.Entry entry = (Entry)o;
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ @Override
+ public void clear()
+ {
+ authenticationType = null;
+ delegate = null;
+ }
+
+ @Override
+ public Set keySet()
+ {
+ return getDelegate(true).keySet();
+ }
+
+ @Override
+ public Collection values()
+ {
+ return getDelegate(true).values();
+ }
+
+ @Override
+ public Set entrySet()
+ {
+ return getDelegate(true).entrySet();
+ }
+
+ private Map getDelegate(boolean create)
+ {
+ if (!create || delegate != null)
+ return delegate;
+ if (create)
+ {
+ delegate = new HashMap();
+ if (authenticationType != null)
+ delegate.put(AUTHENTICATION_TYPE_KEY, authenticationType);
+ }
+ return delegate;
+ }
+
+ String getAuthenticationType()
+ {
+ return authenticationType;
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/ServletCallbackHandler.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/ServletCallbackHandler.java
new file mode 100644
index 00000000000..f4ba919ffae
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/ServletCallbackHandler.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.io.IOException;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import jakarta.security.auth.message.callback.CallerPrincipalCallback;
+import jakarta.security.auth.message.callback.CertStoreCallback;
+import jakarta.security.auth.message.callback.GroupPrincipalCallback;
+import jakarta.security.auth.message.callback.PasswordValidationCallback;
+import jakarta.security.auth.message.callback.PrivateKeyCallback;
+import jakarta.security.auth.message.callback.SecretKeyCallback;
+import jakarta.security.auth.message.callback.TrustStoreCallback;
+import org.eclipse.jetty.ee11.security.jaspi.callback.CredentialValidationCallback;
+import org.eclipse.jetty.ee11.servlet.security.authentication.LoginCallback;
+import org.eclipse.jetty.ee11.servlet.security.authentication.LoginCallbackImpl;
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.security.UserIdentity;
+
+/**
+ * This {@link CallbackHandler} will bridge {@link Callback}s to handle to the given to the Jetty {@link LoginService}.
+ */
+public class ServletCallbackHandler implements CallbackHandler
+{
+ private final LoginService _loginService;
+ private final ThreadLocal _callerPrincipals = new ThreadLocal<>();
+ private final ThreadLocal _groupPrincipals = new ThreadLocal<>();
+
+ public ServletCallbackHandler(LoginService loginService)
+ {
+ _loginService = loginService;
+ }
+
+ @Override
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
+ {
+ for (Callback callback : callbacks)
+ {
+ // jaspi to server communication
+ if (callback instanceof CallerPrincipalCallback)
+ {
+ _callerPrincipals.set((CallerPrincipalCallback)callback);
+ }
+ else if (callback instanceof GroupPrincipalCallback)
+ {
+ _groupPrincipals.set((GroupPrincipalCallback)callback);
+ }
+ else if (callback instanceof PasswordValidationCallback passwordValidationCallback)
+ {
+ @SuppressWarnings("unused")
+ Subject subject = passwordValidationCallback.getSubject();
+
+ UserIdentity user = _loginService.login(passwordValidationCallback.getUsername(), passwordValidationCallback.getPassword(), null, null);
+
+ if (user != null)
+ {
+ passwordValidationCallback.setResult(true);
+ passwordValidationCallback.getSubject().getPrincipals().addAll(user.getSubject().getPrincipals());
+ passwordValidationCallback.getSubject().getPrivateCredentials().add(user);
+ }
+ }
+ else if (callback instanceof CredentialValidationCallback credentialValidationCallback)
+ {
+ Subject subject = credentialValidationCallback.getSubject();
+ LoginCallback loginCallback = new LoginCallbackImpl(subject,
+ credentialValidationCallback.getUsername(),
+ credentialValidationCallback.getCredential());
+
+ UserIdentity user = _loginService.login(credentialValidationCallback.getUsername(), credentialValidationCallback.getCredential(), null, null);
+
+ if (user != null)
+ {
+ loginCallback.setUserPrincipal(user.getUserPrincipal());
+ credentialValidationCallback.getSubject().getPrivateCredentials().add(loginCallback);
+ credentialValidationCallback.setResult(true);
+ credentialValidationCallback.getSubject().getPrincipals().addAll(user.getSubject().getPrincipals());
+ credentialValidationCallback.getSubject().getPrivateCredentials().add(user);
+ }
+ }
+ // server to jaspi communication
+ else if (callback instanceof CertStoreCallback)
+ {
+ // TODO implement this
+ }
+ else if (callback instanceof PrivateKeyCallback)
+ {
+ // TODO implement this
+ }
+ else if (callback instanceof SecretKeyCallback)
+ {
+ // TODO implement this
+ }
+ else if (callback instanceof TrustStoreCallback)
+ {
+ // TODO implement this
+ }
+ else
+ {
+ throw new UnsupportedCallbackException(callback);
+ }
+ }
+ }
+
+ public CallerPrincipalCallback getThreadCallerPrincipalCallback()
+ {
+ CallerPrincipalCallback callerPrincipalCallback = _callerPrincipals.get();
+ _callerPrincipals.set(null);
+ return callerPrincipalCallback;
+ }
+
+ public GroupPrincipalCallback getThreadGroupPrincipalCallback()
+ {
+ GroupPrincipalCallback groupPrincipalCallback = _groupPrincipals.get();
+ _groupPrincipals.set(null);
+ return groupPrincipalCallback;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/SimpleAuthConfig.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/SimpleAuthConfig.java
new file mode 100644
index 00000000000..34b14f0ed39
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/SimpleAuthConfig.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.util.Map;
+import javax.security.auth.Subject;
+
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.security.auth.message.config.ServerAuthConfig;
+import jakarta.security.auth.message.config.ServerAuthContext;
+import org.eclipse.jetty.ee11.security.jaspi.provider.JaspiAuthConfigProvider;
+
+/**
+ * @deprecated use {@link JaspiAuthConfigProvider}.
+ */
+@Deprecated
+public class SimpleAuthConfig implements ServerAuthConfig
+{
+ public static final String HTTP_SERVLET = "HttpServlet";
+
+ private final String _appContext;
+
+ private final ServerAuthContext _serverAuthContext;
+
+ public SimpleAuthConfig(String appContext, ServerAuthContext serverAuthContext)
+ {
+ this._appContext = appContext;
+ this._serverAuthContext = serverAuthContext;
+ }
+
+ @Override
+ public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties)
+ {
+ return _serverAuthContext;
+ }
+
+ // supposed to be of form host-namecontext-path
+ @Override
+ public String getAppContext()
+ {
+ return _appContext;
+ }
+
+ // not used yet
+ @Override
+ public String getAuthContextID(MessageInfo messageInfo) throws IllegalArgumentException
+ {
+ return null;
+ }
+
+ @Override
+ public String getMessageLayer()
+ {
+ return HTTP_SERVLET;
+ }
+
+ @Override
+ public boolean isProtected()
+ {
+ return true;
+ }
+
+ @Override
+ public void refresh()
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/callback/CredentialValidationCallback.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/callback/CredentialValidationCallback.java
new file mode 100644
index 00000000000..e07b8980c1a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/callback/CredentialValidationCallback.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi.callback;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * CredentialValidationCallback
+ *
+ * Store a jetty Credential for a user so that it can be
+ * validated by jaspi
+ */
+public class CredentialValidationCallback implements Callback
+{
+ private Credential _credential;
+ private boolean _result;
+ private Subject _subject;
+ private String _userName;
+
+ public CredentialValidationCallback(Subject subject, String userName, Credential credential)
+ {
+ _subject = subject;
+ _userName = userName;
+ _credential = credential;
+ }
+
+ public Credential getCredential()
+ {
+ return _credential;
+ }
+
+ public void clearCredential()
+ {
+ _credential = null;
+ }
+
+ public boolean getResult()
+ {
+ return _result;
+ }
+
+ public javax.security.auth.Subject getSubject()
+ {
+ return _subject;
+ }
+
+ public java.lang.String getUsername()
+ {
+ return _userName;
+ }
+
+ public void setResult(boolean result)
+ {
+ _result = result;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/callback/package-info.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/callback/package-info.java
new file mode 100644
index 00000000000..3b612429ce2
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/callback/package-info.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty Jaspi : Callbacks
+ */
+package org.eclipse.jetty.ee11.security.jaspi.callback;
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/BaseAuthModule.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/BaseAuthModule.java
new file mode 100644
index 00000000000..0afaa60d3c0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/BaseAuthModule.java
@@ -0,0 +1,127 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi.modules;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Set;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import jakarta.security.auth.message.AuthException;
+import jakarta.security.auth.message.AuthStatus;
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.security.auth.message.MessagePolicy;
+import jakarta.security.auth.message.callback.CallerPrincipalCallback;
+import jakarta.security.auth.message.callback.GroupPrincipalCallback;
+import jakarta.security.auth.message.config.ServerAuthContext;
+import jakarta.security.auth.message.module.ServerAuthModule;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee11.security.jaspi.JaspiMessageInfo;
+import org.eclipse.jetty.ee11.security.jaspi.callback.CredentialValidationCallback;
+import org.eclipse.jetty.ee11.servlet.security.authentication.LoginCallbackImpl;
+import org.eclipse.jetty.util.security.Credential;
+import org.eclipse.jetty.util.security.Password;
+
+/**
+ * Simple abstract module implementing a Jakarta Authentication {@link ServerAuthModule} and {@link ServerAuthContext}.
+ * To be used as a building block for building more sophisticated auth modules.
+ */
+public abstract class BaseAuthModule implements ServerAuthModule, ServerAuthContext
+{
+ private static final Class[] SUPPORTED_MESSAGE_TYPES = new Class[]{HttpServletRequest.class, HttpServletResponse.class};
+
+ protected static final String LOGIN_SERVICE_KEY = "org.eclipse.jetty.ee11.security.jaspi.modules.LoginService";
+
+ protected CallbackHandler callbackHandler;
+
+ @Override
+ public Class[] getSupportedMessageTypes()
+ {
+ return SUPPORTED_MESSAGE_TYPES;
+ }
+
+ public BaseAuthModule()
+ {
+ }
+
+ public BaseAuthModule(CallbackHandler callbackHandler)
+ {
+ this.callbackHandler = callbackHandler;
+ }
+
+ @Override
+ public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, Map options) throws AuthException
+ {
+ this.callbackHandler = handler;
+ }
+
+ @Override
+ public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException
+ {
+ // TODO apparently we either get the LoginCallback or the LoginService
+ // but not both :-(
+ // Set loginCallbacks =
+ // subject.getPrivateCredentials(LoginCallback.class);
+ // if (!loginCallbacks.isEmpty()) {
+ // LoginCallback loginCallback = loginCallbacks.iterator().next();
+ // }
+ // try {
+ // loginService.logout(subject);
+ // } catch (ServerAuthException e) {
+ // throw new AuthException(e.getMessage());
+ // }
+ }
+
+ @Override
+ public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException
+ {
+ // servlets do not need secured responses
+ return AuthStatus.SEND_SUCCESS;
+ }
+
+ protected boolean login(Subject clientSubject, String credentials, String authenticationType, MessageInfo messageInfo) throws IOException, UnsupportedCallbackException
+ {
+ credentials = credentials.substring(credentials.indexOf(' ') + 1);
+ credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1);
+ int i = credentials.indexOf(':');
+ String userName = credentials.substring(0, i);
+ String password = credentials.substring(i + 1);
+ return login(clientSubject, userName, new Password(password), authenticationType, messageInfo);
+ }
+
+ protected boolean login(Subject clientSubject, String username, Credential credential, String authenticationType, MessageInfo messageInfo) throws IOException, UnsupportedCallbackException
+ {
+ CredentialValidationCallback credValidationCallback = new CredentialValidationCallback(clientSubject, username, credential);
+ callbackHandler.handle(new Callback[]{credValidationCallback});
+ if (credValidationCallback.getResult())
+ {
+ Set loginCallbacks = clientSubject.getPrivateCredentials(LoginCallbackImpl.class);
+ if (!loginCallbacks.isEmpty())
+ {
+ LoginCallbackImpl loginCallback = loginCallbacks.iterator().next();
+ CallerPrincipalCallback callerPrincipalCallback = new CallerPrincipalCallback(clientSubject, loginCallback.getUserPrincipal());
+ GroupPrincipalCallback groupPrincipalCallback = new GroupPrincipalCallback(clientSubject, loginCallback.getRoles());
+ callbackHandler.handle(new Callback[]{callerPrincipalCallback, groupPrincipalCallback});
+ }
+ messageInfo.getMap().put(JaspiMessageInfo.AUTHENTICATION_TYPE_KEY, authenticationType);
+ }
+ return credValidationCallback.getResult();
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/BasicAuthenticationAuthModule.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/BasicAuthenticationAuthModule.java
new file mode 100644
index 00000000000..09a462067f2
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/BasicAuthenticationAuthModule.java
@@ -0,0 +1,97 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi.modules;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import jakarta.security.auth.message.AuthException;
+import jakarta.security.auth.message.AuthStatus;
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.security.auth.message.MessagePolicy;
+import jakarta.security.auth.message.module.ServerAuthModule;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee11.security.jaspi.JaspiMessageInfo;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.security.Authenticator;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link ServerAuthModule} implementation of HTTP Basic Authentication.
+ */
+public class BasicAuthenticationAuthModule extends BaseAuthModule
+{
+ private static final Logger LOG = LoggerFactory.getLogger(BasicAuthenticationAuthModule.class);
+
+ private String _realmName;
+
+ private static final String REALM_KEY = "org.eclipse.jetty.ee11.security.jaspi.modules.RealmName";
+
+ public BasicAuthenticationAuthModule()
+ {
+ }
+
+ public BasicAuthenticationAuthModule(CallbackHandler callbackHandler, String realmName)
+ {
+ super(callbackHandler);
+ _realmName = realmName;
+ }
+
+ @Override
+ public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler callbackHandler, Map options) throws AuthException
+ {
+ super.initialize(requestPolicy, responsePolicy, callbackHandler, options);
+ _realmName = (String)options.get(REALM_KEY);
+ }
+
+ @Override
+ public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException
+ {
+ if (!(messageInfo instanceof JaspiMessageInfo))
+ throw new IllegalStateException("Not a JaspiMessageInfo");
+ JaspiMessageInfo jaspiMessageInfo = (JaspiMessageInfo)messageInfo;
+
+ Request request = jaspiMessageInfo.getBaseRequest();
+ Response response = jaspiMessageInfo.getBaseResponse();
+
+ String credentials = request.getHeaders().get(HttpHeader.AUTHORIZATION.asString());
+
+ try
+ {
+ if (credentials != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Credentials: {}", credentials);
+ if (login(clientSubject, credentials, Authenticator.BASIC_AUTH, messageInfo))
+ {
+ return AuthStatus.SUCCESS;
+ }
+ }
+
+ response.getHeaders().put(HttpHeader.WWW_AUTHENTICATE.asString(), "Basic realm=\"" + _realmName + '"');
+ Response.writeError(request, response, jaspiMessageInfo.getCallback(), HttpServletResponse.SC_UNAUTHORIZED);
+ return AuthStatus.SEND_CONTINUE;
+ }
+ catch (IOException | UnsupportedCallbackException e)
+ {
+ throw new AuthException(e.getMessage());
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/package-info.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/package-info.java
new file mode 100644
index 00000000000..ba8c59c26ae
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/modules/package-info.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty Jaspi : Authentication Modules
+ */
+package org.eclipse.jetty.ee11.security.jaspi.modules;
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/package-info.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/package-info.java
new file mode 100644
index 00000000000..bd1ce831035
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/package-info.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty Jaspi : Java Authentication SPI
+ */
+package org.eclipse.jetty.ee11.security.jaspi;
+
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/JaspiAuthConfigProvider.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/JaspiAuthConfigProvider.java
new file mode 100644
index 00000000000..e3283b32e4c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/JaspiAuthConfigProvider.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi.provider;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.CallbackHandler;
+
+import jakarta.security.auth.message.config.AuthConfigFactory;
+import jakarta.security.auth.message.config.AuthConfigProvider;
+import jakarta.security.auth.message.config.ClientAuthConfig;
+import jakarta.security.auth.message.config.ServerAuthConfig;
+import jakarta.security.auth.message.module.ServerAuthModule;
+import org.eclipse.jetty.ee11.security.jaspi.JaspiAuthenticatorFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
A Jetty implementation of the {@link AuthConfigProvider} to allow registration of a {@link ServerAuthModule}
+ * directly without having to write a custom {@link AuthConfigProvider}.
+ *
If this is being constructed by an {@link AuthConfigFactory} after being passed in as a className, then
+ * you will need to provide the property {@code ServerAuthModule} containing the fully qualified name of
+ * the {@link ServerAuthModule} class you wish to use.
+ */
+@SuppressWarnings("rawtypes")
+public class JaspiAuthConfigProvider implements AuthConfigProvider
+{
+ private static final Logger LOG = LoggerFactory.getLogger(JaspiAuthConfigProvider.class);
+ private final Map providerProperties;
+ private final ServerAuthModule serverAuthModule;
+
+ /**
+ *
Constructor with signature and implementation that's required by API.
+ *
The property map must contain the {@code ServerAuthModule} property containing the fully qualified name of
+ * the {@link ServerAuthModule} class you wish to use. If this constructor is being used for self-registration an
+ * optional property of {@code appContext} can be used specify the appContext value to register the provider.
+ *
+ * @param properties A Map of initialization properties.
+ * @param factory The {@link AuthConfigFactory} to register on.
+ */
+ public JaspiAuthConfigProvider(Map properties, AuthConfigFactory factory)
+ {
+ if (properties == null || !properties.containsKey("ServerAuthModule"))
+ throw new IllegalArgumentException("Missing property 'ServerAuthModule', cannot create JaspiAuthConfigProvider");
+
+ this.providerProperties = Map.copyOf(properties);
+ this.serverAuthModule = createServerAuthModule((String)properties.get("ServerAuthModule"));
+
+ // API requires self registration if factory is provided.
+ if (factory != null)
+ factory.registerConfigProvider(this, JaspiAuthenticatorFactory.MESSAGE_LAYER, (String)properties.get("appContext"), "Self Registration");
+ }
+
+ /**
+ * @param className The fully qualified name of a {@link ServerAuthModule} class.
+ */
+ public JaspiAuthConfigProvider(String className)
+ {
+ this(className, null);
+ }
+
+ /**
+ * @param className The fully qualified name of a {@link ServerAuthModule} class.
+ * @param properties A Map of initialization properties.
+ */
+ public JaspiAuthConfigProvider(String className, Map properties)
+ {
+ this(createServerAuthModule(className), properties);
+ }
+
+ /**
+ * @param serverAuthModule The instance of {@link ServerAuthModule} to use.
+ */
+ public JaspiAuthConfigProvider(ServerAuthModule serverAuthModule)
+ {
+ this.serverAuthModule = Objects.requireNonNull(serverAuthModule);
+ this.providerProperties = Collections.emptyMap();
+ }
+
+ /**
+ * @param serverAuthModule The instance of {@link ServerAuthModule} to use.
+ * @param properties A Map of initialization properties.
+ */
+ public JaspiAuthConfigProvider(ServerAuthModule serverAuthModule, Map properties)
+ {
+ this.serverAuthModule = Objects.requireNonNull(serverAuthModule);
+ this.providerProperties = properties == null ? Collections.emptyMap() : Map.copyOf(properties);
+ }
+
+ @Override
+ public ClientAuthConfig getClientAuthConfig(String layer, String appContext, CallbackHandler handler)
+ {
+ return null;
+ }
+
+ @Override
+ public ServerAuthConfig getServerAuthConfig(String layer, String appContext, CallbackHandler handler)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("getServerAuthConfig");
+ return new SimpleAuthConfig(layer, appContext, handler, providerProperties, serverAuthModule);
+ }
+
+ @Override
+ public void refresh()
+ {
+ }
+
+ private static ServerAuthModule createServerAuthModule(String serverAuthModuleClassName)
+ {
+ try
+ {
+ return (ServerAuthModule)Class.forName(serverAuthModuleClassName).getDeclaredConstructor().newInstance();
+ }
+ catch (ReflectiveOperationException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/SimpleAuthConfig.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/SimpleAuthConfig.java
new file mode 100644
index 00000000000..cb7b6281256
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/SimpleAuthConfig.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi.provider;
+
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+
+import jakarta.security.auth.message.AuthException;
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.security.auth.message.config.ServerAuthConfig;
+import jakarta.security.auth.message.config.ServerAuthContext;
+import jakarta.security.auth.message.module.ServerAuthModule;
+
+/**
+ * Simple implementation of the {@link ServerAuthConfig} interface.
+ *
+ * This implementation wires up the given {@link ServerAuthModule} to the appropriate Jakarta Authentication {@link ServerAuthContext} responsible
+ * for providing it.
+ */
+@SuppressWarnings("rawtypes")
+class SimpleAuthConfig implements ServerAuthConfig
+{
+ private final String _messageLayer;
+ private final String _appContext;
+ private final CallbackHandler _callbackHandler;
+ private final Map _properties;
+ private final ServerAuthModule _serverAuthModule;
+
+ public SimpleAuthConfig(String messageLayer, String appContext, CallbackHandler callbackHandler, Map properties, ServerAuthModule serverAuthModule)
+ {
+ _messageLayer = messageLayer;
+ _appContext = appContext;
+ _callbackHandler = callbackHandler;
+ _properties = properties;
+ _serverAuthModule = serverAuthModule;
+ }
+
+ @Override
+ public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties) throws AuthException
+ {
+ return new SimpleServerAuthContext(_callbackHandler, _serverAuthModule, _properties);
+ }
+
+ @Override
+ public String getAppContext()
+ {
+ return _appContext;
+ }
+
+ @Override
+ public String getAuthContextID(MessageInfo messageInfo)
+ {
+ return null;
+ }
+
+ @Override
+ public String getMessageLayer()
+ {
+ return _messageLayer;
+ }
+
+ @Override
+ public boolean isProtected()
+ {
+ return true;
+ }
+
+ @Override
+ public void refresh()
+ {
+ }
+}
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/SimpleServerAuthContext.java b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/SimpleServerAuthContext.java
new file mode 100644
index 00000000000..150957c9365
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/java/org/eclipse/jetty/ee11/security/jaspi/provider/SimpleServerAuthContext.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi.provider;
+
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+
+import jakarta.security.auth.message.AuthException;
+import jakarta.security.auth.message.AuthStatus;
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.security.auth.message.config.ServerAuthContext;
+import jakarta.security.auth.message.module.ServerAuthModule;
+
+/**
+ * Simple bridge implementation of the Jakarta Authentication {@link ServerAuthContext} interface.
+ *
+ * This implementation will only delegate to the provided {@link ServerAuthModule} implementation.
+ */
+class SimpleServerAuthContext implements ServerAuthContext
+{
+ private final ServerAuthModule serverAuthModule;
+
+ @SuppressWarnings("rawtypes")
+ public SimpleServerAuthContext(CallbackHandler callbackHandler, ServerAuthModule serverAuthModule, Map properties) throws AuthException
+ {
+ this.serverAuthModule = serverAuthModule;
+ serverAuthModule.initialize(null, null, callbackHandler, properties);
+ }
+
+ @Override
+ public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException
+ {
+ return serverAuthModule.validateRequest(messageInfo, clientSubject, serviceSubject);
+ }
+
+ @Override
+ public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException
+ {
+ return serverAuthModule.secureResponse(messageInfo, serviceSubject);
+ }
+
+ @Override
+ public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException
+ {
+ serverAuthModule.cleanSubject(messageInfo, subject);
+ }
+}
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory b/jetty-ee11/jetty-ee11-jaspi/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory
new file mode 100644
index 00000000000..4986436902a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory
@@ -0,0 +1 @@
+org.eclipse.jetty.ee11.security.jaspi.JaspiAuthenticatorFactory
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/DefaultAuthConfigFactoryTest.java b/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/DefaultAuthConfigFactoryTest.java
new file mode 100644
index 00000000000..ee6a81d6e28
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/DefaultAuthConfigFactoryTest.java
@@ -0,0 +1,115 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.util.Map;
+
+import jakarta.security.auth.message.config.AuthConfigFactory;
+import jakarta.security.auth.message.config.AuthConfigProvider;
+import jakarta.security.auth.message.config.RegistrationListener;
+import org.eclipse.jetty.ee11.security.jaspi.provider.JaspiAuthConfigProvider;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+public class DefaultAuthConfigFactoryTest
+{
+
+ private static final String MESSAGE_LAYER = "HttpServlet";
+
+ private final String jettyAuthConfigProvider = "org.eclipse.jetty.ee11.security.jaspi.provider.JaspiAuthConfigProvider";
+ private final String appContext = "server /test";
+
+ private final Map serverAuthModuleProperties = Map.of("ServerAuthModule",
+ "org.eclipse.jetty.ee11.security.jaspi.modules.BasicAuthenticationAuthModule", "AppContextID", appContext,
+ "org.eclipse.jetty.ee11.security.jaspi.modules.RealmName", "TestRealm");
+
+ private final String serverAuthModuleClassName = "org.eclipse.jetty.ee11.security.jaspi.modules.BasicAuthenticationAuthModule";
+
+ @Test
+ public void testRegisterConfigProviderByClassName() throws Exception
+ {
+ AuthConfigFactory factory = new DefaultAuthConfigFactory();
+ String registrationId = factory.registerConfigProvider(jettyAuthConfigProvider,
+ serverAuthModuleProperties, MESSAGE_LAYER, appContext, "a test provider");
+ AuthConfigProvider registeredProvider = factory.getConfigProvider(MESSAGE_LAYER, appContext, null);
+ assertThat(registeredProvider, instanceOf(JaspiAuthConfigProvider.class));
+ assertThat(registeredProvider.getServerAuthConfig(MESSAGE_LAYER, appContext, null), notNullValue());
+
+ assertThat(factory.getRegistrationContext(registrationId), notNullValue());
+ assertThat(factory.getRegistrationIDs(registeredProvider), arrayContaining(registrationId));
+ }
+
+ @Test
+ public void testRegisterAuthConfigProviderDirect() throws Exception
+ {
+ AuthConfigProvider provider = new JaspiAuthConfigProvider(
+ serverAuthModuleClassName,
+ serverAuthModuleProperties);
+
+ AuthConfigFactory factory = new DefaultAuthConfigFactory();
+ String registrationId = factory.registerConfigProvider(provider, MESSAGE_LAYER, appContext, "a test provider");
+
+ AuthConfigProvider registeredProvider = factory.getConfigProvider(MESSAGE_LAYER, appContext, null);
+ assertThat(registeredProvider, instanceOf(JaspiAuthConfigProvider.class));
+ assertThat(registeredProvider.getServerAuthConfig(MESSAGE_LAYER, appContext, null), notNullValue());
+
+ assertThat(factory.getRegistrationContext(registrationId), notNullValue());
+ assertThat(factory.getRegistrationIDs(registeredProvider), arrayContaining(registrationId));
+ }
+
+ @Test
+ public void testRemoveRegistration() throws Exception
+ {
+ // Arrange
+ AuthConfigProvider provider = new JaspiAuthConfigProvider(
+ serverAuthModuleClassName,
+ serverAuthModuleProperties);
+
+ AuthConfigFactory factory = new DefaultAuthConfigFactory();
+ String registrationId = factory.registerConfigProvider(provider, MESSAGE_LAYER, appContext, "a test provider");
+
+ DummyRegistrationListener dummyListener = new DummyRegistrationListener();
+ assertThat(factory.getConfigProvider(MESSAGE_LAYER, appContext, dummyListener), notNullValue());
+
+ // Act
+ factory.removeRegistration(registrationId);
+
+ // Assert config provider removed
+ assertThat(factory.getConfigProvider(MESSAGE_LAYER, appContext, null), nullValue());
+
+ // Assert listeners invoked
+ assertThat(dummyListener.appContext, equalTo(appContext));
+ assertThat(dummyListener.layer, equalTo(MESSAGE_LAYER));
+
+ }
+
+ static class DummyRegistrationListener implements RegistrationListener
+ {
+ String layer;
+ String appContext;
+
+ @Override
+ public void notify(String layer, String appContext)
+ {
+ this.layer = layer;
+ this.appContext = appContext;
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/HttpHeaderAuthModule.java b/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/HttpHeaderAuthModule.java
new file mode 100644
index 00000000000..921e233ee16
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/HttpHeaderAuthModule.java
@@ -0,0 +1,117 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+
+import jakarta.security.auth.message.AuthException;
+import jakarta.security.auth.message.AuthStatus;
+import jakarta.security.auth.message.MessageInfo;
+import jakarta.security.auth.message.MessagePolicy;
+import jakarta.security.auth.message.callback.CallerPrincipalCallback;
+import jakarta.security.auth.message.callback.GroupPrincipalCallback;
+import jakarta.security.auth.message.module.ServerAuthModule;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * Example JASPI Auth Module based on http://www.trajano.net/2014/06/creating-a-simple-jaspic-auth-module/
+ */
+public class HttpHeaderAuthModule implements ServerAuthModule
+{
+
+ /**
+ * Supported message types. For our case we only need to deal with HTTP servlet request and responses. On Java EE 7 this will handle WebSockets as well.
+ */
+ private static final Class>[] SUPPORTED_MESSAGE_TYPES = new Class>[]
+ {HttpServletRequest.class, HttpServletResponse.class};
+
+ /**
+ * Callback handler that is passed in initialize by the container. This processes the callbacks which are objects that populate the "subject".
+ */
+ private CallbackHandler handler;
+
+ /**
+ * Does nothing of note for what we need.
+ */
+ @Override
+ public void cleanSubject(final MessageInfo messageInfo, final Subject subject) throws AuthException
+ {
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Class[] getSupportedMessageTypes()
+ {
+ return SUPPORTED_MESSAGE_TYPES;
+ }
+
+ /**
+ * Initializes the module. Allows you to pass in options.
+ *
+ * @param requestPolicy request policy, ignored
+ * @param responsePolicy response policy, ignored
+ * @param h callback handler
+ * @param options options
+ */
+ @Override
+ public void initialize(final MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler h, Map options) throws AuthException
+ {
+ handler = h;
+ }
+
+ /**
+ * @return AuthStatus.SEND_SUCCESS
+ */
+ @Override
+ public AuthStatus secureResponse(final MessageInfo paramMessageInfo, final Subject subject) throws AuthException
+ {
+ return AuthStatus.SEND_SUCCESS;
+ }
+
+ /**
+ * Validation occurs here.
+ */
+ @Override
+ public AuthStatus validateRequest(final MessageInfo messageInfo, final Subject client, final Subject serviceSubject) throws AuthException
+ {
+ // Take the request from the messageInfo structure.
+ final HttpServletRequest req = (HttpServletRequest)messageInfo.getRequestMessage();
+ try
+ {
+ // Get the user name from the header. If not there then fail authentication.
+ final String userName = req.getHeader("X-Forwarded-User");
+ if (userName == null)
+ {
+ return AuthStatus.FAILURE;
+ }
+
+ // Store the user name that was in the header and also set a group.
+ handler.handle(new Callback[]
+ {
+ new CallerPrincipalCallback(client, userName),
+ new GroupPrincipalCallback(client, new String[]
+ {"users"})
+ });
+ return AuthStatus.SUCCESS;
+ }
+ catch (final Exception e)
+ {
+ throw new AuthException(e.getMessage());
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/JaspiTest.java b/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/JaspiTest.java
new file mode 100644
index 00000000000..c1498d1d354
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jaspi/src/test/java/org/eclipse/jetty/ee11/security/jaspi/JaspiTest.java
@@ -0,0 +1,229 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.security.jaspi;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import jakarta.security.auth.message.config.AuthConfigFactory;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintMapping;
+import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.security.AbstractLoginService;
+import org.eclipse.jetty.security.Constraint;
+import org.eclipse.jetty.security.RolePrincipal;
+import org.eclipse.jetty.security.UserPrincipal;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.util.security.Credential;
+import org.eclipse.jetty.util.security.Password;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.startsWith;
+
+public class JaspiTest
+{
+ Server _server;
+ LocalConnector _connector;
+
+ public static class TestLoginService extends AbstractLoginService
+ {
+ protected Map _users = new HashMap<>();
+ protected Map> _roles = new HashMap<>();
+
+ public TestLoginService(String name)
+ {
+ setName(name);
+ }
+
+ public void putUser(String username, Credential credential, String[] roles)
+ {
+ UserPrincipal userPrincipal = new UserPrincipal(username, credential);
+ _users.put(username, userPrincipal);
+ if (roles != null)
+ {
+ List rps = Arrays.stream(roles).map(RolePrincipal::new).collect(Collectors.toList());
+ _roles.put(username, rps);
+ }
+ }
+
+ @Override
+ protected List loadRoleInfo(UserPrincipal user)
+ {
+ return _roles.get(user.getName());
+ }
+
+ @Override
+ protected UserPrincipal loadUserInfo(String username)
+ {
+ return _users.get(username);
+ }
+ }
+
+ @BeforeAll
+ public static void beforeAll() throws Exception
+ {
+ AuthConfigFactory factory = new DefaultAuthConfigFactory();
+
+ factory.registerConfigProvider("org.eclipse.jetty.ee11.security.jaspi.provider.JaspiAuthConfigProvider",
+ Map.of("ServerAuthModule", "org.eclipse.jetty.ee11.security.jaspi.modules.BasicAuthenticationAuthModule",
+ "AppContextID", "server /ctx",
+ "org.eclipse.jetty.ee11.security.jaspi.modules.RealmName", "TestRealm"),
+ "HttpServlet", "server /ctx", "a test provider");
+
+ factory.registerConfigProvider("org.eclipse.jetty.ee11.security.jaspi.provider.JaspiAuthConfigProvider",
+ Map.of("ServerAuthModule", "org.eclipse.jetty.ee11.security.jaspi.HttpHeaderAuthModule",
+ "AppContextID", "server /other"),
+ "HttpServlet", "server /other", "another test provider");
+
+ AuthConfigFactory.setFactory(factory);
+ }
+
+ @AfterAll
+ public static void afterAll() throws Exception
+ {
+ AuthConfigFactory.setFactory(null);
+ }
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _server = new Server();
+ _connector = new LocalConnector(_server);
+ _server.addConnector(_connector);
+
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ _server.setHandler(contexts);
+
+ TestLoginService loginService = new TestLoginService("TestRealm");
+ loginService.putUser("user", new Password("password"), new String[] {"users"});
+ loginService.putUser("admin", new Password("secret"), new String[] {"users", "admins"});
+ _server.addBean(loginService);
+
+ ServletContextHandler context = new ServletContextHandler();
+ contexts.addHandler(context);
+ context.setContextPath("/ctx");
+ context.addServlet(new TestServlet(), "/");
+
+ JaspiAuthenticatorFactory jaspiAuthFactory = new JaspiAuthenticatorFactory();
+
+ ConstraintSecurityHandler security = new ConstraintSecurityHandler();
+ context.setSecurityHandler(security);
+ security.setAuthenticatorFactory(jaspiAuthFactory);
+
+ Constraint constraint = new Constraint.Builder()
+ .name("All")
+ .roles("users")
+ .build();
+ ConstraintMapping mapping = new ConstraintMapping();
+ mapping.setPathSpec("/jaspi/*");
+ mapping.setConstraint(constraint);
+ security.addConstraintMapping(mapping);
+
+ ServletContextHandler other = new ServletContextHandler();
+ contexts.addHandler(other);
+ other.setContextPath("/other");
+ other.addServlet(new TestServlet(), "/");
+ ConstraintSecurityHandler securityOther = new ConstraintSecurityHandler();
+ other.setSecurityHandler(securityOther);
+ securityOther.setAuthenticatorFactory(jaspiAuthFactory);
+ securityOther.addConstraintMapping(mapping);
+
+ _server.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testNoConstraint() throws Exception
+ {
+ String response = _connector.getResponse("GET /ctx/test HTTP/1.0\n\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testConstraintNoAuth() throws Exception
+ {
+ String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, Matchers.containsString("WWW-Authenticate: Basic realm=\"TestRealm\""));
+ }
+
+ @Test
+ public void testConstraintWrongAuth() throws Exception
+ {
+ String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" + "Authorization: Basic " +
+ Base64.getEncoder().encodeToString("user:wrong".getBytes(ISO_8859_1)) + "\n\n");
+ assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
+ assertThat(response, Matchers.containsString("WWW-Authenticate: Basic realm=\"TestRealm\""));
+ }
+
+ @Test
+ public void testConstraintAuth() throws Exception
+ {
+ String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" + "Authorization: Basic " +
+ Base64.getEncoder().encodeToString("user:password".getBytes(ISO_8859_1)) + "\n\n");
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ @Test
+ public void testOtherNoAuth() throws Exception
+ {
+ String response = _connector.getResponse("GET /other/jaspi/test HTTP/1.0\n\n");
+ assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
+ }
+
+ @Test
+ public void testOtherAuth() throws Exception
+ {
+ String response = _connector.getResponse("""
+ GET /other/jaspi/test HTTP/1.0
+ X-Forwarded-User: user
+
+ """);
+ assertThat(response, startsWith("HTTP/1.1 200 OK"));
+ }
+
+ public static class TestServlet extends HttpServlet
+ {
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
+ {
+ resp.setStatus(200);
+ resp.setContentType("text/plain");
+ resp.getWriter().println("All OK");
+ resp.getWriter().println("requestURI=" + req.getRequestURI());
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jndi/pom.xml b/jetty-ee11/jetty-ee11-jndi/pom.xml
new file mode 100644
index 00000000000..5e0ad875562
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jndi/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-jndi
+ EE11 :: JNDI
+ EE11 JNDI factories
+
+
+ ${project.groupId}.jndi
+ org.eclipse.jetty.ee11.jndi.*
+
+
+
+
+ jakarta.activation
+ jakarta.activation-api
+ true
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.slf4j
+ slf4j-api
+
+
+ jakarta.mail
+ jakarta.mail-api
+ provided
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ test
+
+
+ org.eclipse.jetty.toolchain
+ jetty-test-helper
+ test
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ ${osgi.slf4j.import.packages},jakarta.mail.*;resolution:=optional,*
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jndi/src/main/config/modules/ee11-jndi.mod b/jetty-ee11/jetty-ee11-jndi/src/main/config/modules/ee11-jndi.mod
new file mode 100644
index 00000000000..144db52c08a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jndi/src/main/config/modules/ee11-jndi.mod
@@ -0,0 +1,13 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Adds the Jetty EE11 JNDI reference factories
+
+[environment]
+ee11
+
+[depend]
+jndi
+
+[lib]
+lib/jetty-ee11-jndi-${jetty.version}.jar
diff --git a/jetty-ee11/jetty-ee11-jndi/src/main/java/module-info.java b/jetty-ee11/jetty-ee11-jndi/src/main/java/module-info.java
new file mode 100644
index 00000000000..b4e80d9390c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jndi/src/main/java/module-info.java
@@ -0,0 +1,25 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+module org.eclipse.jetty.ee11.jndi
+{
+ requires org.slf4j;
+
+ requires transitive org.eclipse.jetty.util;
+ requires transitive java.naming;
+
+ // Only required if using MailSessionReference.
+ requires static jakarta.mail;
+
+ exports org.eclipse.jetty.ee11.jndi.factories;
+}
diff --git a/jetty-ee11/jetty-ee11-jndi/src/main/java/org/eclipse/jetty/ee11/jndi/factories/MailSessionReference.java b/jetty-ee11/jetty-ee11-jndi/src/main/java/org/eclipse/jetty/ee11/jndi/factories/MailSessionReference.java
new file mode 100644
index 00000000000..29e10b1ee52
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jndi/src/main/java/org/eclipse/jetty/ee11/jndi/factories/MailSessionReference.java
@@ -0,0 +1,168 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jndi.factories;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.RefAddr;
+import javax.naming.Reference;
+import javax.naming.StringRefAddr;
+import javax.naming.spi.ObjectFactory;
+
+import jakarta.mail.Authenticator;
+import jakarta.mail.PasswordAuthentication;
+import jakarta.mail.Session;
+import org.eclipse.jetty.util.security.Password;
+
+/**
+ * MailSessionReference
+ *
+ * This is a subclass of jakarta.mail.Reference and an ObjectFactory for jakarta.mail.Session objects.
+ *
+ * The subclassing of Reference allows all of the setup for a jakarta.mail.Session
+ * to be captured without necessitating first instantiating a Session object. The
+ * reference is bound into JNDI and it is only when the reference is looked up that
+ * this object factory will create an instance of jakarta.mail.Session using the
+ * information captured in the Reference.
+ */
+public class MailSessionReference extends Reference implements ObjectFactory
+{
+ public static class PasswordAuthenticator extends Authenticator
+ {
+ PasswordAuthentication passwordAuthentication;
+ private String user;
+ private String password;
+
+ public PasswordAuthenticator()
+ {
+
+ }
+
+ public PasswordAuthenticator(String user, String password)
+ {
+ passwordAuthentication = new PasswordAuthentication(user, (password.startsWith(Password.__OBFUSCATE) ? Password.deobfuscate(password) : password));
+ }
+
+ @Override
+ public PasswordAuthentication getPasswordAuthentication()
+ {
+ return passwordAuthentication;
+ }
+
+ public void setUser(String user)
+ {
+ this.user = user;
+ }
+
+ public String getUser()
+ {
+ return this.user;
+ }
+
+ public String getPassword()
+ {
+ return this.password;
+ }
+
+ public void setPassword(String password)
+ {
+ this.password = password;
+ }
+ }
+
+ public MailSessionReference()
+ {
+ super("jakarta.mail.Session", MailSessionReference.class.getName(), null);
+ }
+
+
+ /**
+ * Create a jakarta.mail.Session instance based on the information passed in the Reference
+ *
+ * @param ref the Reference
+ * @param arg1 not used
+ * @param arg2 not used
+ * @param arg3 not used
+ * @return the object found
+ * @throws Exception if unable to get object instance
+ * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context, java.util.Hashtable)
+ */
+ @Override
+ public Object getObjectInstance(Object ref, Name arg1, Context arg2, Hashtable arg3) throws Exception
+ {
+ if (ref == null)
+ return null;
+
+ Reference reference = (Reference)ref;
+
+ Properties props = new Properties();
+ String user = null;
+ String password = null;
+
+ Enumeration refs = reference.getAll();
+ while (refs.hasMoreElements())
+ {
+ RefAddr refAddr = (RefAddr)refs.nextElement();
+ String name = refAddr.getType();
+ String value = (String)refAddr.getContent();
+ if (name.equalsIgnoreCase("user"))
+ user = value;
+ else if (name.equalsIgnoreCase("pwd"))
+ password = value;
+ else
+ props.put(name, value);
+ }
+
+ if (password == null)
+ return Session.getInstance(props);
+ else
+ return Session.getInstance(props, new PasswordAuthenticator(user, password));
+ }
+
+ public void setUser(String user)
+ {
+ StringRefAddr addr = (StringRefAddr)get("user");
+ if (addr != null)
+ {
+ throw new RuntimeException("user already set on SessionReference, can't be changed");
+ }
+ add(new StringRefAddr("user", user));
+ }
+
+ public void setPassword(String password)
+ {
+ StringRefAddr addr = (StringRefAddr)get("pwd");
+ if (addr != null)
+ throw new RuntimeException("password already set on SessionReference, can't be changed");
+ add(new StringRefAddr("pwd", password));
+ }
+
+ public void setProperties(Properties properties)
+ {
+ Iterator entries = properties.entrySet().iterator();
+ while (entries.hasNext())
+ {
+ Map.Entry e = (Map.Entry)entries.next();
+ StringRefAddr sref = (StringRefAddr)get((String)e.getKey());
+ if (sref != null)
+ throw new RuntimeException("property " + e.getKey() + " already set on Session reference, can't be changed");
+ add(new StringRefAddr((String)e.getKey(), (String)e.getValue()));
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jndi/src/main/java/org/eclipse/jetty/ee11/jndi/factories/package-info.java b/jetty-ee11/jetty-ee11-jndi/src/main/java/org/eclipse/jetty/ee11/jndi/factories/package-info.java
new file mode 100644
index 00000000000..f9ec719ac4d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jndi/src/main/java/org/eclipse/jetty/ee11/jndi/factories/package-info.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty EE11 Jndi : Factories
+ */
+package org.eclipse.jetty.ee11.jndi.factories;
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/pom.xml b/jetty-ee11/jetty-ee11-jspc-maven-plugin/pom.xml
new file mode 100644
index 00000000000..52a73b3f257
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/pom.xml
@@ -0,0 +1,123 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-jspc-maven-plugin
+ maven-plugin
+ EE11 :: Jetty JSPC Maven Plugin
+
+ ${project.groupId}.jspc.plugin
+ true
+
+
+
+ org.apache.ant
+ ant
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-tools-api
+
+
+ junit
+ junit
+
+
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+ ${project.version}
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-glassfish-jstl
+ ${project.version}
+
+
+ org.apache.maven
+ maven-artifact
+ provided
+
+
+ org.apache.maven
+ maven-core
+ provided
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+
+
+ org.apache.maven
+ maven-model
+ provided
+
+
+ org.apache.maven
+ maven-plugin-api
+ provided
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-annotations
+ provided
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-invoker-plugin
+
+
+ ${maven.surefire.plugin.version}
+
+
+ clean
+
+
+
+
+ integration-test
+
+ install
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-plugin-plugin
+
+
+ exec-plugin-doc
+
+ descriptor
+ helpmojo
+
+ process-classes
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/invoker.properties b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/invoker.properties
new file mode 100644
index 00000000000..df6cbf2d0bc
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = test -fae
+invoker.debug = true
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/pom.xml b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/pom.xml
new file mode 100644
index 00000000000..9d83a70810b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.its.jspc
+ simple-jsp
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple Jsp
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jspc-maven-plugin
+ ${jetty.version}
+
+
+
+ jspc
+
+ compile
+
+
+ org.eclipse.jetty.test
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/postbuild.groovy b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/postbuild.groovy
new file mode 100644
index 00000000000..4a73995f2a0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/postbuild.groovy
@@ -0,0 +1,9 @@
+
+
+System.out.println( "running postbuild.groovy" )
+
+File file = new File( basedir, "target/classes/org/eclipse/jetty/test/foo_jsp.class" );
+if ( !file.isFile() )
+{
+ throw new FileNotFoundException( "Could not find generated class in the proper package name: " + file );
+}
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/src/main/webapp/foo.jsp b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/src/main/webapp/foo.jsp
new file mode 100644
index 00000000000..fb73b0b0002
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/package-root/src/main/webapp/foo.jsp
@@ -0,0 +1,23 @@
+
+<%@ page import="java.util.Enumeration" %>
+
+
JSP Dump
+
+
+
Request URI:
<%= request.getRequestURI() %>
+
ServletPath:
<%= request.getServletPath() %>
+
PathInfo:
<%= request.getPathInfo() %>
+
+<%
+ Enumeration e =request.getParameterNames();
+ while(e.hasMoreElements())
+ {
+ String name = (String)e.nextElement();
+%>
+
+
+<%
+ Enumeration e =request.getParameterNames();
+ while(e.hasMoreElements())
+ {
+ String name = (String)e.nextElement();
+%>
+
+
getParameter("<%= name %>")
+
<%= request.getParameter(name) %>
+<% } %>
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/invoker.properties b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/invoker.properties
new file mode 100644
index 00000000000..df6cbf2d0bc
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = test -fae
+invoker.debug = true
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/pom.xml b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/pom.xml
new file mode 100644
index 00000000000..ce444ac58be
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.its.jspc
+ simple-jsp
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple Jsp
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-jspc-maven-plugin
+ ${jetty.version}
+
+
+
+ jspc
+
+ compile
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/postbuild.groovy b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/postbuild.groovy
new file mode 100644
index 00000000000..77118926cfd
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/postbuild.groovy
@@ -0,0 +1,20 @@
+import groovy.xml.XmlSlurper
+
+System.out.println( "running postbuild.groovy" )
+
+File webfrag = new File(basedir, 'target/webfrag.xml')
+assert webfrag.exists()
+assert webfrag.text.contains("org.apache.jsp.foo_jsp")
+assert webfrag.text.contains("org.apache.jsp.foo_jsp")
+assert webfrag.text.contains("/foo.jsp")
+
+// cannot use such parsing as it is not real xml
+//def rootXml = new XmlSlurper().parse(new File(basedir, 'target/webfrag.xml'))
+// so fake it
+def rootXml = new XmlSlurper().parseText(""+webfrag.text+"")
+
+assert rootXml.servlet.'servlet-name'.text() == "org.apache.jsp.foo_jsp"
+assert rootXml.servlet.'servlet-class'.text() == "org.apache.jsp.foo_jsp"
+
+assert rootXml.'servlet-mapping'.'servlet-name'.text() == "org.apache.jsp.foo_jsp"
+assert rootXml.'servlet-mapping'.'url-pattern'.text() == "/foo.jsp"
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/src/main/webapp/foo.jsp b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/src/main/webapp/foo.jsp
new file mode 100644
index 00000000000..fb73b0b0002
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/it/simple-jsp/src/main/webapp/foo.jsp
@@ -0,0 +1,23 @@
+
+<%@ page import="java.util.Enumeration" %>
+
+
JSP Dump
+
+
+
Request URI:
<%= request.getRequestURI() %>
+
ServletPath:
<%= request.getServletPath() %>
+
PathInfo:
<%= request.getPathInfo() %>
+
+<%
+ Enumeration e =request.getParameterNames();
+ while(e.hasMoreElements())
+ {
+ String name = (String)e.nextElement();
+%>
+
+
getParameter("<%= name %>")
+
<%= request.getParameter(name) %>
+<% } %>
+
+
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/java/org/eclipse/jetty/ee11/jspc/plugin/JspcMojo.java b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/java/org/eclipse/jetty/ee11/jspc/plugin/JspcMojo.java
new file mode 100644
index 00000000000..84bf821ee63
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/java/org/eclipse/jetty/ee11/jspc/plugin/JspcMojo.java
@@ -0,0 +1,623 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.jspc.plugin;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.jasper.JspC;
+import org.apache.jasper.servlet.JspCServletContext;
+import org.apache.jasper.servlet.TldScanner;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+import org.apache.tomcat.JarScanner;
+import org.apache.tomcat.util.scan.StandardJarScanner;
+import org.codehaus.plexus.util.FileUtils;
+import org.codehaus.plexus.util.StringUtils;
+import org.eclipse.jetty.util.IO;
+
+/**
+ * This goal will compile jsps for a webapp so that they can be included in a
+ * war.
+ *
+ * At runtime, the plugin will use the jspc compiler to precompile jsps and tags.
+ *
+ *
+ * Note that the same java compiler will be used as for on-the-fly compiled
+ * jsps, which will be the Eclipse java compiler.
+ *
+ * See Usage Guide for
+ * instructions on using this plugin.
+ *
+ * Runs jspc compiler to produce .java and .class files
+ */
+@Mojo(name = "jspc", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
+ threadSafe = true)
+public class JspcMojo extends AbstractMojo
+{
+ public static final String END_OF_WEBAPP = "";
+ public static final String PRECOMPILED_FLAG = "org.eclipse.jetty.jsp.precompiled";
+
+ /**
+ * JettyJspC
+ *
+ * Add some extra setters to standard JspC class to help configure it
+ * for running in maven.
+ *
+ * TODO move all setters on the plugin onto this jspc class instead.
+ */
+ public static class JettyJspC extends JspC
+ {
+
+ private boolean scanAll;
+ private boolean scanManifest;
+
+ public void setClassLoader(ClassLoader loader)
+ {
+ this.loader = loader;
+ }
+
+ public void setScanAllDirectories(boolean scanAll)
+ {
+ this.scanAll = scanAll;
+ }
+
+ public boolean getScanAllDirectories()
+ {
+ return this.scanAll;
+ }
+
+ public void setScanManifest(boolean scanManifest)
+ {
+ this.scanManifest = scanManifest;
+ }
+
+ public boolean getScanManifest()
+ {
+ return this.scanManifest;
+ }
+
+ @Override
+ protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal)
+ {
+ if (context != null && context.getAttribute(JarScanner.class.getName()) == null)
+ {
+ StandardJarScanner jarScanner = new StandardJarScanner();
+ jarScanner.setScanAllDirectories(getScanAllDirectories());
+ jarScanner.setScanManifest(getScanManifest());
+ context.setAttribute(JarScanner.class.getName(), jarScanner);
+ }
+
+ return super.newTldScanner(context, namespaceAware, validate, blockExternal);
+ }
+ }
+
+ /**
+ * Whether or not to include dependencies on the plugin's classpath with <scope>provided</scope>
+ * Use WITH CAUTION as you may wind up with duplicate jars/classes.
+ *
+ * @since jetty-7.6.3
+ */
+ @Parameter(defaultValue = "false")
+ private boolean useProvidedScope;
+
+ /**
+ * The artifacts for the project.
+ *
+ * @since jetty-7.6.3
+ */
+ @Parameter(defaultValue = "${project.artifacts}", readonly = true)
+ private Set projectArtifacts;
+
+ /**
+ * The maven project.
+ */
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ private MavenProject project;
+
+ /**
+ * The artifacts for the plugin itself.
+ */
+ @Parameter(defaultValue = "${plugin.artifacts}", readonly = true)
+ private List pluginArtifacts;
+
+ /**
+ * File into which to generate the <servlet> and
+ * <servlet-mapping> tags for the compiled jsps
+ */
+ @Parameter(defaultValue = "${basedir}/target/webfrag.xml")
+ private String webXmlFragment;
+
+ /**
+ * Optional. A marker string in the src web.xml file which indicates where
+ * to merge in the generated web.xml fragment. Note that the marker string
+ * will NOT be preserved during the insertion. Can be left blank, in which
+ * case the generated fragment is inserted just before the </web-app>
+ * line
+ */
+ @Parameter
+ private String insertionMarker;
+
+ /**
+ * Merge the generated fragment file with the web.xml from
+ * webAppSourceDirectory. The merged file will go into the same directory as
+ * the webXmlFragment.
+ */
+ @Parameter(defaultValue = "true")
+ private boolean mergeFragment;
+
+ /**
+ * The destination directory into which to put the compiled jsps.
+ */
+ @Parameter(defaultValue = "${project.build.outputDirectory}")
+ private String generatedClasses;
+
+ /**
+ * Controls whether or not .java files generated during compilation will be
+ * preserved.
+ */
+ @Parameter(defaultValue = "false")
+ private boolean keepSources;
+
+ /**
+ * Root directory for all html/jsp etc files
+ */
+ @Parameter(defaultValue = "${basedir}/src/main/webapp")
+ private String webAppSourceDirectory;
+
+ /**
+ * Location of web.xml. Defaults to src/main/webapp/web.xml.
+ */
+ @Parameter(defaultValue = "${basedir}/src/main/webapp/WEB-INF/web.xml")
+ private String webXml;
+
+ /**
+ * The comma separated list of patterns for file extensions to be processed. By default
+ * will include all .jsp and .jspx files.
+ */
+ @Parameter(defaultValue = "**\\/*.jsp, **\\/*.jspx")
+ private String includes;
+
+ /**
+ * The comma separated list of file name patters to exclude from compilation.
+ */
+ @Parameter(defaultValue = "**\\/.svn\\/**")
+ private String excludes;
+
+ /**
+ * The location of the compiled classes for the webapp
+ */
+ @Parameter(defaultValue = "${project.build.outputDirectory}")
+ private File classesDirectory;
+
+ /**
+ * Patterns of jars on the system path that contain tlds. Use | to separate each pattern.
+ */
+ @Parameter(defaultValue = ".*taglibs[^/]*\\.jar|.*jstl[^/]*\\.jar$")
+ private String tldJarNamePatterns;
+
+ /**
+ * Source version - if not set defaults to jsp default (currently 1.7)
+ */
+ @Parameter
+ private String sourceVersion;
+
+ /**
+ * Target version - if not set defaults to jsp default (currently 1.7)
+ */
+ @Parameter
+ private String targetVersion;
+
+ /**
+ * The JspC instance being used to compile the jsps.
+ */
+ @Parameter
+ private JettyJspC jspc;
+
+ /**
+ * Whether dirs on the classpath should be scanned as well as jars.
+ * True by default. This allows for scanning for tlds of dependent projects that
+ * are in the reactor as unassembled jars.
+ */
+ @Parameter(defaultValue = "true")
+ private boolean scanAllDirectories;
+
+ /**
+ * Determines if the manifest of JAR files found on the classpath should be scanned.
+ * True by default.
+ */
+ @Parameter(defaultValue = "true")
+ private boolean scanManifest;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException
+ {
+ if (getLog().isDebugEnabled())
+ {
+
+ getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
+ getLog().info("generatedClasses=" + generatedClasses);
+ getLog().info("webXmlFragment=" + webXmlFragment);
+ getLog().info("webXml=" + webXml);
+ getLog().info("insertionMarker=" + (insertionMarker == null || insertionMarker.isEmpty() ? END_OF_WEBAPP : insertionMarker));
+ getLog().info("keepSources=" + keepSources);
+ getLog().info("mergeFragment=" + mergeFragment);
+ if (sourceVersion != null)
+ getLog().info("sourceVersion=" + sourceVersion);
+ if (targetVersion != null)
+ getLog().info("targetVersion=" + targetVersion);
+ }
+ try
+ {
+ prepare();
+ compile();
+ cleanupSrcs();
+ mergeWebXml();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Failure processing jsps", e);
+ }
+ }
+
+ public void compile() throws Exception
+ {
+ ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
+
+ //set up the classpath of the webapp
+ List webAppUrls = setUpWebAppClassPath();
+
+ //set up the classpath of the container (ie jetty and jsp jars)
+ Set pluginJars = getPluginJars();
+ Set providedJars = getProvidedScopeJars(pluginJars);
+
+ //Make a classloader so provided jars will be on the classpath
+ List sysUrls = new ArrayList<>();
+ sysUrls.addAll(providedJars);
+ URLClassLoader sysClassLoader = new URLClassLoader(sysUrls.toArray(new URL[0]), currentClassLoader);
+
+ //make a classloader with the webapp classpath
+ URLClassLoader webAppClassLoader = new URLClassLoader(webAppUrls.toArray(new URL[0]), sysClassLoader);
+ StringBuilder webAppClassPath = new StringBuilder();
+
+ for (int i = 0; i < webAppUrls.size(); i++)
+ {
+ if (getLog().isDebugEnabled())
+ getLog().debug("webappclassloader contains: " + webAppUrls.get(i));
+ webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
+ if (getLog().isDebugEnabled())
+ getLog().debug("added to classpath: " + (webAppUrls.get(i)).getFile());
+ if (i + 1 < webAppUrls.size())
+ webAppClassPath.append(System.getProperty("path.separator"));
+ }
+
+ //Interpose a fake classloader as the webapp class loader. This is because the Apache JspC class
+ //uses a TldScanner which ignores jars outside of the WEB-INF/lib path on the webapp classloader.
+ //It will, however, look at all jars on the parents of the webapp classloader.
+ URLClassLoader fakeWebAppClassLoader = new URLClassLoader(new URL[0], webAppClassLoader);
+ Thread.currentThread().setContextClassLoader(fakeWebAppClassLoader);
+
+ if (jspc == null)
+ jspc = new JettyJspC();
+
+ jspc.setWebXmlInclude(webXmlFragment);
+ jspc.setUriroot(webAppSourceDirectory);
+ jspc.setOutputDir(generatedClasses);
+ jspc.setClassLoader(fakeWebAppClassLoader);
+ jspc.setScanAllDirectories(scanAllDirectories);
+ jspc.setScanManifest(scanManifest);
+ jspc.setCompile(true);
+ if (sourceVersion != null)
+ jspc.setCompilerSourceVM(sourceVersion);
+ if (targetVersion != null)
+ jspc.setCompilerTargetVM(targetVersion);
+
+ // JspC#setExtensions() does not exist, so
+ // always set concrete list of files that will be processed.
+ String jspFiles = getJspFiles(webAppSourceDirectory);
+
+ try
+ {
+ if (jspFiles == null | jspFiles.isEmpty())
+ {
+ getLog().info("No files selected to precompile");
+ }
+ else
+ {
+ getLog().info("Compiling " + jspFiles + " from includes=" + includes + " excludes=" + excludes);
+ jspc.setJspFiles(jspFiles);
+ jspc.execute();
+ }
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(currentClassLoader);
+ }
+ }
+
+ private String getJspFiles(String webAppSourceDirectory)
+ throws Exception
+ {
+ List fileNames = FileUtils.getFileNames(new File(webAppSourceDirectory), includes, excludes, false);
+ return StringUtils.join(fileNames.toArray(new String[0]), ",");
+ }
+
+ /**
+ * Until Jasper supports the option to generate the srcs in a different dir
+ * than the classes, this is the best we can do.
+ *
+ * @throws Exception if unable to clean srcs
+ */
+ public void cleanupSrcs() throws Exception
+ {
+ // delete the .java files - depending on keepGenerated setting
+ if (!keepSources)
+ {
+ File generatedClassesDir = new File(generatedClasses);
+
+ if (generatedClassesDir.exists() && generatedClassesDir.isDirectory())
+ {
+ delete(generatedClassesDir, pathname ->
+ {
+ return pathname.isDirectory() || pathname.getName().endsWith(".java");
+ });
+ }
+ }
+ }
+
+ static void delete(File dir, FileFilter filter)
+ {
+ File[] files = dir.listFiles(filter);
+ if (files != null)
+ {
+ for (File f : files)
+ {
+ if (f.isDirectory())
+ delete(f, filter);
+ else
+ f.delete();
+ }
+ }
+ }
+
+ /**
+ * Take the web fragment and put it inside a copy of the web.xml.
+ *
+ * You can specify the insertion point by specifying the string in the
+ * insertionMarker configuration entry.
+ *
+ * If you dont specify the insertionMarker, then the fragment will be
+ * inserted at the end of the file just before the </webapp>
+ *
+ * @throws Exception if unable to merge the web xml
+ */
+ public void mergeWebXml() throws Exception
+ {
+ if (mergeFragment)
+ {
+ // open the src web.xml
+ File webXml = getWebXmlFile();
+
+ if (!webXml.exists())
+ {
+ getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
+ return;
+ }
+
+ File fragmentWebXml = new File(webXmlFragment);
+ File mergedWebXml = new File(fragmentWebXml.getParentFile(), "web.xml");
+
+ try (BufferedReader webXmlReader = new BufferedReader(new FileReader(webXml));
+ PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(mergedWebXml)))
+ {
+
+ if (!fragmentWebXml.exists())
+ {
+ getLog().info("No fragment web.xml file generated");
+ //just copy existing web.xml to expected position
+ IO.copy(webXmlReader, mergedWebXmlWriter);
+ }
+ else
+ {
+ // read up to the insertion marker or the if there is no
+ // marker
+ boolean atInsertPoint = false;
+ boolean atEOF = false;
+ String marker = (insertionMarker == null || insertionMarker.isEmpty() ? END_OF_WEBAPP : insertionMarker);
+ while (!atInsertPoint && !atEOF)
+ {
+ String line = webXmlReader.readLine();
+ if (line == null)
+ atEOF = true;
+ else if (line.indexOf(marker) >= 0)
+ {
+ atInsertPoint = true;
+ }
+ else
+ {
+ mergedWebXmlWriter.println(line);
+ }
+ }
+
+ if (atEOF && !atInsertPoint)
+ throw new IllegalStateException("web.xml does not contain insertionMarker " + insertionMarker);
+
+ //put in a context init-param to flag that the contents have been precompiled
+ mergedWebXmlWriter.println("" + PRECOMPILED_FLAG + "true");
+
+ // put in the generated fragment
+ try (BufferedReader fragmentWebXmlReader =
+ new BufferedReader(new FileReader(fragmentWebXml)))
+ {
+ IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);
+
+ // if we inserted just before the , put it back in
+ if (marker.equals(END_OF_WEBAPP))
+ mergedWebXmlWriter.println(END_OF_WEBAPP);
+
+ // copy in the rest of the original web.xml file
+ IO.copy(webXmlReader, mergedWebXmlWriter);
+ }
+ }
+ }
+ }
+ }
+
+ private void prepare() throws Exception
+ {
+ // For some reason JspC doesn't like it if the dir doesn't
+ // already exist and refuses to create the web.xml fragment
+ File generatedSourceDirectoryFile = new File(generatedClasses);
+ if (!generatedSourceDirectoryFile.exists())
+ generatedSourceDirectoryFile.mkdirs();
+ }
+
+ /**
+ * Set up the execution classpath for Jasper.
+ *
+ * Put everything in the classesDirectory and all of the dependencies on the
+ * classpath.
+ *
+ * @returns a list of the urls of the dependencies
+ */
+ private List setUpWebAppClassPath() throws Exception
+ {
+ //add any classes from the webapp
+ List urls = new ArrayList();
+ urls.add(classesDirectory.toURI().toURL());
+
+ if (getLog().isDebugEnabled())
+ getLog().debug("Adding to classpath classes dir: " + classesDirectory);
+
+ //add the dependencies of the webapp (which will form WEB-INF/lib)
+ for (Iterator iter = project.getArtifacts().iterator(); iter.hasNext(); )
+ {
+ Artifact artifact = iter.next();
+
+ // Include runtime and compile time libraries
+ if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
+ {
+ String filePath = artifact.getFile().getCanonicalPath();
+ if (getLog().isDebugEnabled())
+ getLog().debug("Adding to classpath dependency file: " + filePath);
+
+ urls.add(artifact.getFile().toURI().toURL());
+ }
+ }
+ return urls;
+ }
+
+ /**
+ *
+ */
+ private Set getPluginJars() throws MalformedURLException
+ {
+ HashSet pluginJars = new HashSet<>();
+ for (Iterator iter = pluginArtifacts.iterator(); iter.hasNext(); )
+ {
+ Artifact pluginArtifact = iter.next();
+ if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
+ {
+ if (getLog().isDebugEnabled())
+ {
+ getLog().debug("Adding plugin artifact " + pluginArtifact);
+ }
+ pluginJars.add(pluginArtifact.getFile().toURI().toURL());
+ }
+ }
+
+ return pluginJars;
+ }
+
+ /**
+ *
+ */
+ private Set getProvidedScopeJars(Set pluginJars) throws MalformedURLException
+ {
+ if (!useProvidedScope)
+ return Collections.emptySet();
+
+ HashSet providedJars = new HashSet<>();
+
+ for (Iterator iter = projectArtifacts.iterator(); iter.hasNext(); )
+ {
+ Artifact artifact = iter.next();
+ if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
+ {
+ //test to see if the provided artifact was amongst the plugin artifacts
+ URL jar = artifact.getFile().toURI().toURL();
+ if (!pluginJars.contains(jar))
+ {
+ providedJars.add(jar);
+ if (getLog().isDebugEnabled())
+ {
+ getLog().debug("Adding provided artifact: " + artifact);
+ }
+ }
+ else
+ {
+ if (getLog().isDebugEnabled())
+ {
+ getLog().debug("Skipping provided artifact: " + artifact);
+ }
+ }
+ }
+ }
+ return providedJars;
+ }
+
+ private File getWebXmlFile()
+ throws IOException
+ {
+ File file = null;
+ File baseDir = project.getBasedir().getCanonicalFile();
+ File defaultWebAppSrcDir = new File(baseDir, "src/main/webapp").getCanonicalFile();
+ File webAppSrcDir = new File(webAppSourceDirectory).getCanonicalFile();
+ File defaultWebXml = new File(defaultWebAppSrcDir, "web.xml").getCanonicalFile();
+
+ //If the web.xml has been changed from the default, try that
+ File webXmlFile = new File(webXml).getCanonicalFile();
+ if (webXmlFile.compareTo(defaultWebXml) != 0)
+ {
+ file = new File(webXml);
+ return file;
+ }
+
+ //If the web app src directory has not been changed from the default, use whatever
+ //is set for the web.xml location
+ file = new File(webAppSrcDir, "web.xml");
+ return file;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/java/org/eclipse/jetty/ee11/jspc/plugin/package-info.java b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/java/org/eclipse/jetty/ee11/jspc/plugin/package-info.java
new file mode 100644
index 00000000000..2bce6ad70f8
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/java/org/eclipse/jetty/ee11/jspc/plugin/package-info.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty Jspc Maven Plugin : Support for precompiling jsps
+ */
+package org.eclipse.jetty.ee11.jspc.plugin;
+
diff --git a/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
new file mode 100644
index 00000000000..4f28b2b73cf
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-jspc-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ jspc
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/README_INTEGRATION_TEST.md b/jetty-ee11/jetty-ee11-maven-plugin/README_INTEGRATION_TEST.md
new file mode 100644
index 00000000000..1ef95192deb
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/README_INTEGRATION_TEST.md
@@ -0,0 +1,22 @@
+Running Maven Integration tests
+=====================
+The project contains a set of Maven Integration test projects.
+They are running using the Maven Invoker plugin which starts an external Maven build to run the project and some post build check.
+More details [http://maven.apache.org/plugins/maven-invoker-plugin/].
+
+Integration tests location
+--------------------
+Test projects are located within the folder: src/it
+
+Running single test
+--------------------
+You can run single or set of test as well using the command line argument: ```-Dinvoker.test=it-parent-pom,jetty-run-mojo-it,jetty-run-war*-it,!jetty-run-distro*```
+The parameter supports pattern and exclusion with !
+
+Due to [files filtering](http://maven.apache.org/plugins/maven-invoker-plugin/examples/filtering.html), ```it-parent-pom``` must be included - otherwise tests will fail during execution.
+
+Running Logs
+--------------------
+The output of each Maven build will be located in /target/it/${project-name}/build.log
+
+The jetty log output for those goals that fork a new process (currently "home" and "run-forked") can be found in /target/it/${project-name}/jetty-simple-webapp/target/jetty.out.
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/pom.xml
new file mode 100644
index 00000000000..03c3d9ebae9
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/pom.xml
@@ -0,0 +1,359 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11
+ jetty-ee11
+ 12.1.0-SNAPSHOT
+ ../pom.xml
+
+ jetty-ee11-maven-plugin
+ maven-plugin
+ EE11 :: Jetty Maven Plugin
+ Jetty EE11 maven plugins
+
+ ${project.groupId}.maven.plugin
+ true
+
+ FREEBEER
+ false
+
+
+
+ jakarta.transaction
+ jakarta.transaction-api
+ true
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-tools-api
+
+
+ org.codehaus.plexus
+ plexus-xml
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+ org.apache.maven
+ maven-api-xml
+
+
+ org.apache.maven
+ maven-xml-impl
+
+
+ org.apache.maven
+ maven-xml-meta
+
+
+
+
+ org.eclipse.jetty
+ jetty-home
+ zip
+
+
+ *
+ *
+
+
+
+
+ org.eclipse.jetty
+ jetty-http
+ true
+
+
+ org.eclipse.jetty
+ jetty-io
+ true
+
+
+ org.eclipse.jetty
+ jetty-jmx
+ true
+
+
+ org.eclipse.jetty
+ jetty-jndi
+ true
+
+
+ org.eclipse.jetty
+ jetty-maven
+ true
+
+
+ org.eclipse.jetty
+ jetty-security
+ true
+
+
+ org.eclipse.jetty
+ jetty-server
+ true
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-annotations
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-glassfish-jstl
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-plus
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-quickstart
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ true
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-webapp
+ true
+
+
+ jakarta.servlet
+ servlet-api
+
+
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jakarta-server
+ true
+
+
+ org.eclipse.jetty.ee11.websocket
+ jetty-ee11-websocket-jetty-server
+ true
+
+
+ org.slf4j
+ slf4j-api
+ true
+
+
+ org.apache.maven
+ maven-artifact
+ provided
+
+
+ org.apache.maven
+ maven-core
+ provided
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+
+
+ org.apache.maven
+ maven-model
+ provided
+
+
+ org.apache.maven
+ maven-plugin-api
+ provided
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-annotations
+ provided
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ org.eclipse.jetty
+ jetty-home
+ zip
+ false
+ ${project.build.directory}
+ ${jettyHomeZipFileName}
+
+
+ false
+ true
+
+
+
+ copy
+
+ copy
+
+ package
+
+
+
+
+ org.apache.maven.plugins
+ maven-invoker-plugin
+
+
+ org.eclipse.jetty:jetty-slf4j-impl:${project.version}
+ org.eclipse.jetty.ee11:jetty-ee11-apache-jsp:${project.version}
+ org.eclipse.jetty.ee11:jetty-ee11-glassfish-jstl:${project.version}
+ org.eclipse.jetty.ee11:jetty-ee11-webapp:${project.version}
+ org.eclipse.jetty:jetty-server:${project.version}
+ org.eclipse.jetty:jetty-deploy:${project.version}
+ org.eclipse.jetty:jetty-home:${project.version}:zip
+
+ org.eclipse.jetty.maven.its.ee11
+
+ it-parent-pom/pom.xml
+
+
+
+ javax-annotation-api/pom.xml
+
+ jetty-start-gwt-it/pom.xml
+
+ jetty-start-war-mojo-it/pom.xml
+
+
+ ${jetty.stopKey}
+ ${jetty.stopPort}
+ ${maven.surefire.plugin.version}
+ ${localRepoPath}
+ ${jettyHomeZip}
+
+
+ clean
+
+
+
+
+ integration-test
+
+ install
+ integration-test
+ verify
+
+ integration-test
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-plugin-plugin
+
+ jetty
+
+
+
+ exec-plugin-doc
+
+ helpmojo
+
+ generate-sources
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ -Dstop.port=@{test.stopPort} -Djetty.port=@{test.jettyPort}
+
+ **/IntegrationTest*.java
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ test-reserve-ports
+
+ reserve-network-port
+
+ process-test-classes
+
+
+ test.stopPort
+ test.jettyPort
+
+
+
+
+ reserve-ports
+
+ reserve-network-port
+
+ pre-integration-test
+
+
+ jetty.stopPort
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/it-parent-pom/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/it-parent-pom/invoker.properties
new file mode 100644
index 00000000000..521301a6d91
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/it-parent-pom/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = install
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/it-parent-pom/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/it-parent-pom/pom.xml
new file mode 100644
index 00000000000..d6b50e3fd58
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/it-parent-pom/pom.xml
@@ -0,0 +1,152 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+ pom
+
+
+ @project.version@
+ UTF-8
+
+
+
+
+
+ commons-io
+ commons-io
+ 2.7
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ @jakarta.servlet.api.version@
+ provided
+
+
+ org.eclipse.jetty.toolchain
+ jetty-perf-helper
+ @jetty.perf-helper.version@
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ @jackson.version@
+
+
+ org.slf4j
+ slf4j-api
+ @slf4j.version@
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ @project.version@
+
+
+ org.eclipse.jetty
+ jetty-client
+ @project.version@
+
+
+ org.eclipse.jetty
+ jetty-util
+ @project.version@
+
+
+ org.eclipse.jetty
+ jetty-http
+ @project.version@
+
+
+ org.eclipse.jetty
+ jetty-io
+ @project.version@
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ @project.version@
+ tests
+ test-jar
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ @junit.version@
+ test
+
+
+ org.awaitility
+ awaitility
+ @awaitility.version@
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ @maven.compiler.plugin.version@
+
+ 11
+
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ @maven.dependency.plugin.version@
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ @maven.surefire.plugin.version@
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ @maven.resources.plugin.version@
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ @maven.source.plugin.version@
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ ${jetty.version}
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ @maven.surefire.plugin.version@
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ @maven.war.plugin.version@
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ @maven.install.plugin.version@
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ @maven.deploy.plugin.version@
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/invoker.properties
new file mode 100644
index 00000000000..2fc6409821b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test -fae
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/pom.xml
new file mode 100644
index 00000000000..b4ed00b2126
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/pom.xml
@@ -0,0 +1,106 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-cdi-start-forked-mojo-it
+ jetty-weld-minimal
+ 1.0-SNAPSHOT
+ war
+
+
+ ${project.build.directory}/jetty-cdi-start-forked-port.txt
+ FORK
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ true
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ ${jetty.version}
+
+
+
+
+ start-jetty
+ process-test-classes
+
+ start
+
+
+
+ ${basedir}/src/main/jetty/jetty-context.xml
+
+ ${basedir}/src/main/jetty/jetty.xml
+
+ @jetty.stopPort@
+ @jetty.stopKey@
+ ${jetty.jvmArgs}
+
+ ${jetty.port.file}
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/postbuild.groovy
new file mode 100644
index 00000000000..fd16d88a69b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/postbuild.groovy
@@ -0,0 +1,17 @@
+
+System.out.println( "postbuild.groovy port " + jettyStopPort + ", key:" + jettyStopKey )
+
+int port = Integer.parseInt( jettyStopPort )
+
+Socket s=new Socket(InetAddress.getByName("127.0.0.1"),port )
+
+OutputStream out=s.getOutputStream()
+out.write(( jettyStopKey +"\r\nforcestop\r\n").getBytes())
+out.flush()
+s.close()
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Forked process starting' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'helloServlet')
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/java/test/Greeter.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/java/test/Greeter.java
new file mode 100644
index 00000000000..913f2fb08dd
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/java/test/Greeter.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package test;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+@WebServlet("/*")
+public class Greeter
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("Hello " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/jetty/jetty-context.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/jetty/jetty-context.xml
new file mode 100644
index 00000000000..78aea23ca19
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/jetty/jetty-context.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+ -org.eclipse.jetty.util.Decorator
+ -org.eclipse.jetty.util.DecoratedObjectFactory
+ -org.eclipse.jetty.server.handler.ContextHandler.
+ -org.eclipse.jetty.server.handler.ContextHandler
+ -org.eclipse.jetty.ee11.servlet.ServletContextHandler
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/jetty/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/jetty/jetty.xml
new file mode 100644
index 00000000000..8a8f26b49f4
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-cdi-start-forked/src/main/jetty/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/invoker.properties
new file mode 100644
index 00000000000..816c3f38def
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = verify -V -e
+#test-compile failsafe:integration-test
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/pom.xml
new file mode 100644
index 00000000000..b6342e9984a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/pom.xml
@@ -0,0 +1,113 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-combinedresource-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Webapp with multiple base resources
+
+
+ ${project.build.directory}/jetty-combinedresource-port.txt
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ ${jetty.port.file}
+ ${project.groupId}:${project.artifactId}
+ BLAH
+ /blah.html
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+ **/*TestGetContent*
+
+
+
+
+ integration-test
+
+ integration-test
+
+
+
+ verify
+
+ verify
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ test-compile
+
+ start
+
+
+
+ pom
+
+
+
+ ${project.basedir}/public
+ ${project.basedir}/src/main/webapp
+
+
+
+ ${jetty.port.file}
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/postbuild.groovy
new file mode 100644
index 00000000000..4f05a1ca8f1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/postbuild.groovy
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'contentCheck')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/public/blah.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/public/blah.html
new file mode 100644
index 00000000000..e4e860f38ac
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/public/blah.html
@@ -0,0 +1 @@
+BLAH
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..7a8e003f946
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,14 @@
+
+
+
+ test
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/main/webapp/index.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/main/webapp/index.html
new file mode 100644
index 00000000000..593ac2e6079
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-combinedresource-it/src/main/webapp/index.html
@@ -0,0 +1 @@
+JJJJJ
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/invoker.properties
new file mode 100644
index 00000000000..fd18ebccf10
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/pom.xml
new file mode 100644
index 00000000000..2d1a8b78afc
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-effective-web-xml-it
+ jetty-effective-web-xml-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ Jetty :: effective web xml multi-module project
+
+
+ resources
+ webapp-war
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-effective-web-xml-it
+ resources
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/postbuild.groovy
new file mode 100644
index 00000000000..c7c6437beb2
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/postbuild.groovy
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import groovy.xml.XmlParser
+
+def rootNode = new XmlParser().parse(new File( basedir, 'webapp-war/target/effective-web.xml'))
+// find context-param node with param-name == org.eclipse.jetty.resources
+def ctxParam = rootNode.'**'.find{it.text() == "org.eclipse.jetty.resources"}.parent()
+def paramValue = ctxParam.'param-value'.get(0).text().trim()
+// assert the value of param-value child node
+assert paramValue.contains('${user.dir.uri}/resources/target/classes/META-INF/resources')
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/pom.xml
new file mode 100644
index 00000000000..7db12ad6fbd
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/pom.xml
@@ -0,0 +1,11 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.its.jetty-effective-web-xml-it
+ jetty-effective-web-xml-project
+ 0.0.1-SNAPSHOT
+
+ resources
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/src/main/java/Placeholder.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/src/main/java/Placeholder.java
new file mode 100644
index 00000000000..f5eee5ac95a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/src/main/java/Placeholder.java
@@ -0,0 +1,16 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+public class Placeholder
+{
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/src/main/resources/META-INF/resources/extra.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/src/main/resources/META-INF/resources/extra.html
new file mode 100644
index 00000000000..5ddcf18b968
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/resources/src/main/resources/META-INF/resources/extra.html
@@ -0,0 +1,3 @@
+
+
Extra Resource
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/pom.xml
new file mode 100644
index 00000000000..c33ee91baba
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.its.jetty-effective-web-xml-it
+ jetty-effective-web-xml-project
+ 0.0.1-SNAPSHOT
+
+ webapp-war
+ war
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+
+
+ org.eclipse.jetty.ee11.its.jetty-effective-web-xml-it
+ resources
+
+
+
+
+ ${project.build.directory}/jetty-effective-web-xml-mojo.txt
+ EMBED
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ gen-effective-web-xml
+ test-compile
+
+ effective-web-xml
+
+
+
+ src/main/webapp
+
+
+ ${jetty.port.file}
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/config/jetty.xml
new file mode 100644
index 00000000000..4e43b5305df
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/main/java/WebAppServletListener.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/main/java/WebAppServletListener.java
new file mode 100644
index 00000000000..8f60688e24c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/main/java/WebAppServletListener.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+import java.net.URL;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+import static java.lang.String.format;
+
+public class WebAppServletListener implements ServletContextListener
+{
+
+ //Empty class, just to have something in src/main/java
+ @Override
+ public void contextInitialized(ServletContextEvent servletContextEvent)
+ {
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent servletContextEvent)
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..4de3fcefbce
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-effective-web-xml-it/webapp-war/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ WebAppServletListener
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/api/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/api/pom.xml
new file mode 100755
index 00000000000..c2546ccaa02
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/api/pom.xml
@@ -0,0 +1,11 @@
+
+ 4.0.0
+
+
+ test.jetty-ee11-maven-plugin-provided-module-dep
+ parent
+ 1.0-SNAPSHOT
+
+ api
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/api/src/main/java/test/Api.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/api/src/main/java/test/Api.java
new file mode 100755
index 00000000000..681defb764b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/api/src/main/java/test/Api.java
@@ -0,0 +1,22 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package test;
+
+public class Api
+{
+
+ public void noOp()
+ {
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/invoker.properties
new file mode 100644
index 00000000000..816c3f38def
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = verify -V -e
+#test-compile failsafe:integration-test
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/pom.xml
new file mode 100755
index 00000000000..589fda49d37
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/pom.xml
@@ -0,0 +1,20 @@
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ test.jetty-ee11-maven-plugin-provided-module-dep
+ parent
+ 1.0-SNAPSHOT
+ pom
+
+
+ api
+ web
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/postbuild.groovy
new file mode 100644
index 00000000000..bfadcee89c4
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/postbuild.groovy
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'ClassNotFoundException')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/pom.xml
new file mode 100755
index 00000000000..e78eea1e0f0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/pom.xml
@@ -0,0 +1,62 @@
+
+ 4.0.0
+
+
+ test.jetty-ee11-maven-plugin-provided-module-dep
+ parent
+ 1.0-SNAPSHOT
+
+ web
+ war
+
+
+ EMBED
+
+
+
+
+ ${project.groupId}
+ api
+ ${project.version}
+ provided
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ pre-integration-test
+
+ start
+
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml
new file mode 100644
index 00000000000..ef0c9bcad93
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+ https
+
+
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/main/java/test/ClassLoadingTestingServletContextListener.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/main/java/test/ClassLoadingTestingServletContextListener.java
new file mode 100755
index 00000000000..62ae1c193bf
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/main/java/test/ClassLoadingTestingServletContextListener.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package test;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.annotation.WebListener;
+
+
+@WebListener
+public class ClassLoadingTestingServletContextListener
+ implements ServletContextListener
+{
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ try
+ {
+ Api api = new Api();
+ System.err.println("Class " + api.getClass().getName() + " is available and loaded by classloader " + api.getClass().getClassLoader().toString() + ". Expected CNFE.");
+ ClassLoader cl = api.getClass().getClassLoader();
+ while (cl != null)
+ {
+ if (cl instanceof URLClassLoader)
+ {
+ URLClassLoader ucl = (URLClassLoader)cl;
+ System.err.println("-----");
+ printURLs(ucl);
+ System.err.println("-----");
+ }
+ cl = cl.getParent();
+ }
+ }
+ catch (java.lang.Exception exception)
+ {
+ exception.printStackTrace();
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ }
+
+ private void printURLs(URLClassLoader l)
+ {
+ if (l == null)
+ return;
+
+ for (URL u: l.getURLs())
+ {
+ System.err.println(u);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/pom.xml
new file mode 100644
index 00000000000..748f0321323
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-run-mojo-jar-scan-it
+ jetty-jar-scan
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ jetty-jar-scan-library
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/java/jettyissue/MyAnnotation.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/java/jettyissue/MyAnnotation.java
new file mode 100644
index 00000000000..368684a98b1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/java/jettyissue/MyAnnotation.java
@@ -0,0 +1,24 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package jettyissue;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface MyAnnotation {
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/java/jettyissue/MyServletContainerInitializer.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/java/jettyissue/MyServletContainerInitializer.java
new file mode 100644
index 00000000000..7fbccdf3adc
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/java/jettyissue/MyServletContainerInitializer.java
@@ -0,0 +1,28 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package jettyissue;
+
+import java.util.Set;
+
+import jakarta.servlet.ServletContainerInitializer;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.HandlesTypes;
+
+@HandlesTypes({MyAnnotation.class})
+public class MyServletContainerInitializer implements ServletContainerInitializer {
+ public void onStartup(Set> c, ServletContext ctx) throws ServletException {
+ System.out.println("STARTED"+c);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
new file mode 100644
index 00000000000..9e9784f1616
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyLibrary/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
@@ -0,0 +1 @@
+jettyissue.MyServletContainerInitializer
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/pom.xml
new file mode 100644
index 00000000000..c88a8a1bbed
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-run-mojo-jar-scan-it
+ jetty-jar-scan
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ jetty-jar-scan-webapp
+ jar
+
+
+ ${project.build.directory}/jetty-run-mojo.txt
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.eclipse.jetty.ee11.its.jetty-run-mojo-jar-scan-it
+ jetty-jar-scan-library
+
+
+
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ test-compile
+
+ start
+
+
+
+ ${jetty.port.file}
+
+
+ ${basedir}/src/config/jetty.xml
+
+ ${basedir}/src/config/context.xml
+ true
+
+ jar
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/config/context.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/config/context.xml
new file mode 100644
index 00000000000..f09f38e188e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/config/context.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ /setbycontextxml
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/config/jetty.xml
new file mode 100644
index 00000000000..58c5b275121
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/config/jetty.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/main/java/jettyissue/NormalClass.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/main/java/jettyissue/NormalClass.java
new file mode 100644
index 00000000000..987f8d89d42
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/main/java/jettyissue/NormalClass.java
@@ -0,0 +1,19 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package jettyissue;
+
+
+@MyAnnotation
+public class NormalClass {
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/main/webapp/index.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/main/webapp/index.html
new file mode 100644
index 00000000000..b7b5cdc61de
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/MyWebApp/src/main/webapp/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Title
+
+
+ Hello World!
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/invoker.properties
new file mode 100644
index 00000000000..ac620b04a8b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test -e
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/pom.xml
new file mode 100644
index 00000000000..5ac2f521163
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-run-mojo-jar-scan-it
+ jetty-jar-scan
+ pom
+ 1.0-SNAPSHOT
+
+ MyLibrary
+ MyWebApp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-run-mojo-jar-scan-it
+ jetty-jar-scan-library
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/postbuild.groovy
new file mode 100644
index 00000000000..caf7a534eab
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-run-mojo-jar-scan-it/postbuild.groovy
@@ -0,0 +1,3 @@
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'STARTED[class jettyissue.NormalClass]')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/invoker.properties
new file mode 100644
index 00000000000..94591882576
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = verify -fae -e
+#invoker.debug = true
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/pom.xml
new file mode 100644
index 00000000000..1455fc33a6d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-distro-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-base
+ jar
+
+ EE11 :: Simple :: Base
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.slf4j
+ slf4j-api
+
+
+ commons-io
+ commons-io
+
+
+ org.eclipse.jetty.toolchain
+ jetty-perf-helper
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_distro_mojo_it/HelloServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_distro_mojo_it/HelloServlet.java
new file mode 100644
index 00000000000..c9e27791a2c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_distro_mojo_it/HelloServlet.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_distro_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ *
+ */
+@WebServlet("/hello")
+public class HelloServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("Hello " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_distro_mojo_it/PingServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_distro_mojo_it/PingServlet.java
new file mode 100644
index 00000000000..64b854660e8
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_distro_mojo_it/PingServlet.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_distro_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class PingServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("pong " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
new file mode 100644
index 00000000000..3198a38948d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ FragmentA
+
+
+
+
+
+
+ Ping
+ org.eclipse.jetty.its.jetty_start_distro_mojo_it.PingServlet
+ 1
+
+ extra1123
+
+
+ extra2345
+
+
+
+
+ Ping
+ /ping
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/pom.xml
new file mode 100644
index 00000000000..90967a23e52
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/pom.xml
@@ -0,0 +1,132 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-distro-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-webapp
+ war
+
+ EE11 :: Simple :: WebApp
+
+
+ ${project.build.directory}/jetty-start-distro-port.txt
+ @jetty.jvmArgs@
+ EXTERNAL
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-distro-mojo-it
+ jetty-simple-base
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ provided
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ true
+ true
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ ${jetty.version}
+
+
+
+ @jetty.stopPort@
+ @jetty.stopKey@
+
+
+
+ start-jetty
+ process-test-classes
+
+ start
+
+
+ ${basedir}/src/base
+
+ true
+ ${jetty.port.file}
+ 0
+ @localRepoPath@
+
+ ee11-apache-jsp,ee11-glassfish-jstl,ee11-testmod,resources
+ --debug
+ @jettyHomeZip@
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty-ee11.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty-ee11.xml
new file mode 100644
index 00000000000..9857cb546ef
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty-ee11.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/modules/ee11-testmod.mod b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/modules/ee11-testmod.mod
new file mode 100644
index 00000000000..200733fb07e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/modules/ee11-testmod.mod
@@ -0,0 +1,14 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables test setup
+
+[environment]
+ee11
+
+[depend]
+http
+
+[xml]
+etc/test-jetty-ee11.xml
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/resources/jetty-logging.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/resources/jetty-logging.properties
new file mode 100644
index 00000000000..3c430260203
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/base/resources/jetty-logging.properties
@@ -0,0 +1,8 @@
+# Jetty Logging using jetty-slf4j-impl
+org.eclipse.jetty.LEVEL=INFO
+#org.eclipse.jetty.deploy.LEVEL=DEBUG
+#org.eclipse.jetty.server.LEVEL=DEBUG
+#org.eclipse.jetty.ee11.servlet.LEVEL=DEBUG
+#org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG
+#org.eclipse.jetty.server.DebugListener.LEVEL=DEBUG
+#org.eclipse.jetty.server.internal.HttpChannelState.LEVEL=DEBUG
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..2a5ac4b71bf
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+
+
+ Jetty Simple Webapp run-mojo-it
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/pom.xml
new file mode 100644
index 00000000000..5536c0c1722
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-distro-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+ jetty-simple-base
+ jetty-simple-webapp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-distro-mojo-it
+ jetty-simple-base
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/postbuild.groovy
new file mode 100644
index 00000000000..d412d3a7822
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-distro-mojo-it/postbuild.groovy
@@ -0,0 +1,18 @@
+
+System.out.println( "running postbuild.groovy port " + jettyStopPort + ", key:" + jettyStopKey )
+
+int port = Integer.parseInt( jettyStopPort )
+
+Socket s=new Socket(InetAddress.getByName("127.0.0.1"),port )
+
+OutputStream out=s.getOutputStream()
+out.write(( jettyStopKey +"\r\nforcestop\r\n").getBytes())
+out.flush()
+s.close()
+
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Home process starting' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'pingServlet ok')
+assert buildLog.text.contains( 'helloServlet')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/invoker.properties
new file mode 100644
index 00000000000..850d38aa127
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = verify -fae
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/pom.xml
new file mode 100644
index 00000000000..ed3d1892a5f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-forked-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-base
+ jar
+
+ EE11 :: Simple :: Base
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.slf4j
+ slf4j-api
+
+
+ commons-io
+ commons-io
+
+
+ org.eclipse.jetty.toolchain
+ jetty-perf-helper
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/Counter.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/Counter.java
new file mode 100644
index 00000000000..4adc06e2bfe
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/Counter.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_forked;
+
+@SuppressWarnings("serial")
+public class Counter implements java.io.Serializable
+{
+ int counter = 0;
+ String last;
+
+ public int getCount()
+ {
+ counter++;
+ return counter;
+ }
+
+ public void setLast(String uri)
+ {
+ last = uri;
+ }
+
+ public String getLast()
+ {
+ return last;
+ }
+}
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/HelloServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/HelloServlet.java
new file mode 100644
index 00000000000..4030055d398
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/HelloServlet.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_forked;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ *
+ */
+@WebServlet("/hello")
+public class HelloServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("Hello " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/PingServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/PingServlet.java
new file mode 100644
index 00000000000..99b6be59233
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_forked/PingServlet.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_forked;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class PingServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("pong " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
new file mode 100644
index 00000000000..028bdc91255
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ FragmentA
+
+
+
+
+
+
+ Ping
+ org.eclipse.jetty.its.jetty_start_forked.PingServlet
+
+ extra1123
+
+
+ extra2345
+
+
+
+
+ Ping
+ /ping
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/pom.xml
new file mode 100644
index 00000000000..792b96f3c3c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/pom.xml
@@ -0,0 +1,131 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-forked-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-webapp
+ war
+
+ EE11 :: Simple :: WebApp
+
+
+ @jetty.jvmArgs@
+ ${project.build.directory}/jetty-start-forked-port.txt
+ FORK
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-forked-mojo-it
+ jetty-simple-base
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ provided
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ true
+ true
+ Counter accessed 1 times.
+ /jsp/bean1.jsp
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+ @jetty.stopPort@
+ @jetty.stopKey@
+
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ ${jetty.version}
+
+
+
+
+ start-jetty
+ process-test-classes
+
+ start
+
+
+
+ ${basedir}/src/config/jetty.xml
+
+ ${jetty.jvmArgs}
+
+ ${jetty.port.file}
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..b2264670c78
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+
+
+ Jetty Simple Webapp start-jetty-forked
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/main/webapp/jsp/bean1.jsp b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/main/webapp/jsp/bean1.jsp
new file mode 100644
index 00000000000..08dd1ce9280
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/jetty-simple-webapp/src/main/webapp/jsp/bean1.jsp
@@ -0,0 +1,13 @@
+
+<%@ page session="true"%>
+
+
+
+
JSP1.2 Beans: 1
+
+Counter accessed times.
+Counter last accessed by
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/pom.xml
new file mode 100644
index 00000000000..db48a3f0b63
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-forked-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 3.0.0
+ @project.version@
+
+
+
+ jetty-simple-base
+ jetty-simple-webapp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-forked-mojo-it
+ jetty-simple-base
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/postbuild.groovy
new file mode 100644
index 00000000000..e193aaab819
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-forked-mojo-it/postbuild.groovy
@@ -0,0 +1,19 @@
+
+
+System.out.println( "running postbuild.groovy port " + jettyStopPort + ", key:" + jettyStopKey )
+
+int port = Integer.parseInt( jettyStopPort )
+
+Socket s=new Socket(InetAddress.getByName("127.0.0.1"),port )
+
+OutputStream out=s.getOutputStream()
+out.write(( jettyStopKey +"\r\nforcestop\r\n").getBytes())
+out.flush()
+s.close()
+
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Forked process starting' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'pingServlet ok')
+assert buildLog.text.contains( 'helloServlet')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/pom.xml
new file mode 100644
index 00000000000..d1602c95c72
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+
+ org.olamy
+ beer
+ 1.0-SNAPSHOT
+
+
+ beer-client
+ gwt-app
+
+
+
+ ${project.groupId}
+ beer-shared
+ ${project.version}
+
+
+ ${project.groupId}
+ beer-shared
+ ${project.version}
+ sources
+
+
+ com.google.gwt
+ gwt-user
+
+
+ com.google.gwt
+ gwt-dev
+
+
+
+
+
+
+ net.ltgt.gwt.maven
+ gwt-maven-plugin
+
+ org.olamy.App
+ app
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/src/main/java/org/olamy/App.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/src/main/java/org/olamy/App.java
new file mode 100644
index 00000000000..5e229d1082c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/src/main/java/org/olamy/App.java
@@ -0,0 +1,184 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.olamy;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.DialogBox;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+/**
+ * Entry point classes define onModuleLoad().
+ */
+public class App implements EntryPoint
+{
+ /**
+ * The message displayed to the user when the server cannot be reached or
+ * returns an error.
+ */
+ private static final String SERVER_ERROR = "An error occurred while " +
+ "attempting to contact the server. Please check your network " +
+ "connection and try again.";
+
+ /**
+ * Create a remote service proxy to talk to the server-side Greeting service.
+ */
+ private final GreetingServiceAsync greetingService = GWT
+ .create(GreetingService.class);
+
+ /**
+ * This is the entry point method.
+ */
+ public void onModuleLoad()
+ {
+ final Button sendButton = new Button("Send");
+ final TextBox nameField = new TextBox();
+ nameField.setText("GWT User");
+ final Label errorLabel = new Label();
+
+ // We can add style names to widgets
+ sendButton.addStyleName("sendButton");
+
+ // Add the nameField and sendButton to the RootPanel
+ // Use RootPanel.get() to get the entire body element
+ RootPanel.get("nameFieldContainer").add(nameField);
+ RootPanel.get("sendButtonContainer").add(sendButton);
+ RootPanel.get("errorLabelContainer").add(errorLabel);
+
+ // Focus the cursor on the name field when the app loads
+ nameField.setFocus(true);
+ nameField.selectAll();
+
+ // Create the popup dialog box
+ final DialogBox dialogBox = new DialogBox();
+ dialogBox.setText("Remote Procedure Call");
+ dialogBox.setAnimationEnabled(true);
+ final Button closeButton = new Button("Close");
+ // We can set the id of a widget by accessing its Element
+ closeButton.getElement().setId("closeButton");
+ final Label textToServerLabel = new Label();
+ final HTML serverResponseLabel = new HTML();
+ VerticalPanel dialogVPanel = new VerticalPanel();
+ dialogVPanel.addStyleName("dialogVPanel");
+ dialogVPanel.add(new HTML("Sending name to the server:"));
+ dialogVPanel.add(textToServerLabel);
+ dialogVPanel.add(new HTML(" Server replies:"));
+ dialogVPanel.add(serverResponseLabel);
+ dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
+ dialogVPanel.add(closeButton);
+ dialogBox.setWidget(dialogVPanel);
+
+ // Add a handler to close the DialogBox
+ closeButton.addClickHandler(new ClickHandler()
+ {
+ public void onClick(ClickEvent event)
+ {
+ dialogBox.hide();
+ sendButton.setEnabled(true);
+ sendButton.setFocus(true);
+ }
+ });
+
+ // Create a handler for the sendButton and nameField
+ class MyHandler implements ClickHandler, KeyUpHandler
+ {
+ /**
+ * Fired when the user clicks on the sendButton.
+ */
+ public void onClick(ClickEvent event)
+ {
+ sendNameToServer();
+ }
+
+ /**
+ * Fired when the user types in the nameField.
+ */
+ public void onKeyUp(KeyUpEvent event)
+ {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER)
+ {
+ sendNameToServer();
+ }
+ }
+
+ /**
+ * Send the name from the nameField to the server and wait for a response.
+ */
+ private void sendNameToServer()
+ {
+ // First, we validate the input.
+ errorLabel.setText("");
+ String textToServer = nameField.getText();
+ if (!FieldVerifier.isValidName(textToServer))
+ {
+ errorLabel.setText("Please enter at least four characters");
+ return;
+ }
+
+ // Then, we send the input to the server.
+ sendButton.setEnabled(false);
+ textToServerLabel.setText(textToServer);
+ serverResponseLabel.setText("");
+ greetingService.greetServer(textToServer,
+ new AsyncCallback()
+ {
+ public void onFailure(Throwable caught)
+ {
+ // Show the RPC error message to the user
+ dialogBox
+ .setText("Remote Procedure Call - Failure");
+ serverResponseLabel
+ .addStyleName("serverResponseLabelError");
+ serverResponseLabel.setHTML(SERVER_ERROR);
+ dialogBox.center();
+ closeButton.setFocus(true);
+ }
+
+ public void onSuccess(GreetingResponse result)
+ {
+ dialogBox.setText("Remote Procedure Call");
+ serverResponseLabel
+ .removeStyleName("serverResponseLabelError");
+ serverResponseLabel.setHTML(new SafeHtmlBuilder()
+ .appendEscaped(result.getGreeting())
+ .appendHtmlConstant("
I am running ")
+ .appendEscaped(result.getServerInfo())
+ .appendHtmlConstant(".
It looks like you are using: ")
+ .appendEscaped(result.getUserAgent())
+ .toSafeHtml());
+ dialogBox.center();
+ closeButton.setFocus(true);
+ }
+ });
+ }
+ }
+
+ // Add a handler to send the name to the server
+ MyHandler handler = new MyHandler();
+ sendButton.addClickHandler(handler);
+ nameField.addKeyUpHandler(handler);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/src/main/module.gwt.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/src/main/module.gwt.xml
new file mode 100644
index 00000000000..6cda0e54be6
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-client/src/main/module.gwt.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/pom.xml
new file mode 100644
index 00000000000..400ef1b6676
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/pom.xml
@@ -0,0 +1,118 @@
+
+
+ 4.0.0
+
+
+ org.olamy
+ beer
+ 1.0-SNAPSHOT
+
+
+ beer-server
+ war
+
+
+ ${project.build.directory}/jetty-start-mojo.txt
+ EMBED
+
+
+
+
+ ${project.groupId}
+ beer-shared
+ ${project.version}
+
+
+ com.google.gwt
+ gwt-servlet
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.eclipse.jetty
+ jetty-util
+ test
+
+
+ org.eclipse.jetty
+ jetty-http
+ test
+
+
+ org.eclipse.jetty
+ jetty-io
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ Please enter your name
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ run
+ test-compile
+
+ start
+
+
+
+ ${jetty.port.file}
+
+ ${basedir}/src/main/jettyconf/context.xml
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/java/org/olamy/GreetingServiceImpl.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/java/org/olamy/GreetingServiceImpl.java
new file mode 100644
index 00000000000..fa439e7b5a0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/java/org/olamy/GreetingServiceImpl.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.olamy;
+
+import com.google.gwt.user.server.rpc.RemoteServiceServlet;
+
+/**
+ * The server side implementation of the RPC service.
+ */
+@SuppressWarnings("serial")
+public class GreetingServiceImpl extends RemoteServiceServlet implements
+ GreetingService
+{
+
+ public GreetingResponse greetServer(String input) throws IllegalArgumentException
+ {
+ // Verify that the input is valid.
+ if (!FieldVerifier.isValidName(input))
+ {
+ // If the input is not valid, throw an IllegalArgumentException back to
+ // the client.
+ throw new IllegalArgumentException(
+ "Name must be at least 4 characters long");
+ }
+
+ GreetingResponse response = new GreetingResponse();
+
+ response.setServerInfo(getServletContext().getServerInfo());
+ response.setUserAgent(getThreadLocalRequest().getHeader("User-Agent"));
+
+ response.setGreeting("Hello, " + input + "!");
+
+ return response;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/jettyconf/context.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/jettyconf/context.xml
new file mode 100644
index 00000000000..00798e56645
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/jettyconf/context.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ org.eclipse.jetty.ee11.servlet.Default.useFileMappedBuffer
+ false
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..fe9829584d3
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ greetServlet
+ org.olamy.GreetingServiceImpl
+
+
+
+ greetServlet
+ /app/greet
+
+
+
+
+ index.html
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/beer.css b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/beer.css
new file mode 100644
index 00000000000..7aca7ac7b65
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/beer.css
@@ -0,0 +1,34 @@
+/** Add css rules here for your application. */
+
+
+/** Example rules used by the template application (remove for your app) */
+h1 {
+ font-size: 2em;
+ font-weight: bold;
+ color: #777777;
+ margin: 40px 0px 70px;
+ text-align: center;
+}
+
+.sendButton {
+ display: block;
+ font-size: 16pt;
+}
+
+/** Most GWT widgets already have a style name defined */
+.gwt-DialogBox {
+ width: 400px;
+}
+
+.dialogVPanel {
+ margin: 5px;
+}
+
+.serverResponseLabelError {
+ color: red;
+}
+
+/** Set ids using widget.getElement().setId("idOfElement") */
+#closeButton {
+ margin: 15px 6px 6px;
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/favicon.ico b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/favicon.ico
new file mode 100644
index 00000000000..858a707523f
Binary files /dev/null and b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/favicon.ico differ
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/index.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/index.html
new file mode 100644
index 00000000000..96cb49b86b1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-server/src/main/webapp/index.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Web Application Starter Project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Web Application Starter Project
+
+
+
+
Please enter your name:
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/pom.xml
new file mode 100644
index 00000000000..054ba242e95
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+
+ org.olamy
+ beer
+ 1.0-SNAPSHOT
+
+
+ beer-shared
+
+
+
+ com.google.gwt
+ gwt-servlet
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/FieldVerifier.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/FieldVerifier.java
new file mode 100644
index 00000000000..6f56ee3a65a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/FieldVerifier.java
@@ -0,0 +1,58 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.olamy;
+
+/**
+ *
+ * FieldVerifier validates that the name the user enters is valid.
+ *
+ *
+ * This class is in the shared project because we use it in both
+ * the client code and on the server. On the client, we verify that the name is
+ * valid before sending an RPC request so the user doesn't have to wait for a
+ * network round trip to get feedback. On the server, we verify that the name is
+ * correct to ensure that the input is correct regardless of where the RPC
+ * originates.
+ *
+ *
+ * When creating a class that is used on both the client and the server, be sure
+ * that all code is translatable and does not use native JavaScript. Code that
+ * is not translatable (such as code that interacts with a database or the file
+ * system) cannot be compiled into client side JavaScript. Code that uses native
+ * JavaScript (such as Widgets) cannot be run on the server.
+ *
+ */
+public class FieldVerifier
+{
+
+ /**
+ * Verifies that the specified name is valid for our service.
+ *
+ * In this example, we only require that the name is at least four
+ * characters. In your application, you can use more complex checks to ensure
+ * that usernames, passwords, email addresses, URLs, and other fields have the
+ * proper syntax.
+ *
+ * @param name the name to validate
+ * @return true if valid, false if invalid
+ */
+ public static boolean isValidName(String name)
+ {
+ if (name == null)
+ {
+ return false;
+ }
+ return name.length() > 3;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingResponse.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingResponse.java
new file mode 100644
index 00000000000..d5e04f8ec4a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingResponse.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.olamy;
+
+import java.io.Serializable;
+
+@SuppressWarnings("serial")
+public class GreetingResponse implements Serializable
+{
+ private String greeting;
+ private String serverInfo;
+ private String userAgent;
+
+ public String getGreeting()
+ {
+ return greeting;
+ }
+
+ public void setGreeting(String greeting)
+ {
+ this.greeting = greeting;
+ }
+
+ public String getServerInfo()
+ {
+ return serverInfo;
+ }
+
+ public void setServerInfo(String serverInfo)
+ {
+ this.serverInfo = serverInfo;
+ }
+
+ public String getUserAgent()
+ {
+ return userAgent;
+ }
+
+ public void setUserAgent(String userAgent)
+ {
+ this.userAgent = userAgent;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingService.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingService.java
new file mode 100644
index 00000000000..707679cd41f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingService.java
@@ -0,0 +1,26 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.olamy;
+
+import com.google.gwt.user.client.rpc.RemoteService;
+import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
+
+/**
+ * The client side stub for the RPC service.
+ */
+@RemoteServiceRelativePath("greet")
+public interface GreetingService extends RemoteService
+{
+ GreetingResponse greetServer(String name) throws IllegalArgumentException;
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingServiceAsync.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingServiceAsync.java
new file mode 100644
index 00000000000..0351adfd0a7
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/beer-shared/src/main/java/org/olamy/GreetingServiceAsync.java
@@ -0,0 +1,25 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.olamy;
+
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * The async counterpart of GreetingService.
+ */
+public interface GreetingServiceAsync
+{
+ void greetServer(String input, AsyncCallback callback)
+ throws IllegalArgumentException;
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/invoker.properties
new file mode 100644
index 00000000000..26652d0273a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = verify
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/pom.xml
new file mode 100644
index 00000000000..b1430b7b082
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/pom.xml
@@ -0,0 +1,82 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.olamy
+ beer
+ 1.0-SNAPSHOT
+ pom
+
+
+ UTF-8
+ @project.version@
+
+
+
+
+
+ com.google.gwt
+ gwt
+ 2.8.2
+ pom
+ import
+
+
+
+
+
+
+
+ net.ltgt.gwt.maven
+ gwt-maven-plugin
+ false
+
+ ${project.build.directory}/gwt/launcherDir
+
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+
+ net.ltgt.gwt.maven
+ gwt-maven-plugin
+ 1.0-rc-9
+ true
+
+ 1.8
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+
+
+ beer-client
+ beer-shared
+ beer-server
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/postbuild.groovy
new file mode 100644
index 00000000000..4f05a1ca8f1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-gwt-it/postbuild.groovy
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'contentCheck')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/invoker.properties
new file mode 100644
index 00000000000..998d6702ac5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test -e
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/pom.xml
new file mode 100644
index 00000000000..85b298f8eb3
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-base
+ jar
+
+ EE11 :: Simple :: Base
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.slf4j
+ slf4j-api
+
+
+ commons-io
+ commons-io
+
+
+ org.eclipse.jetty.toolchain
+ jetty-perf-helper
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/Counter.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/Counter.java
new file mode 100644
index 00000000000..89f6c4b0f05
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/Counter.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_mojo_it;
+
+@SuppressWarnings("serial")
+public class Counter implements java.io.Serializable
+{
+ int counter = 0;
+ String last;
+
+ public int getCount()
+ {
+ counter++;
+ return counter;
+ }
+
+ public void setLast(String uri)
+ {
+ last = uri;
+ }
+
+ public String getLast()
+ {
+ return last;
+ }
+}
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/HelloServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/HelloServlet.java
new file mode 100644
index 00000000000..e653b64b039
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/HelloServlet.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ *
+ */
+@WebServlet("/hello")
+public class HelloServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("Hello " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/PingServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/PingServlet.java
new file mode 100644
index 00000000000..995072a4c99
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_start_mojo_it/PingServlet.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_start_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class PingServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("pong " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
new file mode 100644
index 00000000000..5a8075691c8
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ FragmentA
+
+
+
+
+
+
+ Ping
+ org.eclipse.jetty.its.jetty_start_mojo_it.PingServlet
+
+ extra1123
+
+
+ extra2345
+
+
+
+
+ Ping
+ /ping
+
+
+
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/pom.xml
new file mode 100644
index 00000000000..f5676b3ddb6
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/pom.xml
@@ -0,0 +1,130 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-webapp
+ war
+
+ EE11 :: Simple :: WebApp
+
+
+ ${project.build.directory}/jetty-start-port.txt
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-mojo-it
+ jetty-simple-base
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ provided
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ /setbycontextxml
+ true
+ true
+ Counter accessed 1 times.
+ /jsp/bean1.jsp
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ test-compile
+
+ start
+
+
+ ${basedir}/src/config/context.xml
+
+ ${jetty.port.file}
+ EMBED
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+ ${basedir}/src/config/login.xml
+
+
+
+
+ ${basedir}/src/config/jetty-env.xml
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/context.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/context.xml
new file mode 100644
index 00000000000..f09f38e188e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/context.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ /setbycontextxml
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty-env.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty-env.xml
new file mode 100644
index 00000000000..ac26e7a42f0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty-env.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+ fooBoolean
+ 100
+ true
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/login.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/login.xml
new file mode 100644
index 00000000000..2cfa722dd89
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/login.xml
@@ -0,0 +1,6 @@
+jetty: MD5:164c88b302622e17050af52c89945d44,user
+admin: CRYPT:adpexzg3FUZAk,server-administrator,content-administrator,admin
+other: OBF:1xmk1w261u9r1w1c1xmq,user
+plain: plain,user
+user: password,user
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..2a5ac4b71bf
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+
+
+ Jetty Simple Webapp run-mojo-it
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/main/webapp/jsp/bean1.jsp b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/main/webapp/jsp/bean1.jsp
new file mode 100644
index 00000000000..586a27ebf2e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/main/webapp/jsp/bean1.jsp
@@ -0,0 +1,13 @@
+
+<%@ page session="true"%>
+
+
+
+
JSP1.2 Beans: 1
+
+Counter accessed times.
+Counter last accessed by
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/pom.xml
new file mode 100644
index 00000000000..e2d56685855
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+ jetty-simple-base
+ jetty-simple-webapp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-mojo-it
+ jetty-simple-base
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/postbuild.groovy
new file mode 100644
index 00000000000..10d76c83e13
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-it/postbuild.groovy
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'pingServlet ok')
+assert buildLog.text.contains( 'helloServlet')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/common/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/common/pom.xml
new file mode 100644
index 00000000000..8c694e8e536
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/common/pom.xml
@@ -0,0 +1,11 @@
+
+
+ 4.0.0
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ jetty-multi-module-project
+ 1.0-SNAPSHOT
+
+ common
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/common/src/main/java/mca/common/CommonService.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/common/src/main/java/mca/common/CommonService.java
new file mode 100644
index 00000000000..3c4d6bca665
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/common/src/main/java/mca/common/CommonService.java
@@ -0,0 +1,19 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package mca.common;
+
+public class CommonService
+{
+
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/invoker.properties
new file mode 100644
index 00000000000..fd18ebccf10
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-api/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-api/pom.xml
new file mode 100644
index 00000000000..6a2e9932521
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-api/pom.xml
@@ -0,0 +1,11 @@
+
+
+ 4.0.0
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ module
+ 1.0-SNAPSHOT
+
+ module-api
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-api/src/main/java/mca/module/ModuleApi.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-api/src/main/java/mca/module/ModuleApi.java
new file mode 100644
index 00000000000..46af5853ead
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-api/src/main/java/mca/module/ModuleApi.java
@@ -0,0 +1,19 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package mca.module;
+
+public interface ModuleApi
+{
+
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-impl/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-impl/pom.xml
new file mode 100644
index 00000000000..aec295601ce
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-impl/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ module
+ 1.0-SNAPSHOT
+
+ module-impl
+
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ module-api
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-impl/src/main/java/mca/module/ModuleImpl.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-impl/src/main/java/mca/module/ModuleImpl.java
new file mode 100644
index 00000000000..60f6567ee01
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/module-impl/src/main/java/mca/module/ModuleImpl.java
@@ -0,0 +1,22 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package mca.module;
+
+import mca.common.CommonService;
+
+public class ModuleImpl implements ModuleApi
+{
+
+ private static final CommonService cs = new CommonService();
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/pom.xml
new file mode 100644
index 00000000000..42fa93fed4b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/module/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ jetty-multi-module-project
+ 1.0-SNAPSHOT
+
+ module
+ pom
+
+
+ module-api
+ module-impl
+
+
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ common
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/pom.xml
new file mode 100644
index 00000000000..611a0e41f18
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ jetty-multi-module-project
+ 1.0-SNAPSHOT
+ pom
+
+ EE11 :: multi-module project
+
+
+ common
+ module
+ webapp-war
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ common
+ ${project.version}
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ module-api
+ ${project.version}
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ module-impl
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/postbuild.groovy
new file mode 100644
index 00000000000..dff8bd20fe7
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/postbuild.groovy
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+
+assert buildLog.text.contains( '(1a) >> jakarta.servlet.ServletContextListener loaded from jar:' )
+
+assert buildLog.text.contains( '(2a) >> mca.common.CommonService loaded from file:' )
+assert buildLog.text.contains( 'common/target/classes/mca/common/CommonService.class << (2b)' )
+
+assert buildLog.text.contains( '(3a) >> mca.module.ModuleApi loaded from file:' )
+assert buildLog.text.contains( 'module/module-api/target/classes/mca/module/ModuleApi.class << (3b)' )
+
+assert buildLog.text.contains( '(4a) >> mca.module.ModuleImpl loaded from file:' )
+assert buildLog.text.contains( 'module/module-impl/target/classes/mca/module/ModuleImpl.class << (4b)' )
+
+assert buildLog.text.contains( '(5a) >> mca.webapp.WebAppServletListener loaded from file:' )
+assert buildLog.text.contains( 'webapp-war/target/classes/mca/webapp/WebAppServletListener.class << (5b)' )
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/pom.xml
new file mode 100644
index 00000000000..bcf40c55f23
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ jetty-multi-module-project
+ 1.0-SNAPSHOT
+
+ webapp-war
+ war
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ test.jetty-ee11-start-mojo-multi-module-single-war-it
+ module-impl
+
+
+
+
+ ${project.build.directory}/jetty-start-mojo.txt
+ EMBED
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ test-compile
+
+ start
+
+
+
+ ${jetty.port.file}
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/main/java/mca/webapp/WebAppServletListener.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/main/java/mca/webapp/WebAppServletListener.java
new file mode 100644
index 00000000000..8ac69a87091
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/main/java/mca/webapp/WebAppServletListener.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package mca.webapp;
+
+import java.net.URL;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+import static java.lang.String.format;
+
+public class WebAppServletListener implements ServletContextListener
+{
+
+ @Override
+ public void contextInitialized(ServletContextEvent servletContextEvent)
+ {
+ print("1", "jakarta.servlet.ServletContextListener");
+ print("2", "mca.common.CommonService");
+ print("3", "mca.module.ModuleApi");
+ print("4", "mca.module.ModuleImpl");
+ print("5", "mca.webapp.WebAppServletListener");
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent servletContextEvent)
+ {
+
+ }
+
+ private void print(String counter, String className)
+ {
+ String res = className.replaceAll("\\.", "/") + ".class";
+ URL url = Thread.currentThread().getContextClassLoader().getResource(res);
+ System.out.println(
+ format("(%sa) >> %s loaded from %s << (%sb)",
+ counter, className, url, counter)
+ );
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..5d48dd82060
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-mojo-multi-module-single-war-it/webapp-war/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ mca.webapp.WebAppServletListener
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/invoker.properties
new file mode 100644
index 00000000000..26652d0273a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = verify
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/pom.xml
new file mode 100644
index 00000000000..3f5ccb0d529
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-overlay-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-base-webapp
+ war
+
+ Jetty :: Simple :: Base WebApp
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/src/main/java/org/eclipse/jetty/its/jetty_simple_base_webapp/Foo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/src/main/java/org/eclipse/jetty/its/jetty_simple_base_webapp/Foo.java
new file mode 100644
index 00000000000..e18d66fe310
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/src/main/java/org/eclipse/jetty/its/jetty_simple_base_webapp/Foo.java
@@ -0,0 +1,19 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_simple_base_webapp;
+
+public class Foo
+{
+
+}
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/src/main/webapp/base/index.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/src/main/webapp/base/index.html
new file mode 100644
index 00000000000..66174ffb095
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-base-webapp/src/main/webapp/base/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/pom.xml
new file mode 100644
index 00000000000..755d570f1ca
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/pom.xml
@@ -0,0 +1,142 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-overlay-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-webapp
+ war
+
+ Jetty :: Simple :: WebApp
+
+
+ ${project.build.directory}/jetty-start-port.txt
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-overlay-it
+ jetty-simple-base-webapp
+ war
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ provided
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-overlay-it
+ jetty-simple-base-webapp
+
+ index.html
+ base/**
+ WEB-INF/**
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ run-tests
+ integration-test
+
+ test
+
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ /setbycontextxml
+ Base Index
+ /base/index.html
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ pre-integration-test
+
+ start
+
+
+ ${basedir}/src/config/context.xml
+
+ ${jetty.port.file}
+ EMBED
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/config/context.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/config/context.xml
new file mode 100644
index 00000000000..f09f38e188e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/config/context.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ /setbycontextxml
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/main/java/org/eclipse/jetty/its/jetty_simple_webapp/Bar.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/main/java/org/eclipse/jetty/its/jetty_simple_webapp/Bar.java
new file mode 100644
index 00000000000..b535e611732
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/main/java/org/eclipse/jetty/its/jetty_simple_webapp/Bar.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_simple_webapp;
+
+public class Bar
+{
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/main/webapp/index.html b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/main/webapp/index.html
new file mode 100644
index 00000000000..a92e124a123
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/jetty-simple-webapp/src/main/webapp/index.html
@@ -0,0 +1,5 @@
+
+
+
Simple WebApp Index
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/pom.xml
new file mode 100644
index 00000000000..9ed08baea62
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-overlay-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ Jetty :: Simple Overlay
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+ jetty-simple-base-webapp
+ jetty-simple-webapp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-overlay-it
+ jetty-simple-base-webapp
+ ${project.version}
+ war
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/postbuild.groovy
new file mode 100644
index 00000000000..7d175d16693
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-overlay-it/postbuild.groovy
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/invoker.properties
new file mode 100644
index 00000000000..55675300e77
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = verify -V
+#test-compile failsafe:integration-test
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/pom.xml
new file mode 100644
index 00000000000..4b8e12659c6
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-distro-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-base
+ jar
+
+ EE11 :: Simple :: Base
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.slf4j
+ slf4j-api
+
+
+ commons-io
+ commons-io
+
+
+ org.eclipse.jetty.toolchain
+ jetty-perf-helper
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_mojo_it/HelloServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_mojo_it/HelloServlet.java
new file mode 100644
index 00000000000..629b83ad4c5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_mojo_it/HelloServlet.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_run_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ *
+ */
+@WebServlet("/hello")
+public class HelloServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("Hello " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_mojo_it/PingServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_mojo_it/PingServlet.java
new file mode 100644
index 00000000000..b41b2ab0974
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_mojo_it/PingServlet.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_run_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class PingServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("pong " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
new file mode 100644
index 00000000000..a1ec4e27ce4
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ FragmentA
+
+
+
+
+
+
+ Ping
+ org.eclipse.jetty.its.jetty_run_mojo_it.PingServlet
+
+ extra1123
+
+
+ extra2345
+
+
+
+
+ Ping
+ /ping
+
+
+
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/pom.xml
new file mode 100644
index 00000000000..8f6bd908b82
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/pom.xml
@@ -0,0 +1,148 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-distro-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-webapp
+ war
+
+ EE11 :: Simple :: WebApp
+
+
+ ${project.build.directory}/jetty-start-war-distro-port.txt
+ @jetty.jvmArgs@
+ HOME
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-distro-mojo-it
+ jetty-simple-base
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ provided
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ IntegrationTest*.java
+
+
+ ${jetty.port.file}
+ true
+ true
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+
+
+ integration-test
+
+ integration-test
+
+
+
+ verify
+
+ verify
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ ${jetty.version}
+
+
+
+
+ start-jetty
+ pre-integration-test
+
+ start-war
+
+
+ @jetty.stopPort@
+ @jetty.stopKey@
+
+
+
+ ${basedir}/src/base
+ ${java.home}/bin/java
+
+ true
+ ${jetty.port.file}
+ 0
+
+ @jettyHomeZip@
+ ee11-apache-jsp,ee11-glassfish-jstl,ee11-testmod,resources
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty-ee11.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty-ee11.xml
new file mode 100644
index 00000000000..9857cb546ef
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty-ee11.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/base/modules/ee11-testmod.mod b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/base/modules/ee11-testmod.mod
new file mode 100644
index 00000000000..9270d5e3ad6
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/base/modules/ee11-testmod.mod
@@ -0,0 +1,13 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables test setup
+
+[environment]
+ee11
+
+[depend]
+http
+
+[xml]
+etc/test-jetty-ee11.xml
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..2a5ac4b71bf
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+
+
+ Jetty Simple Webapp run-mojo-it
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/pom.xml
new file mode 100644
index 00000000000..0d7cedbdc7f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-distro-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+ jetty-simple-base
+ jetty-simple-webapp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-distro-mojo-it
+ jetty-simple-base
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/postbuild.groovy
new file mode 100644
index 00000000000..66b37d6a318
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-distro-mojo-it/postbuild.groovy
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+System.out.println( "running postbuild.groovy port " + jettyStopPort + ", key:" + jettyStopKey )
+
+int port = Integer.parseInt( jettyStopPort )
+
+Socket s=new Socket(InetAddress.getByName("127.0.0.1"),port )
+
+OutputStream out=s.getOutputStream()
+out.write(( jettyStopKey +"\r\nforcestop\r\n").getBytes())
+out.flush()
+s.close()
+
+
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Home process starting' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'pingServlet ok')
+assert buildLog.text.contains( 'helloServlet')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/invoker.properties
new file mode 100644
index 00000000000..55675300e77
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = verify -V
+#test-compile failsafe:integration-test
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/pom.xml
new file mode 100644
index 00000000000..e5ccb260119
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-forked-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+
+
+ jetty-simple-base
+ jar
+
+ EE11 :: Simple :: Base
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.slf4j
+ slf4j-api
+
+
+ commons-io
+ commons-io
+
+
+ org.eclipse.jetty.toolchain
+ jetty-perf-helper
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_war_exploded_mojo_it/HelloServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_war_exploded_mojo_it/HelloServlet.java
new file mode 100644
index 00000000000..4eda9384269
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_war_exploded_mojo_it/HelloServlet.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_run_war_exploded_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ *
+ */
+@WebServlet("/hello")
+public class HelloServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("Hello " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_war_exploded_mojo_it/PingServlet.java b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_war_exploded_mojo_it/PingServlet.java
new file mode 100644
index 00000000000..66d7be783fe
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/java/org/eclipse/jetty/its/jetty_run_war_exploded_mojo_it/PingServlet.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.its.jetty_run_war_exploded_mojo_it;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class PingServlet
+ extends HttpServlet
+{
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ String who = req.getParameter("name");
+
+ resp.getWriter().write("pong " + (who == null ? "unknown" : who));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
new file mode 100644
index 00000000000..827c1f7b294
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-base/src/main/resources/META-INF/web-fragment.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ FragmentA
+
+
+
+
+
+
+ Ping
+ org.eclipse.jetty.its.jetty_run_war_exploded_mojo_it.PingServlet
+
+ extra1123
+
+
+ extra2345
+
+
+
+
+ Ping
+ /ping
+
+
+
+
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/pom.xml
new file mode 100644
index 00000000000..4d522c4d055
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/pom.xml
@@ -0,0 +1,145 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-forked-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ ../pom.xml
+
+
+ jetty-simple-webapp
+ war
+
+ EE11 :: Simple :: WebApp
+
+
+ ${project.build.directory}/jetty-start-war-forked-port.txt
+ FORK
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-forked-mojo-it
+ jetty-simple-base
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-servlet
+ provided
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+ ${jetty.version}
+
+
+
+
+ start-jetty
+ pre-integration-test
+
+ start-war
+
+
+ @jetty.stopPort@
+ @jetty.stopKey@
+
+ ${project.build.directory}/${project.artifactId}-${project.version}
+
+
+ ${jetty.port.file}
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ ${jetty.port.file}
+ true
+ true
+ ${project.groupId}:${project.artifactId}
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+ **/*TestGetContent*
+
+
+
+
+ integration-test
+
+ integration-test
+
+
+
+ verify
+
+ verify
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..2a5ac4b71bf
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/jetty-simple-webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+
+
+ Jetty Simple Webapp run-mojo-it
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/pom.xml
new file mode 100644
index 00000000000..2121f8f7a84
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+ ../it-parent-pom/pom.xml
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-forked-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ @project.version@
+
+
+
+ jetty-simple-base
+ jetty-simple-webapp
+
+
+
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-forked-mojo-it
+ jetty-simple-base
+ ${project.version}
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/postbuild.groovy
new file mode 100644
index 00000000000..f570f904507
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-forked-mojo-it/postbuild.groovy
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+System.out.println( "running postbuild.groovy port " + jettyStopPort + ", key:" + jettyStopKey )
+
+int port = Integer.parseInt( jettyStopPort )
+
+Socket s=new Socket(InetAddress.getByName("127.0.0.1"),port )
+
+OutputStream out=s.getOutputStream()
+out.write(( jettyStopKey +"\r\nforcestop\r\n").getBytes())
+out.flush()
+s.close()
+
+File outputLog = new File( basedir, 'build.log' )
+assert outputLog.text.contains( 'Forked process starting' )
+assert outputLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert outputLog.text.contains( 'pingServlet ok')
+assert outputLog.text.contains( 'helloServlet')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/invoker.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/invoker.properties
new file mode 100644
index 00000000000..816c3f38def
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/invoker.properties
@@ -0,0 +1,2 @@
+invoker.goals = verify -V -e
+#test-compile failsafe:integration-test
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/pom.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/pom.xml
new file mode 100644
index 00000000000..176905b7dc7
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/pom.xml
@@ -0,0 +1,139 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.jetty.ee11.its
+ it-parent-pom
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.jetty.ee11.its.jetty-start-war-mojo-it
+ jetty-simple-project
+ 0.0.1-SNAPSHOT
+ pom
+
+ EE11 :: Simple start war mojo test
+
+
+ ${project.build.directory}/jetty-start-war-port.txt
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+ tests
+ test-jar
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ ${jetty.port.file}
+ ${project.groupId}:${project.artifactId}
+ Bean Validation Webapp example
+
+
+ org.eclipse.jetty.ee11:jetty-ee11-maven-plugin
+
+
+ **/*TestGetContent*
+
+
+
+
+ integration-test
+
+ integration-test
+
+
+
+ verify
+
+ verify
+
+
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-maven-plugin
+
+
+ start-jetty
+ test-compile
+
+ start-war
+
+
+
+ pom
+
+
+ ${project.build.directory}/bean-validation-webapp-2.25.1.war
+
+
+ ${jetty.port.file}
+
+
+ ${basedir}/src/config/jetty.xml
+
+
+
+
+
+
+ jakarta.xml.bind
+ jakarta.xml.bind-api
+ 2.3.3
+
+
+ jakarta.activation
+ jakarta.activation-api
+ 1.2.2
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ load-war
+ generate-resources
+
+ copy
+
+
+
+
+ org.glassfish.jersey.examples
+ bean-validation-webapp
+ 2.25.1
+ war
+ true
+ **
+ ${project.build.directory}
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/postbuild.groovy b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/postbuild.groovy
new file mode 100644
index 00000000000..4f05a1ca8f1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/postbuild.groovy
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Server' )
+assert buildLog.text.contains( 'Running org.eclipse.jetty.ee11.maven.plugin.it.IntegrationTestGetContent')
+assert buildLog.text.contains( 'contentCheck')
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/src/config/jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/src/config/jetty.xml
new file mode 100644
index 00000000000..343f5e7f99b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/jetty-start-war-mojo-it/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ https
+
+ 32768
+ 8192
+ 8192
+ 1024
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30000
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/it/settings.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/it/settings.xml
new file mode 100644
index 00000000000..e62d9bab8b8
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/it/settings.xml
@@ -0,0 +1,37 @@
+
+
+
+ @localRepoPath@
+
+
+ it-repo
+
+ true
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/AbstractUnassembledWebAppMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/AbstractUnassembledWebAppMojo.java
new file mode 100644
index 00000000000..61abf6bb7a9
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/AbstractUnassembledWebAppMojo.java
@@ -0,0 +1,255 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.eclipse.jetty.maven.ScanPattern;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.Resources;
+
+/**
+ * Base class for all goals that operate on unassembled webapps.
+ *
+ */
+public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo
+{
+ /**
+ * The default location of the web.xml file. Will be used
+ * if <webApp><descriptor> is not set.
+ */
+ @Parameter (defaultValue = "${project.baseDir}/src/main/webapp/WEB-INF/web.xml")
+ protected File webXml;
+
+ /**
+ * The directory containing generated test classes.
+ *
+ */
+ @Parameter (defaultValue = "${project.build.testOutputDirectory}", required = true)
+ protected File testClassesDirectory;
+
+ /**
+ * An optional pattern for includes/excludes of classes in the testClassesDirectory
+ */
+ @Parameter
+ protected ScanPattern scanTestClassesPattern;
+
+ /**
+ * The directory containing generated classes.
+ */
+ @Parameter (defaultValue = "${project.build.outputDirectory}", required = true)
+ protected File classesDirectory;
+
+
+ /**
+ * An optional pattern for includes/excludes of classes in the classesDirectory
+ */
+ @Parameter
+ protected ScanPattern scanClassesPattern;
+
+
+ /**
+ * Default root directory for all html/jsp etc files.
+ * Used to initialize webApp.setBaseResource().
+ */
+ @Parameter (defaultValue = "${project.basedir}/src/main/webapp", readonly = true)
+ protected File webAppSourceDirectory;
+
+ protected void verifyPomConfiguration() throws MojoExecutionException
+ {
+ //Does the default webapp static resource location exist?
+ if (!webAppSourceDirectory.exists())
+ {
+ //try last resort of a fake directory
+ File target = new File(project.getBuild().getDirectory());
+ webAppSourceDirectory = new File(target, FAKE_WEBAPP);
+ }
+
+ // check the classes to form a classpath with
+ try
+ {
+ //allow a webapp with no classes in it (just jsps/html)
+ if (classesDirectory != null)
+ {
+ if (!classesDirectory.exists())
+ getLog().info("Classes directory " + classesDirectory.getCanonicalPath() + " does not exist");
+ else
+ getLog().info("Classes = " + classesDirectory.getCanonicalPath());
+ }
+ else
+ getLog().info("Classes directory not set");
+ }
+ catch (IOException e)
+ {
+ throw new MojoExecutionException("Location of classesDirectory does not exist");
+ }
+ }
+
+ @Override
+ protected void configureWebApp() throws Exception
+ {
+ super.configureWebApp();
+ try
+ {
+ configureUnassembledWebApp();
+ }
+ catch (IOException e)
+ {
+ throw new MojoFailureException("Unable to configure unassembled webapp", e);
+ }
+ }
+
+ /**
+ * Configure a webapp that has not been assembled into a war.
+ *
+ * @throws IOException if there is an IO problem
+ */
+ protected void configureUnassembledWebApp() throws Exception
+ {
+ //Set up the location of the webapp.
+ //There are 2 parts to this: setWar() and setBaseResource(). On standalone jetty,
+ //the former could be the location of a packed war, while the latter is the location
+ //after any unpacking. With this mojo, you are running an unpacked, unassembled webapp,
+ //so the two locations should be equal.
+
+ //The first time we run, remember the original base dir
+ if (originalBaseResource == null)
+ {
+ if (webApp.getBaseResource() == null)
+ {
+ //Use the default static resource location
+ if (!webAppSourceDirectory.exists())
+ webAppSourceDirectory.mkdirs();
+ originalBaseResource = webApp.getResourceFactory().newResource(webAppSourceDirectory.getCanonicalPath());
+ }
+ else
+ originalBaseResource = webApp.getBaseResource();
+ }
+
+ //On every subsequent re-run set it back to the original base dir before
+ //we might have applied any war overlays onto it
+ webApp.setBaseResource(originalBaseResource);
+
+ //TODO the war does not need to be set, _except_ that QuickStartConfiguration checks for non null
+ if (webApp.getWar() == null)
+ webApp.setWar(originalBaseResource.toString());
+
+ if (classesDirectory != null)
+ webApp.setClasses(classesDirectory);
+
+ if (useTestScope && (testClassesDirectory != null))
+ webApp.setTestClasses(testClassesDirectory);
+
+ List webInfLibs = getWebInfLibArtifacts().stream()
+ .map(a ->
+ {
+ Path p = mavenProjectHelper.getPathFor(a);
+ getLog().debug("Artifact " + a.getId() + " loaded from " + p + " added to WEB-INF/lib");
+ return p.toFile();
+ }).collect(Collectors.toList());
+
+ webApp.setWebInfLib(webInfLibs);
+
+ //if we have not already set web.xml location, need to set one up
+ if (webApp.getDescriptor() == null)
+ {
+ //Has an explicit web.xml file been configured to use?
+ if (webXml != null)
+ {
+ Resource r = webApp.getResourceFactory().newResource(webXml.toPath());
+ if (Resources.isReadableFile(r))
+ {
+ webApp.setDescriptor(r.getURI().toASCIIString());
+ }
+ }
+
+ //Still don't have a web.xml file: try the resourceBase of the webapp, if it is set
+ if (webApp.getDescriptor() == null && webApp.getBaseResource() != null)
+ {
+ // TODO: should never return from WEB-INF/lib/foo.jar!/WEB-INF/web.xml
+ // TODO: should also never return from a META-INF/versions/#/WEB-INF/web.xml location
+ Resource r = webApp.getBaseResource().resolve("WEB-INF/web.xml");
+ if (Resources.isReadableFile(r))
+ {
+ webApp.setDescriptor(r.getURI().toASCIIString());
+ }
+ }
+
+ //Still don't have a web.xml file: finally try the configured static resource directory if there is one
+ if (webApp.getDescriptor() == null && (webAppSourceDirectory != null))
+ {
+ // TODO: fix, use Resource or Path
+ File f = new File(new File(webAppSourceDirectory, "WEB-INF"), "web.xml");
+ if (f.exists() && f.isFile())
+ {
+ webApp.setDescriptor(f.getCanonicalPath());
+ }
+ }
+ }
+
+ //process any overlays and the war type artifacts, and
+ //sets up the base resource collection for the webapp
+ mavenProjectHelper.getOverlayManager().applyOverlays(webApp, webApp.getBaseAppFirst());
+
+ getLog().info("web.xml file = " + webApp.getDescriptor());
+ getLog().info("Webapp directory = " + webAppSourceDirectory.getCanonicalPath());
+ getLog().info("Web defaults = " + (webApp.getDefaultsDescriptor() == null ? " jetty default" : webApp.getDefaultsDescriptor()));
+ getLog().info("Web overrides = " + (webApp.getOverrideDescriptor() == null ? " none" : webApp.getOverrideDescriptor()));
+ }
+
+ /**
+ * Find which dependencies are suitable for addition to the virtual
+ * WEB-INF lib.
+ */
+ protected Collection getWebInfLibArtifacts()
+ {
+ return project.getArtifacts().stream()
+ .filter(this::isArtifactOKForWebInfLib)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Check if the artifact is suitable to be considered part of the
+ * virtual web-inf/lib.
+ *
+ * @param artifact the artifact to check
+ * @return true if the artifact represents a jar, isn't scope provided and
+ * is scope test, if useTestScope is enabled. False otherwise.
+ */
+ private boolean isArtifactOKForWebInfLib(Artifact artifact)
+ {
+ //The dependency cannot be of type war
+ if ("war".equalsIgnoreCase(artifact.getType()))
+ return false;
+
+ //The dependency cannot be scope provided (those should be added to the plugin classpath)
+ if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
+ return false;
+
+ //Test dependencies not added by default
+ if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope)
+ return false;
+
+ return true;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/AbstractWebAppMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/AbstractWebAppMojo.java
new file mode 100644
index 00000000000..ba04ae44029
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/AbstractWebAppMojo.java
@@ -0,0 +1,888 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.AbstractMojoExecutionException;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.util.StringUtils;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.jetty.maven.MavenProjectHelper;
+import org.eclipse.jetty.maven.MavenServerConnector;
+import org.eclipse.jetty.maven.PluginLog;
+import org.eclipse.jetty.maven.ScanTargetPattern;
+import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.IncludeExcludeSet;
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.resource.Resource;
+
+/**
+ * AbstractWebAppMojo
+ *
+ * Base class for common behaviour of jetty mojos.
+ */
+public abstract class AbstractWebAppMojo extends AbstractMojo
+{
+ public static final String JETTY_HOME_GROUPID = "org.eclipse.jetty";
+ public static final String JETTY_HOME_ARTIFACTID = "jetty-home";
+ public static final String FAKE_WEBAPP = "webapp-synth";
+
+ public enum DeploymentMode
+ {
+ EMBED,
+ FORK,
+ HOME, //alias for EXTERNAL
+ DISTRO, //alias for EXTERNAL
+ EXTERNAL
+ }
+
+ /**
+ * Max number of times to check to see if jetty has started correctly
+ * when running in FORK or EXTERNAL mode.
+ */
+ @Parameter (defaultValue = "10")
+ protected int maxChildStartChecks;
+
+ /**
+ * How long to wait in msec between checks to see if jetty has started
+ * correctly when running in FORK or EXTERNAL mode.
+ */
+ @Parameter (defaultValue = "200")
+ protected long maxChildStartCheckMs;
+ /**
+ * Whether or not to include dependencies on the plugin's classpath with <scope>provided</scope>
+ * Use WITH CAUTION as you may wind up with duplicate jars/classes.
+ *
+ * @since jetty-7.5.2
+ */
+ @Parameter (defaultValue = "false")
+ protected boolean useProvidedScope;
+
+
+ /**
+ * List of goals that are NOT to be used
+ *
+ * @since jetty-7.5.2
+ */
+ @Parameter
+ protected String[] excludedGoals;
+
+ /**
+ * An instance of org.eclipse.jetty.ee11.webapp.WebAppContext that represents the webapp.
+ * Use any of its setters to configure the webapp. This is the preferred and most
+ * flexible method of configuration, rather than using the (deprecated) individual
+ * parameters like "tmpDirectory", "contextPath" etc.
+ *
+ */
+ @Parameter
+ protected MavenWebAppContext webApp;
+
+ /**
+ * Skip this mojo execution.
+ */
+ @Parameter (property = "jetty.skip", defaultValue = "false")
+ protected boolean skip;
+
+
+ /**
+ * Location of a context xml configuration file whose contents
+ * will be applied to the webapp AFTER anything in <webApp>.Optional.
+ */
+ @Parameter
+ protected String contextXml;
+
+
+ /**
+ * The maven project.
+ */
+ @Parameter(defaultValue = "${project}", readonly = true)
+ protected MavenProject project;
+
+
+ /**
+ * The artifacts for the project.
+ */
+ @Parameter (defaultValue = "${project.artifacts}", readonly = true)
+ protected Set projectArtifacts;
+
+ /**
+ * The maven build executing.
+ */
+ @Parameter (defaultValue = "${mojoExecution}", readonly = true)
+ protected org.apache.maven.plugin.MojoExecution execution;
+
+
+ /**
+ * The artifacts for the plugin itself.
+ */
+ @Parameter (defaultValue = "${plugin.artifacts}", readonly = true)
+ protected List pluginArtifacts;
+
+
+ /**
+ * If true, the <testOutputDirectory>
+ * and the dependencies of <scope>test<scope>
+ * will be put first on the runtime classpath.
+ */
+ @Parameter (defaultValue = "false")
+ protected boolean useTestScope;
+
+ /**
+ * List of directories with ant-style <include> and <exclude> patterns
+ * for extra targets to periodically scan for changes.Optional.
+ */
+ @Parameter
+ protected List scanTargetPatterns;
+
+ @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
+ protected List reactorProjects;
+
+ /**
+ * The target directory
+ */
+ @Parameter (defaultValue = "${project.build.directory}", required = true, readonly = true)
+ protected File target;
+
+
+ /**
+ * List of jetty xml configuration files whose contents
+ * will be applied (in order declared) before any plugin configuration. Optional.
+ */
+ @Parameter
+ protected List jettyXmls;
+
+
+ /**
+ * Optional jetty properties to put on the command line
+ */
+ @Parameter
+ protected Map jettyProperties;
+
+
+ /**
+ * File containing system properties to be set before execution
+ *
+ * Note that these properties will NOT override System properties
+ * that have been set on the command line, by the JVM, or directly
+ * in the POM via systemProperties. Optional.
+ *
+ *
+ */
+ @Parameter (property = "jetty.systemPropertiesFile")
+ protected File systemPropertiesFile;
+
+
+ /**
+ * System properties to set before execution.
+ * Note that these properties will NOT override System properties
+ * that have been set on the command line or by the JVM. They WILL
+ * override System properties that have been set via systemPropertiesFile.
+ * Optional.
+ */
+ @Parameter
+ protected Map systemProperties;
+
+ /**
+ * Controls how to run jetty. Valid values are EMBED,FORK,EXTERNAL.
+ */
+ @Parameter (property = "jetty.deployMode", defaultValue = "EMBED")
+ protected DeploymentMode deployMode;
+
+
+ /**
+ * List of other contexts to set up. Consider using instead
+ * the <jettyXml> element to specify external jetty xml config file.
+ * Optional.
+ */
+ @Parameter
+ protected List contextHandlers;
+
+ /**
+ * List of security realms to set up. Consider using instead
+ * the <jettyXml> element to specify external jetty xml config file.
+ * Optional.
+ */
+ @Parameter
+ protected List loginServices;
+
+ /**
+ * A RequestLog implementation to use for the webapp at runtime.
+ * Consider using instead the <jettyXml> element to specify external jetty xml config file.
+ * Optional.
+ */
+ @Parameter
+ protected RequestLog requestLog;
+
+ /**
+ * A ServerConnector to use.
+ */
+ @Parameter
+ protected MavenServerConnector httpConnector;
+
+
+ /**
+ * A wrapper for the Server object
+ */
+ @Parameter
+ protected Server server;
+ //End of EMBED only
+
+
+ //Start of parameters only valid for FORK/EXTERNAL
+ /**
+ * Extra environment variables to be passed to the forked process
+ */
+ @Parameter
+ protected Map env = new HashMap<>();
+
+ /**
+ * Arbitrary jvm args to pass to the forked process
+ */
+ @Parameter (property = "jetty.jvmArgs")
+ protected String jvmArgs;
+
+ /**
+ * Port to listen to stop jetty on executing -DSTOP.PORT=<stopPort>
+ * -DSTOP.KEY=<stopKey> -jar start.jar --stop
+ *
+ */
+ @Parameter
+ protected int stopPort;
+
+ /**
+ * Key to provide when stopping jetty on executing java -DSTOP.KEY=<stopKey>
+ * -DSTOP.PORT=<stopPort> -jar start.jar --stop
+ *
+ */
+ @Parameter
+ protected String stopKey;
+ //End of FORK or EXTERNAL parameters
+
+ //Start of parameters only valid for EXTERNAL
+ /**
+ * Location of jetty home directory
+ */
+ @Parameter
+ protected File jettyHome;
+
+ /**
+ * Location of Jetty home zipped
+ */
+ @Parameter
+ public File jettyHomeZip;
+
+ /**
+ * Location of jetty base directory
+ */
+ @Parameter
+ protected File jettyBase;
+
+ /**
+ * Optional list of other modules to
+ * activate.
+ */
+ @Parameter
+ protected String[] modules;
+
+ /**
+ * Extra options that can be passed to the jetty command line
+ */
+ @Parameter (property = "jetty.options")
+ protected String jettyOptions;
+
+ //End of EXTERNAL only parameters
+
+ //Start of parameters only valid for FORK
+ /**
+ * The file into which to generate the quickstart web xml for the forked process to use
+ *
+ */
+ @Parameter (defaultValue = "${project.build.directory}/fork-web.xml")
+ protected File forkWebXml;
+ //End of FORK only parameters
+
+ /**
+ * Helper for interacting with the maven project space
+ */
+ protected MavenProjectHelper mavenProjectHelper;
+
+ /**
+ * This plugin
+ */
+ @Parameter (defaultValue = "${plugin}", readonly = true, required = true)
+ protected PluginDescriptor plugin;
+
+ /**
+ * The project's remote repositories to use for the resolution.
+ */
+ @Parameter (defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true)
+ private List remoteRepositories;
+
+ /**
+ *
+ */
+ @Component
+ private RepositorySystem repositorySystem;
+
+ /**
+ * The current maven session
+ */
+ @Parameter (defaultValue = "${session}", required = true, readonly = true)
+ private MavenSession session;
+
+ /**
+ * Default supported project type is war packaging.
+ */
+ @Parameter
+ protected List supportedPackagings = Collections.singletonList("war");
+
+ /**
+ * List of deps that are wars
+ */
+ protected List warArtifacts;
+
+ /**
+ * Webapp base before applying overlays etc
+ */
+ protected Resource originalBaseResource;
+
+ /**
+ * List of jars with scope=provided
+ */
+ protected List providedJars;
+
+ /**
+ * System properties from both systemPropertyFile and systemProperties.
+ */
+ protected Map mergedSystemProperties;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException
+ {
+ if (isPackagingSupported())
+ {
+ if (skip)
+ {
+ getLog().info("Skipping Jetty start: jetty.skip==true");
+ return;
+ }
+
+ if (isExcludedGoal(execution.getMojoDescriptor().getGoal()))
+ {
+ getLog().info("The goal \"" + execution.getMojoDescriptor().getFullGoalName() +
+ "\" is unavailable for this web app because of an configuration.");
+ return;
+ }
+
+ getLog().info("Configuring Jetty for project: " + getProjectName());
+ mavenProjectHelper = new MavenProjectHelper(project, repositorySystem, remoteRepositories, session);
+ mergedSystemProperties = mergeSystemProperties();
+ configureSystemProperties();
+ augmentPluginClasspath();
+ PluginLog.setLog(getLog());
+ verifyPomConfiguration();
+ startJetty();
+ }
+ else
+ getLog().info("Packaging type [" + project.getPackaging() + "] is unsupported");
+ }
+
+ protected void startJetty()
+ throws MojoExecutionException, MojoFailureException
+ {
+ try
+ {
+ configureWebApp();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Webapp config failure", e);
+ }
+
+ switch (deployMode)
+ {
+ case EMBED:
+ {
+ startJettyEmbedded();
+ break;
+ }
+ case FORK:
+ {
+ startJettyForked();
+ break;
+ }
+ case DISTRO:
+ case HOME:
+ case EXTERNAL:
+ {
+ if (deployMode != DeploymentMode.EXTERNAL)
+ getLog().warn(deployMode + " mode is deprecated, use mode EXTERNAL");
+ startJettyHome();
+ break;
+ }
+ default:
+ throw new MojoExecutionException("Unrecognized runType=" + deployMode);
+ }
+
+ }
+
+ protected abstract void startJettyEmbedded() throws MojoExecutionException;
+
+ protected abstract void startJettyForked() throws MojoExecutionException;
+
+ protected abstract void startJettyHome() throws MojoExecutionException;
+
+ protected JettyEmbedder newJettyEmbedder()
+ throws Exception
+ {
+ JettyEmbedder jetty = new JettyEmbedder();
+ jetty.setStopKey(stopKey);
+ jetty.setStopPort(stopPort);
+ jetty.setServer(server);
+ jetty.setContextHandlers(contextHandlers);
+ jetty.setRequestLog(requestLog);
+ jetty.setJettyXmlFiles(jettyXmls);
+ jetty.setHttpConnector(httpConnector);
+ jetty.setJettyProperties(jettyProperties);
+ jetty.setLoginServices(loginServices);
+ jetty.setContextXml(contextXml);
+ jetty.setWebApp(webApp);
+ return jetty;
+ }
+
+ protected JettyForker newJettyForker()
+ throws Exception
+ {
+ JettyForker jetty = new JettyForker();
+ jetty.setServer(server);
+ jetty.setWorkDir(project.getBasedir());
+ jetty.setStopKey(stopKey);
+ jetty.setStopPort(stopPort);
+ jetty.setEnv(env);
+ jetty.setJvmArgs(jvmArgs);
+ jetty.setSystemProperties(mergedSystemProperties);
+ jetty.setContainerClassPath(getContainerClassPath());
+ jetty.setJettyXmlFiles(jettyXmls);
+ jetty.setJettyProperties(jettyProperties);
+ jetty.setForkWebXml(forkWebXml);
+ jetty.setContextXml(contextXml);
+ jetty.setWebAppPropsFile(new File(target, "webApp.props"));
+ Random random = new Random();
+ String token = Long.toString(random.nextLong() ^ System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
+ jetty.setTokenFile(target.toPath().resolve(token + ".txt").toFile());
+ jetty.setWebApp(webApp);
+ return jetty;
+ }
+
+ protected JettyHomeForker newJettyHomeForker()
+ throws Exception
+ {
+ JettyHomeForker jetty = new JettyHomeForker();
+ jetty.setStopKey(stopKey);
+ jetty.setStopPort(stopPort);
+ jetty.setEnv(env);
+ jetty.setJvmArgs(jvmArgs);
+ jetty.setJettyOptions(jettyOptions);
+ jetty.setJettyXmlFiles(jettyXmls);
+ jetty.setJettyProperties(jettyProperties);
+ jetty.setModules(modules);
+ jetty.setSystemProperties(mergedSystemProperties);
+ Random random = new Random();
+ String token = Long.toString(random.nextLong() ^ System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
+ jetty.setTokenFile(target.toPath().resolve(token + ".txt").toFile());
+
+ List libExtJars = new ArrayList<>();
+
+ List pdeps = plugin.getPlugin().getDependencies();
+ if (pdeps != null && !pdeps.isEmpty())
+ {
+ boolean warned = false;
+ for (Dependency d:pdeps)
+ {
+ if (d.getGroupId().equalsIgnoreCase("org.eclipse.jetty"))
+ {
+ if (!warned)
+ {
+ getLog().warn("Jetty jars detected in : use in parameter instead to select appropriate jetty modules.");
+ warned = true;
+ }
+ }
+ else
+ {
+ libExtJars.add(mavenProjectHelper.resolveArtifact(d.getGroupId(), d.getArtifactId(), d.getVersion(), d.getType()));
+ }
+ }
+ jetty.setLibExtJarFiles(libExtJars);
+ }
+
+ jetty.setWebApp(webApp);
+ jetty.setContextXml(contextXml);
+
+ if (jettyHome == null)
+ jetty.setJettyHomeZip(jettyHomeZip != null ? jettyHomeZip : mavenProjectHelper.resolveArtifact(JETTY_HOME_GROUPID, JETTY_HOME_ARTIFACTID, plugin.getVersion(), "zip"));
+
+ jetty.setVersion(plugin.getVersion());
+ jetty.setJettyHome(jettyHome);
+ jetty.setJettyBase(jettyBase);
+ jetty.setBaseDir(target);
+
+ return jetty;
+ }
+
+ /**
+ * Used by subclasses.
+ * @throws MojoExecutionException if there is a mojo execution problem
+ */
+ protected void verifyPomConfiguration() throws MojoExecutionException
+ {
+ }
+
+ /**
+ * Unite system properties set via systemPropertiesFile element and the systemProperties element.
+ * Properties from the pom override properties from the file.
+ *
+ * @return united properties map
+ * @throws MojoExecutionException if there is a mojo execution problem
+ */
+ protected Map mergeSystemProperties()
+ throws MojoExecutionException
+ {
+ Map properties = new HashMap<>();
+
+ //Get the properties from any file first
+ if (systemPropertiesFile != null)
+ {
+ Properties tmp = new Properties();
+ try (InputStream propFile = new FileInputStream(systemPropertiesFile))
+ {
+ tmp.load(propFile);
+ for (Object k:tmp.keySet())
+ properties.put(k.toString(), tmp.get(k).toString());
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Problem applying system properties from file " + systemPropertiesFile.getName(), e);
+ }
+ }
+ //Allow systemProperties defined in the pom to override the file
+ if (systemProperties != null)
+ {
+ properties.putAll(systemProperties);
+ }
+ return properties;
+ }
+
+ protected void configureSystemProperties()
+ throws MojoExecutionException
+ {
+ if (mergedSystemProperties != null)
+ {
+ for (Map.Entry e : mergedSystemProperties.entrySet())
+ {
+ if (!StringUtil.isEmpty(e.getKey()) && !StringUtil.isEmpty(e.getValue()))
+ {
+ System.setProperty(e.getKey(), e.getValue());
+ if (getLog().isDebugEnabled())
+ getLog().debug("Set system property " + e.getKey() + "=" + e.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * Augment jetty's classpath with dependencies marked as scope=provided
+ * if useProvidedScope==true.
+ *
+ * @throws MojoExecutionException if there is a mojo execution problem
+ */
+ protected void augmentPluginClasspath() throws MojoExecutionException
+ {
+ //Filter out ones that will clash with jars that are plugin dependencies, then
+ //create a new classloader that we setup in the parent chain.
+ providedJars = getProvidedJars();
+
+ if (!providedJars.isEmpty())
+ {
+ try
+ {
+ URL[] urls = new URL[providedJars.size()];
+ int i = 0;
+ for (File providedJar:providedJars)
+ urls[i++] = providedJar.toURI().toURL();
+ URLClassLoader loader = new URLClassLoader(urls, getClass().getClassLoader());
+ Thread.currentThread().setContextClassLoader(loader);
+ getLog().info("Plugin classpath augmented with provided dependencies: " + Arrays.toString(urls));
+ }
+ catch (MalformedURLException e)
+ {
+ throw new MojoExecutionException("Invalid url", e);
+ }
+ }
+ }
+
+ /**
+ * Get any dependencies that are scope "provided" if useProvidedScope == true. Ensure
+ * that only those dependencies that are not already present via the plugin are
+ * included.
+ *
+ * @return provided scope dependencies that are not also plugin dependencies.
+ * @throws MojoExecutionException if there is a mojo execution problem
+ */
+ protected List getProvidedJars() throws MojoExecutionException
+ {
+ if (useProvidedScope)
+ {
+ return project.getArtifacts()
+ .stream()
+ .filter(a -> Artifact.SCOPE_PROVIDED.equals(a.getScope()) && !isPluginArtifact(a))
+ .map(a -> a.getFile()).collect(Collectors.toList());
+ }
+ else
+ return Collections.emptyList();
+ }
+
+ /**
+ * Synthesize a classpath appropriate for a forked jetty based off
+ * the artifacts associated with the jetty plugin, plus any dependencies
+ * that are marked as provided and useProvidedScope is true.
+ *
+ * @return jetty classpath
+ * @throws Exception if there is an unspecified problem
+ */
+ protected String getContainerClassPath() throws Exception
+ {
+ //Add in all the plugin artifacts
+ StringBuilder classPath = new StringBuilder();
+ for (Object obj : pluginArtifacts)
+ {
+ Artifact artifact = (Artifact)obj;
+ if ("jar".equals(artifact.getType()))
+ {
+ if (classPath.length() > 0)
+ classPath.append(File.pathSeparator);
+ classPath.append(artifact.getFile().getAbsolutePath());
+ }
+ else
+ {
+ if (artifact.getArtifactId().equals(plugin.getArtifactId())) //get the jetty-maven-plugin jar
+ classPath.append(artifact.getFile().getAbsolutePath());
+ }
+ }
+
+ //Any jars that we need from the project's dependencies because we're useProvided
+ if (providedJars != null && !providedJars.isEmpty())
+ {
+ for (File jar:providedJars)
+ {
+ classPath.append(File.pathSeparator);
+ classPath.append(jar.getAbsolutePath());
+ if (getLog().isDebugEnabled()) getLog().debug("Adding provided jar: " + jar);
+ }
+ }
+
+ return classPath.toString();
+ }
+
+ /**
+ * Check to see if the given artifact is one of the dependency artifacts for this plugin.
+ *
+ * @param artifact to check
+ * @return true if it is a plugin dependency, false otherwise
+ */
+ protected boolean isPluginArtifact(Artifact artifact)
+ {
+ if (pluginArtifacts == null)
+ return false;
+
+ return pluginArtifacts.stream().anyMatch(pa -> pa.getGroupId().equals(artifact.getGroupId()) && pa.getArtifactId().equals(artifact.getArtifactId()));
+ }
+
+ /**
+ * Check if the goal that we're executing as is excluded or not.
+ *
+ * @param goal the goal to check
+ * @return true if the goal is excluded, false otherwise
+ */
+ protected boolean isExcludedGoal(String goal)
+ {
+ if (excludedGoals == null || goal == null)
+ return false;
+
+ goal = goal.trim();
+ if ("".equals(goal))
+ return false;
+
+ boolean excluded = false;
+ for (int i = 0; i < excludedGoals.length && !excluded; i++)
+ {
+ if (excludedGoals[i].equalsIgnoreCase(goal))
+ excluded = true;
+ }
+
+ return excluded;
+ }
+
+ protected boolean isPackagingSupported()
+ {
+ if (!supportedPackagings.contains(project.getPackaging()))
+ return false;
+ return true;
+ }
+
+ protected String getProjectName()
+ {
+ String projectName = project.getName();
+ if (StringUtils.isBlank(projectName))
+ {
+ projectName = project.getGroupId() + ":" + project.getArtifactId();
+ }
+ return projectName;
+ }
+
+ /**
+ * Ensure there is a webapp, and that some basic defaults are applied
+ * if the user has not supplied them.
+ *
+ * @throws AbstractMojoExecutionException if there is an abstract mojo execution problem
+ */
+ protected void configureWebApp()
+ throws Exception
+ {
+ if (webApp == null)
+ webApp = new MavenWebAppContext();
+
+ //If no contextPath was specified, go with default of project artifactid
+ String cp = webApp.getContextPath();
+ if (cp == null || "".equals(cp))
+ {
+ cp = "/" + project.getArtifactId();
+ webApp.setContextPath(cp);
+ }
+
+ //If no tmp directory was specified, and we have one, use it
+ if (webApp.getTempDirectory() == null)
+ {
+ File target = new File(project.getBuild().getDirectory());
+ File tmp = new File(target, "tmp");
+ if (!tmp.exists())
+ {
+ if (!tmp.mkdirs())
+ {
+ throw new MojoFailureException("Unable to create temp directory: " + tmp);
+ }
+ }
+
+ webApp.setTempDirectory(tmp);
+ }
+
+ getLog().info("Context path = " + webApp.getContextPath());
+ getLog().info("Tmp directory = " + (webApp.getTempDirectory() == null ? " determined at runtime" : webApp.getTempDirectory()));
+ }
+
+ /**
+ * Try and find a jetty-web.xml file, using some
+ * historical naming conventions if necessary.
+ *
+ * @param webInfDir the web inf directory
+ * @return the jetty web xml file
+ */
+ protected File findJettyWebXmlFile(File webInfDir)
+ {
+ if (webInfDir == null)
+ return null;
+ if (!webInfDir.exists())
+ return null;
+
+ File f = new File(webInfDir, "jetty-web.xml");
+ if (f.exists())
+ return f;
+
+ //try some historical alternatives
+ f = new File(webInfDir, "web-jetty.xml");
+ if (f.exists())
+ return f;
+
+ return null;
+ }
+
+ /**
+ * Get a file into which to write output from jetty.
+ *
+ * @param name the name of the file
+ * @return the created file
+ * @throws Exception if there is an unspecified problem
+ */
+ protected File getJettyOutputFile(String name) throws Exception
+ {
+ File outputFile = new File(target, name);
+ if (outputFile.exists())
+ outputFile.delete();
+ outputFile.createNewFile();
+ return outputFile;
+ }
+
+ /**
+ * Configure any extra files, directories or patterns thereof for the
+ * scanner to watch for changes.
+ *
+ * @param scanner Scanner that notices changes in files and dirs.
+ * @throws IOException
+ */
+ protected void configureScanTargetPatterns(Scanner scanner) throws IOException
+ {
+ //handle the extra scan patterns
+ if (scanTargetPatterns != null)
+ {
+ for (ScanTargetPattern p : scanTargetPatterns)
+ {
+ IncludeExcludeSet includesExcludes = scanner.addDirectory(p.getDirectory().toPath());
+ p.configureIncludesExcludeSet(includesExcludes);
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyEffectiveWebXml.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyEffectiveWebXml.java
new file mode 100644
index 00000000000..3e2ff76aa1b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyEffectiveWebXml.java
@@ -0,0 +1,107 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+/**
+ * Generate the effective web.xml for a pre-built webapp. This goal will NOT
+ * first build the webapp, it must already exist.
+ *
+ */
+@Mojo(name = "effective-web-xml", requiresDependencyResolution = ResolutionScope.RUNTIME)
+public class JettyEffectiveWebXml extends AbstractUnassembledWebAppMojo
+{
+ /**
+ * The name of the file to generate into
+ */
+ @Parameter (defaultValue = "${project.build.directory}/effective-web.xml")
+ protected File effectiveWebXml;
+
+ @Override
+ public void configureWebApp() throws Exception
+ {
+ //Try to determine if we're using an unassembled webapp, or an
+ //external||prebuilt webapp
+ String war = webApp.getWar();
+ Path path = null;
+ if (war != null)
+ {
+ try
+ {
+ URL url = new URL(war);
+ path = Paths.get(url.toURI());
+ }
+ catch (MalformedURLException e)
+ {
+ path = Paths.get(war);
+ }
+ }
+
+ Path start = path.getName(0);
+ int count = path.getNameCount();
+ Path end = path.getName(count > 0 ? count - 1 : count);
+ //if the war is not assembled, we must configure it
+ if (start.startsWith("src") || !end.toString().endsWith(".war"))
+ super.configureUnassembledWebApp();
+ }
+
+ /**
+ * Override so we can call the parent's method in a different order.
+ */
+ @Override
+ protected void configureUnassembledWebApp()
+ {
+ }
+
+ @Override
+ protected void startJettyEmbedded() throws MojoExecutionException
+ {
+ generate();
+ }
+
+ @Override
+ protected void startJettyForked() throws MojoExecutionException
+ {
+ generate();
+ }
+
+ @Override
+ protected void startJettyHome() throws MojoExecutionException
+ {
+ generate();
+ }
+
+ private void generate() throws MojoExecutionException
+ {
+ try
+ {
+ QuickStartGenerator generator = new QuickStartGenerator(effectiveWebXml.toPath(), webApp);
+ generator.generate();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error generating effective web xml", e);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyEmbedder.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyEmbedder.java
new file mode 100644
index 00000000000..28c90ed646d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyEmbedder.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration.Mode;
+import org.eclipse.jetty.ee11.servlet.ServletHandler;
+import org.eclipse.jetty.ee11.webapp.Configurations;
+import org.eclipse.jetty.maven.AbstractJettyEmbedder;
+import org.eclipse.jetty.maven.ServerSupport;
+import org.eclipse.jetty.server.handler.ContextHandler;
+
+/**
+ * JettyEmbedder
+ *
+ * Starts jetty within the current process.
+ */
+public class JettyEmbedder extends AbstractJettyEmbedder
+{
+ protected MavenWebAppContext webApp;
+
+ public List getContextHandlers()
+ {
+ return contextHandlers;
+ }
+
+ public void setWebApp(MavenWebAppContext app)
+ {
+ webApp = app;
+ }
+
+ protected void redeployWebApp() throws Exception
+ {
+ stopWebApp();
+
+ //clear the ServletHandler, which may have
+ //remembered "durable" Servlets, Filters, Listeners
+ //from the context xml file, but as we will re-apply
+ //the context xml, we should not retain them
+ webApp.setServletHandler(new ServletHandler());
+
+ //regenerate config properties
+ applyWebAppProperties();
+
+ webApp.start();
+ }
+
+ @Override
+ public void stopWebApp() throws Exception
+ {
+ if (webApp != null && !webApp.isStopped())
+ webApp.stop();
+ }
+
+ /**
+ * Configure the webapp
+ * @throws Exception if there is an unspecified problem
+ */
+ public void configureWebApp() throws Exception
+ {
+ //Set up list of default Configurations to apply to a webapp
+ Configurations.setServerDefault(server);
+
+ /* Configure the webapp */
+ if (webApp == null)
+ webApp = new MavenWebAppContext();
+
+ applyWebAppProperties();
+
+ //If there is a quickstart file, then quickstart the webapp.
+ if (webApp.getTempDirectory() != null)
+ {
+ Path qs = webApp.getTempDirectory().toPath().resolve("quickstart-web.xml");
+ if (Files.exists(qs) && Files.isRegularFile(qs))
+ {
+ webApp.addConfiguration(new MavenQuickStartConfiguration());
+ webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, qs);
+ webApp.setAttribute(QuickStartConfiguration.MODE, Mode.QUICKSTART);
+ }
+ }
+ }
+
+ public void applyWebAppProperties() throws Exception
+ {
+ super.applyWebAppProperties();
+ WebAppPropertyConverter.fromProperties(webApp, webAppProperties, server, jettyProperties);
+ }
+
+ public void addWebAppToServer() throws Exception
+ {
+ //add the webapp to the server
+ ServerSupport.addWebApplication(server, webApp);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyForkedChild.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyForkedChild.java
new file mode 100644
index 00000000000..93cd6a128c5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyForkedChild.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import org.eclipse.jetty.maven.AbstractForkedChild;
+import org.eclipse.jetty.maven.AbstractJettyEmbedder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JettyForkedChild
+ *
+ * This is the class that is executed when the jetty maven plugin
+ * forks a process when DeploymentMode=FORKED.
+ */
+public class JettyForkedChild extends AbstractForkedChild
+{
+ private static final Logger LOG = LoggerFactory.getLogger(JettyForkedChild.class);
+
+ /**
+ * @param args arguments that were passed to main
+ * @throws Exception if unable to configure
+ */
+ public JettyForkedChild(String[] args) throws Exception
+ {
+ super(args);
+ }
+
+ @Override
+ protected AbstractJettyEmbedder newJettyEmbedder()
+ {
+ return new JettyEmbedder();
+ }
+
+ public static void main(String[] args)
+ throws Exception
+ {
+ if (args == null)
+ System.exit(1);
+
+ JettyForkedChild child = new JettyForkedChild(args);
+ child.start();
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyForker.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyForker.java
new file mode 100644
index 00000000000..72434aa0c66
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyForker.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import org.eclipse.jetty.maven.AbstractServerForker;
+
+/**
+ * JettyForker
+ *
+ * Uses quickstart to generate a webapp and forks a process to run it.
+ */
+public class JettyForker extends AbstractServerForker
+{
+ protected MavenWebAppContext webApp;
+ QuickStartGenerator generator;
+
+ public JettyForker()
+ {
+ executionClassName = JettyForkedChild.class.getCanonicalName();
+ }
+
+ public void setWebApp(MavenWebAppContext app)
+ {
+ webApp = app;
+ }
+
+ @Override
+ public void generateWebApp() throws Exception
+ {
+ //Run the webapp to create the quickstart file and properties file
+ generator = new QuickStartGenerator(forkWebXml.toPath(), webApp);
+ generator.setContextXml(contextXml);
+ generator.setWebAppProps(webAppPropsFile.toPath());
+ generator.setServer(server);
+ generator.generate();
+ }
+
+ protected void redeployWebApp()
+ throws Exception
+ {
+ //regenerating the quickstart will be noticed by the JettyForkedChild process
+ //which will redeploy the webapp
+ generator.generate();
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyHomeForker.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyHomeForker.java
new file mode 100644
index 00000000000..0fe9c2c0419
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyHomeForker.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+
+import org.eclipse.jetty.maven.AbstractHomeForker;
+
+/**
+ * JettyHomeBaseForker
+ *
+ * Unpacks a jetty-home and configures it with a base that allows it
+ * to run an unassembled webapp.
+ */
+public class JettyHomeForker extends AbstractHomeForker
+{
+ protected MavenWebAppContext webApp;
+
+ public JettyHomeForker()
+ {
+ environment = "ee11";
+ }
+
+ public void setWebApp(MavenWebAppContext webApp)
+ {
+ this.webApp = webApp;
+ }
+
+ public File getBaseDir()
+ {
+ return baseDir;
+ }
+
+ public void setBaseDir(File baseDir)
+ {
+ this.baseDir = baseDir;
+ }
+
+ protected void redeployWebApp()
+ throws Exception
+ {
+ generateWebAppPropertiesFile();
+ webappPath.resolve("maven.xml").toFile().setLastModified(System.currentTimeMillis());
+ }
+
+ protected void generateWebAppPropertiesFile()
+ throws Exception
+ {
+ WebAppPropertyConverter.toProperties(webApp, etcPath.resolve("maven.props").toFile(), contextXml);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyRunMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyRunMojo.java
new file mode 100644
index 00000000000..261b635844a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyRunMojo.java
@@ -0,0 +1,423 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.Date;
+import java.util.Set;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Execute;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.maven.ConsoleReader;
+import org.eclipse.jetty.util.IncludeExcludeSet;
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+/**
+ * This goal is used in-situ on a Maven project without first requiring that the project
+ * is assembled into a war, saving time during the development cycle.
+ *
+ * The plugin runs a parallel lifecycle to ensure that the "test-compile" phase has been completed before invoking Jetty. This means
+ * that you do not need to explicity execute a "mvn compile" first. It also means that a "mvn clean jetty:run" will ensure that
+ * a full fresh compile is done before invoking Jetty.
+ *
+ * Once invoked, the plugin can be configured to run continuously, scanning for changes in the project and automatically performing a
+ * hot redeploy when necessary. This allows the developer to concentrate on coding changes to the project using their IDE of choice and have those changes
+ * immediately and transparently reflected in the running web container, eliminating development time that is wasted on rebuilding, reassembling and redeploying.
+ * Alternatively, you can configure the plugin to wait for an <enter> at the command line to manually control redeployment.
+ *
+ * You can configure this goal to run your unassembled webapp either in-process with maven, or forked into a new process, or deployed into a
+ * jetty distribution.
+ */
+@Mojo (name = "run", requiresDependencyResolution = ResolutionScope.TEST)
+@Execute (phase = LifecyclePhase.TEST_COMPILE)
+public class JettyRunMojo extends AbstractUnassembledWebAppMojo
+{
+ //Start of parameters only valid for deploymentType=EMBED
+ /**
+ * Controls redeployment of the webapp.
+ *
+ *
-1 : means no redeployment will be done
+ *
0 : means redeployment only occurs if you hit the ENTER key
+ *
otherwise, the interval in seconds to pause before checking and redeploying if necessary
+ *
+ */
+ @Parameter(defaultValue = "-1", property = "jetty.scan", required = true)
+ protected int scan;
+
+ /**
+ * Scanner to check for files changes to cause redeploy
+ */
+ protected Scanner scanner;
+
+ /**
+ * Only one of the following will be used, depending the mode
+ * the mojo is started in: EMBED, FORK, EXTERNAL
+ */
+ protected JettyEmbedder embedder;
+ protected JettyForker forker;
+ protected JettyHomeForker homeForker;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException
+ {
+ super.execute();
+ }
+
+ @Override
+ public void startJettyEmbedded() throws MojoExecutionException
+ {
+ try
+ {
+ //start jetty
+ embedder = newJettyEmbedder();
+ embedder.setExitVm(true);
+ embedder.setStopAtShutdown(true);
+ embedder.start();
+ startScanner();
+ embedder.join();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ @Override
+ public void startJettyForked() throws MojoExecutionException
+ {
+ try
+ {
+ forker = newJettyForker();
+ forker.setWaitForChild(true); //we run at the command line, echo child output and wait for it
+ //if we can do hot redeploy, tell the forked process to watch for changes to the generated webapp file
+ if (scan >= 0)
+ {
+ forker.setScan(true);
+ forker.setScanInterval((scan == 0 ? 1 : scan)); //if redeploying on ENTER key, the forked process still needs to watch the generated webapp file
+ }
+ //TODO is it ok to start the scanner before we start jetty?
+ startScanner();
+ forker.start(); //forks jetty instance
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ @Override
+ public void startJettyHome() throws MojoExecutionException
+ {
+ try
+ {
+ homeForker = newJettyHomeForker();
+ homeForker.setWaitForChild(true); //we always run at the command line, echo child output and wait for it
+ //TODO is it ok to start the scanner before we start jetty?
+ startScanner();
+ homeForker.start(); //forks a jetty distro
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ private void startScanner()
+ throws Exception
+ {
+ if (scan < 0)
+ {
+ getLog().info("Automatic redeployment disabled, see 'mvn jetty:help' for more redeployment options");
+ return; //no automatic or manual redeployment
+ }
+
+ // start scanning for changes, or wait for linefeed on stdin
+ if (scan > 0)
+ {
+ scanner = new Scanner();
+ scanner.setScanInterval(scan);
+ scanner.setScanDepth(Scanner.MAX_SCAN_DEPTH); //always fully walk directory hierarchies
+ scanner.setReportExistingFilesOnStartup(false);
+ scanner.addListener(new Scanner.BulkListener()
+ {
+ public void filesChanged(Set changes)
+ {
+ try
+ {
+ restartWebApp(changes.contains(project.getFile().getCanonicalPath()));
+ }
+ catch (Exception e)
+ {
+ getLog().error("Error reconfiguring/restarting webapp after change in watched files", e);
+ }
+ }
+ });
+ configureScanner();
+ getLog().info("Scan interval sec = " + scan);
+
+ //unmanage scheduler so it is not stopped with the scanner
+ Scheduler scheduler = scanner.getBean(Scheduler.class);
+ scanner.unmanage(scheduler);
+ LifeCycle.start(scheduler);
+ scanner.start();
+ }
+ else
+ {
+ ConsoleReader creader = new ConsoleReader();
+ creader.addListener(new ConsoleReader.Listener()
+ {
+ @Override
+ public void consoleEvent(String line)
+ {
+ try
+ {
+ restartWebApp(false);
+ }
+ catch (Exception e)
+ {
+ getLog().debug(e);
+ }
+ }
+ });
+ Thread cthread = new Thread(creader, "ConsoleReader");
+ cthread.setDaemon(true);
+ cthread.start();
+ }
+ }
+
+ protected void configureScanner()
+ throws MojoExecutionException
+ {
+ try
+ {
+ gatherScannables();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error forming scan list", e);
+ }
+ }
+
+ public void gatherScannables() throws Exception
+ {
+ if (webApp.getDescriptor() != null)
+ {
+ Resource r = webApp.getResourceFactory().newResource(webApp.getDescriptor());
+ scanner.addFile(r.getPath());
+ }
+
+ if (webApp.getJettyEnvXml() != null)
+ scanner.addFile(new File(webApp.getJettyEnvXml()).toPath());
+
+ if (webApp.getDefaultsDescriptor() != null)
+ {
+ if (!WebAppContext.WEB_DEFAULTS_XML.equals(webApp.getDefaultsDescriptor()))
+ scanner.addFile(new File(webApp.getDefaultsDescriptor()).toPath());
+ }
+
+ if (webApp.getOverrideDescriptor() != null)
+ {
+ scanner.addFile(new File(webApp.getOverrideDescriptor()).toPath());
+ }
+
+ File jettyWebXmlFile = findJettyWebXmlFile(new File(webAppSourceDirectory, "WEB-INF"));
+ if (jettyWebXmlFile != null)
+ {
+ scanner.addFile(jettyWebXmlFile.toPath());
+ }
+
+ //make sure each of the war artifacts is added to the scanner
+ for (Artifact a:mavenProjectHelper.getWarPluginInfo().getWarArtifacts())
+ {
+ File f = a.getFile();
+ if (a.getFile().isDirectory())
+ scanner.addDirectory(f.toPath());
+ else
+ scanner.addFile(f.toPath());
+ }
+
+ //set up any extra files or dirs to watch
+ configureScanTargetPatterns(scanner);
+
+ scanner.addFile(project.getFile().toPath());
+
+ if (webApp.getTestClasses() != null && webApp.getTestClasses().exists())
+ {
+ Path p = webApp.getTestClasses().toPath();
+ IncludeExcludeSet includeExcludeSet = scanner.addDirectory(p);
+ if (scanTestClassesPattern != null)
+ {
+ for (String s : scanTestClassesPattern.getExcludes())
+ {
+ if (!s.startsWith("glob:"))
+ s = "glob:" + s;
+ includeExcludeSet.exclude(p.getFileSystem().getPathMatcher(s));
+ }
+ for (String s : scanTestClassesPattern.getIncludes())
+ {
+ if (!s.startsWith("glob:"))
+ s = "glob:" + s;
+ includeExcludeSet.include(p.getFileSystem().getPathMatcher(s));
+ }
+ }
+ }
+
+ if (webApp.getClasses() != null && webApp.getClasses().exists())
+ {
+ Path p = webApp.getClasses().toPath();
+ IncludeExcludeSet includeExcludes = scanner.addDirectory(p);
+ if (scanClassesPattern != null)
+ {
+ for (String s : scanClassesPattern.getExcludes())
+ {
+ if (!s.startsWith("glob:"))
+ s = "glob:" + s;
+ includeExcludes.exclude(p.getFileSystem().getPathMatcher(s));
+ }
+
+ for (String s : scanClassesPattern.getIncludes())
+ {
+ if (!s.startsWith("glob:"))
+ s = "glob:" + s;
+ includeExcludes.include(p.getFileSystem().getPathMatcher(s));
+ }
+ }
+ }
+
+ if (webApp.getWebInfLib() != null)
+ {
+ for (File f : webApp.getWebInfLib())
+ {
+ if (f.isDirectory())
+ scanner.addDirectory(f.toPath());
+ else
+ scanner.addFile(f.toPath());
+ }
+ }
+ }
+
+ /**
+ * Stop an executing webapp and restart it after optionally
+ * reconfiguring it.
+ *
+ * @param reconfigure if true, the scanner will
+ * be reconfigured after changes to the pom. If false, only
+ * the webapp will be reconfigured.
+ *
+ * @throws Exception if there is an unspecified problem
+ */
+ public void restartWebApp(boolean reconfigure) throws Exception
+ {
+ getLog().info("Restarting " + webApp);
+ getLog().debug("Stopping webapp ...");
+ if (scanner != null)
+ scanner.stop();
+
+ switch (deployMode)
+ {
+ case EMBED:
+ {
+ getLog().debug("Reconfiguring webapp ...");
+
+ verifyPomConfiguration();
+ // check if we need to reconfigure the scanner,
+ // which is if the pom changes
+ if (reconfigure)
+ {
+ getLog().info("Reconfiguring scanner after change to pom.xml ...");
+ warArtifacts = null; //will be regenerated by configureWebApp
+ if (scanner != null)
+ {
+ scanner.reset();
+ configureScanner();
+ }
+ }
+
+ embedder.stopWebApp();
+ configureWebApp();
+ embedder.redeployWebApp();
+ if (scanner != null)
+ scanner.start();
+ getLog().info("Restart completed at " + new Date().toString());
+
+ break;
+ }
+ case FORK:
+ {
+ verifyPomConfiguration();
+ if (reconfigure)
+ {
+ getLog().info("Reconfiguring scanner after change to pom.xml ...");
+ warArtifacts = null; ///TODO if the pom changes for the forked case, how would we get the forked process to stop and restart?
+ if (scanner != null)
+ {
+ scanner.reset();
+ configureScanner();
+ }
+ }
+ configureWebApp();
+ //regenerate with new config and restart the webapp
+ forker.redeployWebApp();
+ //restart scanner
+ if (scanner != null)
+ scanner.start();
+ break;
+ }
+ case DISTRO:
+ case HOME:
+ case EXTERNAL:
+ {
+ if (deployMode != DeploymentMode.EXTERNAL)
+ getLog().warn(deployMode + " mode is deprecated, use mode EXTERNAL");
+ verifyPomConfiguration();
+ if (reconfigure)
+ {
+ getLog().info("Reconfiguring scanner after change to pom.xml ...");
+
+ warArtifacts = null; //TODO if there are any changes to the pom, then we would have to tell the
+ //existing forked home process to stop, then rerun the configuration and then refork - too complicated??!
+ if (scanner != null)
+ {
+ scanner.reset();
+ configureScanner();
+ }
+ }
+ configureWebApp();
+ //regenerate the webapp and redeploy it
+ homeForker.redeployWebApp();
+ //restart scanner
+ if (scanner != null)
+ scanner.start();
+
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException("Unrecognized run type " + deployMode);
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyRunWarMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyRunWarMojo.java
new file mode 100644
index 00000000000..7609f7c81fb
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyRunWarMojo.java
@@ -0,0 +1,295 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Date;
+import java.util.Set;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Execute;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.eclipse.jetty.maven.ConsoleReader;
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+*
+* This goal is used to assemble your webapp into a war and automatically deploy it to Jetty.
+*
+*
+* Once invoked, the plugin runs continuously and can be configured to scan for changes in the project and to the
+* war file and automatically perform a hot redeploy when necessary.
+*
+*
+* You may also specify the location of a jetty.xml file whose contents will be applied before any plugin configuration.
+*
+*
+* You can configure this goal to run your webapp either in-process with maven, or forked into a new process, or deployed into a
+* jetty distribution.
+*
+*/
+@Mojo(name = "run-war", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
+@Execute(phase = LifecyclePhase.PACKAGE)
+public class JettyRunWarMojo extends AbstractWebAppMojo
+{
+ /**
+ * The interval in seconds to pause before checking if changes
+ * have occurred and re-deploying as necessary. A value
+ * of 0 indicates no re-deployment will be done. In that case, you
+ * can force redeployment by typing a linefeed character at the command line.
+ */
+ @Parameter(defaultValue = "0", property = "jetty.scan", required = true)
+ protected int scan;
+
+ /**
+ * Scanner to check for files changes to cause redeploy
+ */
+ protected Scanner scanner;
+ protected JettyEmbedder embedder;
+ protected JettyForker forker;
+ protected JettyHomeForker homeForker;
+ protected Path war;
+
+ @Override
+ public void configureWebApp() throws Exception
+ {
+ super.configureWebApp();
+ //if no war has been explicitly configured, use the one from the webapp project
+ if (StringUtil.isBlank(webApp.getWar()))
+ {
+ war = target.toPath().resolve(project.getBuild().getFinalName() + ".war");
+ webApp.setWar(war.toFile().getAbsolutePath());
+ }
+ else
+ war = Paths.get(webApp.getWar());
+
+ getLog().info("War = " + war);
+ }
+
+ /**
+ * Start a jetty instance in process to run the built war.
+ */
+ @Override
+ public void startJettyEmbedded() throws MojoExecutionException
+ {
+ try
+ {
+ embedder = newJettyEmbedder();
+ embedder.setExitVm(true);
+ embedder.setStopAtShutdown(true);
+ embedder.start();
+ startScanner();
+ embedder.join();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+
+ /**
+ * Fork a jetty instance to run the built war.
+ */
+ @Override
+ public void startJettyForked() throws MojoExecutionException
+ {
+ try
+ {
+ forker = newJettyForker();
+ forker.setWaitForChild(true); //we run at the command line, echo child output and wait for it
+ startScanner();
+ forker.start(); //forks jetty instance
+
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ /**
+ * Deploy the built war to a jetty distro.
+ */
+ @Override
+ public void startJettyHome() throws MojoExecutionException
+ {
+ try
+ {
+ homeForker = newJettyHomeForker();
+ homeForker.setWaitForChild(true); //we always run at the command line, echo child output and wait for it
+ startScanner();
+ homeForker.start(); //forks a jetty distro
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ public void startScanner()
+ throws Exception
+ {
+ // start scanning for changes, or wait for linefeed on stdin
+ if (scan > 0)
+ {
+ scanner = new Scanner();
+ scanner.setScanInterval(scan);
+ scanner.setScanDepth(Scanner.MAX_SCAN_DEPTH); //always fully walk directory hierarchies
+ scanner.setReportExistingFilesOnStartup(false);
+ configureScanner();
+ getLog().info("Scan interval ms = " + scan);
+ scanner.start();
+ }
+ else
+ {
+ ConsoleReader creader = new ConsoleReader();
+ creader.addListener(new ConsoleReader.Listener()
+ {
+ @Override
+ public void consoleEvent(String line)
+ {
+ try
+ {
+ restartWebApp(false);
+ }
+ catch (Exception e)
+ {
+ getLog().debug(e);
+ }
+ }
+ });
+ Thread cthread = new Thread(creader, "ConsoleReader");
+ cthread.setDaemon(true);
+ cthread.start();
+ }
+ }
+
+ public void configureScanner() throws MojoExecutionException
+ {
+ try
+ {
+ scanner.addFile(project.getFile().toPath());
+ scanner.addFile(war);
+
+ //set up any extra files or dirs to watch
+ configureScanTargetPatterns(scanner);
+ scanner.addListener(new Scanner.BulkListener()
+ {
+ public void filesChanged(Set changes)
+ {
+ try
+ {
+ boolean reconfigure = changes.contains(project.getFile().getCanonicalPath());
+ restartWebApp(reconfigure);
+ }
+ catch (Exception e)
+ {
+ getLog().error("Error reconfiguring/restarting webapp after change in watched files", e);
+ }
+ }
+ });
+ }
+ catch (IOException e)
+ {
+ throw new MojoExecutionException("Error configuring scanner", e);
+ }
+ }
+
+ public void restartWebApp(boolean reconfigure) throws Exception
+ {
+ getLog().info("Restarting webapp ...");
+ getLog().debug("Stopping scanner ...");
+ if (scanner != null)
+ scanner.stop();
+
+ switch (deployMode)
+ {
+ case EMBED:
+ {
+ getLog().debug("Reconfiguring webapp ...");
+
+ verifyPomConfiguration();
+ // check if we need to reconfigure the scanner,
+ // which is if the pom changes
+ if (reconfigure)
+ {
+ getLog().info("Reconfiguring scanner after change to pom.xml ...");
+ scanner.reset();
+ warArtifacts = null;
+ configureScanner();
+ }
+ embedder.stopWebApp();
+ configureWebApp();
+ embedder.redeployWebApp();
+ scanner.start();
+ getLog().info("Restart completed at " + new Date().toString());
+
+ break;
+ }
+ case FORK:
+ {
+ verifyPomConfiguration();
+ if (reconfigure)
+ {
+ getLog().info("Reconfiguring scanner after change to pom.xml ...");
+ scanner.reset();
+ warArtifacts = null;
+ configureScanner();
+ }
+
+ configureWebApp();
+ //regenerate with new config and restart the webapp
+ forker.redeployWebApp();
+ //restart scanner
+ scanner.start();
+
+ break;
+ }
+ case HOME:
+ case DISTRO:
+ case EXTERNAL:
+ {
+ if (deployMode != DeploymentMode.EXTERNAL)
+ getLog().warn(deployMode + " mode is deprecated, use mode EXTERNAL");
+ verifyPomConfiguration();
+ if (reconfigure)
+ {
+ getLog().info("Reconfiguring scanner after change to pom.xml ...");
+ scanner.reset();
+ warArtifacts = null;
+ configureScanner();
+ }
+ configureWebApp();
+ //regenerate the webapp and redeploy it
+ homeForker.redeployWebApp();
+ //restart scanner
+ scanner.start();
+
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException("Unrecognized run type " + deployMode);
+ }
+ }
+ getLog().info("Restart completed.");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStartMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStartMojo.java
new file mode 100644
index 00000000000..69b09721fc1
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStartMojo.java
@@ -0,0 +1,102 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+/**
+ *
+ * This goal is similar to the jetty:run goal in that it it starts jetty on an unassembled webapp,
+ * EXCEPT that it is designed to be bound to an execution inside your pom. Thus, this goal does NOT
+ * run a parallel build cycle, so you must be careful to ensure that you bind it to a phase in
+ * which all necessary generated files and classes for the webapp have been created.
+ *
+ *
+ * This goal will NOT scan for changes in either the webapp project or any scanTargets or scanTargetPatterns.
+ *
+ *
+ * You can configure this goal to run your webapp either in-process with maven, or forked into a new process, or deployed into a
+ * jetty distribution.
+ *
+ */
+@Mojo(name = "start", requiresDependencyResolution = ResolutionScope.TEST)
+public class JettyStartMojo extends AbstractUnassembledWebAppMojo
+{
+
+ /**
+ * Starts the webapp - without first compiling the classes -
+ * in the same process as maven.
+ */
+ @Override
+ public void startJettyEmbedded() throws MojoExecutionException
+ {
+ try
+ {
+ JettyEmbedder jetty = newJettyEmbedder();
+ jetty.setExitVm(false);
+ jetty.setStopAtShutdown(false);
+ jetty.start();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ /**
+ * Start the webapp in a forked jetty process. Use the
+ * jetty:stop goal to terminate.
+ */
+ @Override
+ public void startJettyForked() throws MojoExecutionException
+ {
+ try
+ {
+ JettyForker jetty = newJettyForker();
+ jetty.setWaitForChild(false); //we never wait for child to finish
+ jetty.setMaxChildStartChecks(maxChildStartChecks);
+ jetty.setMaxChildStartCheckMs(maxChildStartCheckMs);
+ jetty.setJettyOutputFile(getJettyOutputFile("jetty-start.out"));
+ jetty.start(); //forks jetty instance
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ /**
+ * Start the webapp in a forked jetty distribution. Use the
+ * jetty:stop goal to terminate
+ */
+ @Override
+ public void startJettyHome() throws MojoExecutionException
+ {
+ try
+ {
+ JettyHomeForker jetty = newJettyHomeForker();
+ jetty.setWaitForChild(false); //never wait for child to finish
+ jetty.setMaxChildStartChecks(maxChildStartChecks);
+ jetty.setMaxChildStartCheckMs(maxChildStartCheckMs);
+ jetty.setJettyOutputFile(getJettyOutputFile("jetty-start.out"));
+ jetty.start(); //forks a jetty home
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStartWarMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStartWarMojo.java
new file mode 100644
index 00000000000..0b042925950
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStartWarMojo.java
@@ -0,0 +1,144 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.eclipse.jetty.util.StringUtil;
+
+/**
+ *
+ * This goal is used to run Jetty with any pre-assembled war. This goal does not have
+ * to be used with a project of packaging type "war".
+ *
+ *
+ * You can configure the "webApp" element with the location of either a war file or
+ * an unpacked war that you wish to deploy - in either case, the webapp must be
+ * fully compiled and assembled as this goal does not do anything other than start
+ * jetty with the given webapp. If you do not configure the "webApp" element, then
+ * the goal will default to using the war of the webapp project.
+ *
+ *
+ * This goal is designed to be bound to a build phase, and NOT to be run at the
+ * command line. It will not block waiting for jetty to execute, but rather continue
+ * execution.
+ *
+ *
+ * This goal is useful e.g. for launching a web app in Jetty as a target for unit-tested
+ * HTTP client components via binding to the test-integration build phase.
+ *
+ *
+ * You can configure this goal to run the webapp either in-process with maven, or
+ * forked into a new process, or deployed into a {@code ${jetty.base}} directory.
+ *
+ */
+@Mojo(name = "start-war", requiresDependencyResolution = ResolutionScope.RUNTIME)
+public class JettyStartWarMojo extends AbstractWebAppMojo
+{
+ @Parameter (defaultValue = "${project.baseDir}/src/main/webapp")
+ protected File webAppSourceDirectory;
+
+ protected JettyEmbedder embedder;
+ protected JettyForker forker;
+ protected JettyHomeForker homeForker;
+
+ @Override
+ public void configureWebApp() throws Exception
+ {
+ super.configureWebApp();
+ //if a war has not been explicitly configured, use the one from the project
+ if (StringUtil.isBlank(webApp.getWar()))
+ {
+ Path war = target.toPath().resolve(project.getBuild().getFinalName() + ".war");
+ webApp.setWar(war.toFile().getAbsolutePath());
+ }
+
+ getLog().info("War = " + webApp.getWar());
+ }
+
+ /**
+ * Start a jetty instance in process to run given war.
+ */
+ @Override
+ public void startJettyEmbedded() throws MojoExecutionException
+ {
+ try
+ {
+ embedder = newJettyEmbedder();
+ embedder.setExitVm(false);
+ embedder.setStopAtShutdown(false);
+ embedder.start();
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ /**
+ * Fork a jetty instance to run the given war.
+ */
+ @Override
+ public void startJettyForked() throws MojoExecutionException
+ {
+ try
+ {
+ forker = newJettyForker();
+ forker.setWaitForChild(false); //we never wait for child to finish
+ forker.setMaxChildStartChecks(maxChildStartChecks);
+ forker.setMaxChildStartCheckMs(maxChildStartCheckMs);
+ forker.setJettyOutputFile(getJettyOutputFile("jetty-start-war.out"));
+ forker.start(); //forks jetty instance
+
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ /**
+ * Fork a jetty distro to run the given war.
+ */
+ @Override
+ public void startJettyHome() throws MojoExecutionException
+ {
+ try
+ {
+ homeForker = newJettyHomeForker();
+ homeForker.setWaitForChild(false); //never wait for child tofinish
+ homeForker.setMaxChildStartCheckMs(maxChildStartCheckMs);
+ homeForker.setMaxChildStartChecks(maxChildStartChecks);
+ homeForker.setJettyOutputFile(getJettyOutputFile("jetty-start-war.out"));
+ homeForker.start(); //forks a jetty distro
+ }
+ catch (Exception e)
+ {
+ throw new MojoExecutionException("Error starting jetty", e);
+ }
+ }
+
+ @Override
+ protected void verifyPomConfiguration() throws MojoExecutionException
+ {
+ //Do nothing here, as we want the user to configure a war to deploy,
+ //or we default to the webapp that is running the jetty plugin, but
+ //we need to delay that decision until configureWebApp().
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStopMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStopMojo.java
new file mode 100644
index 00000000000..3be4615abfa
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/JettyStopMojo.java
@@ -0,0 +1,235 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * This goal stops a running instance of jetty.
+ *
+ * The stopPort and stopKey parameters can be used to
+ * configure which jetty to stop.
+ */
+@Mojo(name = "stop")
+public class JettyStopMojo extends AbstractWebAppMojo
+{
+ /**
+ * Max time in seconds that the plugin will wait for confirmation that jetty has stopped.
+ */
+ @Parameter
+ protected int stopWait;
+
+ @Override
+ protected void startJettyEmbedded() throws MojoExecutionException
+ {
+ //Does not start jetty
+ return;
+ }
+
+ @Override
+ protected void startJettyForked() throws MojoExecutionException
+ {
+ //Does not start jetty
+ return;
+ }
+
+ @Override
+ protected void startJettyHome() throws MojoExecutionException
+ {
+ //Does not start jetty
+ return;
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException
+ {
+ if (stopPort <= 0)
+ throw new MojoExecutionException("Please specify a valid port");
+ if (stopKey == null)
+ throw new MojoExecutionException("Please specify a valid stopKey");
+
+ String command = "forcestop";
+
+ if (stopWait > 0)
+ {
+ //try to get the pid of the forked jetty process
+ Long pid = null;
+ try
+ {
+ String response = send(stopKey + "\r\n" + "pid" + "\r\n", stopWait);
+ pid = Long.valueOf(response);
+ }
+ catch (NumberFormatException e)
+ {
+ if (getLog().isDebugEnabled())
+ {
+ getLog().debug(e);
+ }
+ getLog().info("Server returned bad pid");
+ }
+ catch (ConnectException e)
+ {
+ //jetty not running, no point continuing
+ getLog().info("Jetty not running!");
+ return;
+ }
+ catch (Exception e)
+ {
+ //jetty running, try to stop it regardless of error
+ getLog().error(e);
+ }
+
+ //now send the stop command and wait for confirmation - either an ack from jetty, or
+ //that the process has stopped
+ if (pid == null)
+ {
+ //no pid, so just wait until jetty reports itself stopped
+ try
+ {
+ getLog().info("Waiting " + stopWait + " seconds for jetty to stop");
+ String response = send(stopKey + "\r\n" + command + "\r\n", stopWait);
+
+ if ("Stopped".equals(response))
+ getLog().info("Server reports itself as stopped");
+ else
+ getLog().info("Couldn't verify server as stopped, received " + response);
+ }
+ catch (ConnectException e)
+ {
+ getLog().info("Jetty not running!");
+ }
+ catch (Exception e)
+ {
+ getLog().error(e);
+ }
+ }
+ else
+ {
+ //wait for pid to stop
+ getLog().info("Waiting " + stopWait + " seconds for jetty " + pid + " to stop");
+ Optional optional = ProcessHandle.of(pid);
+ final long remotePid = pid.longValue();
+ optional.ifPresentOrElse(p ->
+ {
+ try
+ {
+ //if running in the same process, just send the stop
+ //command and wait for the response
+ if (ProcessHandle.current().pid() == remotePid)
+ {
+ send(stopKey + "\r\n" + command + "\r\n", stopWait);
+ }
+ else
+ {
+ //running forked, so wait for the process
+ send(stopKey + "\r\n" + command + "\r\n", 0);
+ CompletableFuture future = p.onExit();
+ if (p.isAlive())
+ {
+ p = future.get(stopWait, TimeUnit.SECONDS);
+ }
+
+ if (p.isAlive())
+ getLog().info("Couldn't verify server process stop");
+ else
+ getLog().info("Server process stopped");
+ }
+ }
+ catch (ConnectException e)
+ {
+ //jetty not listening on the given port, don't wait for the process
+ getLog().info("Jetty not running!");
+ }
+ catch (TimeoutException e)
+ {
+ getLog().error("Timeout expired while waiting for server process to stop");
+ }
+ catch (Throwable e)
+ {
+ getLog().error(e);
+ }
+ }, () -> getLog().info("Process not running"));
+ }
+ }
+ else
+ {
+ //send the stop command but don't wait to verify the stop
+ getLog().info("Stopping jetty");
+ try
+ {
+ send(stopKey + "\r\n" + command + "\r\n", 0);
+ }
+ catch (ConnectException e)
+ {
+ getLog().info("Jetty not running!");
+ }
+ catch (Exception e)
+ {
+ getLog().error(e);
+ }
+ }
+ }
+
+ /**
+ * Send a command to a jetty process, optionally waiting for a response.
+ *
+ * @param command the command to send
+ * @param wait length of time in sec to wait for a response
+ * @return the response, if any, to the command
+ * @throws Exception if there is an unspecified problem
+ */
+ private String send(String command, int wait)
+ throws Exception
+ {
+ String response = null;
+ try (Socket s = new Socket(InetAddress.getByName("127.0.0.1"), stopPort); OutputStream out = s.getOutputStream();)
+ {
+ out.write(command.getBytes());
+ out.flush();
+
+ if (wait > 0)
+ {
+ //Wait for a response
+ s.setSoTimeout(wait * 1000);
+
+ try (LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream()));)
+ {
+ response = lin.readLine();
+ }
+ }
+ else
+ {
+ //Wait only a small amount of time to ensure TCP has sent the message
+ s.setSoTimeout(1000);
+ s.getInputStream().read();
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenMetaInfConfiguration.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenMetaInfConfiguration.java
new file mode 100644
index 00000000000..f6195fce9f5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenMetaInfConfiguration.java
@@ -0,0 +1,121 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.ee11.webapp.Configuration;
+import org.eclipse.jetty.ee11.webapp.MetaInfConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.util.FileID;
+import org.eclipse.jetty.util.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * MavenMetaInfConfiguration
+ *
+ * MetaInfConfiguration to take account of overlaid wars expressed as project dependencies and
+ * potential configured via the maven-war-plugin.
+ */
+public class MavenMetaInfConfiguration extends MetaInfConfiguration
+{
+ private static final Logger LOG = LoggerFactory.getLogger(MavenMetaInfConfiguration.class);
+
+ protected static int COUNTER = 0;
+
+ @Override
+ public Class extends Configuration> replaces()
+ {
+ return MetaInfConfiguration.class;
+ }
+
+ /**
+ * Get the jars to examine from the files from which we have
+ * synthesized the classpath. Note that the classpath is not
+ * set at this point, so we cannot get them from the classpath.
+ *
+ * @param context the web app context
+ * @return the list of jars found
+ */
+ @Override
+ protected List findJars(WebAppContext context)
+ throws Exception
+ {
+ List list = new ArrayList<>();
+ MavenWebAppContext jwac = (MavenWebAppContext)context;
+ List files = jwac.getWebInfLib();
+ if (files != null)
+ {
+ files.forEach(file ->
+ {
+ if (FileID.isJavaArchive(file.getName()) || file.isDirectory())
+ {
+ try
+ {
+ LOG.debug(" add resource to resources to examine {}", file);
+ list.add(context.getResourceFactory().newResource(file.toURI()));
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Bad url ", e);
+ }
+ }
+ });
+ }
+
+ List superList = super.findJars(context);
+ if (superList != null)
+ list.addAll(superList);
+ return list;
+ }
+
+ /**
+ * Add in the classes dirs from test/classes and target/classes
+ */
+ @Override
+ protected List findClassDirs(WebAppContext context) throws Exception
+ {
+ List list = new ArrayList<>();
+
+ MavenWebAppContext jwac = (MavenWebAppContext)context;
+ List files = jwac.getWebInfClasses();
+ if (files != null)
+ {
+ files.forEach(file ->
+ {
+ if (file.exists() && file.isDirectory())
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Add file {}", file.toURI());
+ list.add(context.getResourceFactory().newResource(file.toURI()));
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Bad url ", e);
+ }
+ }
+ });
+ }
+
+ List classesDirs = super.findClassDirs(context);
+ if (classesDirs != null)
+ list.addAll(classesDirs);
+ return list;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenQuickStartConfiguration.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenQuickStartConfiguration.java
new file mode 100644
index 00000000000..cb852e8f445
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenQuickStartConfiguration.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.ee11.webapp.Configuration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.CombinedResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * MavenQuickStartConfiguration
+ */
+public class MavenQuickStartConfiguration extends QuickStartConfiguration
+{
+ private static final Logger LOG = LoggerFactory.getLogger(QuickStartConfiguration.class);
+
+ @Override
+ public Class extends Configuration> replaces()
+ {
+ return QuickStartConfiguration.class;
+ }
+
+ @Override
+ public void deconfigure(WebAppContext context) throws Exception
+ {
+ //if we're not persisting the temp dir, get rid of any overlays
+ if (!context.isTempDirectoryPersistent())
+ {
+ Resource originalBases = (Resource)context.getAttribute("org.eclipse.jetty.resources.originalBases");
+ String originalBaseStr = originalBases.toString();
+
+ //Iterate over all of the resource bases and ignore any that were original bases, just
+ //deleting the overlays
+ Resource res = context.getBaseResource();
+ if (res instanceof CombinedResource)
+ {
+ for (Resource r : ((CombinedResource)res).getResources())
+ {
+ if (originalBaseStr.contains(r.toString()))
+ continue;
+ IO.delete(r.getPath());
+ }
+ }
+ }
+ super.deconfigure(context);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenWebAppContext.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenWebAppContext.java
new file mode 100644
index 00000000000..cd1099371ac
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenWebAppContext.java
@@ -0,0 +1,505 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+
+import org.eclipse.jetty.ee11.plus.webapp.EnvConfiguration;
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.ee11.servlet.FilterHolder;
+import org.eclipse.jetty.ee11.servlet.FilterMapping;
+import org.eclipse.jetty.ee11.servlet.ServletHolder;
+import org.eclipse.jetty.ee11.servlet.ServletMapping;
+import org.eclipse.jetty.ee11.webapp.Configuration;
+import org.eclipse.jetty.ee11.webapp.Configurations;
+import org.eclipse.jetty.ee11.webapp.MetaInfConfiguration;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.maven.Overlay;
+import org.eclipse.jetty.util.FileID;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.CombinedResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.Resources;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * MavenWebAppContext
+ *
+ * Extends the WebAppContext to specialize for the maven environment. We pass in
+ * the list of files that should form the classpath for the webapp when
+ * executing in the plugin, and any jetty-env.xml file that may have been
+ * configured.
+ */
+public class MavenWebAppContext extends WebAppContext
+{
+ private static final Logger LOG = LoggerFactory.getLogger(MavenWebAppContext.class);
+
+ private static final String DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN = ".*/jakarta.servlet-[^/]*\\.jar$|.*/jetty-jakarta-servlet-api-[^/]*\\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\\.jar|.*taglibs-standard-[^/]*\\.jar$";
+
+ private static final String WEB_INF_CLASSES_PREFIX = "/WEB-INF/classes";
+
+ private static final String WEB_INF_LIB_PREFIX = "/WEB-INF/lib";
+
+ private File _classes = null;
+
+ private File _testClasses = null;
+
+ private final List _webInfClasses = new ArrayList<>();
+
+ private final List _webInfJars = new ArrayList<>();
+
+ private final Map _webInfJarMap = new HashMap<>();
+
+ private List _classpathUris; // webInfClasses+testClasses+webInfJars
+
+ private String _jettyEnvXml;
+
+ private List _overlays;
+
+ /**
+ * Set the "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern" with
+ * a pattern for matching jars on container classpath to scan. This is
+ * analogous to the WebAppContext.setAttribute() call.
+ */
+ private String _containerIncludeJarPattern = null;
+
+ /**
+ * Set the "org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern" with a
+ * pattern for matching jars on webapp's classpath to scan. This is
+ * analogous to the WebAppContext.setAttribute() call.
+ */
+ private String _webInfIncludeJarPattern = null;
+
+ /**
+ * If there is no maven-war-plugin config for ordering of the current
+ * project in the sequence of overlays, use this to control whether the
+ * current project is added first or last in list of overlaid resources
+ */
+ private boolean _baseAppFirst = true;
+
+ public MavenWebAppContext()
+ {
+ super();
+ // Turn off copyWebInf option as it is not applicable for plugin.
+ super.setCopyWebInf(false);
+ }
+
+ public void setContainerIncludeJarPattern(String pattern)
+ {
+ _containerIncludeJarPattern = pattern;
+ }
+
+ public String getContainerIncludeJarPattern()
+ {
+ return _containerIncludeJarPattern;
+ }
+
+ public String getWebInfIncludeJarPattern()
+ {
+ return _webInfIncludeJarPattern;
+ }
+
+ public void setWebInfIncludeJarPattern(String pattern)
+ {
+ _webInfIncludeJarPattern = pattern;
+ }
+
+ public List getClassPathUris()
+ {
+ return this._classpathUris;
+ }
+
+ public void setJettyEnvXml(String jettyEnvXml)
+ {
+ this._jettyEnvXml = jettyEnvXml;
+ }
+
+ public String getJettyEnvXml()
+ {
+ return this._jettyEnvXml;
+ }
+
+ public void setClasses(File dir)
+ {
+ _classes = dir;
+ }
+
+ public File getClasses()
+ {
+ return _classes;
+ }
+
+ public void setWebInfLib(List jars)
+ {
+ _webInfJars.addAll(jars);
+ }
+
+ public void setTestClasses(File dir)
+ {
+ _testClasses = dir;
+ }
+
+ public File getTestClasses()
+ {
+ return _testClasses;
+ }
+
+ /**
+ * Ordered list of wars to overlay on top of the current project. The list
+ * may contain an overlay that represents the current project.
+ *
+ * @param overlays the list of overlays
+ */
+ public void setOverlays(List overlays)
+ {
+ _overlays = overlays;
+ }
+
+ /**
+ * Set the name of the attribute that is used in each generated xml element
+ * to indicate the source of the xml element (eg annotation, web.xml etc).
+ *
+ * @param name the name of the attribute to use.
+ */
+ public void setOriginAttribute(String name)
+ {
+ setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, name);
+ }
+
+ /**
+ * Get the originAttribute.
+ * @return the originAttribute
+ */
+ public String getOriginAttribute()
+ {
+ Object attr = getAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE);
+ return attr == null ? null : attr.toString();
+ }
+
+ public List getOverlays()
+ {
+ return _overlays;
+ }
+
+ public void setBaseAppFirst(boolean value)
+ {
+ _baseAppFirst = value;
+ }
+
+ public boolean getBaseAppFirst()
+ {
+ return _baseAppFirst;
+ }
+
+ /**
+ * This method is provided as a convenience for jetty maven plugin
+ * configuration
+ *
+ * @param resourceBases Array of resources strings to set as a
+ * {@link CombinedResource}.
+ */
+ public void setResourceBases(String[] resourceBases)
+ {
+ try
+ {
+ // TODO: what happens if this is called more than once?
+
+ // This is a user provided list of configurations.
+ // We have to assume that mounting can happen.
+ List uris = Stream.of(resourceBases)
+ .map(URI::create)
+ .toList();
+
+ setBaseResource(this.getResourceFactory().newResource(uris));
+ }
+ catch (Throwable t)
+ {
+ throw new IllegalArgumentException("Bad resourceBases: [" + String.join(", ", resourceBases) + "]", t);
+ }
+ }
+
+ public List getWebInfLib()
+ {
+ return _webInfJars;
+ }
+
+ public List getWebInfClasses()
+ {
+ return _webInfClasses;
+ }
+
+ @Override
+ public void doStart() throws Exception
+ {
+ // Set up the pattern that tells us where the jars are that need
+ // scanning
+
+ // Allow user to set up pattern for names of jars from the container
+ // classpath
+ // that will be scanned - note that by default NO jars are scanned
+ String tmp = _containerIncludeJarPattern;
+ if (tmp == null || "".equals(tmp))
+ tmp = (String)getAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN);
+
+ tmp = addPattern(tmp, DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN);
+ setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN, tmp);
+
+ // Allow user to set up pattern of jar names from WEB-INF that will be
+ // scanned.
+ // Note that by default ALL jars considered to be in WEB-INF will be
+ // scanned - setting
+ // a pattern restricts scanning
+ if (_webInfIncludeJarPattern != null)
+ setAttribute(MetaInfConfiguration.WEBINF_JAR_PATTERN, _webInfIncludeJarPattern);
+
+ // Set up the classes dirs that comprises the equivalent of
+ // WEB-INF/classes
+ if (_testClasses != null && _testClasses.exists())
+ _webInfClasses.add(_testClasses);
+ if (_classes != null && _classes.exists())
+ _webInfClasses.add(_classes);
+
+ // Set up the classpath
+ _classpathUris = new ArrayList<>();
+ _webInfClasses.forEach(f -> _classpathUris.add(f.toURI()));
+ _webInfJars.forEach(f ->
+ {
+ // ensure our JAR file references are `jar:file:...` URI references
+ URI jarFileUri = URIUtil.toJarFileUri(f.toURI());
+ // else use file uri as-is
+ _classpathUris.add(Objects.requireNonNullElseGet(jarFileUri, f::toURI));
+ });
+
+ // Initialize map containing all jars in /WEB-INF/lib
+ _webInfJarMap.clear();
+ for (File file : _webInfJars)
+ {
+ // Return all jar files from class path
+ String fileName = file.getName();
+ if (FileID.isJavaArchive(fileName))
+ _webInfJarMap.put(fileName, file);
+ }
+
+ // check for CDI
+ initCDI();
+
+ // CHECK setShutdown(false);
+ super.doStart();
+ }
+
+ @Override
+ protected Configurations newConfigurations()
+ {
+ Configurations configurations = super.newConfigurations();
+
+ if (getJettyEnvXml() != null)
+ {
+ // inject configurations with config from maven plugin
+ for (Configuration c : configurations)
+ {
+ if (c instanceof EnvConfiguration envConfiguration)
+ setAttribute(EnvConfiguration.JETTY_ENV_XML, this.getResourceFactory().newResource(getJettyEnvXml()));
+ }
+ }
+
+ return configurations;
+ }
+
+ @Override
+ public void doStop() throws Exception
+ {
+ if (_classpathUris != null)
+ _classpathUris.clear();
+ _classpathUris = null;
+
+ _classes = null;
+ _testClasses = null;
+
+ if (_webInfJarMap != null)
+ _webInfJarMap.clear();
+
+ _webInfClasses.clear();
+ _webInfJars.clear();
+
+ // CHECK setShutdown(true);
+ // just wait a little while to ensure no requests are still being processed
+ // TODO do better than a sleep
+ Thread.sleep(500L);
+
+ super.doStop();
+
+ // remove all servlets and filters. This is because we will
+ // re-appy any context xml file, which means they would potentially be
+ // added multiple times.
+ getServletHandler().setFilters(new FilterHolder[0]);
+ getServletHandler().setFilterMappings(new FilterMapping[0]);
+ getServletHandler().setServlets(new ServletHolder[0]);
+ getServletHandler().setServletMappings(new ServletMapping[0]);
+ }
+
+ @Override
+ public Resource getResource(String pathInContext) throws MalformedURLException
+ {
+ Resource resource;
+ // Try to get regular resource
+ resource = super.getResource(pathInContext);
+
+ // If no regular resource exists check for access to /WEB-INF/lib or
+ // /WEB-INF/classes
+ if ((resource == null || !resource.exists()) && pathInContext != null && _classes != null)
+ {
+ // Normalize again to look for the resource inside /WEB-INF subdirectories.
+ String uri = URIUtil.normalizePath(pathInContext);
+ if (uri == null)
+ return null;
+
+ // Replace /WEB-INF/classes with candidates for the classpath
+ if (uri.startsWith(WEB_INF_CLASSES_PREFIX))
+ {
+ if (uri.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX) || uri.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX + "/"))
+ {
+ // exact match for a WEB-INF/classes, so preferentially
+ // return the resource matching the web-inf classes
+ // rather than the test classes
+ if (_classes != null)
+ return this.getResourceFactory().newResource(_classes.toPath());
+ else if (_testClasses != null)
+ return this.getResourceFactory().newResource(_testClasses.toPath());
+ }
+ else
+ {
+ // try matching
+ Resource res = null;
+ int i = 0;
+ while (Resources.missing(res) && (i < _webInfClasses.size()))
+ {
+ String newPath = StringUtil.replace(uri, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
+ res = this.getResourceFactory().newResource(newPath);
+ if (Resources.missing(res))
+ {
+ res = null;
+ i++;
+ }
+ }
+ return res;
+ }
+ }
+ else if (uri.startsWith(WEB_INF_LIB_PREFIX))
+ {
+ // Return the real jar file for all accesses to
+ // /WEB-INF/lib/*.jar
+ String jarName = StringUtil.strip(uri, WEB_INF_LIB_PREFIX);
+ if (jarName.startsWith("/") || jarName.startsWith("\\"))
+ jarName = jarName.substring(1);
+ if (jarName.length() == 0)
+ return null;
+ File jarFile = _webInfJarMap.get(jarName);
+ if (jarFile != null)
+ return this.getResourceFactory().newResource(jarFile.getPath());
+
+ return null;
+ }
+ }
+ return resource;
+ }
+
+ @Override
+ public Set getResourcePaths(String path)
+ {
+ // Try to get regular resource paths - this will get appropriate paths
+ // from any overlaid wars etc
+ Set paths = super.getResourcePaths(path);
+
+ if (path != null)
+ {
+ TreeSet allPaths = new TreeSet<>(paths);
+
+ // add in the dependency jars as a virtual WEB-INF/lib entry
+ if (path.startsWith(WEB_INF_LIB_PREFIX))
+ {
+ for (String fileName : _webInfJarMap.keySet())
+ {
+ // Return all jar files from class path
+ allPaths.add(WEB_INF_LIB_PREFIX + "/" + fileName);
+ }
+ }
+ else if (path.startsWith(WEB_INF_CLASSES_PREFIX))
+ {
+ int i = 0;
+
+ while (i < _webInfClasses.size())
+ {
+ String newPath = StringUtil.replace(path, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
+ allPaths.addAll(super.getResourcePaths(newPath));
+ i++;
+ }
+ }
+ return allPaths;
+ }
+ return paths;
+ }
+
+ public String addPattern(String s, String pattern)
+ {
+ if (s == null)
+ s = "";
+ else
+ s = s.trim();
+
+ if (!s.contains(pattern))
+ {
+ if (s.length() != 0)
+ s = s + "|";
+ s = s + pattern;
+ }
+
+ return s;
+ }
+
+ public void initCDI()
+ {
+ Class> cdiInitializer;
+ try
+ {
+ cdiInitializer = Thread.currentThread().getContextClassLoader().loadClass("org.eclipse.jetty.ee11.cdi.servlet.JettyWeldInitializer");
+ Method initWebAppMethod = cdiInitializer.getMethod("initWebApp", WebAppContext.class);
+ initWebAppMethod.invoke(null, this);
+ }
+ catch (ClassNotFoundException e)
+ {
+ LOG.debug("o.e.j.cdi.servlet.JettyWeldInitializer not found, no cdi integration available");
+ }
+ catch (NoSuchMethodException e)
+ {
+ LOG.warn("o.e.j.cdi.servlet.JettyWeldInitializer.initWebApp() not found, no cdi integration available");
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Problem initializing cdi", e);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenWebInfConfiguration.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenWebInfConfiguration.java
new file mode 100644
index 00000000000..7b8fb55a29d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/MavenWebInfConfiguration.java
@@ -0,0 +1,68 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.net.URI;
+
+import org.eclipse.jetty.ee11.webapp.Configuration;
+import org.eclipse.jetty.ee11.webapp.WebAppClassLoader;
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.ee11.webapp.WebInfConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * MavenWebInfConfiguration
+ *
+ * WebInfConfiguration to take account of overlaid wars expressed as project dependencies and
+ * potential configured via the maven-war-plugin.
+ */
+public class MavenWebInfConfiguration extends WebInfConfiguration
+{
+ private static final Logger LOG = LoggerFactory.getLogger(MavenWebInfConfiguration.class);
+
+ public MavenWebInfConfiguration()
+ {
+ super(new Builder()
+ .hide("org.apache.maven.",
+ "org.codehaus.plexus.",
+ "jakarta.enterprise.",
+ "javax.decorator."));
+ }
+
+ @Override
+ public Class extends Configuration> replaces()
+ {
+ return WebInfConfiguration.class;
+ }
+
+ @Override
+ public void configure(WebAppContext context) throws Exception
+ {
+ MavenWebAppContext jwac = (MavenWebAppContext)context;
+
+ //put the classes dir and all dependencies into the classpath
+ if (jwac.getClassPathUris() != null && context.getClassLoader() instanceof WebAppClassLoader loader)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Setting up classpath ...");
+ for (URI uri : jwac.getClassPathUris())
+ {
+ loader.addClassPath(uri.toASCIIString());
+ }
+ }
+
+ super.configure(context);
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/QuickStartGenerator.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/QuickStartGenerator.java
new file mode 100644
index 00000000000..5b1b406e09f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/QuickStartGenerator.java
@@ -0,0 +1,187 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.nio.file.Path;
+
+import org.eclipse.jetty.ee11.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration.Mode;
+import org.eclipse.jetty.ee11.webapp.Configurations;
+import org.eclipse.jetty.maven.ServerSupport;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+/**
+ * Run enough of jetty in order to generate a quickstart file for a
+ * webapp. Optionally, some essential elements of the WebAppContext
+ * configuration can also be converted to properties and saved to
+ * a file after the quickstart generation.
+ *
+ */
+public class QuickStartGenerator
+{
+ private final Path quickstartXml;
+ private final MavenWebAppContext webApp;
+ private Path webAppProps;
+ private String contextXml;
+ private boolean prepared = false;
+ private Server server;
+ private QueuedThreadPool tpool;
+
+ /**
+ * @param quickstartXml the file to generate quickstart into
+ * @param webApp the webapp for which to generate quickstart
+ */
+ public QuickStartGenerator(Path quickstartXml, MavenWebAppContext webApp)
+ {
+ this.quickstartXml = quickstartXml;
+ this.webApp = webApp == null ? new MavenWebAppContext() : webApp;
+ }
+
+ /**
+ * Get the webApp.
+ * @return the webApp
+ */
+ public MavenWebAppContext getWebApp()
+ {
+ return webApp;
+ }
+
+ /**
+ * Get the quickstartXml.
+ * @return the quickstartXml
+ */
+ public Path getQuickstartXml()
+ {
+ return quickstartXml;
+ }
+
+ /**
+ * Get the server.
+ * @return the server
+ */
+ public Server getServer()
+ {
+ return server;
+ }
+
+ /**
+ * Set the server to use.
+ * @param server the server to use
+ */
+ public void setServer(Server server)
+ {
+ this.server = server;
+ }
+
+ public Path getWebAppProps()
+ {
+ return webAppProps;
+ }
+
+ /**
+ * Set properties file describing the webapp.
+ * @param webAppProps properties file describing the webapp
+ */
+ public void setWebAppProps(Path webAppProps)
+ {
+ this.webAppProps = webAppProps;
+ }
+
+ public String getContextXml()
+ {
+ return contextXml;
+ }
+
+ /**
+ * Set a context xml file to apply to the webapp.
+ * @param contextXml a context xml file to apply to the webapp
+ */
+ public void setContextXml(String contextXml)
+ {
+ this.contextXml = contextXml;
+ }
+
+ /**
+ * Configure the webapp in preparation for quickstart generation.
+ */
+ private void prepareWebApp()
+ {
+ //set the webapp up to do very little other than generate the quickstart-web.xml
+ webApp.addConfiguration(new MavenQuickStartConfiguration());
+ webApp.setAttribute(QuickStartConfiguration.MODE, Mode.GENERATE);
+ webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, quickstartXml);
+ webApp.setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, "o");
+ webApp.setCopyWebDir(false);
+ webApp.setCopyWebInf(false);
+ }
+
+ /**
+ * Run enough of jetty to generate a full quickstart xml file for the
+ * webapp. The tmp directory is persisted.
+ *
+ * @throws Exception if there is an unspecified problem
+ */
+ public void generate() throws Exception
+ {
+ if (quickstartXml == null)
+ throw new IllegalStateException("No quickstart xml output file");
+
+ if (!prepared)
+ {
+ prepared = true;
+ prepareWebApp();
+
+ if (server == null)
+ server = new Server();
+
+ //ensure handler structure enabled
+ ServerSupport.configureHandlers(server, null, null);
+
+ Configurations.setServerDefault(server);
+
+ //if our server has a thread pool associated we can do annotation scanning multithreaded,
+ //otherwise scanning will be single threaded
+ if (tpool == null)
+ tpool = server.getBean(QueuedThreadPool.class);
+
+ //add webapp to our fake server instance
+ ServerSupport.addWebApplication(server, webApp);
+
+ //leave everything unpacked for the forked process to use
+ webApp.setTempDirectoryPersistent(true);
+ }
+
+ try
+ {
+ if (tpool != null)
+ tpool.start();
+ else
+ webApp.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE.toString());
+
+ webApp.start(); //just enough to generate the quickstart
+
+ //save config of the webapp BEFORE we stop
+ if (webAppProps != null)
+ WebAppPropertyConverter.toProperties(webApp, webAppProps.toFile(), contextXml);
+ }
+ finally
+ {
+ webApp.stop();
+ if (tpool != null)
+ tpool.stop();
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/WebAppPropertyConverter.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/WebAppPropertyConverter.java
new file mode 100644
index 00000000000..94a5088f914
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/WebAppPropertyConverter.java
@@ -0,0 +1,342 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+import org.eclipse.jetty.ee11.quickstart.QuickStartConfiguration;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.resource.CombinedResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.eclipse.jetty.xml.XmlConfiguration;
+
+/**
+ * WebAppPropertyConverter
+ *
+ * Converts a webapp's configuration to a properties file, and
+ * vice versa.
+ */
+public class WebAppPropertyConverter
+{
+ public static String WEB_XML = "web.xml";
+ public static String QUICKSTART_WEB_XML = "quickstart.web.xml";
+ public static String CONTEXT_XML = "context.xml";
+ public static String CONTEXT_PATH = "context.path";
+ public static String TMP_DIR = "tmp.dir";
+ public static String TMP_DIR_PERSIST = "tmp.dir.persist";
+ public static String BASE_DIRS = "base.dirs";
+ public static String WAR_FILE = "war.file";
+ public static String CLASSES_DIR = "classes.dir";
+ public static String TEST_CLASSES_DIR = "testClasses.dir";
+ public static String LIB_JARS = "lib.jars";
+ public static String DEFAULTS_DESCRIPTOR = "web.default.xml";
+ public static String OVERRIDE_DESCRIPTORS = "web.overrides.xml";
+
+ //TODO :Support defaults descriptor!
+
+ /**
+ * Convert a webapp to properties stored in a file.
+ *
+ * @param webApp the webapp to convert
+ * @param propsFile the file to put the properties into
+ * @param contextXml the optional context xml file related to the webApp
+ * @throws IOException if any I/O exception occurs
+ */
+ public static void toProperties(MavenWebAppContext webApp, File propsFile, String contextXml)
+ throws IOException
+ {
+ if (webApp == null)
+ throw new IllegalArgumentException("No webapp");
+ if (propsFile == null)
+ throw new IllegalArgumentException("No properties file");
+
+ //work out the configuration based on what is configured in the pom
+ if (propsFile.exists())
+ propsFile.delete();
+
+ propsFile.createNewFile();
+
+ Properties props = new Properties();
+ //web.xml
+ if (webApp.getDescriptor() != null)
+ {
+ props.put(WEB_XML, webApp.getDescriptor());
+ }
+
+ Object tmp = webApp.getAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML);
+ if (tmp != null)
+ {
+ props.put(QUICKSTART_WEB_XML, tmp.toString());
+ }
+
+ //sort out the context path
+ if (webApp.getContextPath() != null)
+ {
+ props.put(CONTEXT_PATH, webApp.getContextPath());
+ }
+
+ //tmp dir
+ props.put(TMP_DIR, webApp.getTempDirectory().getAbsolutePath());
+ //props.put("tmp.dir.persist", Boolean.toString(originalPersistTemp));
+ props.put(TMP_DIR_PERSIST, Boolean.toString(webApp.isTempDirectoryPersistent()));
+
+ //send over the calculated resource bases that includes unpacked overlays
+ Resource baseResource = webApp.getBaseResource();
+ if (baseResource instanceof CombinedResource)
+ props.put(BASE_DIRS, toCSV(((CombinedResource)webApp.getBaseResource()).getResources()));
+ else if (baseResource instanceof Resource)
+ props.put(BASE_DIRS, webApp.getBaseResource().toString());
+
+ //if there is a war file, use that
+ if (webApp.getWar() != null)
+ props.put(WAR_FILE, webApp.getWar());
+
+ //web-inf classes
+ if (webApp.getClasses() != null)
+ {
+ props.put(CLASSES_DIR, webApp.getClasses().getAbsolutePath());
+ }
+
+ if (webApp.getTestClasses() != null)
+ {
+ props.put(TEST_CLASSES_DIR, webApp.getTestClasses().getAbsolutePath());
+ }
+
+ //web-inf lib
+ List deps = webApp.getWebInfLib();
+ StringBuilder strbuff = new StringBuilder();
+ if (deps != null)
+ {
+ for (int i = 0; i < deps.size(); i++)
+ {
+ File d = deps.get(i);
+ strbuff.append(d.getAbsolutePath());
+ if (i < deps.size() - 1)
+ strbuff.append(",");
+ }
+ }
+ props.put(LIB_JARS, strbuff.toString());
+
+ //context xml to apply
+ if (contextXml != null)
+ props.put(CONTEXT_XML, contextXml);
+
+ if (webApp.getDefaultsDescriptor() != null)
+ props.put(DEFAULTS_DESCRIPTOR, webApp.getDefaultsDescriptor());
+
+ if (webApp.getOverrideDescriptors() != null)
+ {
+ props.put(OVERRIDE_DESCRIPTORS, String.join(",", webApp.getOverrideDescriptors()));
+ }
+
+ try (BufferedWriter out = Files.newBufferedWriter(propsFile.toPath()))
+ {
+ props.store(out, "properties for webapp");
+ }
+ }
+
+ /**
+ * Configure a webapp from a properties file.
+ *
+ * @param webApp the webapp to configure
+ * @param resource the properties file to apply
+ * @param server the Server instance to use
+ * @param jettyProperties jetty properties to use if there is a context xml file to apply
+ * @throws Exception if there is an unspecified problem
+ */
+ public static void fromProperties(MavenWebAppContext webApp, String resource, Server server, Map jettyProperties)
+ throws Exception
+ {
+ if (resource == null)
+ throw new IllegalStateException("No resource");
+
+ fromProperties(webApp, webApp.getResourceFactory().newResource(resource).getPath(), server, jettyProperties);
+ }
+
+ /**
+ * Configure a webapp from properties.
+ *
+ * @param webApp the webapp to configure
+ * @param webAppProperties properties that describe the configuration of the webapp
+ * @param server the jetty Server instance
+ * @param jettyProperties jetty properties
+ *
+ * @throws Exception if there is an unspecified problem
+ */
+ public static void fromProperties(MavenWebAppContext webApp, Properties webAppProperties, Server server, Map jettyProperties)
+ throws Exception
+ {
+ if (webApp == null)
+ throw new IllegalArgumentException("No webapp");
+
+ if (webAppProperties == null)
+ return;
+
+ String str = webAppProperties.getProperty(CONTEXT_PATH);
+ if (!StringUtil.isBlank(str))
+ webApp.setContextPath(str);
+
+ // - web.xml
+ str = webAppProperties.getProperty(WEB_XML);
+ if (!StringUtil.isBlank(str))
+ webApp.setDescriptor(str);
+
+ //if there is a pregenerated quickstart file
+ str = webAppProperties.getProperty(QUICKSTART_WEB_XML);
+ if (!StringUtil.isBlank(str))
+ {
+ webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, webApp.getResourceFactory().newResource(str));
+ }
+
+ // - the tmp directory
+ str = webAppProperties.getProperty(TMP_DIR);
+ if (!StringUtil.isBlank(str))
+ webApp.setTempDirectory(new File(str.trim()));
+
+ str = webAppProperties.getProperty(TMP_DIR_PERSIST);
+ if (!StringUtil.isBlank(str))
+ webApp.setTempDirectoryPersistent(Boolean.valueOf(str));
+
+ //Get the calculated base dirs which includes the overlays
+ str = webAppProperties.getProperty(BASE_DIRS);
+ if (!StringUtil.isBlank(str))
+ {
+ // This is a use provided list of overlays, which could have mountable entries.
+ webApp.setWar(null);
+ webApp.setBaseResource(ResourceFactory.combine(webApp.getResourceFactory().split(str)));
+ }
+
+ str = webAppProperties.getProperty(WAR_FILE);
+ if (!StringUtil.isBlank(str))
+ {
+ webApp.setWar(str);
+ }
+
+ // - the equivalent of web-inf classes
+ str = webAppProperties.getProperty(CLASSES_DIR);
+ if (!StringUtil.isBlank(str))
+ {
+ webApp.setClasses(new File(str));
+ }
+
+ str = webAppProperties.getProperty(TEST_CLASSES_DIR);
+ if (!StringUtil.isBlank(str))
+ {
+ webApp.setTestClasses(new File(str));
+ }
+
+ // - the equivalent of web-inf lib
+ str = webAppProperties.getProperty(LIB_JARS);
+ if (!StringUtil.isBlank(str))
+ {
+ List jars = new ArrayList();
+ String[] names = StringUtil.csvSplit(str);
+ for (int j = 0; names != null && j < names.length; j++)
+ {
+ jars.add(new File(names[j].trim()));
+ }
+ webApp.setWebInfLib(jars);
+ }
+
+ //any defaults descriptor
+ str = (String)webAppProperties.getProperty(DEFAULTS_DESCRIPTOR);
+ if (!StringUtil.isBlank(str))
+ {
+ webApp.setDefaultsDescriptor(str);
+ }
+
+ //any override descriptors
+ str = (String)webAppProperties.getProperty(OVERRIDE_DESCRIPTORS);
+ if (!StringUtil.isBlank(str))
+ {
+ String[] names = StringUtil.csvSplit(str);
+ for (int j = 0; names != null && j < names.length; j++)
+ {
+ webApp.addOverrideDescriptor(names[j]);
+ }
+ }
+
+ //set up the webapp from the context xml file provided
+ //NOTE: just like jetty:run mojo this means that the context file can
+ //potentially override settings made in the pom. Ideally, we'd like
+ //the pom to override the context xml file, but as the other mojos all
+ //configure a WebAppContext in the pom (the element), it is
+ //already configured by the time the context xml file is applied.
+ str = (String)webAppProperties.getProperty(CONTEXT_XML);
+ if (!StringUtil.isBlank(str))
+ {
+ XmlConfiguration xmlConfiguration = new XmlConfiguration(webApp.getResourceFactory().newResource(str));
+ xmlConfiguration.getIdMap().put("Server", server);
+ //add in any properties
+ if (jettyProperties != null)
+ {
+ for (Map.Entry prop : jettyProperties.entrySet())
+ {
+ xmlConfiguration.getProperties().put(prop.getKey(), prop.getValue());
+ }
+ }
+ xmlConfiguration.configure(webApp);
+ }
+ }
+
+ /**
+ * Configure a webapp from a properties file
+ * @param webApp the webapp to configure
+ * @param propsFile the properties to apply
+ * @param server the Server instance to use if there is a context xml file to apply
+ * @param jettyProperties jetty properties to use if there is a context xml file to apply
+ * @throws Exception if there is an unspecified problem
+ */
+ public static void fromProperties(MavenWebAppContext webApp, Path propsFile, Server server, Map jettyProperties)
+ throws Exception
+ {
+
+ if (propsFile == null)
+ throw new IllegalArgumentException("No properties file");
+
+ if (!Files.exists(propsFile))
+ throw new IllegalArgumentException(propsFile + " does not exist");
+
+ Properties props = new Properties();
+ try (InputStream in = Files.newInputStream(propsFile))
+ {
+ props.load(in);
+ }
+
+ fromProperties(webApp, props, server, jettyProperties);
+ }
+
+ /**
+ * Convert an array of Resources to csv file names
+ *
+ * @param resources the resources to convert
+ * @return csv string of resource filenames
+ */
+ private static String toCSV(List resources)
+ {
+ return resources.stream().map(Object::toString).collect(Collectors.joining(","));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/package-info.java b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/package-info.java
new file mode 100644
index 00000000000..debc817b63c
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/java/org/eclipse/jetty/ee11/maven/plugin/package-info.java
@@ -0,0 +1,18 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * Jetty Maven Plugin : Support for Jetty in Maven build lifecycle
+ */
+package org.eclipse.jetty.ee11.maven.plugin;
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/META-INF/services/org.eclipse.jetty.ee11.webapp.Configuration b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/META-INF/services/org.eclipse.jetty.ee11.webapp.Configuration
new file mode 100644
index 00000000000..dbe7f8cfec5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/META-INF/services/org.eclipse.jetty.ee11.webapp.Configuration
@@ -0,0 +1,4 @@
+org.eclipse.jetty.ee11.maven.plugin.MavenWebInfConfiguration
+org.eclipse.jetty.ee11.maven.plugin.MavenMetaInfConfiguration
+#Do not list this because Quickstart is not present for distro
+#org.eclipse.jetty.maven.plugin.MavenQuickStartConfiguration
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/ee11-maven.mod b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/ee11-maven.mod
new file mode 100644
index 00000000000..9931f8669a6
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/ee11-maven.mod
@@ -0,0 +1,18 @@
+# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
+
+[description]
+Enables an un-assembled Maven webapp to run in a Jetty distribution.
+
+[environment]
+ee11
+
+[depends]
+server
+ee11-webapp
+ee11-annotations
+
+[lib]
+lib/ee11-maven/*.jar
+
+[xml]
+etc/jetty-ee11-maven.xml
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/jetty-ee11-maven.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/jetty-ee11-maven.xml
new file mode 100644
index 00000000000..b83be87a647
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/jetty-ee11-maven.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/maven-ee11.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/maven-ee11.xml
new file mode 100644
index 00000000000..ef2f071ec1a
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/main/resources/maven-ee11.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+ /etc/maven.props
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/MockShutdownMonitor.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/MockShutdownMonitor.java
new file mode 100644
index 00000000000..7f0f9490d8b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/MockShutdownMonitor.java
@@ -0,0 +1,74 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.net.ServerSocket;
+
+import org.eclipse.jetty.toolchain.test.IO;
+
+/**
+ * MockShutdownMonitor
+ * A helper class that grabs a ServerSocket, spawns a thread and then
+ * passes the ServerSocket to the Runnable. This class has a main so
+ * that it can be used for forking, to mimic the actions of the
+ * org.eclipse.jetty.server.ShutdownMonitor.
+ */
+public class MockShutdownMonitor
+{
+ String key;
+ MockShutdownMonitorRunnable testerRunnable;
+ ServerSocket serverSocket;
+
+ public MockShutdownMonitor(String key, MockShutdownMonitorRunnable testerRunnable)
+ throws Exception
+ {
+ this.key = key;
+ this.testerRunnable = testerRunnable;
+ listen();
+ }
+
+ private ServerSocket listen()
+ throws Exception
+ {
+ serverSocket = new ServerSocket(0);
+ try
+ {
+ serverSocket.setReuseAddress(true);
+ return serverSocket;
+ }
+ catch (Throwable e)
+ {
+ IO.close(serverSocket);
+ throw e;
+ }
+ }
+
+ public int getPort()
+ {
+ if (serverSocket == null)
+ return 0;
+ return serverSocket.getLocalPort();
+ }
+
+ public void start()
+ throws Exception
+ {
+ testerRunnable.setServerSocket(serverSocket);
+ testerRunnable.setKey(key);
+ Thread thread = new Thread(testerRunnable);
+ thread.setDaemon(true);
+ thread.setName("Tester Thread");
+ thread.start();
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/MockShutdownMonitorRunnable.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/MockShutdownMonitorRunnable.java
new file mode 100644
index 00000000000..c8d6bebf76d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/MockShutdownMonitorRunnable.java
@@ -0,0 +1,111 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jetty.toolchain.test.IO;
+
+/**
+ * MockShutdownMonitorRunnable
+ *
+ * Mimics the actions of the org.eclipse.jetty.server.ShutdownMonitor.ShutdownMonitorRunnable
+ * to aid testing.
+ */
+public class MockShutdownMonitorRunnable implements Runnable
+{
+ ServerSocket serverSocket;
+ String key;
+ String statusResponse = "OK";
+ String pidResponse;
+ String defaultResponse = "Stopped";
+ boolean exit;
+
+ public void setExit(boolean exit)
+ {
+ this.exit = exit;
+ }
+
+ public void setKey(String key)
+ {
+ this.key = key;
+ }
+
+ public void setServerSocket(ServerSocket serverSocket)
+ {
+ this.serverSocket = serverSocket;
+ }
+
+ public void setPidResponse(String pidResponse)
+ {
+ this.pidResponse = pidResponse;
+ }
+
+ public void run()
+ {
+ try
+ {
+ while (true)
+ {
+ try (Socket socket = serverSocket.accept())
+ {
+ LineNumberReader reader = new LineNumberReader(new InputStreamReader(socket.getInputStream()));
+ String receivedKey = reader.readLine();
+ if (!key.equals(receivedKey))
+ {
+ continue;
+ }
+
+ String cmd = reader.readLine();
+ OutputStream out = socket.getOutputStream();
+
+ if ("status".equalsIgnoreCase(cmd))
+ {
+ out.write((statusResponse + "\r\n").getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ }
+ else if ("pid".equalsIgnoreCase(cmd))
+ {
+ out.write((pidResponse + "\r\n").getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ }
+ else
+ {
+ out.write((defaultResponse + "\r\n").getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ if (exit)
+ System.exit(0);
+ }
+ }
+ catch (Throwable x)
+ {
+ x.printStackTrace();
+ }
+ }
+ }
+ catch (Throwable x)
+ {
+ x.printStackTrace();
+ }
+ finally
+ {
+ IO.close(serverSocket);
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/SomeListener.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/SomeListener.java
new file mode 100644
index 00000000000..80ee3b06270
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/SomeListener.java
@@ -0,0 +1,21 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.util.EventListener;
+
+public class SomeListener implements EventListener
+{
+
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestForkedChild.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestForkedChild.java
new file mode 100644
index 00000000000..5510f1b2fba
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestForkedChild.java
@@ -0,0 +1,176 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+
+import org.awaitility.Awaitility;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.IO;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Test the JettyForkedChild class, which
+ * is the main that is executed by jetty:run/start in mode FORKED.
+ */
+@ExtendWith(WorkDirExtension.class)
+public class TestForkedChild
+{
+ File testDir;
+ File baseDir;
+ Path tmpDir;
+ File tokenFile;
+ File webappPropsFile;
+ int stopPort;
+ String stopKey = "FERMATI";
+ String jettyPortString;
+ int jettyPort;
+ String token;
+ JettyForkedChild child;
+ Thread starter;
+ JettyRunner runner = new JettyRunner();
+
+ public class JettyRunner implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ List cmd = new ArrayList<>();
+ cmd.add("--stop-port");
+ cmd.add(String.valueOf(stopPort));
+ cmd.add("--stop-key");
+ cmd.add(stopKey);
+ cmd.add("--webprops");
+ cmd.add(webappPropsFile.getAbsolutePath());
+ cmd.add("--token");
+ cmd.add(tokenFile.getAbsolutePath());
+
+ MavenWebAppContext webapp = new MavenWebAppContext();
+ webapp.setContextPath("/foo");
+ webapp.setTempDirectory(tmpDir.toFile());
+ webapp.setBaseResourceAsPath(baseDir.toPath());
+ WebAppPropertyConverter.toProperties(webapp, webappPropsFile, null);
+ child = new JettyForkedChild(cmd.toArray(new String[0]));
+ child.getJettyEmbedder().setExitVm(false); //ensure jetty doesn't stop vm for testing
+ child.start();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @BeforeEach
+ public void setUp(WorkDir workDir)
+ {
+ tmpDir = workDir.getEmptyPathDir();
+ baseDir = MavenTestingUtils.getTestResourceDir("root");
+ testDir = MavenTestingUtils.getTargetTestingDir("forkedChild");
+ FS.ensureEmpty(testDir);
+ webappPropsFile = new File(tmpDir.toFile(), "webapp.props");
+
+ String stopPortString = System.getProperty("stop.port");
+ assertNotNull(stopPortString, "stop.port System property");
+ stopPort = Integer.parseInt(stopPortString);
+ jettyPortString = System.getProperty("jetty.port");
+ assertNotNull(jettyPortString, "jetty.port System property");
+ jettyPort = Integer.parseInt(jettyPortString);
+
+ Random random = new Random();
+ token = Long.toString(random.nextLong() ^ System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
+ tokenFile = tmpDir.resolve(token + ".txt").toFile();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ String command = "forcestop";
+
+ try (Socket s = new Socket(InetAddress.getByName("127.0.0.1"), stopPort))
+ {
+ OutputStream out = s.getOutputStream();
+ out.write((stopKey + "\r\n" + command + "\r\n").getBytes());
+ out.flush();
+
+ s.setSoTimeout(1000);
+ s.getInputStream();
+
+ LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream()));
+ String response;
+ boolean stopped = false;
+ while (!stopped && ((response = lin.readLine()) != null))
+ {
+ if ("Stopped".equals(response))
+ {
+ stopped = true;
+ }
+ }
+ }
+ }
+
+ @Test
+ public void test() throws Exception
+ {
+ starter = new Thread(runner, "JettyForkedChild");
+ starter.start();
+
+ //wait for the token file to be created
+ Awaitility.waitAtMost(Duration.ofSeconds(10)).until(tokenFile::exists);
+
+ URL url = new URL("http://localhost:" + jettyPortString + "/foo/");
+ HttpURLConnection connection = null;
+
+ try
+ {
+ connection = (HttpURLConnection)url.openConnection();
+ connection.connect();
+ assertThat(connection.getResponseCode(), Matchers.is(200));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IO.copy(connection.getInputStream(), baos);
+ assertThat(baos.toString(), Matchers.containsString("ROOT"));
+ }
+ finally
+ {
+ if (connection != null)
+ connection.disconnect();
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestJettyEmbedder.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestJettyEmbedder.java
new file mode 100644
index 00000000000..249e7b9ce5f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestJettyEmbedder.java
@@ -0,0 +1,157 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.ee11.servlet.ListenerHolder;
+import org.eclipse.jetty.maven.MavenServerConnector;
+import org.eclipse.jetty.maven.ServerSupport;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestJettyEmbedder
+{
+ @Test
+ public void testJettyEmbedderFromDefaults(WorkDir workDir) throws Exception
+ {
+ Path basePath = workDir.getEmptyPathDir();
+ MavenWebAppContext webApp = new MavenWebAppContext();
+ webApp.setBaseResourceAsPath(basePath);
+ MavenServerConnector connector = new MavenServerConnector();
+ connector.setPort(0);
+
+ JettyEmbedder jetty = new JettyEmbedder();
+ jetty.setHttpConnector(connector);
+ jetty.setExitVm(false);
+ jetty.setServer(null);
+ jetty.setContextHandlers(null);
+ jetty.setRequestLog(null);
+ jetty.setJettyXmlFiles(null);
+ jetty.setJettyProperties(null);
+ jetty.setLoginServices(null);
+ jetty.setContextXml(MavenTestingUtils.getTestResourceFile("embedder-context.xml").getAbsolutePath());
+ jetty.setWebApp(webApp);
+
+ try
+ {
+ jetty.start();
+ assertEquals("/embedder", webApp.getContextPath());
+ assertTrue(webApp.isAvailable());
+ assertNotNull(jetty.getServer());
+ assertTrue(jetty.getServer().isStarted());
+ assertNotNull(jetty.getServer().getConnectors());
+ assertNotNull(ServerSupport.findContextHandlerCollection(jetty.getServer()));
+ }
+ finally
+ {
+ jetty.stop();
+ }
+ }
+
+ @Test
+ public void testJettyEmbedder(WorkDir workDir)
+ throws Exception
+ {
+ Path basePath = workDir.getPath();
+ MavenWebAppContext webApp = new MavenWebAppContext();
+ webApp.setBaseResourceAsPath(basePath);
+ Server server = new Server();
+ Map jettyProperties = new HashMap<>();
+ jettyProperties.put("jetty.server.dumpAfterStart", "false");
+
+ ContextHandler otherHandler = new ContextHandler();
+ otherHandler.setContextPath("/other");
+ otherHandler.setBaseResourceAsPath(MavenTestingUtils.getTestResourcePathDir("root"));
+
+ MavenServerConnector connector = new MavenServerConnector();
+ connector.setPort(0);
+
+ JettyEmbedder jetty = new JettyEmbedder();
+ jetty.setHttpConnector(connector);
+ jetty.setExitVm(false);
+ jetty.setServer(server);
+ jetty.setContextHandlers(List.of(otherHandler));
+ jetty.setRequestLog(null);
+ jetty.setJettyXmlFiles(List.of(MavenTestingUtils.getTestResourceFile("embedder-jetty.xml")));
+ jetty.setJettyProperties(jettyProperties);
+ jetty.setLoginServices(null);
+ jetty.setContextXml(MavenTestingUtils.getTestResourceFile("embedder-context.xml").getAbsolutePath());
+ jetty.setWebApp(webApp);
+
+ try
+ {
+ jetty.start();
+ assertEquals("/embedder", webApp.getContextPath());
+ assertTrue(webApp.isAvailable());
+ assertNotNull(jetty.getServer());
+ assertTrue(jetty.getServer().isStarted());
+ assertNotNull(jetty.getServer().getConnectors());
+ ContextHandlerCollection contexts = ServerSupport.findContextHandlerCollection(jetty.getServer());
+ assertNotNull(contexts);
+ assertTrue(contexts.contains(otherHandler));
+ assertTrue(contexts.contains(webApp));
+
+ //stop the webapp and check durable listener retained
+ jetty.stopWebApp();
+ boolean someListener = false;
+ for (ListenerHolder h : webApp.getServletHandler().getListeners())
+ {
+ if (h.getHeldClass() != null && "org.eclipse.jetty.ee11.maven.plugin.SomeListener".equalsIgnoreCase(h.getHeldClass().getName()))
+ {
+ if (someListener)
+ fail("Duplicate listeners");
+ else
+ someListener = true;
+ }
+ }
+
+
+ //restart the webapp
+ jetty.redeployWebApp();
+ someListener = false;
+
+ //ensure still only 1 listener
+ for (ListenerHolder h : webApp.getServletHandler().getListeners())
+ {
+ if (h.getHeldClass() != null && "org.eclipse.jetty.ee11.maven.plugin.SomeListener".equalsIgnoreCase(h.getHeldClass().getName()))
+ {
+ if (someListener)
+ fail("Duplicate listeners");
+ else
+ someListener = true;
+ }
+ }
+ }
+ finally
+ {
+ jetty.stop();
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestJettyStopMojo.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestJettyStopMojo.java
new file mode 100644
index 00000000000..3deacd5d59f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestJettyStopMojo.java
@@ -0,0 +1,299 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.awaitility.Awaitility;
+import org.eclipse.jetty.server.ShutdownMonitor;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class TestJettyStopMojo
+{
+ /**
+ * ShutdownMonitorMain
+ * Kick off the ShutdownMonitor and wait for it to exit.
+ */
+ public static final class ShutdownMonitorMain
+ {
+ public static void main(String[] args)
+ {
+ try
+ {
+ ShutdownMonitor monitor = ShutdownMonitor.getInstance();
+ monitor.setPort(0);
+ monitor.start();
+ monitor.await();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static class TestLog implements org.apache.maven.plugin.logging.Log
+ {
+ List sink = new ArrayList<>();
+
+ @Override
+ public boolean isDebugEnabled()
+ {
+ return true;
+ }
+
+ @Override
+ public void debug(CharSequence content)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void debug(CharSequence content, Throwable error)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void debug(Throwable error)
+ {
+ }
+
+ @Override
+ public boolean isInfoEnabled()
+ {
+ return true;
+ }
+
+ @Override
+ public void info(CharSequence content)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void info(CharSequence content, Throwable error)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void info(Throwable error)
+ {
+ }
+
+ @Override
+ public boolean isWarnEnabled()
+ {
+ return true;
+ }
+
+ @Override
+ public void warn(CharSequence content)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void warn(CharSequence content, Throwable error)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void warn(Throwable error)
+ {
+ }
+
+ @Override
+ public boolean isErrorEnabled()
+ {
+ return true;
+ }
+
+ @Override
+ public void error(CharSequence content)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void error(CharSequence content, Throwable error)
+ {
+ sink.add(content.toString());
+ }
+
+ @Override
+ public void error(Throwable error)
+ {
+ }
+
+ public void assertContains(String str)
+ {
+ assertThat(sink, Matchers.hasItem(str));
+ }
+
+ public void dumpStdErr()
+ {
+ for (String s : sink)
+ {
+ System.err.println(s);
+ }
+ }
+ }
+
+ @Test
+ public void testStopNoWait() throws Exception
+ {
+ //send a stop message and don't wait for the reply or the process to shutdown
+ String stopKey = "foo";
+ MockShutdownMonitorRunnable runnable = new MockShutdownMonitorRunnable();
+ runnable.setPidResponse("abcd");
+ MockShutdownMonitor monitor = new MockShutdownMonitor(stopKey, runnable);
+ monitor.start();
+
+ TestLog log = new TestLog();
+ JettyStopMojo mojo = new JettyStopMojo();
+ mojo.stopKey = stopKey;
+ mojo.stopPort = monitor.getPort();
+ mojo.setLog(log);
+
+ mojo.execute();
+
+ log.assertContains("Stopping jetty");
+ }
+
+ @Test
+ public void testStopWaitBadPid() throws Exception
+ {
+ //test that even if we receive a bad pid, we still send the stop command and wait to
+ //receive acknowledgement, but we don't wait for the process to exit
+ String stopKey = "foo";
+ MockShutdownMonitorRunnable runnable = new MockShutdownMonitorRunnable();
+ runnable.setPidResponse("abcd");
+ MockShutdownMonitor monitor = new MockShutdownMonitor(stopKey, runnable);
+ monitor.start();
+
+ TestLog log = new TestLog();
+ JettyStopMojo mojo = new JettyStopMojo();
+ mojo.stopWait = 5;
+ mojo.stopKey = stopKey;
+ mojo.stopPort = monitor.getPort();
+ mojo.setLog(log);
+
+ mojo.execute();
+
+ log.assertContains("Server returned bad pid");
+ log.assertContains("Server reports itself as stopped");
+ }
+
+ @Test
+ public void testStopSameProcess() throws Exception
+ {
+ //test that if we need to stop a jetty in the same process as us
+ //we will wait for it to exit
+ String stopKey = "foo";
+ long thisPid = ProcessHandle.current().pid();
+ MockShutdownMonitorRunnable runnable = new MockShutdownMonitorRunnable();
+ runnable.setPidResponse(Long.toString(thisPid));
+ MockShutdownMonitor monitor = new MockShutdownMonitor(stopKey, runnable);
+ monitor.start();
+
+ TestLog log = new TestLog();
+ JettyStopMojo mojo = new JettyStopMojo();
+ mojo.stopWait = 5;
+ mojo.stopKey = stopKey;
+ mojo.stopPort = monitor.getPort();
+ mojo.setLog(log);
+
+ mojo.execute();
+
+ log.assertContains("Waiting 5 seconds for jetty " + thisPid + " to stop");
+ }
+
+ @Test
+ public void testStopWait() throws Exception
+ {
+ //test that we will communicate with a remote process and wait for it to exit
+ String stopKey = "foo";
+ List cmd = new ArrayList<>();
+ String java = "java";
+ String[] javaexes = new String[]{"java", "java.exe"};
+ File javaHomeDir = new File(System.getProperty("java.home"));
+ Path javaHomePath = javaHomeDir.toPath();
+ for (String javaexe : javaexes)
+ {
+ Path javaBinPath = javaHomePath.resolve(Paths.get("bin", javaexe));
+ if (Files.exists(javaBinPath) && !Files.isDirectory(javaBinPath))
+ java = javaBinPath.toFile().getAbsolutePath();
+ }
+
+ cmd.add(java);
+ cmd.add("-DSTOP.KEY=" + stopKey);
+ cmd.add("-DDEBUG=true");
+ cmd.add("-cp");
+ cmd.add(System.getProperty("java.class.path"));
+ cmd.add(ShutdownMonitorMain.class.getName());
+
+ ProcessBuilder command = new ProcessBuilder(cmd);
+ File file = MavenTestingUtils.getTargetFile("tester.out");
+ command.redirectOutput(file);
+ command.redirectErrorStream(true);
+ command.directory(MavenTestingUtils.getTargetDir());
+ Process fork = command.start();
+
+ Awaitility.await().atMost(Duration.ofSeconds(5)).until(file::exists);
+ final String[] port = {null};
+ Awaitility.await().atMost(Duration.ofSeconds(5)).until(() ->
+ {
+ Optional tmp = Files.readAllLines(file.toPath()).stream()
+ .filter(s -> s.startsWith("STOP.PORT=")).findFirst();
+ if (tmp.isPresent())
+ {
+ // TODO validate it's an integer
+ port[0] = tmp.get().substring(10);
+ return true;
+ }
+ return false;
+
+ });
+
+ assertNotNull(port[0]);
+
+ TestLog log = new TestLog();
+ JettyStopMojo mojo = new JettyStopMojo();
+ mojo.stopWait = 5;
+ mojo.stopKey = stopKey;
+ mojo.stopPort = Integer.parseInt(port[0]);
+ mojo.setLog(log);
+
+ mojo.execute();
+
+ log.dumpStdErr();
+ log.assertContains("Waiting " + mojo.stopWait + " seconds for jetty " + fork.pid() + " to stop");
+ log.assertContains("Server process stopped");
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestQuickStartGenerator.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestQuickStartGenerator.java
new file mode 100644
index 00000000000..045307d5468
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestQuickStartGenerator.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ *
+ */
+@ExtendWith(WorkDirExtension.class)
+public class TestQuickStartGenerator
+{
+ @Test
+ public void testGenerator(WorkDir workDir) throws Exception
+ {
+ Path tmpDir = workDir.getEmptyPathDir();
+ MavenWebAppContext webApp = new MavenWebAppContext();
+ webApp.setContextPath("/shouldbeoverridden");
+ Path rootDir = MavenTestingUtils.getTargetPath("test-classes/root");
+ assertTrue(Files.exists(rootDir));
+ assertTrue(Files.isDirectory(rootDir));
+ webApp.setBaseResource(ResourceFactory.root().newResource(rootDir));
+
+ Path quickstartFile = tmpDir.resolve("quickstart-web.xml");
+ QuickStartGenerator generator = new QuickStartGenerator(quickstartFile, webApp);
+ generator.setContextXml(MavenTestingUtils.getTargetFile("test-classes/embedder-context.xml").getAbsolutePath());
+ generator.setServer(new Server());
+
+ Path propsFile = tmpDir.resolve("webapp.props");
+ Files.createFile(propsFile);
+ generator.setWebAppProps(propsFile);
+ generator.generate();
+ assertTrue(Files.exists(propsFile));
+ assertThat(Files.size(propsFile), greaterThan(0L));
+ assertTrue(Files.exists(quickstartFile));
+ assertThat(Files.size(quickstartFile), greaterThan(0L));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestSelectiveJarResource.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestSelectiveJarResource.java
new file mode 100644
index 00000000000..03ef98dc2d0
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestSelectiveJarResource.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.maven.SelectiveJarResource;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class TestSelectiveJarResource
+{
+
+ @Test
+ public void testIncludesNoExcludes(WorkDir workDir) throws Exception
+ {
+ Path unpackDir = workDir.getPath();
+ Path testJar = MavenTestingUtils.getTestResourcePathFile("selective-jar-test.jar");
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ Resource resource = resourceFactory.newJarFileResource(testJar.toUri());
+ SelectiveJarResource sjr = new SelectiveJarResource(resource);
+ sjr.setCaseSensitive(false);
+ List includes = new ArrayList<>();
+ includes.add("**/*.html");
+ sjr.setIncludes(includes);
+ sjr.copyTo(unpackDir);
+ assertTrue(Files.exists(unpackDir.resolve("top.html")));
+ assertTrue(Files.exists(unpackDir.resolve("aa/a1.html")));
+ assertTrue(Files.exists(unpackDir.resolve("aa/a2.html")));
+ assertTrue(Files.exists(unpackDir.resolve("aa/deep/a3.html")));
+ assertTrue(Files.exists(unpackDir.resolve("bb/b1.html")));
+ assertTrue(Files.exists(unpackDir.resolve("bb/b2.html")));
+ assertTrue(Files.exists(unpackDir.resolve("cc/c1.html")));
+ assertTrue(Files.exists(unpackDir.resolve("cc/c2.html")));
+ }
+ }
+
+ @Test
+ public void testExcludesNoIncludes(WorkDir workDir) throws Exception
+ {
+ Path unpackDir = workDir.getPath();
+ Path testJar = MavenTestingUtils.getTestResourcePathFile("selective-jar-test.jar");
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ Resource resource = resourceFactory.newJarFileResource(testJar.toUri());
+ SelectiveJarResource sjr = new SelectiveJarResource(resource);
+ sjr.setCaseSensitive(false);
+ List excludes = new ArrayList<>();
+ excludes.add("**/*");
+ sjr.setExcludes(excludes);
+ sjr.copyTo(unpackDir);
+ assertFalse(Files.exists(unpackDir.resolve("top.html")));
+ assertFalse(Files.exists(unpackDir.resolve("aa/a1.html")));
+ assertFalse(Files.exists(unpackDir.resolve("aa/a2.html")));
+ assertFalse(Files.exists(unpackDir.resolve("aa/deep/a3.html")));
+ assertFalse(Files.exists(unpackDir.resolve("bb/b1.html")));
+ assertFalse(Files.exists(unpackDir.resolve("bb/b2.html")));
+ assertFalse(Files.exists(unpackDir.resolve("cc/c1.html")));
+ assertFalse(Files.exists(unpackDir.resolve("cc/c2.html")));
+ }
+ }
+
+ @Test
+ public void testIncludesExcludes(WorkDir workDir) throws Exception
+ {
+ Path unpackDir = workDir.getPath();
+ Path testJar = MavenTestingUtils.getTestResourcePathFile("selective-jar-test.jar");
+ try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
+ {
+ Resource resource = resourceFactory.newJarFileResource(testJar.toUri());
+ SelectiveJarResource sjr = new SelectiveJarResource(resource);
+ sjr.setCaseSensitive(false);
+ List excludes = new ArrayList<>();
+ excludes.add("**/deep/*");
+ sjr.setExcludes(excludes);
+ List includes = new ArrayList<>();
+ includes.add("bb/*");
+ sjr.setIncludes(includes);
+ sjr.copyTo(unpackDir);
+ assertFalse(Files.exists(unpackDir.resolve("top.html")));
+ assertFalse(Files.exists(unpackDir.resolve("aa/a1.html")));
+ assertFalse(Files.exists(unpackDir.resolve("aa/a2.html")));
+ assertFalse(Files.exists(unpackDir.resolve("aa/deep/a3.html")));
+ assertTrue(Files.exists(unpackDir.resolve("bb/b1.html")));
+ assertTrue(Files.exists(unpackDir.resolve("bb/b2.html")));
+ assertFalse(Files.exists(unpackDir.resolve("cc/c1.html")));
+ assertFalse(Files.exists(unpackDir.resolve("cc/c2.html")));
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestWebAppPropertyConverter.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestWebAppPropertyConverter.java
new file mode 100644
index 00000000000..ee27db0c9b8
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/TestWebAppPropertyConverter.java
@@ -0,0 +1,165 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+import org.eclipse.jetty.ee11.webapp.WebAppContext;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.resource.CombinedResource;
+import org.eclipse.jetty.util.resource.Resource;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestWebAppPropertyConverter
+{
+ static File testDir;
+ static String contextXml;
+ static File tmpDir;
+ static File classesDir;
+ static File testClassesDir;
+ static File jar1;
+ static File jar2;
+ static File war;
+ static File override1;
+ static File override2;
+ static File webXml;
+
+ @BeforeAll
+ public static void setUp() throws Exception
+ {
+ testDir = MavenTestingUtils.getTargetTestingDir("TestWebApPropertyConverter");
+ testDir.mkdirs();
+ contextXml = MavenTestingUtils.getTestResourceFile("embedder-context.xml").getAbsolutePath();
+ tmpDir = new File(testDir, "testToProperties");
+ tmpDir.mkdirs();
+ classesDir = new File(testDir, "imaginaryClasses");
+ classesDir.mkdirs();
+ testClassesDir = new File(testDir, "imaginaryTestClasses");
+ testClassesDir.mkdirs();
+ jar1 = new File(testDir, "imaginary1.jar");
+ jar1.createNewFile();
+ jar2 = new File(testDir, "imaginary2.jar");
+ jar2.createNewFile();
+ war = new File(testDir, "imaginary.war");
+ war.createNewFile();
+ override1 = new File(testDir, "override-web1.xml");
+ override1.createNewFile();
+ override2 = new File(testDir, "override-web2.xml");
+ override2.createNewFile();
+ webXml = new File(testDir, "web.xml");
+ webXml.createNewFile();
+ }
+
+ @AfterAll
+ public static void tearDown() throws Exception
+ {
+ IO.delete(testDir);
+ }
+
+ @Test
+ public void testToProperties() throws Exception
+ {
+ File propsFile = new File(testDir, "webapp.props");
+ if (propsFile.exists())
+ propsFile.delete();
+ propsFile.createNewFile();
+
+ MavenWebAppContext webApp = new MavenWebAppContext();
+ webApp.setContextPath("/foo");
+ webApp.setBaseResourceAsPath(MavenTestingUtils.getTestResourcePathDir("root"));
+ webApp.setTempDirectory(tmpDir);
+ webApp.setTempDirectoryPersistent(false);
+ webApp.setClasses(classesDir);
+ webApp.setTestClasses(testClassesDir);
+ webApp.setWebInfLib(Arrays.asList(jar1, jar2));
+ webApp.setWar(war.getAbsolutePath());
+ webApp.addOverrideDescriptor(override1.getAbsolutePath());
+ webApp.addOverrideDescriptor(override2.getAbsolutePath());
+ WebAppPropertyConverter.toProperties(webApp, propsFile, contextXml);
+
+ assertTrue(propsFile.exists());
+ Properties props = new Properties();
+ props.load(new FileInputStream(propsFile));
+ assertEquals("/foo", props.get(WebAppPropertyConverter.CONTEXT_PATH));
+ assertEquals(contextXml, props.get(WebAppPropertyConverter.CONTEXT_XML));
+ assertEquals(tmpDir.getAbsolutePath(), props.get(WebAppPropertyConverter.TMP_DIR));
+ assertEquals("false", props.get(WebAppPropertyConverter.TMP_DIR_PERSIST));
+ assertEquals(classesDir.getAbsolutePath(), props.get(WebAppPropertyConverter.CLASSES_DIR));
+ assertEquals(testClassesDir.getAbsolutePath(), props.get(WebAppPropertyConverter.TEST_CLASSES_DIR));
+ assertEquals(String.join(",", jar1.getAbsolutePath(), jar2.getAbsolutePath()), props.get(WebAppPropertyConverter.LIB_JARS));
+ assertEquals(war.getAbsolutePath(), props.get(WebAppPropertyConverter.WAR_FILE));
+ assertEquals(WebAppContext.WEB_DEFAULTS_XML, props.get(WebAppPropertyConverter.DEFAULTS_DESCRIPTOR));
+ assertEquals(String.join(",", override1.getAbsolutePath(), override2.getAbsolutePath()), props.get(WebAppPropertyConverter.OVERRIDE_DESCRIPTORS));
+ }
+
+ @Test
+ public void testFromProperties() throws Exception
+ {
+ File base1 = new File(testDir, "base1");
+ base1.mkdirs();
+ File base2 = new File(testDir, "base2");
+ base2.mkdirs();
+ MavenWebAppContext webApp = new MavenWebAppContext();
+ Properties props = new Properties();
+ props.setProperty(WebAppPropertyConverter.BASE_DIRS, String.join(",", base1.getAbsolutePath(), base2.getAbsolutePath()));
+ props.setProperty(WebAppPropertyConverter.CLASSES_DIR, classesDir.getAbsolutePath());
+ props.setProperty(WebAppPropertyConverter.CONTEXT_PATH, "/foo");
+ props.setProperty(WebAppPropertyConverter.CONTEXT_XML, contextXml);
+ props.setProperty(WebAppPropertyConverter.LIB_JARS, String.join(",", jar1.getAbsolutePath(), jar2.getAbsolutePath()));
+ props.setProperty(WebAppPropertyConverter.OVERRIDE_DESCRIPTORS, String.join(",", override1.getAbsolutePath(), override2.getAbsolutePath()));
+ //props.setProperty(WebAppPropertyConverter.QUICKSTART_WEB_XML, value);
+ props.setProperty(WebAppPropertyConverter.TEST_CLASSES_DIR, testClassesDir.getAbsolutePath());
+ props.setProperty(WebAppPropertyConverter.TMP_DIR, tmpDir.getAbsolutePath());
+ props.setProperty(WebAppPropertyConverter.TMP_DIR_PERSIST, "true");
+ props.setProperty(WebAppPropertyConverter.WAR_FILE, war.getAbsolutePath());
+ props.setProperty(WebAppPropertyConverter.WEB_XML, webXml.getAbsolutePath());
+ WebAppPropertyConverter.fromProperties(webApp, props, new Server(), null);
+
+ assertEquals("/embedder", webApp.getContextPath()); //the embedder-context file changes the context path
+ assertEquals(classesDir, webApp.getClasses());
+ assertEquals(testClassesDir, webApp.getTestClasses());
+ assertThat(webApp.getWebInfLib(), Matchers.contains(jar1, jar2));
+ assertThat(webApp.getOverrideDescriptors(), Matchers.contains(override1.getAbsolutePath(), override2.getAbsolutePath()));
+ assertEquals(tmpDir, webApp.getTempDirectory());
+ assertEquals(true, webApp.isTempDirectoryPersistent());
+ assertEquals(war.getAbsolutePath(), webApp.getWar());
+ assertEquals(webXml.getAbsolutePath(), webApp.getDescriptor());
+ assertThat(webApp.getBaseResource(), instanceOf(CombinedResource.class));
+
+ Resource combinedResource = webApp.getBaseResource();
+ List actual = new ArrayList<>();
+ for (Resource r : combinedResource)
+ if (r != null)
+ actual.add(r.getURI());
+ URI[] expected = new URI[]{base1.toURI(), base2.toURI()};
+ assertThat(actual, containsInAnyOrder(expected));
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/it/IntegrationTestGetContent.java b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/it/IntegrationTestGetContent.java
new file mode 100644
index 00000000000..d115e1a7734
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/java/org/eclipse/jetty/ee11/maven/plugin/it/IntegrationTestGetContent.java
@@ -0,0 +1,120 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.maven.plugin.it;
+
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+import org.awaitility.Awaitility;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.StringUtil;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class IntegrationTestGetContent
+{
+ @Test
+ public void getContentResponse()
+ throws Exception
+ {
+ int port = getPort();
+ assertTrue(port > 0);
+ String contextPath = getContextPath();
+ if (contextPath.endsWith("/"))
+ contextPath = contextPath.substring(0, contextPath.lastIndexOf('/'));
+
+ HttpClient httpClient = new HttpClient();
+ try
+ {
+ httpClient.start();
+
+ if (Boolean.getBoolean("helloServlet"))
+ {
+ String response = httpClient.GET("http://localhost:" + port + contextPath + "/hello?name=beer").getContentAsString();
+ assertEquals("Hello beer", response.trim(), "it test " + System.getProperty("maven.it.name"));
+ response = httpClient.GET("http://localhost:" + port + contextPath + "/hello?name=foo").getContentAsString();
+ assertEquals("Hello foo", response.trim(), "it test " + System.getProperty("maven.it.name"));
+ System.out.println("helloServlet");
+ }
+ if (Boolean.getBoolean("pingServlet"))
+ {
+ System.out.println("pingServlet");
+ String response = httpClient.GET("http://localhost:" + port + contextPath + "/ping?name=beer").getContentAsString();
+ assertEquals("pong beer", response.trim(), "it test " + System.getProperty("maven.it.name"));
+ System.out.println("pingServlet ok");
+ }
+ String contentCheck = System.getProperty("contentCheck");
+ String pathToCheck = System.getProperty("pathToCheck");
+ if (StringUtil.isNotBlank(contentCheck))
+ {
+ String url = "http://localhost:" + port + contextPath;
+ if (pathToCheck != null)
+ {
+ url += pathToCheck;
+ }
+ String response = httpClient.GET(url).getContentAsString();
+ assertTrue(response.contains(contentCheck), "it test " + System.getProperty("maven.it.name") +
+ ", response not contentCheck: " + contentCheck + ", response:" + response);
+ System.out.println("contentCheck");
+ }
+ if (Boolean.getBoolean("helloTestServlet"))
+ {
+ String response = httpClient.GET("http://localhost:" + port + contextPath + "/testhello?name=beer").getContentAsString();
+ assertEquals("Hello from test beer", response.trim(), "it test " + System.getProperty("maven.it.name"));
+ response = httpClient.GET("http://localhost:" + port + contextPath + "/testhello?name=foo").getContentAsString();
+ assertEquals("Hello from test foo", response.trim(), "it test " + System.getProperty("maven.it.name"));
+ System.out.println("helloServlet");
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+
+ public static String getContextPath()
+ {
+ return System.getProperty("context.path", "/");
+ }
+
+ public static int getPort()
+ throws Exception
+ {
+ String s = System.getProperty("jetty.port.file");
+ assertNotNull(s, "jetty.port.file System property");
+ Path p = Paths.get(s);
+
+ System.err.println("Looking for port file: " + p);
+
+ Awaitility.await()
+ .pollInterval(1, TimeUnit.MILLISECONDS)
+ .atMost(30, TimeUnit.SECONDS)
+ .until(() -> Files.exists(p));
+
+ try (Reader r = Files.newBufferedReader(p);
+ LineNumberReader lnr = new LineNumberReader(r))
+ {
+ s = lnr.readLine();
+ assertNotNull(s);
+ return Integer.parseInt(s.trim());
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/embedder-context.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/embedder-context.xml
new file mode 100644
index 00000000000..4263daeb576
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/embedder-context.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ /embedder
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/embedder-jetty.xml b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/embedder-jetty.xml
new file mode 100644
index 00000000000..08157651885
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/embedder-jetty.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/jetty-logging.properties b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..1ab206d22d5
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/jetty-logging.properties
@@ -0,0 +1,6 @@
+# Jetty Logging using jetty-slf4j-impl
+#org.eclipse.jetty.maven.plugin.LEVEL=DEBUG
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.server.LEVEL=DEBUG
+#org.eclipse.jetty.http.LEVEL=DEBUG
+#org.eclipse.jetty.http.pathmap.LEVEL=DEBUG
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/root/index.html b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/root/index.html
new file mode 100644
index 00000000000..e8d4c65dd0d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/root/index.html
@@ -0,0 +1 @@
+
ROOT
\ No newline at end of file
diff --git a/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/selective-jar-test.jar b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/selective-jar-test.jar
new file mode 100644
index 00000000000..dc7dc4b596c
Binary files /dev/null and b/jetty-ee11/jetty-ee11-maven-plugin/src/test/resources/selective-jar-test.jar differ
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-alpn/pom.xml b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-alpn/pom.xml
new file mode 100644
index 00000000000..6a7cab1cd61
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-alpn/pom.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-osgi-alpn
+ jar
+ EE11 :: OSGi :: ALPN Fragment
+
+ ${project.groupId}.alpn.fragment
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+ ${bundle-symbolic-name};singleton:=true
+ Jetty OSGi ALPN Fragment
+ !javax.*;!org.eclipse.jetty.*
+ system.bundle;extension:=framework
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/pom.xml b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/pom.xml
new file mode 100644
index 00000000000..a7a2b996717
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/pom.xml
@@ -0,0 +1,149 @@
+
+
+ 4.0.0
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi
+ 12.1.0-SNAPSHOT
+
+ jetty-ee11-osgi-boot-jsp
+ EE11 :: OSGi :: Boot JSP
+ Jetty OSGi Boot JSP bundle
+
+ ${project.groupId}.boot.jsp
+ org.eclipse.jetty.ee11.osgi.boot.jsp.*
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ org.eclipse.jetty
+ jetty-deploy
+
+
+
+ org.eclipse.jetty.ee11
+ jetty-ee11-apache-jsp
+
+
+ org.eclipse.platform
+ org.eclipse.osgi
+
+
+ org.eclipse.platform
+ org.eclipse.osgi.services
+
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi-boot
+ provided
+
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ Jetty-OSGi-Jasper Integration
+
+ org.eclipse.jetty.ee11.osgi.boot
+ ${osgi.slf4j.import.packages},
+ org.eclipse.jdt.*;resolution:=optional,
+ org.eclipse.jdt.core.compiler.*;resolution:=optional,
+ com.sun.el;resolution:=optional,
+ com.sun.el.lang;resolution:=optional,
+ com.sun.el.parser;resolution:=optional,
+ com.sun.el.util;resolution:=optional,
+ jakarta.el;version="[5.0,6.0)",
+ jakarta.servlet;version="[$(version;==;${jakarta.servlet.api.version}),$(version;+;${jakarta.servlet.api.version}))",
+ jakarta.servlet.resources;version="[$(version;==;${jakarta.servlet.api.version}),$(version;+;${jakarta.servlet.api.version}))",
+ jakarta.servlet.jsp.resources;version="[$(version;==;${jakarta.servlet.jsp.api.version}),$(version;+;${jakarta.servlet.jsp.api.version}))",
+ jakarta.servlet.jsp;version="[$(version;==;${jakarta.servlet.jsp.api.version}),$(version;+;${jakarta.servlet.jsp.api.version}))",
+ jakarta.servlet.jsp.el;version="[$(version;==;${jakarta.servlet.jsp.api.version}),$(version;+;${jakarta.servlet.jsp.api.version}))",
+ jakarta.servlet.jsp.tagext;version="[$(version;==;${jakarta.servlet.jsp.api.version}),$(version;+;${jakarta.servlet.jsp.api.version}))",
+ jakarta.servlet.jsp.jstl.core;version="2.0";resolution:=optional,
+ jakarta.servlet.jsp.jstl.fmt;version="2.0";resolution:=optional,
+ jakarta.servlet.jsp.jstl.sql;version="2.0";resolution:=optional,
+ jakarta.servlet.jsp.jstl.tlv;version="2.0";resolution:=optional,
+ org.apache.el;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.el.lang;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.el.stream;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.el.util;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.el.parser;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.compiler;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.compiler.util;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.compiler.tagplugin;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.runtime;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.security;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.servlet;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.tagplugins.jstl;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.util;version="[$(version;===;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.jasper.xmlparser;version="[$(version;==;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))";resolution:=optional,
+ org.apache.taglibs.standard;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.extra.spath;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.functions;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.lang.jstl;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.lang.jstl.parser;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.lang.jstl.test;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.lang.jstl.test.beans;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.lang.support;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.resources;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.common.core;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.common.fmt;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.common.sql;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.common.xml;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.el.core;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.el.fmt;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.el.sql;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.el.xml;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.rt.core;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.rt.fmt;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.rt.sql;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tag.rt.xml;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tei;version="2.0";resolution:=optional,
+ org.apache.taglibs.standard.tlv;version="2.0";resolution:=optional,
+ org.apache.tomcat;version="[10,11)";resolution:=optional,
+ org.eclipse.jetty.ee11.jsp;version="[$(version;===;${parsedVersion.osgiVersion}),$(version;==+;${parsedVersion.osgiVersion}))";resolution:=optional,
+ org.osgi.*,
+ org.xml.*;resolution:=optional,
+ org.xml.sax.*;resolution:=optional,
+ javax.xml.*;resolution:=optional,
+ org.w3c.dom;resolution:=optional,
+ org.w3c.dom.ls;resolution:=optional,
+ javax.xml.parser;resolution:=optional
+ org.eclipse.jetty.ee11.jsp.*;version="[$(version;===;${parsedVersion.osgiVersion}),$(version;==+;${parsedVersion.osgiVersion}))",
+ org.apache.jasper.*;version="[$(version;===;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))",
+ org.apache.el.*;version="[$(version;===;${jspImpl.osgiVersion}),$(version;+;${jspImpl.osgiVersion}))"
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ set-jsp-api-version
+
+ parse-version
+
+ validate
+
+ ${jsp.impl.version}
+ jspImpl
+
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/src/main/java/org/eclipse/jetty/ee11/osgi/boot/jsp/FragmentActivator.java b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/src/main/java/org/eclipse/jetty/ee11/osgi/boot/jsp/FragmentActivator.java
new file mode 100644
index 00000000000..d75597d4b8d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/src/main/java/org/eclipse/jetty/ee11/osgi/boot/jsp/FragmentActivator.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.osgi.boot.jsp;
+
+import org.eclipse.jetty.ee11.osgi.boot.EE11Activator;
+import org.eclipse.jetty.osgi.util.ServerClasspathContributor;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * FragmentActivator
+ *
+ * Sets up support for jsp and jstl. All relevant jsp jars must also be installed
+ * into the osgi environment.
+ *
+ * NOTE that as this is part of a bundle fragment, this activator is NOT
+ * called by the OSGi environment. Instead, the org.eclipse.jetty.osgi.util.internal.PackageAdminTracker
+ * simulates fragment activation and causes this class's start() method to be called.
+ *
+ *
+ * The package of this class MUST match the Bundle-SymbolicName of this fragment
+ * in order for the PackageAdminTracker to find it.
+ *
+ */
+public class FragmentActivator implements BundleActivator
+{
+ ServerClasspathContributor _tldClasspathContributor;
+
+ @Override
+ public void start(BundleContext context) throws Exception
+ {
+ //Register a class that will provide the identity of bundles that
+ //contain TLDs and therefore need to be scanned.
+ _tldClasspathContributor = new TLDServerClasspathContributor();
+ EE11Activator.registerServerClasspathContributor(_tldClasspathContributor);
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception
+ {
+ EE11Activator.unregisterServerClasspathContributor(_tldClasspathContributor);
+ _tldClasspathContributor = null;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/src/main/java/org/eclipse/jetty/ee11/osgi/boot/jsp/TLDServerClasspathContributor.java b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/src/main/java/org/eclipse/jetty/ee11/osgi/boot/jsp/TLDServerClasspathContributor.java
new file mode 100644
index 00000000000..12498cbced9
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-jsp/src/main/java/org/eclipse/jetty/ee11/osgi/boot/jsp/TLDServerClasspathContributor.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.osgi.boot.jsp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jetty.ee11.osgi.boot.OSGiMetaInfConfiguration;
+import org.eclipse.jetty.osgi.util.ServerClasspathContributor;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+
+/**
+ * @author janb
+ *
+ */
+public class TLDServerClasspathContributor implements ServerClasspathContributor
+{
+
+ /**
+ * Name of a class that belongs to the jstl bundle. From that class
+ * we locate the corresponding bundle.
+ */
+ private static String JSTL_BUNDLE_CLASS = "org.apache.taglibs.standard.tag.el.core.WhenTag";
+
+ @Override
+ public List getScannableBundles()
+ {
+ if (!isJspAvailable())
+ {
+ return Collections.emptyList();
+ }
+
+ List scannableBundles = new ArrayList<>();
+ List bundleNames = Collections.emptyList();
+
+ String tmp = System.getProperty(OSGiMetaInfConfiguration.SYS_PROP_TLD_BUNDLES); //comma separated exact names
+
+ if (tmp != null)
+ {
+ String[] names = tmp.split(", \n\r\t");
+ bundleNames = Arrays.asList(names);
+ }
+
+ Bundle jstlBundle = findJstlBundle();
+ if (jstlBundle != null)
+ scannableBundles.add(jstlBundle);
+
+ final Bundle[] bundles = FrameworkUtil.getBundle(getClass()).getBundleContext().getBundles();
+ for (Bundle bundle : bundles)
+ {
+ if (bundleNames.contains(bundle.getSymbolicName()))
+ scannableBundles.add(bundle);
+ }
+
+ return scannableBundles;
+ }
+
+ /**
+ * Check that jsp is on the classpath
+ *
+ * @return true if jsp is available in the environment
+ */
+ public boolean isJspAvailable()
+ {
+ try
+ {
+ getClass().getClassLoader().loadClass("org.apache.jasper.servlet.JspServlet");
+ return true;
+ }
+ catch (Exception e)
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Find the bundle that contains a jstl implementation class, which assumes that
+ * the jstl taglibs will be inside the same bundle.
+ *
+ * @return Bundle contains the jstl implementation class
+ */
+ public Bundle findJstlBundle()
+ {
+ Class> jstlClass = null;
+
+ try
+ {
+ jstlClass = getClass().getClassLoader().loadClass(JSTL_BUNDLE_CLASS);
+ return FrameworkUtil.getBundle(jstlClass);
+ }
+ catch (ClassNotFoundException e)
+ {
+ //no jstl do nothing
+ }
+
+ return null;
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/pom.xml b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/pom.xml
new file mode 100644
index 00000000000..9d6cf4bbf1e
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/pom.xml
@@ -0,0 +1,41 @@
+
+
+ org.eclipse.jetty.ee11.osgi
+ jetty-ee11-osgi
+ 12.0.0-SNAPSHOT
+
+ 4.0.0
+ jetty-ee11-osgi-boot-warurl
+ EE11 :: OSGi :: Boot :: Warurl
+ Jetty OSGi Boot-Warurl bundle
+
+ ${project.groupId}.boot.warurl
+ org.eclipse.jetty.ee11.osgi.boot.warurl.*
+
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.eclipse.platform
+ org.eclipse.osgi
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ RFC66 War URL
+ org.eclipse.jetty.ee11.osgi.boot.warurl.WarUrlActivator
+
+
+
+
+
+
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/WarUrlActivator.java b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/WarUrlActivator.java
new file mode 100644
index 00000000000..a20b66ba97b
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/WarUrlActivator.java
@@ -0,0 +1,69 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.osgi.boot.warurl;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.url.URLConstants;
+import org.osgi.service.url.URLStreamHandlerService;
+
+/**
+ * Register the factory to handle the war scheme specified by rfc66
+ * when the bundle is activated.
+ */
+public class WarUrlActivator implements BundleActivator
+{
+
+ private ServiceRegistration _reg;
+
+ /**
+ * Register the url stream handler factory.
+ *
+ * @param context the {@link BundleContext} to use
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void start(BundleContext context) throws Exception
+ {
+ Dictionary props = new Hashtable();
+ props.put(URLConstants.URL_HANDLER_PROTOCOL, new String[]{"war"});
+ context.registerService(URLStreamHandlerService.class.getName(),
+ new WarUrlStreamHandler(), props);
+ }
+
+ /**
+ * Remove the url stream handler. (probably not required,
+ * as osgi might shutdown every registered service
+ * by default: need test)
+ */
+ @Override
+ public void stop(BundleContext context) throws Exception
+ {
+ try
+ {
+ if (_reg != null)
+ {
+ _reg.unregister();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/WarUrlStreamHandler.java b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/WarUrlStreamHandler.java
new file mode 100644
index 00000000000..96c3300fc2f
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/WarUrlStreamHandler.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.osgi.boot.warurl;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.jar.Manifest;
+
+import org.eclipse.jetty.ee11.osgi.boot.warurl.internal.WarBundleManifestGenerator;
+import org.eclipse.jetty.ee11.osgi.boot.warurl.internal.WarURLConnection;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.osgi.service.url.AbstractURLStreamHandlerService;
+
+/**
+ * RFC-66: support for the "war" protocol We are reusing the parsing of the
+ * query string from jetty. If we wanted to not depend on jetty at all we could
+ * duplicate that method here
+ */
+public class WarUrlStreamHandler extends AbstractURLStreamHandlerService
+{
+
+ /**
+ * @param url The url with a war scheme
+ */
+ @Override
+ public URLConnection openConnection(URL url) throws IOException
+ {
+ // remove the war scheme.
+ URL actual = new URL(url.toString().substring("war:".length()));
+
+ // let's do some basic tests: see if this is a folder or not.
+ // if it is a folder. we will try to support it.
+ if (actual.getProtocol().equals("file"))
+ {
+ File file = new File(URIUtil.encodePath(actual.getPath()));
+ if (file.exists())
+ {
+ if (file.isDirectory())
+ {
+ // TODO (not mandatory for rfc66 though)
+ }
+ }
+ }
+
+ // if (actual.toString().startsWith("file:/") && ! actual.to)
+ URLConnection ori = (URLConnection)actual.openConnection();
+ ori.setDefaultUseCaches(Resource.getDefaultUseCaches());
+ JarURLConnection jarOri = null;
+ try
+ {
+ if (ori instanceof JarURLConnection)
+ {
+ jarOri = (JarURLConnection)ori;
+ }
+ else
+ {
+ jarOri = (JarURLConnection)new URL("jar:" + actual.toString() + "!/").openConnection();
+ jarOri.setDefaultUseCaches(Resource.getDefaultUseCaches());
+ }
+ Manifest mf = WarBundleManifestGenerator.createBundleManifest(jarOri.getManifest(), url, jarOri.getJarFile());
+ try
+ {
+ jarOri.getJarFile().close();
+ jarOri = null;
+ }
+ catch (Throwable ignored)
+ {
+ }
+ return new WarURLConnection(actual, mf);
+ }
+ finally
+ {
+ if (jarOri != null)
+ try
+ {
+ jarOri.getJarFile().close();
+ }
+ catch (Throwable ignored)
+ {
+ }
+ }
+ }
+}
diff --git a/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/internal/WarBundleManifestGenerator.java b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/internal/WarBundleManifestGenerator.java
new file mode 100644
index 00000000000..4496e9a693d
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-osgi/jetty-ee11-osgi-boot-warurl/src/main/java/org/eclipse/jetty/ee11/osgi/boot/warurl/internal/WarBundleManifestGenerator.java
@@ -0,0 +1,276 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.osgi.boot.warurl.internal;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.osgi.framework.Constants;
+
+public class WarBundleManifestGenerator
+{
+ /**
+ * missing version in the url and in the manifest
+ * use this one.
+ */
+ private static final String MISSING_VERSION = "0.0.1.unknown";
+ private static final String MISSING_MANIFEST_VERSION = "2";
+
+ public static Manifest createBundleManifest(Manifest originalManifest, URL url, JarFile jarFile)
+ {
+ Manifest res = new Manifest();
+ res.getMainAttributes().putAll(
+ createBundleManifest(originalManifest.getMainAttributes(),
+ url.toString(), jarFile));
+ return res;
+ }
+
+ private static Attributes createBundleManifest(Attributes originalManifest, String url, JarFile jarFile)
+ {
+ HashMap res = new HashMap();
+ for (Entry