com.flowlogix.util.ShrinkWrapManipulator Maven / Gradle / Ivy
/*
* Copyright (C) 2011-2024 Flow Logix, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.flowlogix.util;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.container.ClassContainer;
import org.jboss.shrinkwrap.api.container.ResourceContainer;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.archive.importer.MavenImporter;
import org.omnifaces.util.Lazy;
import org.slf4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
/**
* modifies xml files inside the archive according to xpath and function
*
* Examples:
* {@snippet class="com.flowlogix.demo.util.ShrinkWrapDemo" region="productionMode"}
* {@snippet class="com.flowlogix.demo.util.ShrinkWrapDemo" region="persistence"}
*
* @author lprimak
*/
@Slf4j
public class ShrinkWrapManipulator {
@RequiredArgsConstructor
public static class Action {
private final String path;
private final Consumer func;
private final boolean optional;
public Action(String path, Consumer func) {
this(path, func, false);
}
}
static final String DEFAULT_SSL_PROPERTY = "sslPort";
static final int DEFAULT_SSL_PORT = 8181;
final Lazy builder = new Lazy<>(this::createDocumentBuilder);
final Lazy transformer = new Lazy<>(this::createTransformer);
/**
* Simple method to create ShrinkWrap (Arquillian archive from existing maven POM file
*
* @param archiveType
* @return
* @param ShrinkWrap archive type
*/
public static > TT createDeployment(Class archiveType) {
return createDeployment(archiveType, name -> name);
}
/**
* Simple method to create ShrinkWrap (Arquillian archive from existing maven POM file
*
* @param archiveType
* @param nameTransformer transforms the UUID to a more suitable name
* @return new archive
* @param ShrinkWrap archive type
*/
public static >
TT createDeployment(Class archiveType, UnaryOperator nameTransformer) {
char firstLetter = archiveType.getSimpleName().toLowerCase().charAt(0);
return createDeployment(archiveType, String.format("s%s.%car",
nameTransformer.apply(UUID.randomUUID().toString()), firstLetter));
}
/**
* Simple method to create ShrinkWrap (Arquillian) archive from existing maven POM file
*
* @param archiveType
* @param archiveName
* @return new archive
* @param ShrinkWrap archive type
*/
public static > TT createDeployment(Class archiveType, @NonNull String archiveName) {
TT deployment = ShrinkWrap.create(MavenImporter.class, archiveName)
.loadPomFromFile("pom.xml").importBuildOutput()
.as(archiveType);
if (deployment instanceof ClassContainer>) {
var containerDeployment = ((ClassContainer>) deployment);
optionalDeploymentOp(() -> containerDeployment
.addClass("com.flowlogix.testcontainers.PayaraServerLifecycleExtension"));
containerDeployment.addPackages(true, "org.assertj");
}
return deployment;
}
/**
* Logs the contents of the archive to the consumer
*
* @param archive to log
* @param consumer to log the contents
* @return the same archive
* @param ShrinkWrap archive type
*/
public static > TT logArchiveContents(TT archive, Consumer consumer) {
consumer.accept(archive.toString(true));
return archive;
}
/**
* Adds payara-web.xml to the archive with the specified class delegation
*
* @param archive to modify
* @param delegate whether to delegate or not
* @return the same archive
* @param ShrinkWrap archive type
*/
public static > TT payaraClassDelegation(TT archive, boolean delegate) {
if (archive instanceof WebArchive webArchive) {
webArchive.addAsWebInfResource(
new StringAsset(" "
.formatted(delegate)), "payara-web.xml");
} else {
getLogger().warn("Cannot add payara-web.xml to non-WebArchive");
}
return archive;
}
/**
* Adds SLF4J to the archive
*
* @param archive to modify
* @return the same archive
* @param ShrinkWrap archive type
*/
public static > TT packageSlf4j(TT archive) {
payaraClassDelegation(archive, false);
if (archive instanceof WebArchive webArchive) {
String slf4jServiceProvider = "services/org.slf4j.spi.SLF4JServiceProvider";
webArchive.addPackages(true, Logger.class.getPackage())
.addAsWebInfResource(String.format("META-INF/%s", slf4jServiceProvider),
String.format("classes/META-INF/%s", slf4jServiceProvider));
} else {
getLogger().warn("Cannot add SLF4J to non-WebArchive");
}
return archive;
}
/**
* modifies web.xml according to xpath and method
*
* @param archive to modify
* @param actions list of actions to perform
*/
public WebArchive webXmlXPath(WebArchive archive, List actions) {
var asset = "WEB-INF/web.xml";
archive.setWebXML(new StringAsset(manipulateXml(archive, actions, asset)));
return archive;
}
/**
* modifies persistence.xml according to xpath and method
*
* @param archive to modify
* @param actions list of actions to perform
*/
@SuppressWarnings("unchecked")
public > Archive persistenceXmlXPath(Archive archive, List actions) {
var asset = "META-INF/persistence.xml";
((ResourceContainer) archive).addAsResource(new StringAsset(manipulateXml(archive, actions,
"WEB-INF/classes/" + asset)), asset);
return archive;
}
/**
* Transform http to https URL using {@code sslPort} system property,
* and default port 8181 if system property is not defined
*
* @param httpUrl http URL
* @return https URL
*/
@SuppressWarnings("MagicNumber")
public static URL toHttpsURL(URL httpUrl) {
return toHttpsURL(httpUrl, DEFAULT_SSL_PROPERTY, DEFAULT_SSL_PORT);
}
/**
* Transform http to https URL using the specified system property and default port,
* if the system property is not defined
*
* @param httpUrl http URL
* @param sslPortPropertyName
* @param defaultPort
* @return https URL
*/
@SneakyThrows({URISyntaxException.class, MalformedURLException.class})
public static URL toHttpsURL(URL httpUrl, String sslPortPropertyName, int defaultPort) {
if (httpUrl.getProtocol().endsWith("s")) {
return httpUrl;
}
int sslPort = Integer.getInteger(sslPortPropertyName, defaultPort);
return new URI(httpUrl.getProtocol() + "s", null, httpUrl.getHost(), sslPort,
httpUrl.getPath(), null, null).toURL();
}
/**
* Constructs XPath for web.xml context param
*
* @param paramName
* @return XPath for web.xml context param
*/
public static String getContextParamValue(String paramName) {
return String.format("//web-app/context-param[param-name = '%s']/param-value", paramName);
}
/**
* Parse XML file from the archive, perform actions to modify the file,
* and return a string representing the modified XML file
* @param archive to retrieve the xml file from
* @param actions to perform on the xml file
* @param xmlFileName xml file name to retrive from the archive
* @return string representation of the modified xml file
*/
@SneakyThrows
public > String manipulateXml(Archive archive, List actions, String xmlFileName) {
Document xmlDocument;
try (InputStream strm = archive.get(xmlFileName).getAsset().openStream()) {
xmlDocument = builder.get().parse(strm);
}
var xpath = XPathFactory.newInstance().newXPath();
for (Action action : actions) {
var expr = xpath.compile(action.path);
Node node = (Node) expr.evaluate(xmlDocument, XPathConstants.NODE);
runActionOnNode(action, node);
}
StringWriter writer = new StringWriter();
transformer.get().transform(new DOMSource(xmlDocument), new StreamResult(writer));
return writer.getBuffer().toString();
}
static void runActionOnNode(Action action, Node node) {
if (node == null && action.optional) {
log.debug("Optional path {} ignored", action.path);
} else {
action.func.accept(node);
}
}
static Logger getLogger() {
return log;
}
@SneakyThrows(ParserConfigurationException.class)
private DocumentBuilder createDocumentBuilder() {
return DocumentBuilderFactory.newInstance().newDocumentBuilder();
}
@SneakyThrows(TransformerConfigurationException.class)
private Transformer createTransformer() {
return TransformerFactory.newInstance().newTransformer();
}
private static void optionalDeploymentOp(Runnable operator) {
try {
operator.run();
} catch (Throwable e) {
log.debug("Could not add optional class to deployment", e);
}
}
}