diff --git a/src/main/java/io/openliberty/tools/common/CommonLoggerI.java b/src/main/java/io/openliberty/tools/common/CommonLoggerI.java index 62e859974..809c6a11d 100644 --- a/src/main/java/io/openliberty/tools/common/CommonLoggerI.java +++ b/src/main/java/io/openliberty/tools/common/CommonLoggerI.java @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corporation 2019. + * (C) Copyright IBM Corporation 2019, 2026. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,27 @@ public interface CommonLoggerI { + /** + * Returns a no-op {@code CommonLoggerI} that silently discards all log calls. + * Useful when a logger instance is required by an API but no output is desired. + */ + static CommonLoggerI noop() { + return NoopLogger.INSTANCE; + } + + /** Singleton backing {@link #noop()}. */ + final class NoopLogger implements CommonLoggerI { + static final NoopLogger INSTANCE = new NoopLogger(); + private NoopLogger() {} + @Override public void debug(String msg) {} + @Override public void debug(String msg, Throwable e) {} + @Override public void debug(Throwable e) {} + @Override public void warn(String msg) {} + @Override public void info(String msg) {} + @Override public void error(String msg) {} + @Override public boolean isDebugEnabled() { return false; } + } + /** * Log debug * diff --git a/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java b/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java index 05ce7f487..0dca466fd 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java +++ b/src/main/java/io/openliberty/tools/common/plugins/config/ServerConfigDocument.java @@ -37,10 +37,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; @@ -53,6 +49,7 @@ import org.apache.commons.io.comparator.NameFileComparator; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; @@ -75,6 +72,7 @@ public class ServerConfigDocument { private Set namelessLocations; private Set locations; private HashMap locationsAndNames; + private Map httpEndpointAttributes; private Properties props; private Properties defaultProps; private Map libertyDirectoryPropertyToFile = null; @@ -82,10 +80,14 @@ public class ServerConfigDocument { Optional springBootAppNodeLocation = Optional.empty(); Optional springBootAppNodeDocumentURI = Optional.empty(); + public static final String HTTP_PORT_ATTR = "httpPort"; + public static final String HTTPS_PORT_ATTR = "httpsPort"; + private static final XPathExpression XPATH_SERVER_APPLICATION; private static final XPathExpression XPATH_SERVER_WEB_APPLICATION; private static final XPathExpression XPATH_SERVER_SPRINGBOOT_APPLICATION; private static final XPathExpression XPATH_SERVER_ENTERPRISE_APPLICATION; + private static final XPathExpression XPATH_SERVER_HTTP_ENDPOINT; private static final XPathExpression XPATH_SERVER_INCLUDE; public static final XPathExpression XPATH_SERVER_VARIABLE; private static final XPathExpression XPATH_ALL_SERVER_APPLICATIONS; @@ -102,6 +104,7 @@ public class ServerConfigDocument { XPATH_SERVER_WEB_APPLICATION = xPath.compile("/server/webApplication"); XPATH_SERVER_SPRINGBOOT_APPLICATION = xPath.compile("/server/springBootApplication"); XPATH_SERVER_ENTERPRISE_APPLICATION = xPath.compile("/server/enterpriseApplication"); + XPATH_SERVER_HTTP_ENDPOINT = xPath.compile("/server/httpEndpoint"); XPATH_SERVER_INCLUDE = xPath.compile("/server/include"); XPATH_SERVER_VARIABLE = xPath.compile("/server/variable"); XPATH_ALL_SERVER_APPLICATIONS = xPath.compile("/server/application | /server/webApplication | /server/enterpriseApplication | /server/springBootApplication"); @@ -127,6 +130,10 @@ public Set getNamelessLocations() { return namelessLocations; } + public Map getHttpEndpointAttributes() { + return httpEndpointAttributes; + } + public Properties getProperties() { return props; } @@ -165,6 +172,7 @@ public ServerConfigDocument(CommonLoggerI log, File originalServerXMLFile, Map(); namelessLocations = new HashSet(); locationsAndNames = new HashMap(); + httpEndpointAttributes = new HashMap(); props = new Properties(); defaultProps = new Properties(); this.originalServerXMLFile = originalServerXMLFile; @@ -196,38 +204,13 @@ public ServerConfigDocument(CommonLoggerI log, File originalServerXMLFile, Map(); namelessLocations = new HashSet(); locationsAndNames = new HashMap(); + httpEndpointAttributes = new HashMap(); props = new Properties(); if (initProperties != null) props.putAll(initProperties); defaultProps = new Properties(); this.originalServerXMLFile = originalServerXMLFile; } - private DocumentBuilder getDocumentBuilder() { - DocumentBuilder docBuilder; - - DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); - docBuilderFactory.setIgnoringComments(true); - docBuilderFactory.setCoalescing(true); - docBuilderFactory.setIgnoringElementContentWhitespace(true); - docBuilderFactory.setValidating(false); - try { - docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); - docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - docBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - docBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - docBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); - docBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - docBuilderFactory.setXIncludeAware(false); - docBuilderFactory.setExpandEntityReferences(false); - docBuilder = docBuilderFactory.newDocumentBuilder(); - } catch (ParserConfigurationException e) { - // fail catastrophically if we can't create a document builder - throw new RuntimeException(e); - } - - return docBuilder; - } - /** // Server variable precedence in ascending order if defined in multiple locations. // 1. variable default values in the server.xml file @@ -290,6 +273,7 @@ public void initializeAppsLocation() throws PluginExecutionException { parseApplication(doc, XPATH_SERVER_ENTERPRISE_APPLICATION); parseApplication(doc, XPATH_SERVER_SPRINGBOOT_APPLICATION); parseNames(doc, XPATH_ALL_SERVER_APPLICATIONS); + parseHttpEndpoint(doc); parseInclude(doc); parseConfigDropinsDir(); @@ -582,6 +566,29 @@ private void parseNames(Document doc, XPathExpression expression) throws XPathEx } } + private void parseHttpEndpoint(Document doc) throws XPathExpressionException { + if (doc == null) { + return; + } + NodeList nodeList = (NodeList) XPATH_SERVER_HTTP_ENDPOINT.evaluate(doc, XPathConstants.NODESET); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node instanceof Element) { + Element elem = (Element) node; + + String httpAttribute = elem.getAttribute(HTTP_PORT_ATTR); + if (!httpAttribute.isEmpty()) { + httpEndpointAttributes.put(HTTP_PORT_ATTR, httpAttribute); + } + + String httpsAttribute = elem.getAttribute(HTTPS_PORT_ATTR); + if (!httpsAttribute.isEmpty()) { + httpEndpointAttributes.put(HTTPS_PORT_ATTR, httpsAttribute); + } + } + } + } + public String findNameForLocation(String location) { String appName = locationsAndNames.get(location); @@ -656,6 +663,7 @@ private void parseInclude(Document doc) throws XPathExpressionException, IOExcep parseApplication(inclDoc, XPATH_SERVER_SPRINGBOOT_APPLICATION); parseApplication(inclDoc, XPATH_SERVER_ENTERPRISE_APPLICATION); parseNames(inclDoc, XPATH_ALL_SERVER_APPLICATIONS); + parseHttpEndpoint(inclDoc); // handle nested include elements parseInclude(inclDoc); } @@ -699,6 +707,7 @@ private void parseDropinsFile(File file) throws IOException, XPathExpressionExce parseApplication(doc, XPATH_SERVER_SPRINGBOOT_APPLICATION); parseApplication(doc, XPATH_SERVER_ENTERPRISE_APPLICATION); parseNames(doc, XPATH_ALL_SERVER_APPLICATIONS); + parseHttpEndpoint(doc); parseInclude(doc); } } @@ -809,10 +818,8 @@ private void parseDocumentsInDirectory(File directory, ArrayList docs) * @throws SAXException */ public Document parseDocument(File file) throws FileNotFoundException, IOException { - try (FileInputStream is = new FileInputStream(file)) { - Document document= parseDocument(is); - document.setDocumentURI(file.getCanonicalPath()); - return document; + try { + return XmlDocument.parseDocument(file); } catch (SAXException ex) { // If the file was not valid XML, assume it was some other non XML // file in dropins. @@ -824,13 +831,7 @@ public Document parseDocument(File file) throws FileNotFoundException, IOExcepti private Document parseDocument(URL url) throws IOException, SAXException { URLConnection connection = url.openConnection(); try (InputStream is = connection.getInputStream()) { - return parseDocument(is); - } - } - - private Document parseDocument(InputStream in) throws SAXException, IOException { - try (InputStream ins = in) { // ins will be auto-closed - return getDocumentBuilder().parse(ins); + return XmlDocument.parseDocument(is); } } diff --git a/src/main/java/io/openliberty/tools/common/plugins/config/XmlDocument.java b/src/main/java/io/openliberty/tools/common/plugins/config/XmlDocument.java index 559acfa2f..95bf81606 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/config/XmlDocument.java +++ b/src/main/java/io/openliberty/tools/common/plugins/config/XmlDocument.java @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corporation 2017, 2024. + * (C) Copyright IBM Corporation 2017, 2026. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; @@ -38,6 +39,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.SAXException; @@ -46,8 +48,7 @@ public abstract class XmlDocument { protected Document doc; public void createDocument(String rootElement) throws ParserConfigurationException { - DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + DocumentBuilder docBuilder = getDocumentBuilder(); doc = docBuilder.newDocument(); doc.setXmlStandalone(true); Element element = doc.createElement(rootElement); @@ -55,20 +56,7 @@ public void createDocument(String rootElement) throws ParserConfigurationExcepti } public void createDocument(File xmlFile) throws ParserConfigurationException, SAXException, IOException { - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - builderFactory.setCoalescing(true); - builderFactory.setIgnoringElementContentWhitespace(true); - builderFactory.setValidating(false); - builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); - builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); - builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - builderFactory.setXIncludeAware(false); - builderFactory.setExpandEntityReferences(false); - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - doc = builder.parse(xmlFile); + doc = parseDocument(xmlFile); } public void writeXMLDocument(String fileName) throws IOException, TransformerException { @@ -112,6 +100,77 @@ protected boolean isWhitespace(Node node) { return node != null && node instanceof Text && ((Text)node).getData().trim().isEmpty(); } + /** + * Creates and returns a securely configured {@link DocumentBuilder}. + */ + public static DocumentBuilder getDocumentBuilder() { + DocumentBuilder docBuilder; + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + docBuilderFactory.setIgnoringComments(true); + docBuilderFactory.setCoalescing(true); + docBuilderFactory.setIgnoringElementContentWhitespace(true); + docBuilderFactory.setValidating(false); + try { + docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + docBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + docBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + docBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + docBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + docBuilderFactory.setXIncludeAware(false); + docBuilderFactory.setExpandEntityReferences(false); + docBuilder = docBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + // fail if we can't create a document builder + throw new RuntimeException(e); + } + return docBuilder; + } + + public static Document parseDocument(File file) throws IOException, SAXException { + try (InputStream is = Files.newInputStream(file.toPath())) { + Document document = parseDocument(is); + document.setDocumentURI(file.getCanonicalPath()); + return document; + } + } + + public static Document parseDocument(InputStream in) throws SAXException, IOException { + try (InputStream ins = in) { + return getDocumentBuilder().parse(ins); + } + } + + /** + * Returns the text content of the first element matching {@code tagName} in an XML file, + * or {@code null} if the file is absent, the tag is missing, or any parse error occurs. + */ + public static String readTextElementFromXmlFile(File xmlFile, String tagName) { + if (xmlFile == null || !xmlFile.isFile()) { + return null; + } + try { + Document doc = parseDocument(xmlFile); + NodeList nodes = doc.getElementsByTagName(tagName); + if (nodes.getLength() == 0) { + return null; + } + String text = nodes.item(0).getTextContent(); + return (text != null && !text.trim().isEmpty()) ? text.trim() : null; + } catch (Exception e) { + return null; + } + } + + /** + * Returns a {@link File} for the text content of the first element matching {@code tagName} + * in an XML file, or {@code null} if the file is absent, the tag is missing, or any parse error occurs. + */ + public static File getFileElementFromXmlFile(File xmlFile, String tagName) { + String path = readTextElementFromXmlFile(xmlFile, tagName); + return (path != null) ? new File(path) : null; + } + public static void addNewlineBeforeFirstElement(File f) throws IOException { // look for "" and add a newline byte[] contents = Files.readAllBytes(f.toPath()); diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/DevUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/DevUtil.java index c746a584f..dd6c7f286 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/DevUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/DevUtil.java @@ -74,19 +74,22 @@ import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; import javax.tools.ToolProvider; -import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import com.sun.nio.file.SensitivityWatchEventModifier; import io.openliberty.tools.ant.ServerTask; +import io.openliberty.tools.common.CommonLoggerI; +import io.openliberty.tools.common.plugins.config.ServerConfigDocument; +import io.openliberty.tools.common.plugins.config.XmlDocument; import io.openliberty.tools.common.plugins.util.ServerFeatureUtil.FeaturesPlatforms; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.NameFileFilter; @@ -98,6 +101,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; @@ -1694,13 +1698,19 @@ private String[] getContainerCommand() throws IOException, PluginExecutionExcept List heldSockets = new ArrayList(); try { if (!skipDefaultPorts) { - int httpPortToUse = findAndHoldPort(LIBERTY_DEFAULT_HTTP_PORT, false, heldSockets); - int httpsPortToUse = findAndHoldPort(LIBERTY_DEFAULT_HTTPS_PORT, false, heldSockets); + Map defaultPorts = new HashMap(); + defaultPorts.put(ServerConfigDocument.HTTP_PORT_ATTR, LIBERTY_DEFAULT_HTTP_PORT); + defaultPorts.put(ServerConfigDocument.HTTPS_PORT_ATTR, LIBERTY_DEFAULT_HTTPS_PORT); + Map effectivePorts = resolveEffectiveContainerPorts(defaultPorts); + int effectiveHttpPort = effectivePorts.get(ServerConfigDocument.HTTP_PORT_ATTR); + int effectiveHttpsPort = effectivePorts.get(ServerConfigDocument.HTTPS_PORT_ATTR); + int httpPortToUse = findAndHoldPort(effectiveHttpPort, false, heldSockets); + int httpsPortToUse = findAndHoldPort(effectiveHttpsPort, false, heldSockets); commandElements.add("-p"); - commandElements.add(httpPortToUse+":"+LIBERTY_DEFAULT_HTTP_PORT); + commandElements.add(httpPortToUse + ":" + effectiveHttpPort); commandElements.add("-p"); - commandElements.add(httpsPortToUse+":"+LIBERTY_DEFAULT_HTTPS_PORT); + commandElements.add(httpsPortToUse + ":" + effectiveHttpsPort); } if (libertyDebug) { @@ -1740,6 +1750,17 @@ private String[] getContainerCommand() throws IOException, PluginExecutionExcept commandElements.add("-v"); commandElements.add(looseApplicationProjectRoot.getAbsolutePath() + ":" + DEVMODE_DIR_NAME); + // Mount configDropins/defaults and configDropins/overrides into the container + // so Liberty sees the same variable overrides (e.g. liberty-plugin-variable-config.xml) + // that port resolution used, ensuring Liberty starts on the published port. + for (String subDir : new String[]{"defaults", "overrides"}) { + File configDropinsDir = new File(serverDirectory, "configDropins/" + subDir); + if (configDropinsDir.isDirectory()) { + commandElements.add("-v"); + commandElements.add(configDropinsDir.getAbsolutePath() + ":/config/configDropins/" + subDir); + } + } + // mount the server logs directory over the /logs used by the open liberty container as defined by the LOG_DIR env. var. File logsDir = new File(serverDirectory.getAbsolutePath(), "logs"); commandElements.add("-v"); @@ -1805,6 +1826,83 @@ private String[] getContainerCommand() throws IOException, PluginExecutionExcept } } + /** + * Resolves the effective container ports for the given map of endpoint attributes and their defaults + * in a single pass using {@link ServerConfigDocument}. + * + * @param defaultPortsByAttr a map of endpoint attribute names to their default port integers (e.g. "httpPort" -> 9080, "httpsPort" -> 9443) + * @return a map containing resolved effective ports for each attribute key + */ + Map resolveEffectiveContainerPorts(Map defaultPortsByAttr) { + Map resolvedPorts = new HashMap(defaultPortsByAttr); + try { + // Read liberty-plugin-config.xml first — it contains the resolved paths for + // configFile (custom server.xml), installDirectory, and userDirectory. + File pluginConfigXml = (buildDirectory != null) + ? new File(buildDirectory, "liberty-plugin-config.xml") : null; + File installDir = XmlDocument.getFileElementFromXmlFile(pluginConfigXml, "installDirectory"); + File userDir = XmlDocument.getFileElementFromXmlFile(pluginConfigXml, "userDirectory"); + // Use the configFile path from the plugin config if available; that is the + // user-specified server.xml (serverXmlFile parameter). Fall back to + // serverXmlFile set by watchFiles(), then to configDirectory/server.xml. + File configFileFromPlugin = XmlDocument.getFileElementFromXmlFile(pluginConfigXml, "configFile"); + File effectiveServerXml; + if (configFileFromPlugin != null && configFileFromPlugin.isFile()) { + effectiveServerXml = configFileFromPlugin; + } else if (serverXmlFile != null && serverXmlFile.isFile()) { + effectiveServerXml = serverXmlFile; + } else { + effectiveServerXml = (configDirectory != null ? new File(configDirectory, "server.xml") : null); + } + if (effectiveServerXml == null || !effectiveServerXml.isFile()) { + return resolvedPorts; + } + + // Fall back to serverDirectory so that at minimum server.env and + // bootstrap.properties are still picked up by ServerConfigDocument. + if (installDir == null) installDir = serverDirectory; + if (userDir == null) userDir = serverDirectory; + + // serverDirectory is where the plugin writes configDropins/overrides/ + // liberty-plugin-variable-config.xml (liberty.var.* values). configDirectory + // is the source config dir and does not contain configDropins at runtime. + ServerConfigDocument scd = new ServerConfigDocument( + CommonLoggerI.noop(), effectiveServerXml, + installDir, userDir, serverDirectory, serverDirectory); + + Map endpointAttrs = scd.getHttpEndpointAttributes(); + for (Map.Entry entry : defaultPortsByAttr.entrySet()) { + String attrName = entry.getKey(); + int defaultPort = entry.getValue(); + String raw = endpointAttrs.get(attrName); + if (raw == null || raw.trim().isEmpty()) { + continue; + } + raw = raw.trim(); + + // Literal integer — no variable resolution needed. + try { + resolvedPorts.put(attrName, Integer.parseInt(raw)); + continue; + } catch (NumberFormatException ignored) { /* fall through */ } + + // Variable reference — resolve through the fully-loaded variable maps. + String resolved = VariableUtility.resolveVariables( + CommonLoggerI.noop(), raw, null, + scd.getProperties(), scd.getDefaultProperties(), + scd.getLibertyDirPropertyFiles()); + if (resolved != null) { + try { + resolvedPorts.put(attrName, Integer.parseInt(resolved.trim())); + } catch (NumberFormatException ignored) { /* fall back to default */ } + } + } + } catch (Exception e) { + debug("resolveEffectiveContainerPorts: could not resolve ports, using defaults: " + e.getMessage()); + } + return resolvedPorts; + } + /** * Finds an available port starting from {@code preferred} and immediately binds a * {@code ServerSocket} on it to hold that port open until the caller is done building @@ -3816,17 +3914,7 @@ protected Collection getOmitFilesList(File looseAppFile, String srcDirecto Collection omitFiles = new ArrayList(); try { if (looseAppFile != null && looseAppFile.exists()) { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); - dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); - dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - dbf.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - DocumentBuilder db = dbf.newDocumentBuilder(); - Document document = db.parse(looseAppFile); + Document document = XmlDocument.parseDocument(looseAppFile); NodeList archiveList = document.getElementsByTagName("archive"); for (int i = 0; i < archiveList.getLength(); i++) { NodeList ar = archiveList.item(i).getChildNodes(); @@ -3847,7 +3935,7 @@ protected Collection getOmitFilesList(File looseAppFile, String srcDirecto } } } - } catch (ParserConfigurationException | SAXException | IOException e) { + } catch (SAXException | IOException e) { error("Unable to read loose application configuration file: " + looseAppFile.toString()); return omitFiles; } diff --git a/src/test/java/io/openliberty/tools/common/plugins/util/DevUtilResolvePortTest.java b/src/test/java/io/openliberty/tools/common/plugins/util/DevUtilResolvePortTest.java new file mode 100644 index 000000000..11a98f0ac --- /dev/null +++ b/src/test/java/io/openliberty/tools/common/plugins/util/DevUtilResolvePortTest.java @@ -0,0 +1,462 @@ +/** + * (C) Copyright IBM Corporation 2026. + * + * 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 io.openliberty.tools.common.plugins.util; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import io.openliberty.tools.common.plugins.config.ServerConfigDocument; + +/** + * Unit tests for {@link DevUtil#resolveEffectiveContainerPorts}. + * + * Each test creates a minimal on-disk Liberty server structure inside a + * {@link TemporaryFolder} and calls the package-private method directly + * through the {@link BaseDevUtilTest.DevTestUtil} subclass. + */ +public class DevUtilResolvePortTest extends BaseDevUtilTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + /** Creates a server.xml with a literal httpEndpoint. */ + private File writeServerXml(File serverDir, String httpPort, String httpsPort) throws IOException { + File serverXml = new File(serverDir, "server.xml"); + String port = httpPort != null ? " httpPort=\"" + httpPort + "\"" : ""; + String sport = httpsPort != null ? " httpsPort=\"" + httpsPort + "\"" : ""; + write(serverXml, + "\n" + + " \n" + + ""); + return serverXml; + } + + /** Creates a server.xml with a variable reference in httpEndpoint. */ + private File writeServerXmlWithVar(File serverDir, String varName) throws IOException { + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + " \n" + + ""); + return serverXml; + } + + private void write(File f, String content) throws IOException { + f.getParentFile().mkdirs(); + try (FileWriter fw = new FileWriter(f)) { fw.write(content); } + } + + private DevTestUtil util(File serverDir, File buildDir, File serverXmlFile) throws IOException { + DevTestUtil u = new DevTestUtil(serverDir, buildDir); + u.serverXmlFile = serverXmlFile; + return u; + } + + /** + * Helper to resolve a single endpoint port for testing convenience by delegating + * to {@link DevUtil#resolveEffectiveContainerPorts(Map)}. + */ + private int resolvePort(DevTestUtil u, int defaultPort, String attrName) { + return u.resolveEffectiveContainerPorts(Collections.singletonMap(attrName, defaultPort)).get(attrName); + } + + // ------------------------------------------------------------------ + // Tests + // ------------------------------------------------------------------ + + @Test + public void testLiteralHttpPort() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = writeServerXml(serverDir, "9090", null); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9090, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testLiteralHttpsPort() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = writeServerXml(serverDir, null, "9453"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9453, resolvePort(u, 9443, ServerConfigDocument.HTTPS_PORT_ATTR)); + } + + @Test + public void testDefaultPortReturnedWhenNoHttpEndpoint() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, "servlet-4.0"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9080, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testDefaultPortReturnedWhenServerXmlMissing() throws Exception { + File serverDir = tmp.newFolder("server"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), new File(serverDir, "nonexistent.xml")); + + assertEquals(9080, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testVariableResolvedFromDefaultValue() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = writeServerXmlWithVar(serverDir, "myHttpPort"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9090, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testVariableResolvedFromBootstrapProperties() throws Exception { + File serverDir = tmp.newFolder("server"); + // server.xml references a variable with no defaultValue + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + // bootstrap.properties defines the variable + write(new File(serverDir, "bootstrap.properties"), "http.port=9091\n"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9091, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testVariableResolvedFromServerEnv() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + write(new File(serverDir, "server.env"), "HTTP_PORT=9092\n"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9092, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testVariableResolvedFromConfigDropinsOverrides() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + // configDropins/overrides sets the variable at the highest precedence + File overrides = new File(serverDir, "configDropins/overrides"); + overrides.mkdirs(); + write(new File(overrides, "port-override.xml"), + "\n" + + " \n" + + ""); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9095, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testOverridePrecedenceOverDefault() throws Exception { + File serverDir = tmp.newFolder("server"); + // server.xml has a defaultValue of 9090; bootstrap.properties overrides to 9093 + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + " \n" + + ""); + write(new File(serverDir, "bootstrap.properties"), "http.port=9093\n"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + // bootstrap.properties (step 3) overrides defaultValue (step 1) + assertEquals(9093, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testDefaultPortWhenPortIsNotANumber() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + // Variable is not defined anywhere — should fall back to default + assertEquals(9080, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testFallsBackToConfigDirectoryWhenServerXmlFileIsNull() throws Exception { + // serverXmlFile is null — resolveEffectiveContainerPort must fall back to configDirectory/server.xml. + // Use separate dirs matching the real Maven layout: + // configDirectory = src/main/liberty/config (source — this is where the fallback reads from) + // serverDirectory = target/.../defaultServer (runtime — SCD looks for server.xml here) + // The plugin copies server.xml from source to server dir at deploy time, so both dirs have it. + File configDir = tmp.newFolder("src-config"); + File serverDir = tmp.newFolder("server"); + String content = "\n" + + " \n" + + ""; + write(new File(configDir, "server.xml"), content); + write(new File(serverDir, "server.xml"), content); // deployed copy for ServerConfigDocument + DevTestUtil u = new DevTestUtil(serverDir, null, null, configDir, + Collections.emptyList(), Collections.emptyList(), false, false); + + assertEquals(9097, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testPluginConfigXmlProvidesInstallAndUserDir() throws Exception { + File serverDir = tmp.newFolder("server"); + File installDir = tmp.newFolder("wlp"); + File userDir = tmp.newFolder("usr"); + File buildDir = tmp.newFolder("build"); + + // Write server.env to the install dir (install/etc/server.env) — highest priority path + File etcDir = new File(installDir, "etc"); + etcDir.mkdirs(); + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + write(new File(etcDir, "server.env"), "env.port=9096\n"); + + // liberty-plugin-config.xml in buildDir points to installDir and userDir + File pluginConfig = new File(buildDir, "liberty-plugin-config.xml"); + write(pluginConfig, + "\n" + + " " + installDir.getAbsolutePath() + "\n" + + " " + userDir.getAbsolutePath() + "\n" + + ""); + + DevTestUtil u = util(serverDir, buildDir, serverXml); + + assertEquals(9096, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testVariableResolvedFromServerDirConfigDropinsOverrides() throws Exception { + // Mirrors the real Maven/Gradle scenario: + // configDirectory = src/main/liberty/config (source config — no configDropins here) + // serverDirectory = target/.../defaultServer (runtime — plugin copies server.xml here + // and writes liberty-plugin-variable-config.xml + // into configDropins/overrides/) + // ServerConfigDocument uses serverDirectory as SERVER_CONFIG_DIR, so it finds + // both server.xml and configDropins/overrides/ under the same root. + File srcConfigDir = tmp.newFolder("src-config"); // DevUtil.configDirectory (source) + File serverDir = tmp.newFolder("server"); // DevUtil.serverDirectory (runtime) + + // The plugin copies server.xml from srcConfigDir into serverDir at deploy time. + // serverXmlFile points to the source copy; serverDir also has the deployed copy. + String serverXmlContent = + "\n" + + " \n" + + ""; + File srcServerXml = new File(srcConfigDir, "server.xml"); + File deployedServerXml = new File(serverDir, "server.xml"); + write(srcServerXml, serverXmlContent); + write(deployedServerXml, serverXmlContent); // deployed copy — SCD finds this via SERVER_CONFIG_DIR + + // liberty-plugin-variable-config.xml written by plugin into serverDir/configDropins/overrides/ + File overrides = new File(serverDir, "configDropins/overrides"); + overrides.mkdirs(); + write(new File(overrides, "liberty-plugin-variable-config.xml"), + "\n" + + " \n" + + ""); + + // DevUtil: configDirectory = srcConfigDir, serverDirectory = serverDir + // serverXmlFile points to source server.xml (set by watchFiles in real usage) + DevTestUtil u = new DevTestUtil(serverDir, null, null, srcConfigDir, + Collections.emptyList(), Collections.emptyList(), false, false); + u.serverXmlFile = srcServerXml; + + assertEquals(9090, resolvePort(u, 9080, ServerConfigDocument.HTTP_PORT_ATTR)); + } + + @Test + public void testVariableResolvedForHttpsPort() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + " \n" + + ""); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9453, resolvePort(u, 9443, ServerConfigDocument.HTTPS_PORT_ATTR)); + } + + /** + * A custom/external server.xml whose httpPort is defined in a sibling file + * pulled in via a relative {@code }. + * + * Layout: + * serverDir/server.xml — contains {@code } + * serverDir/ports.xml — contains the httpEndpoint with the literal port + * + * ServerConfigDocument resolves the relative include against configDirectory + * (= serverDir), so it finds ports.xml and picks up the port value. + */ + @Test + public void testPortDefinedViaRelativeInclude() throws Exception { + File serverDir = tmp.newFolder("server"); + + // ports.xml — the included file that defines the actual port + write(new File(serverDir, "ports.xml"), + "\n" + + " \n" + + ""); + + // server.xml delegates to ports.xml via a relative include + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9094, resolvePort(u, 9080, "httpPort")); + } + + /** + * A custom/external server.xml with a sibling {@code configDropins} directory + * that supplies the port variable. + * + * Layout: + * serverDir/server.xml — references ${ext.http.port} + * serverDir/configDropins/overrides/port.xml — defines ext.http.port = 9098 + * + * {@code ServerConfigDocument} uses {@code serverDirectory} as its config directory, so + * it finds the sibling {@code configDropins} next to the deployed server.xml. + * This test validates that variables from that sibling {@code configDropins} are + * picked up and used to resolve the port, mirroring the real deployment layout. + */ + @Test + public void testPortFromSiblingConfigDropins() throws Exception { + File serverDir = tmp.newFolder("server"); + + // server.xml references a variable defined only in the sibling configDropins + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + + // Sibling configDropins/overrides defines the variable + File overrides = new File(serverDir, "configDropins/overrides"); + overrides.mkdirs(); + write(new File(overrides, "port.xml"), + "\n" + + " \n" + + ""); + + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9098, resolvePort(u, 9080, "httpPort")); + } + + /** + * An included file overrides the {@code httpPort} attribute defined in {@code server.xml}. + */ + @Test + public void testIncludeOverridesHttpEndpointPort() throws Exception { + File serverDir = tmp.newFolder("server"); + + // included file that overrides the port + write(new File(serverDir, "ports.xml"), + "\n" + + " \n" + + ""); + + // server.xml defines httpPort=9090 but includes ports.xml which overrides it to 9094 + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + " \n" + + ""); + + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9094, resolvePort(u, 9080, "httpPort")); + } + + /** + * A {@code configDropins/overrides} file overrides the {@code httpPort} attribute defined in {@code server.xml}. + */ + @Test + public void testConfigDropinsOverridesHttpEndpointPort() throws Exception { + File serverDir = tmp.newFolder("server"); + + // server.xml defines httpPort=9090 + File serverXml = new File(serverDir, "server.xml"); + write(serverXml, + "\n" + + " \n" + + ""); + + // configDropins/overrides/override.xml defines httpPort=9099 which should take precedence + File overrides = new File(serverDir, "configDropins/overrides"); + overrides.mkdirs(); + write(new File(overrides, "override.xml"), + "\n" + + " \n" + + ""); + + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + assertEquals(9099, resolvePort(u, 9080, "httpPort")); + } + + @Test + public void testResolveBothPortsInSinglePass() throws Exception { + File serverDir = tmp.newFolder("server"); + File serverXml = writeServerXml(serverDir, "9090", "9453"); + DevTestUtil u = util(serverDir, tmp.newFolder("build"), serverXml); + + java.util.Map defaults = new java.util.HashMap(); + defaults.put(ServerConfigDocument.HTTP_PORT_ATTR, 9080); + defaults.put(ServerConfigDocument.HTTPS_PORT_ATTR, 9443); + + java.util.Map resolved = u.resolveEffectiveContainerPorts(defaults); + assertEquals(Integer.valueOf(9090), resolved.get(ServerConfigDocument.HTTP_PORT_ATTR)); + assertEquals(Integer.valueOf(9453), resolved.get(ServerConfigDocument.HTTPS_PORT_ATTR)); + } +}