cloud.piranha.micro.core.MicroInnerDeployer Maven / Gradle / Ivy
Show all versions of piranha-micro-core Show documentation
/*
* Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package cloud.piranha.micro.core;
import static java.lang.Boolean.TRUE;
import static java.lang.System.Logger.Level.INFO;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static javax.xml.xpath.XPathConstants.NODESET;
import static org.jboss.jandex.AnnotationTarget.Kind.CLASS;
import static org.jboss.jandex.AnnotationTarget.Kind.FIELD;
import static org.jboss.jandex.AnnotationTarget.Kind.METHOD;
import static org.jboss.jandex.DotName.createSimple;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.net.URLConnection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.Index;
import org.jboss.jandex.IndexReader;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.asset.ArchiveAsset;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import cloud.piranha.core.api.AnnotationManager;
import cloud.piranha.core.api.WebApplication;
import cloud.piranha.core.api.WebApplicationExtension;
import cloud.piranha.core.impl.DefaultWebApplication;
import cloud.piranha.core.impl.DefaultWebApplicationExtensionContext;
import cloud.piranha.extension.annotationscan.internal.InternalAnnotationScanAnnotationManager;
import cloud.piranha.http.api.HttpServer;
import cloud.piranha.http.impl.DefaultHttpServer;
import cloud.piranha.http.webapp.HttpWebApplicationServer;
import cloud.piranha.resource.shrinkwrap.GlobalArchiveStreamHandler;
import cloud.piranha.resource.shrinkwrap.ShrinkWrapResource;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Priority;
import jakarta.annotation.Resource;
import jakarta.annotation.Resources;
import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs;
import jakarta.servlet.annotation.HandlesTypes;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.annotation.WebServlet;
/**
* Deploys a shrinkwrap application archive to a newly started embedded Piranha
* instance.
*
*
* This class is expected to be run within in its own inner (isolated) class
* loader
*
* @author arjan
*/
public class MicroInnerDeployer {
/**
* Defines the attribute name for the MicroPiranha reference.
*/
static final String MICRO_PIRANHA = "cloud.piranha.micro.MicroPiranha";
/**
* Stores the logger.
*/
private static final Logger LOGGER = System.getLogger(MicroInnerDeployer.class.getName());
/**
* Stores the web annotations.
*/
String[] webAnnotations = new String[]{
// Servlet
WebServlet.class.getName(),
WebListener.class.getName(),
WebInitParam.class.getName(),
WebFilter.class.getName(),
ServletSecurity.class.getName(),
MultipartConfig.class.getName(),
// REST
"jakarta.ws.rs.Path",
"jakarta.ws.rs.ext.Provider",
"jakarta.ws.rs.ApplicationPath",
// Faces
"jakarta.faces.lifecycle.ClientWindowScoped",
"jakarta.faces.component.behavior.FacesBehavior",
"jakarta.faces.render.FacesBehaviorRenderer",
"jakarta.faces.component.FacesComponent",
"jakarta.faces.annotation.FacesConfig",
"jakarta.faces.convert.FacesConverter",
"jakarta.faces.model.FacesDataModel",
"jakarta.faces.render.FacesRenderer",
"jakarta.faces.validator.FacesValidator",
"jakarta.faces.flow.builder.FlowBuilderParameter",
"jakarta.faces.flow.builder.FlowDefinition",
"jakarta.faces.flow.FlowScoped",
"jakarta.faces.event.ListenerFor",
"jakarta.faces.event.ListenersFor",
"jakarta.faces.event.NamedEvent",
"jakarta.faces.push.Push",
"jakarta.faces.application.ResourceDependencies",
"jakarta.faces.application.ResourceDependency",
"jakarta.faces.view.ViewScoped",
// Persistence
"jakarta.persistence.Entity",
"jakarta.persistence.Embeddable",
"jakarta.persistence.Converter",
"jakarta.persistence.MappedSuperclass",
// General
DeclareRoles.class.getName(), // Not Servlet, but often used on Servlets
DenyAll.class.getName(),
PermitAll.class.getName(),
RolesAllowed.class.getName(),
RunAs.class.getName(),
PostConstruct.class.getName(),
PreDestroy.class.getName(),
Priority.class.getName(),
Resource.class.getName(),
Resources.class.getName(),};
/**
* Stores the instances.
*/
String[] instances = new String[]{
// REST
"jakarta.ws.rs.core.Application",
// Faces
"jakarta.faces.convert.Converter",
"jakarta.faces.model.DataModel",
"jakarta.faces.event.PhaseListener",
"jakarta.faces.render.Renderer",
"jakarta.faces.component.UIComponent",
"jakarta.faces.validator.Validator",
};
/**
* Stores the HTTP server.
*/
private HttpServer httpServer;
/**
* Start the application.
*
* @param applicationArchive the application archive.
* @param classLoader the classloader.
* @param handlers the handlers.
* @param config the configuration.
* @return the map.
*/
public Map start(Archive> applicationArchive, ClassLoader classLoader, Map> handlers, Map config) {
try {
WebApplication webApplication = getWebApplication(applicationArchive, classLoader);
LOGGER.log(INFO,
"Starting web application " + applicationArchive.getName() + " on Piranha Micro " + webApplication.getAttribute(MICRO_PIRANHA));
// The global archive stream handler is set to resolve "shrinkwrap://" URLs (created from strings).
// Such URLs come into being primarily when code takes resolves a class or resource from the class loader by URL
// and then takes the string form of the URL representing the class or resource.
GlobalArchiveStreamHandler streamHandler = new GlobalArchiveStreamHandler(webApplication);
// Life map to the StaticURLStreamHandlerFactory used by the root class loader
handlers.put("shrinkwrap", streamHandler::connect);
// Source of annotations
Index index = getIndex();
// Target of annotations
AnnotationManager annotationManager = new InternalAnnotationScanAnnotationManager();
webApplication.getManager().setAnnotationManager(annotationManager);
// Copy annotations from our "annotations" collection from source index to target manager
forEachWebAnnotation(webAnnotation -> addAnnotationToIndex(index, webAnnotation, annotationManager));
// Collect sub-classes/interfaces of our "instances" collection from source index to target manager
forEachInstance(instanceClass -> addInstanceToIndex(index, instanceClass, annotationManager));
// Collect any sub-classes/interfaces from any HandlesTypes annotation
getAnnotations(index, HandlesTypes.class)
.map(this::getTarget)
.forEach(annotationTarget
-> getAnnotationInstances(annotationTarget, HandlesTypes.class)
.map(HandlesTypes.class::cast)
.forEach(handlesTypesInstance
-> stream(handlesTypesInstance.value()).forEach(e -> {
if (e.isAnnotation()) {
addAnnotationToIndex(index, e, annotationManager);
} else {
addInstanceToIndex(index, e, annotationManager);
}
})));
// Setup the default identity store, which is used as the default "username and roles database" for
// (Servlet) security.
initIdentityStore(webApplication);
setApplicationContextPath(webApplication, config, applicationArchive);
DefaultWebApplicationExtensionContext extensionContext = new DefaultWebApplicationExtensionContext();
for (WebApplicationExtension extension : ServiceLoader.load(WebApplicationExtension.class)) {
extensionContext.add(extension);
}
extensionContext.configure(webApplication);
webApplication.initialize();
webApplication.start();
if ((boolean) config.get("micro.http.start")) {
HttpWebApplicationServer webApplicationServer = new HttpWebApplicationServer();
webApplicationServer.addWebApplication(webApplication);
httpServer = new DefaultHttpServer();
httpServer.setServerPort((Integer) config.get("micro.port"));
httpServer.setSSL(Boolean.getBoolean("piranha.http.ssl"));
httpServer.setHttpServerProcessor(webApplicationServer);
httpServer.start();
}
return Map.of(
"deployedServlets", webApplication.getServletRegistrations().keySet(),
"deployedApplication", new MicroInnerApplication(webApplication),
"deployedContextRoot", webApplication.getContextPath());
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (Exception e) {
throw e;
}
}
void setApplicationContextPath(WebApplication webApplication, Map config, Archive> applicationArchive) {
if (TRUE.equals(config.get("micro.rootIsWarName"))) {
String contextPath = applicationArchive.getName();
if (contextPath != null && contextPath.endsWith(".war")) {
contextPath = "/" + contextPath.substring(0, contextPath.length()-4);
}
webApplication.setContextPath(contextPath);
} else {
String contextPath = (String) config.get("micro.root");
if (contextPath != null) {
webApplication.setContextPath(contextPath);
}
}
}
WebApplication getWebApplication(Archive> archive, ClassLoader newClassLoader) {
WebApplication webApplication = new DefaultWebApplication();
webApplication.setClassLoader(newClassLoader);
// The main resource representing the (war) archive itself.
webApplication.addResource(new ShrinkWrapResource(archive));
// Get the list of embedded archives containing a "/META-INF/resources" folder.
Node resourceNodes = archive.get("/META-INF/piranha/resource-libs");
if (resourceNodes != null) {
for (Node resourceNode : resourceNodes.getChildren()) {
ArchiveAsset resourceArchiveAsset = (ArchiveAsset) resourceNode.getAsset();
// Add the archive as a resource with the "/META-INF/resources" folder shifted to its root
webApplication.addResource(new ShrinkWrapResource("/META-INF/resources", resourceArchiveAsset.getArchive()));
}
}
return webApplication;
}
/**
* Stop the application.
*/
public void stop() {
if (httpServer != null) {
httpServer.stop();
}
}
Index getIndex() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try (InputStream indexStream = classLoader.getResourceAsStream("META-INF/piranha.idx")) {
return new IndexReader(indexStream).read();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
void forEachWebAnnotation(Consumer super Class>> consumer) {
stream(webAnnotations)
.map(this::toClass)
.flatMap(Optional::stream)
.map(e -> (Class>) e)
.forEach(consumer);
}
void addAnnotationToIndex(Index index, Class> webAnnotation, AnnotationManager annotationManager) {
getAnnotations(index, webAnnotation)
// Get the annotation target and annotation instance corresponding to the
// (raw/abstract) indexed annotation
.map(this::getTarget)
.filter(Objects::nonNull)
.forEach(annotationTarget
-> getAnnotationInstances(annotationTarget, webAnnotation)
.forEach(annotationInstance
-> // Store the matching annotation instance (@WebServlet(name=...)
// and annotation target (@WebServlet public class Target) in the manager
annotationManager.addAnnotation(
new DefaultAnnotationInfo<>(annotationInstance, annotationTarget))));
}
void addInstanceToIndex(Index index, Class> instanceClass, AnnotationManager annotationManager) {
getInstances(index, instanceClass)
.map(this::getTarget)
.forEach(implementingClass
-> annotationManager.addInstance(instanceClass, implementingClass));
}
Optional> toClass(String className) {
try {
return Optional.of(Class.forName(className, true, Thread.currentThread().getContextClassLoader()));
} catch (ClassNotFoundException e) {
return Optional.empty();
}
}
Stream getAnnotations(Index index, Class> webAnnotation) {
return index.getAnnotations(
createSimple(webAnnotation.getName()))
.stream();
}
Stream getInstances(Index index, Class> instanceClass) {
return Stream.concat(
index.getAllKnownSubclasses(
createSimple(instanceClass.getName()))
.stream(),
index.getAllKnownImplementors(
createSimple(instanceClass.getName()))
.stream());
}
Class> getTarget(AnnotationInstance annotationInstance) {
return getTarget(annotationInstance.target());
}
Class> getTarget(AnnotationTarget target) {
try {
if (target.kind() == CLASS) {
return Class.forName(
target.asClass().toString(), false,
Thread.currentThread().getContextClassLoader());
}
if (target.kind() == FIELD) {
return Class.forName(
target.asField().declaringClass().toString(), false,
Thread.currentThread().getContextClassLoader());
}
if (target.kind() == METHOD) {
return Class.forName(
target.asMethod().declaringClass().toString(), false,
Thread.currentThread().getContextClassLoader());
}
return null;
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}
Stream getAnnotationInstances(Class> target, Class> annotationType) {
return stream(target.getAnnotations())
.filter(e -> e.annotationType().isAssignableFrom(annotationType));
}
void forEachInstance(Consumer super Class>> consumer) {
stream(instances)
.map(this::toClass)
.flatMap(Optional::stream)
.map(e -> (Class>) e)
.forEach(consumer);
}
void getCallerCredentials(String callersAsXml) {
if (isEmpty(callersAsXml)) {
return;
}
try {
XPath xPath = XPathFactory
.newInstance()
.newXPath();
NodeList nodes = (NodeList) xPath
.evaluate(
"//caller",
DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(callersAsXml.getBytes())),
NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
NamedNodeMap callerAttributes = nodes.item(i).getAttributes();
String caller = callerAttributes.getNamedItem("callername").getNodeValue();
String password = callerAttributes.getNamedItem("password").getNodeValue();
String groups = callerAttributes.getNamedItem("groups").getNodeValue();
InMemoryIdentityStore.addCredential(caller, password, asList(groups.split(",")));
}
} catch (SAXException | IOException | ParserConfigurationException | XPathExpressionException e) {
LOGGER.log(Level.WARNING, "Unable to get caller credentials", e);
}
}
void initIdentityStore(WebApplication webApplication) throws IOException {
String callers = System.getProperty("io.piranha.identitystore.callers");
if (callers == null) {
InputStream xmlStream = webApplication.getResourceAsStream("WEB-INF/piranha-callers.xml");
if (xmlStream != null) {
callers = new String(xmlStream.readAllBytes(), UTF_8);
}
}
getCallerCredentials(callers);
}
private boolean isEmpty(String string) {
return string == null || string.isEmpty();
}
}