From f6be9ba87cd2201cfc2d122808a7af5ef65d0392 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Wed, 16 Jul 2025 19:10:55 -0400 Subject: [PATCH 1/2] Add a util class to execute the generate features code Signed-off-by: Paul Gooderham --- .../plugins/util/GenerateFeaturesUtil.java | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java new file mode 100644 index 000000000..a72dc56b1 --- /dev/null +++ b/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java @@ -0,0 +1,284 @@ +/** + * (C) Copyright IBM Corporation 2025 + * + * 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 java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import static io.openliberty.tools.common.plugins.util.BinaryScannerUtil.*; + +import io.openliberty.tools.common.plugins.config.ServerConfigXmlDocument; +import io.openliberty.tools.common.plugins.util.ServerFeatureUtil.FeaturesPlatforms; + +import org.w3c.dom.Element; + +public abstract class GenerateFeaturesUtil { + public static final String HEADER_M = "This file was generated by the Liberty Maven Plugin and will be overwritten on subsequent runs of the liberty:generate-features goal." + + "\n It is recommended that you do not edit this file and that you commit this file to your version control."; + public static final String HEADER_G = "This file was generated by the Liberty Gradle Plugin and will be overwritten on subsequent runs of the generateFeatures task." + "\n It is recommended that you do not edit this file and that you commit this file to your version control."; + public static final String GENERATED_FEATURES_COMMENT = "The following features were generated based on API usage detected in your application"; + public static final String NO_NEW_FEATURES_COMMENT = "No additional features generated"; + public static final String NO_CLASSES_DIR_WARNING = "Could not find classes directory to generate features against. Liberty features will not be generated. " + + "Ensure your project has first been compiled."; + + // The object used to scan binaries for the Liberty features they use. + BinaryScannerUtil binaryScannerHandler; + + /** + * Generating features is performed relative to a certain server. We only generate features + * that are missing from a server config. By default we generate features that are missing + * from the server directory in target/liberty/wlp/usr/servers/. + * If generateToSrc is specified then we generate features which are missing from the Liberty + * config specified in the src directory src/main/liberty/config. + * We will select one server config as the context of this operation. + */ + private File generationContextDir; + // src liberty config dir e.g. src/main/liberty/config + private File configDirectory; + // output liberty dir e.g. target/wlp/liberty/usr/servers/defaultServer + private File serverDirectory; + List classFiles; + // helpful info to add to the header of the generated features file + private String header; + // build system specific project object + private Object project; + + // Initialize with project data + public GenerateFeaturesUtil(Object project, BinaryScannerUtil binaryScannerHandler, File configDirectory, File serverDirectory, List classFiles, String header) { + this.project = project; + this.binaryScannerHandler = binaryScannerHandler; + this.configDirectory = configDirectory; + this.serverDirectory = serverDirectory; + this.classFiles = classFiles; + this.header = header; + } + + /** + * Generates features for the application given the API usage detected by the binary scanner and + * taking any user specified features into account + * + * @throws GenerateFeaturesException the caller will rethrow according to build system + * @throws PluginExecutionException indicates the binary-app-scanner.jar could + * not be found + */ + public void generateFeatures(boolean optimize, boolean generateToSrc) throws GenerateFeaturesException, PluginExecutionException { + + // The config dir is in the src directory. Otherwise generate for the target/liberty dir. + generationContextDir = generateToSrc ? configDirectory : serverDirectory; + + debug("--- Generate Features values ---"); + debug("optimize generate features: " + optimize); + debug("generate to src or target: " + generationContextDir); + if (classFiles != null && !classFiles.isEmpty()) { + debug("Generate features for the following class files: " + classFiles.toString()); + } + + // TODO add support for env variables + // commented out for now as the current logic depends on the server dir existing + // and specifying features with env variables is an edge case + /* Map libertyDirPropertyFiles; + try { + libertyDirPropertyFiles = BasicSupport.getLibertyDirectoryPropertyFiles(installDirectory, userDirectory, serverDirectory); + } catch (IOException e) { + debug("Exception reading the server property files", e); + error("Error attempting to generate server feature list. Ensure your user account has read permission to the property files in the server installation directory."); + return; + } */ + + // TODO: get user specified features that have not yet been installed in the + // original case they appear in a server config xml document. + // getSpecifiedFeatures may not return the features in the correct case + // Set featuresToInstall = getSpecifiedFeatures(null); + + // get existing server features from directory of interest + ServerFeatureUtil servUtil = getServerFeatureUtil(true, null); + + Set generatedFiles = new HashSet(); + generatedFiles.add(GENERATED_FEATURES_FILE_NAME); + + Set existingFeatures = getServerFeatures(servUtil, generatedFiles, optimize); + Set nonCustomFeatures = new HashSet(); // binary scanner only handles actual Liberty features + for (String feature : existingFeatures) { // custom features are "usr:feature-1.0" or "myExt:feature-2.0" + if (!feature.contains(":")) nonCustomFeatures.add(feature); + } + + Set scannedFeatureList = null; + String eeVersion = null; + String mpVersion = null; + try { + List projects = getProjectList(project); + Set directories = getClassesDirectories(projects); + if (directories.isEmpty() && (classFiles == null || classFiles.isEmpty())) { + // log as warning and continue to call binary scanner to detect conflicts in + // user specified features + warn(NO_CLASSES_DIR_WARNING); + } + eeVersion = getEEVersion(projects); + mpVersion = getMPVersion(projects); + + String logLocation = getLogLocation(project); + String eeVersionArg = composeEEVersion(eeVersion); + String mpVersionArg = composeMPVersion(mpVersion); + scannedFeatureList = binaryScannerHandler.runBinaryScanner(nonCustomFeatures, classFiles, directories, logLocation, eeVersionArg, mpVersionArg, optimize); + } catch (BinaryScannerUtil.NoRecommendationException noRecommendation) { + throw new GenerateFeaturesException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE3, noRecommendation.getConflicts())); + } catch (BinaryScannerUtil.FeatureModifiedException featuresModified) { + Set userFeatures = (optimize) ? existingFeatures : + getServerFeatures(servUtil, generatedFiles, true); // user features excludes generatedFiles + Set modifiedSet = featuresModified.getFeatures(); // a set that works after being modified by the scanner + if (modifiedSet.containsAll(userFeatures)) { + // none of the user features were modified, only features which were generated earlier. + debug("FeatureModifiedException, modifiedSet containsAll userFeatures, pass modifiedSet on to generateFeatures"); + // features were modified to get a working set with the application's API usage, display warning to users and use modified set + warn(featuresModified.getMessage()); + scannedFeatureList = modifiedSet; + } else { + Set allAppFeatures = featuresModified.getSuggestions(); // suggestions are scanned from binaries + allAppFeatures.addAll(userFeatures); // scanned plus configured features were detected to be in conflict + debug("FeatureModifiedException, combine suggestions from scanner with user features in error msg"); + throw new GenerateFeaturesException( + String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE1, allAppFeatures, modifiedSet)); + + } + } catch (BinaryScannerUtil.RecommendationSetException showRecommendation) { + if (showRecommendation.isExistingFeaturesConflict()) { + throw new GenerateFeaturesException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE2, showRecommendation.getConflicts(), showRecommendation.getSuggestions())); + } else { + throw new GenerateFeaturesException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE1, showRecommendation.getConflicts(), showRecommendation.getSuggestions())); + } + } catch (BinaryScannerUtil.FeatureUnavailableException featureUnavailable) { + throw new GenerateFeaturesException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE5, featureUnavailable.getConflicts(), featureUnavailable.getMPLevel(), featureUnavailable.getEELevel(), featureUnavailable.getUnavailableFeatures())); + } catch (BinaryScannerUtil.IllegalTargetComboException illegalCombo) { + throw new GenerateFeaturesException(String.format(BinaryScannerUtil.BINARY_SCANNER_INVALID_COMBO_MESSAGE, eeVersion, mpVersion)); + } catch (BinaryScannerUtil.IllegalTargetException illegalTargets) { + String messages = buildInvalidArgExceptionMessage(illegalTargets.getEELevel(), illegalTargets.getMPLevel(), eeVersion, mpVersion); + throw new GenerateFeaturesException(messages); + } catch (PluginExecutionException x) { + // throw an error when there is a problem not caught in runBinaryScanner() + Object o = x.getCause(); + if (o != null) { + debug("Caused by exception:" + x.getCause().getClass().getName()); + debug("Caused by exception message:" + x.getCause().getMessage()); + } + throw new GenerateFeaturesException("Failed to generate a working set of features. " + x.getMessage(), x); + } + + Set missingLibertyFeatures = new HashSet(); + if (scannedFeatureList != null) { + missingLibertyFeatures.addAll(scannedFeatureList); + + servUtil.setLowerCaseFeatures(false); + // get set of user defined features so they can be omitted from the generated + // file that will be written + FeaturesPlatforms fp = servUtil.getServerFeatures(generationContextDir, getServerXmlFile(), new HashMap(), + generatedFiles); + Set userDefinedFeatures = optimize ? existingFeatures : (fp !=null) ? fp.getFeatures(): new HashSet(); + debug("User defined features:" + userDefinedFeatures); + servUtil.setLowerCaseFeatures(true); + if (userDefinedFeatures != null) { + missingLibertyFeatures.removeAll(userDefinedFeatures); + } + } + debug("Features detected by binary scanner which are not in server.xml" + missingLibertyFeatures); + + try { + // generate the new features into an xml file in the correct context directory + File generatedXmlFile = new File(generationContextDir, GENERATED_FEATURES_FILE_PATH); + if (missingLibertyFeatures.size() > 0) { + Set existingGeneratedFeatures = getGeneratedFeatures(servUtil, generatedXmlFile); + if (!missingLibertyFeatures.equals(existingGeneratedFeatures)) { + // Create special XML file to contain generated features. + ServerConfigXmlDocument configDocument = ServerConfigXmlDocument.newInstance(); + configDocument.createComment(header); + Element featureManagerElem = configDocument.createFeatureManager(); + configDocument.createComment(featureManagerElem, GENERATED_FEATURES_COMMENT); + for (String missing : missingLibertyFeatures) { + debug(String.format("Adding missing feature %s to %s.", missing, GENERATED_FEATURES_FILE_PATH)); + configDocument.createFeature(missing); + } + // Generate log message before writing file as the file change event kicks off other dev mode actions + info("Generated the following features: " + missingLibertyFeatures); + configDocument.writeXMLDocument(generatedXmlFile); + debug("Created file " + generatedXmlFile); + } else { + info("Regenerated the following features: " + missingLibertyFeatures); + } + } else { + info("No additional features were generated."); + if (generatedXmlFile.exists()) { + // generated-features.xml exists but no additional features were generated + // create empty features list with comment + ServerConfigXmlDocument configDocument = ServerConfigXmlDocument.newInstance(); + configDocument.createComment(header); + Element featureManagerElem = configDocument.createFeatureManager(); + configDocument.createComment(featureManagerElem, NO_NEW_FEATURES_COMMENT); + configDocument.writeXMLDocument(generatedXmlFile); + } + } + } catch (ParserConfigurationException | TransformerException | IOException e) { + debug("Exception creating the server features file", e); + throw new GenerateFeaturesException( + "Automatic generation of features failed. Error attempting to create the " + + GENERATED_FEATURES_FILE_NAME + + ". Ensure your id has write permission to the server configuration directory.", + e); + } + } + + // returns the features specified in the generated-features.xml file in the generation context directory + private Set getGeneratedFeatures(ServerFeatureUtil servUtil, File generatedFeaturesFile) { + servUtil.setLowerCaseFeatures(false); + FeaturesPlatforms result = servUtil.getServerXmlFeatures(new FeaturesPlatforms(), generationContextDir, + generatedFeaturesFile, null, null); + servUtil.setLowerCaseFeatures(true); + Set features = new HashSet(); + if (result != null) { + features = result.getFeatures(); + } + return features; + } + + public class GenerateFeaturesException extends Exception{ + public GenerateFeaturesException(String message, Throwable cause) { + super(message, cause); + } + public GenerateFeaturesException(String message) { + super(message); + } + }; + + public abstract ServerFeatureUtil getServerFeatureUtil(boolean suppress, Map files); + public abstract Set getServerFeatures(ServerFeatureUtil servUtil, Set generatedFiles, boolean excludeGenerated); + public abstract Set getClassesDirectories(List projects) throws GenerateFeaturesException; + public abstract List getProjectList(Object project); + public abstract String getEEVersion(List projects); + public abstract String getMPVersion(List projects); + public abstract String getLogLocation(Object project); + public abstract File getServerXmlFile(); + + public abstract void info(String message); + public abstract void warn(String message); + public abstract void debug(String message); + public abstract void debug(String message, Throwable e); +} From 9244ceb89c531a3705bcaee4524dbe02c9c43253 Mon Sep 17 00:00:00 2001 From: Paul Gooderham Date: Wed, 16 Jul 2025 20:09:14 -0400 Subject: [PATCH 2/2] Pass the generate features context directory to getServerFeatures method Signed-off-by: Paul Gooderham --- .../tools/common/plugins/util/GenerateFeaturesUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java b/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java index a72dc56b1..fe7e282ba 100644 --- a/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java +++ b/src/main/java/io/openliberty/tools/common/plugins/util/GenerateFeaturesUtil.java @@ -117,7 +117,7 @@ public void generateFeatures(boolean optimize, boolean generateToSrc) throws Gen Set generatedFiles = new HashSet(); generatedFiles.add(GENERATED_FEATURES_FILE_NAME); - Set existingFeatures = getServerFeatures(servUtil, generatedFiles, optimize); + Set existingFeatures = getServerFeatures(servUtil, generationContextDir, generatedFiles, optimize); Set nonCustomFeatures = new HashSet(); // binary scanner only handles actual Liberty features for (String feature : existingFeatures) { // custom features are "usr:feature-1.0" or "myExt:feature-2.0" if (!feature.contains(":")) nonCustomFeatures.add(feature); @@ -145,7 +145,7 @@ public void generateFeatures(boolean optimize, boolean generateToSrc) throws Gen throw new GenerateFeaturesException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE3, noRecommendation.getConflicts())); } catch (BinaryScannerUtil.FeatureModifiedException featuresModified) { Set userFeatures = (optimize) ? existingFeatures : - getServerFeatures(servUtil, generatedFiles, true); // user features excludes generatedFiles + getServerFeatures(servUtil, generationContextDir, generatedFiles, true); // user features excludes generatedFiles Set modifiedSet = featuresModified.getFeatures(); // a set that works after being modified by the scanner if (modifiedSet.containsAll(userFeatures)) { // none of the user features were modified, only features which were generated earlier. @@ -269,7 +269,7 @@ public GenerateFeaturesException(String message) { }; public abstract ServerFeatureUtil getServerFeatureUtil(boolean suppress, Map files); - public abstract Set getServerFeatures(ServerFeatureUtil servUtil, Set generatedFiles, boolean excludeGenerated); + public abstract Set getServerFeatures(ServerFeatureUtil servUtil, File generationContextDir, Set generatedFiles, boolean excludeGenerated); public abstract Set getClassesDirectories(List projects) throws GenerateFeaturesException; public abstract List getProjectList(Object project); public abstract String getEEVersion(List projects);