From e32528e4789d7ed68d15386e8dcd11ed1d3e9b69 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Thu, 13 Aug 2026 13:57:19 -0400 Subject: [PATCH 1/7] Add method to parse a loose application xml file Signed-off-by: Paul Gooderham --- .../plugins/config/ServerConfigDocument.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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 2ae4372f3..df3a36bd3 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 @@ -202,7 +202,7 @@ public ServerConfigDocument(CommonLoggerI log, File originalServerXMLFile, Map springBootAppNodeLocat this.springBootAppNodeLocation = springBootAppNodeLocation; } + /** + * Parses a loose application XML file and returns a list of all sourceOnDisk + * attribute values found on {@code } and {@code } elements. + * + * @param looseAppFile - the loose application XML file to parse + * @return a List of sourceOnDisk path strings; empty if none are found or the file cannot be parsed + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the file cannot be read + */ + public static Set getSourceOnDiskPaths(File looseAppFile) throws FileNotFoundException, IOException { + Set result = new HashSet(); + Document doc; + try (FileInputStream is = new FileInputStream(looseAppFile)) { + doc = getDocumentBuilder().parse(is); + } catch (SAXException e) { + return result; // not valid XML + } + for (String tag : new String[]{"file", "dir"}) { + NodeList nodes = doc.getElementsByTagName(tag); + for (int i = 0; i < nodes.getLength(); i++) { + org.w3c.dom.Node attr = nodes.item(i).getAttributes().getNamedItem("sourceOnDisk"); + if (attr != null && !attr.getNodeValue().isEmpty()) { + result.add(attr.getNodeValue()); + } + } + } + return result; + } } \ No newline at end of file From c349d3aecead8ce68e094c580837fb95c19802c9 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Thu, 13 Aug 2026 14:13:13 -0400 Subject: [PATCH 2/7] Parse the loose config xml file to scan Signed-off-by: Paul Gooderham --- .../plugins/util/FeatureGeneratorUtil.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java index c6d031ec2..a82197385 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java @@ -16,6 +16,7 @@ package io.openliberty.tools.common.plugins.util; import java.io.File; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; @@ -26,6 +27,8 @@ import java.util.Map; import java.util.Set; +import io.openliberty.tools.common.plugins.config.ServerConfigDocument; + public abstract class FeatureGeneratorUtil { public static final String FEATURE_GEN_MAVEN_GROUP_ID = "com.ibm.websphere.appmod.tools"; @@ -135,6 +138,7 @@ public FeatureGeneratorUtil(File featureGen) { * @param currentFeatureSet - the features already specified in the server configuration * @param classFiles - a set of class files for the generator to handle. Should be a subset of allClassesDirectories * @param allClassesDirectories - the directories containing all the class files of the application + * @param looseConfigFilePath - the absolute path to the xml config of the loose application * @param logLocation - directory name relative to project or absolute path passed to feature generator * @param targetJavaEE - generate features valid for the indicated version of EE * @param targetMicroProfile - generate features valid for the indicated version of MicroProfile @@ -155,7 +159,7 @@ public FeatureGeneratorUtil(File featureGen) { * generator when used in combination with each other. E.g. EE 7 and MP 2.1 */ public Set runFeatureGenerator(Set currentFeatureSet, List classFiles, Set allClassesDirectories, - String logLocation, String targetJavaEE, String targetMicroProfile, Map featureListFileMap, boolean optimize) + String looseConfigFilePath, String logLocation, String targetJavaEE, String targetMicroProfile, Map featureListFileMap, boolean optimize) throws PluginExecutionException, NoRecommendationException, RecommendationSetException, FeatureModifiedException, FeatureUnavailableException, IllegalTargetException, IllegalTargetComboException, VersionlessFeatureDetectedException { Set generatedFeatureList = null; @@ -167,7 +171,8 @@ public Set runFeatureGenerator(Set currentFeatureSet, List binaryInputs = getBinaryInputs(classFiles, allClassesDirectories, optimize); + Set binaryInputs = getBinaryInputs(classFiles, allClassesDirectories, looseConfigFilePath, optimize); + String logLevel; if (isDebugEnabled()) { logLevel = "*=FINE"; // generate messages for debugging by support team @@ -384,9 +389,23 @@ private Method getGeneratorMethod() throws MalformedURLException, ClassNotFoundE return featureGenMethod; } - private static Set getBinaryInputs(List classFiles, Set classDirectories, boolean optimize) throws PluginExecutionException { + private static Set getBinaryInputs(List classFiles, Set classDirectories, String looseConfigFilePath, boolean optimize) throws PluginExecutionException { Set resultSet; if (optimize) { + // Use either the loose app config or the class directories + if (looseConfigFilePath != null) { + try { + File looseAppFile = new File(looseConfigFilePath); + if (looseAppFile.exists()) { + resultSet = ServerConfigDocument.getSourceOnDiskPaths(looseAppFile); + if (!resultSet.isEmpty()) { + return resultSet; + } + } + } catch (IOException e) { + // if the app config is invalid try the class directories instead + } + } if (classDirectories == null || classDirectories.isEmpty()) { return new HashSet(); } From 16ff36a0a027be36ef1bb33a4d77b4cc0009a850 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Thu, 13 Aug 2026 14:16:19 -0400 Subject: [PATCH 3/7] Update comment for triggerUpstreamModuleCompile() Signed-off-by: Paul Gooderham --- .../java/io/openliberty/tools/common/plugins/util/DevUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a9e8b2b6c..96860eb91 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 @@ -5878,7 +5878,7 @@ protected void triggerMainModuleCompile(boolean testsOnly) throws IOException { /** * Trigger a compile of the entire specified module. This is only used in a - * multi-module scenario. Adds all Java files to the to be compiled list so that + * multi-module scenario. Adds all Java files to the to-be-compiled list so that * they will be compiled on next watch loop. * * @param project ProjectModule, the module to be compiled From 67840acbeac92f97820ff4becce900740b4e27a3 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Fri, 14 Aug 2026 18:31:42 -0400 Subject: [PATCH 4/7] Display a warning message if the loose app file is not found Signed-off-by: Paul Gooderham --- .../tools/common/plugins/util/FeatureGeneratorUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java index a82197385..98f17568d 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java @@ -389,7 +389,7 @@ private Method getGeneratorMethod() throws MalformedURLException, ClassNotFoundE return featureGenMethod; } - private static Set getBinaryInputs(List classFiles, Set classDirectories, String looseConfigFilePath, boolean optimize) throws PluginExecutionException { + private Set getBinaryInputs(List classFiles, Set classDirectories, String looseConfigFilePath, boolean optimize) throws PluginExecutionException { Set resultSet; if (optimize) { // Use either the loose app config or the class directories @@ -405,6 +405,7 @@ private static Set getBinaryInputs(List classFiles, Set } catch (IOException e) { // if the app config is invalid try the class directories instead } + warn("Application descriptor file not found while generating features, using class files instead: " + looseConfigFilePath); } if (classDirectories == null || classDirectories.isEmpty()) { return new HashSet(); From 305317047d60859ec1566007e5b5bc60cebbd58b Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Thu, 17 Sep 2026 19:16:30 -0400 Subject: [PATCH 5/7] Handle jar, war and ear files in generate features Signed-off-by: Paul Gooderham --- .../plugins/util/FeatureGeneratorUtil.java | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java index 98f17568d..76106b0c3 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java @@ -138,7 +138,7 @@ public FeatureGeneratorUtil(File featureGen) { * @param currentFeatureSet - the features already specified in the server configuration * @param classFiles - a set of class files for the generator to handle. Should be a subset of allClassesDirectories * @param allClassesDirectories - the directories containing all the class files of the application - * @param looseConfigFilePath - the absolute path to the xml config of the loose application + * @param deployedAppFilePath - the absolute path to the xml config of the loose application * @param logLocation - directory name relative to project or absolute path passed to feature generator * @param targetJavaEE - generate features valid for the indicated version of EE * @param targetMicroProfile - generate features valid for the indicated version of MicroProfile @@ -158,11 +158,14 @@ public FeatureGeneratorUtil(File featureGen) { * @throws IllegalTargetComboException - indicates the MP or EE version parameters are not supported by the feature * generator when used in combination with each other. E.g. EE 7 and MP 2.1 */ - public Set runFeatureGenerator(Set currentFeatureSet, List classFiles, Set allClassesDirectories, - String looseConfigFilePath, String logLocation, String targetJavaEE, String targetMicroProfile, Map featureListFileMap, boolean optimize) + public Set runFeatureGenerator(Set currentFeatureSet, List classFiles, String deployedAppFilePath, + String logLocation, String targetJavaEE, String targetMicroProfile, Map featureListFileMap, boolean optimize) throws PluginExecutionException, NoRecommendationException, RecommendationSetException, FeatureModifiedException, FeatureUnavailableException, IllegalTargetException, IllegalTargetComboException, VersionlessFeatureDetectedException { Set generatedFeatureList = null; + warn ("classFiles="+classFiles); + // warn ("allClassesDirectories="+allClassesDirectories); + warn ("looseConfigFilePath="+deployedAppFilePath); if (featureGenJar != null && featureGenJar.exists()) { // if we are already generating features for all class files (optimize=true) and // we are not passing any user specified features (currentFeatureSet is empty) @@ -171,7 +174,7 @@ public Set runFeatureGenerator(Set currentFeatureSet, List binaryInputs = getBinaryInputs(classFiles, allClassesDirectories, looseConfigFilePath, optimize); + Set binaryInputs = getBinaryInputs(classFiles, deployedAppFilePath, optimize); String logLevel; if (isDebugEnabled()) { @@ -189,9 +192,19 @@ public Set runFeatureGenerator(Set currentFeatureSet, List) generateFeatureSetMethod.invoke(null, binaryInputs, targetJavaEE, targetMicroProfile, currentFeatureSet, featureListFileMap, logLocation, logLevel, java.util.Locale.getDefault()); for (String s : generatedFeatureList) {debug(s);}; + for (String s : generatedFeatureList) {warn (s);}; } catch (InvocationTargetException ite) { // This is the exception from the JVM that indicates there was an exception in the method we // called through reflection. We must extract the actual exception from the 'cause' field. @@ -211,7 +224,7 @@ public Set runFeatureGenerator(Set currentFeatureSet, List conflicts = getFeatures(generatorException); // always rerun feature generator in this scenario, this exception only occurs if a current feature list is passed to feature generator - Set sampleFeatureList = reRunFeatureGenerator(allClassesDirectories, logLocation, targetJavaEE, targetMicroProfile, featureListFileMap); + Set sampleFeatureList = reRunFeatureGenerator(deployedAppFilePath, logLocation, targetJavaEE, targetMicroProfile, featureListFileMap); if (sampleFeatureList == null) { throw new NoRecommendationException(conflicts); } else { @@ -221,7 +234,7 @@ public Set runFeatureGenerator(Set currentFeatureSet, List conflicts = getFeatures(generatorException); // rerun feature generator with all class files and without the current feature set to get feature recommendations - Set sampleFeatureList = reRunIfFailed ? reRunFeatureGenerator(allClassesDirectories, logLocation, targetJavaEE, targetMicroProfile, featureListFileMap): null; + Set sampleFeatureList = reRunIfFailed ? reRunFeatureGenerator(deployedAppFilePath, logLocation, targetJavaEE, targetMicroProfile, featureListFileMap): null; if (sampleFeatureList == null) { throw new NoRecommendationException(conflicts); } else { @@ -231,7 +244,7 @@ public Set runFeatureGenerator(Set currentFeatureSet, List modifications = getFeatures(generatorException); // rerun feature generator with all class files and without the current feature set - Set sampleFeatureList = reRunIfFailed ? reRunFeatureGenerator(allClassesDirectories, logLocation, targetJavaEE, targetMicroProfile, featureListFileMap) : null; + Set sampleFeatureList = reRunIfFailed ? reRunFeatureGenerator(deployedAppFilePath, logLocation, targetJavaEE, targetMicroProfile, featureListFileMap) : null; throw new FeatureModifiedException(modifications, (sampleFeatureList == null) ? getNoSampleFeatureList() : sampleFeatureList, generatorException.getLocalizedMessage()); } else if (generatorException.getClass().getName().equals(FEATURE_NOT_AVAILABLE_EXCEPTION)) { @@ -291,19 +304,19 @@ public Set runFeatureGenerator(Set currentFeatureSet, List reRunFeatureGenerator(Set allClassesDirectories, String logLocation, String targetJavaEE, String targetMicroProfile, + public Set reRunFeatureGenerator(String deployedAppFilePath, String logLocation, String targetJavaEE, String targetMicroProfile, Map featureListFileMap) throws PluginExecutionException { Set generatedFeatureList = null; try { Method generateFeatureSetMethod = getGeneratorMethod(); - Set binaryInputs = allClassesDirectories; + Set binaryInputs = getBinaryInputs(null, deployedAppFilePath, true); Set currentFeaturesSet = new HashSet(); // when re-running always pass in no features String logLevel; if (isDebugEnabled()) { @@ -389,33 +402,27 @@ private Method getGeneratorMethod() throws MalformedURLException, ClassNotFoundE return featureGenMethod; } - private Set getBinaryInputs(List classFiles, Set classDirectories, String looseConfigFilePath, boolean optimize) throws PluginExecutionException { - Set resultSet; + private Set getBinaryInputs(List classFiles, String deployedAppFilePath, boolean optimize) throws PluginExecutionException { + Set resultSet = new HashSet(); if (optimize) { - // Use either the loose app config or the class directories - if (looseConfigFilePath != null) { + // Use either the loose app config or a regular application deployment binary file (war/jar) + if (deployedAppFilePath != null && deployedAppFilePath.endsWith(".xml")) { try { - File looseAppFile = new File(looseConfigFilePath); + // extract the file names from the xml file + File looseAppFile = new File(deployedAppFilePath); if (looseAppFile.exists()) { resultSet = ServerConfigDocument.getSourceOnDiskPaths(looseAppFile); - if (!resultSet.isEmpty()) { - return resultSet; - } } } catch (IOException e) { // if the app config is invalid try the class directories instead + warn("Application descriptor file not found while generating features, using class files instead: " + deployedAppFilePath); } - warn("Application descriptor file not found while generating features, using class files instead: " + looseConfigFilePath); - } - if (classDirectories == null || classDirectories.isEmpty()) { - return new HashSet(); + } else { + resultSet.add(deployedAppFilePath); } - resultSet = classDirectories; } else { if (classFiles != null && !classFiles.isEmpty()) { resultSet = new HashSet(classFiles); - } else { - return new HashSet(); } } return resultSet; From 45c291cf3e677a667541b412f00c49210b87de68 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Fri, 18 Sep 2026 12:08:24 -0400 Subject: [PATCH 6/7] Check for null deployed app filename Signed-off-by: Paul Gooderham --- .../plugins/util/FeatureGeneratorUtil.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java index 76106b0c3..06e9d3686 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java @@ -173,7 +173,6 @@ public Set runFeatureGenerator(Set currentFeatureSet, List binaryInputs = getBinaryInputs(classFiles, deployedAppFilePath, optimize); String logLevel; @@ -201,6 +200,7 @@ public Set runFeatureGenerator(Set currentFeatureSet, List) generateFeatureSetMethod.invoke(null, binaryInputs, targetJavaEE, targetMicroProfile, currentFeatureSet, featureListFileMap, logLocation, logLevel, java.util.Locale.getDefault()); for (String s : generatedFeatureList) {debug(s);}; @@ -406,21 +406,24 @@ private Set getBinaryInputs(List classFiles, String deployedAppF Set resultSet = new HashSet(); if (optimize) { // Use either the loose app config or a regular application deployment binary file (war/jar) - if (deployedAppFilePath != null && deployedAppFilePath.endsWith(".xml")) { - try { - // extract the file names from the xml file - File looseAppFile = new File(deployedAppFilePath); - if (looseAppFile.exists()) { - resultSet = ServerConfigDocument.getSourceOnDiskPaths(looseAppFile); + if (deployedAppFilePath != null) { + if (deployedAppFilePath.endsWith(".xml")) { + try { + // extract the file names from the xml file + File looseAppFile = new File(deployedAppFilePath); + if (looseAppFile.exists()) { + resultSet = ServerConfigDocument.getSourceOnDiskPaths(looseAppFile); + } + } catch (IOException e) { + // if the app config is invalid try the class directories instead + warn("Application descriptor file not found while generating features, using class files instead: " + deployedAppFilePath); } - } catch (IOException e) { - // if the app config is invalid try the class directories instead - warn("Application descriptor file not found while generating features, using class files instead: " + deployedAppFilePath); + } else { + resultSet.add(deployedAppFilePath); } - } else { - resultSet.add(deployedAppFilePath); } } else { + // incremental generate features passes only individual class files to the scanner if (classFiles != null && !classFiles.isEmpty()) { resultSet = new HashSet(classFiles); } From 7ffe4ba92e0bec92e883c19156180264a1b98871 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Fri, 18 Sep 2026 18:13:44 -0400 Subject: [PATCH 7/7] Throw a new exception if there is an i/o error reading the .war.xml file Signed-off-by: Paul Gooderham --- .../plugins/util/FeatureGeneratorUtil.java | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java index 06e9d3686..37540dead 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/FeatureGeneratorUtil.java @@ -161,11 +161,9 @@ public FeatureGeneratorUtil(File featureGen) { public Set runFeatureGenerator(Set currentFeatureSet, List classFiles, String deployedAppFilePath, String logLocation, String targetJavaEE, String targetMicroProfile, Map featureListFileMap, boolean optimize) throws PluginExecutionException, NoRecommendationException, RecommendationSetException, FeatureModifiedException, - FeatureUnavailableException, IllegalTargetException, IllegalTargetComboException, VersionlessFeatureDetectedException { + FeatureUnavailableException, IllegalTargetException, IllegalTargetComboException, VersionlessFeatureDetectedException, + IOErrorReadingXMLException { Set generatedFeatureList = null; - warn ("classFiles="+classFiles); - // warn ("allClassesDirectories="+allClassesDirectories); - warn ("looseConfigFilePath="+deployedAppFilePath); if (featureGenJar != null && featureGenJar.exists()) { // if we are already generating features for all class files (optimize=true) and // we are not passing any user specified features (currentFeatureSet is empty) @@ -191,20 +189,10 @@ public Set runFeatureGenerator(Set currentFeatureSet, List) generateFeatureSetMethod.invoke(null, binaryInputs, targetJavaEE, targetMicroProfile, currentFeatureSet, featureListFileMap, logLocation, logLevel, java.util.Locale.getDefault()); for (String s : generatedFeatureList) {debug(s);}; - for (String s : generatedFeatureList) {warn (s);}; } catch (InvocationTargetException ite) { // This is the exception from the JVM that indicates there was an exception in the method we // called through reflection. We must extract the actual exception from the 'cause' field. @@ -312,7 +300,7 @@ public Set runFeatureGenerator(Set currentFeatureSet, List reRunFeatureGenerator(String deployedAppFilePath, String logLocation, String targetJavaEE, String targetMicroProfile, - Map featureListFileMap) throws PluginExecutionException { + Map featureListFileMap) throws PluginExecutionException, IOErrorReadingXMLException { Set generatedFeatureList = null; try { Method generateFeatureSetMethod = getGeneratorMethod(); @@ -402,10 +390,11 @@ private Method getGeneratorMethod() throws MalformedURLException, ClassNotFoundE return featureGenMethod; } - private Set getBinaryInputs(List classFiles, String deployedAppFilePath, boolean optimize) throws PluginExecutionException { + // Required is either some type of app file or some class files. This is checked by the caller to runFeatureGenerator() + private Set getBinaryInputs(List classFiles, String deployedAppFilePath, boolean optimize) throws IOErrorReadingXMLException { Set resultSet = new HashSet(); if (optimize) { - // Use either the loose app config or a regular application deployment binary file (war/jar) + // Use either the loose app config or a regular application deployment binary file (ear/war/jar) if (deployedAppFilePath != null) { if (deployedAppFilePath.endsWith(".xml")) { try { @@ -415,8 +404,7 @@ private Set getBinaryInputs(List classFiles, String deployedAppF resultSet = ServerConfigDocument.getSourceOnDiskPaths(looseAppFile); } } catch (IOException e) { - // if the app config is invalid try the class directories instead - warn("Application descriptor file not found while generating features, using class files instead: " + deployedAppFilePath); + throw new IOErrorReadingXMLException(); } } else { resultSet.add(deployedAppFilePath); @@ -670,4 +658,8 @@ public class IllegalTargetComboException extends AbstractIllegalTargetException public class VersionlessFeatureDetectedException extends Exception { private static final long serialVersionUID = 1L; } + + public class IOErrorReadingXMLException extends Exception { + private static final long serialVersionUID = 1L; + } }