Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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/<server name>.
* 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<String> 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<String> 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<String, File> 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<String> featuresToInstall = getSpecifiedFeatures(null);

// get existing server features from directory of interest
ServerFeatureUtil servUtil = getServerFeatureUtil(true, null);

Set<String> generatedFiles = new HashSet<String>();
generatedFiles.add(GENERATED_FEATURES_FILE_NAME);

Set<String> existingFeatures = getServerFeatures(servUtil, generationContextDir, generatedFiles, optimize);
Set<String> nonCustomFeatures = new HashSet<String>(); // 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<String> scannedFeatureList = null;
String eeVersion = null;
String mpVersion = null;
try {
List<Object> projects = getProjectList(project);
Set<String> 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<String> userFeatures = (optimize) ? existingFeatures :
getServerFeatures(servUtil, generationContextDir, generatedFiles, true); // user features excludes generatedFiles
Set<String> 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<String> 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<String> missingLibertyFeatures = new HashSet<String>();
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<String, File>(),
generatedFiles);
Set<String> userDefinedFeatures = optimize ? existingFeatures : (fp !=null) ? fp.getFeatures(): new HashSet<String>();
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<String> 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<String> getGeneratedFeatures(ServerFeatureUtil servUtil, File generatedFeaturesFile) {
servUtil.setLowerCaseFeatures(false);
FeaturesPlatforms result = servUtil.getServerXmlFeatures(new FeaturesPlatforms(), generationContextDir,
generatedFeaturesFile, null, null);
servUtil.setLowerCaseFeatures(true);
Set<String> features = new HashSet<String>();
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<String> getServerFeatures(ServerFeatureUtil servUtil, File generationContextDir, Set<String> generatedFiles, boolean excludeGenerated);
public abstract Set<String> getClassesDirectories(List projects) throws GenerateFeaturesException;
public abstract List<Object> 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);
}