All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.openliberty.tools.gradle.tasks.GenerateFeaturesTask.groovy Maven / Gradle / Ivy

The newest version!
/**
 * (C) Copyright IBM Corporation 2021, 2024.
 *
 * 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.gradle.tasks


import io.openliberty.tools.common.plugins.config.ServerConfigXmlDocument
import io.openliberty.tools.common.plugins.config.XmlDocument
import io.openliberty.tools.common.plugins.util.BinaryScannerUtil
import static io.openliberty.tools.common.plugins.util.BinaryScannerUtil.*;
import io.openliberty.tools.common.plugins.util.PluginExecutionException
import io.openliberty.tools.common.plugins.util.ServerFeatureUtil
import io.openliberty.tools.common.plugins.util.ServerFeatureUtil.FeaturesPlatforms
import io.openliberty.tools.gradle.utils.ArtifactDownloadUtil

import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import org.gradle.api.logging.LogLevel
import org.xml.sax.SAXException
import org.w3c.dom.Element;

import javax.xml.parsers.ParserConfigurationException
import javax.xml.transform.TransformerException

class GenerateFeaturesTask extends AbstractFeatureTask {

    public static final String FEATURES_FILE_MESSAGE = "The Liberty Gradle Plugin has generated Liberty features necessary for your application in " + GENERATED_FEATURES_FILE_PATH;
    public static final String HEADER = "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_CLASS_FILES_WARNING = "Could not find class files to generate features against. Liberty features will not be generated. Ensure your project has first been compiled.";

    private static final boolean DEFAULT_OPTIMIZE = true;

    private File binaryScanner;

    GenerateFeaturesTask() {
        configure({
            description 'Generate the features used by an application and add to the configuration of a Liberty server'
            group 'Liberty'
        })
    }

    private List classFiles;

    @Option(option = 'classFile', description = 'If set and optimize is false, will generate features for the list of classes passed.')
    void setClassFiles(List classFiles) {
        this.classFiles = classFiles;
    }

    private Boolean optimize = null;

    // Need to use a string value to allow the ability to specify a value for the parameter (ie. --optimize=false)
    @Option(option = 'optimize', description = 'Optimize generating features by passing in all classes and only user specified features.')
    void setOptimize(String optimize) {
        this.optimize = Boolean.parseBoolean(optimize);
    }

    @TaskAction
    void generateFeatures() {
        binaryScanner = getBinaryScannerJarFromRepository();
        BinaryScannerHandler binaryScannerHandler = new BinaryScannerHandler(binaryScanner);

        if (optimize == null) {
            optimize = DEFAULT_OPTIMIZE;
        }

        logger.debug("--- Generate Features values ---");
        logger.debug("optimize generate features: " + optimize);
        if (classFiles != null && !classFiles.isEmpty()) {
            logger.debug("Generate features for the following class files: " + classFiles);
        }

        initializeConfigDirectory();

        // 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
        /* def serverDirectory = getServerDir(project);
        def libertyDirPropertyFiles;
        try {
            libertyDirPropertyFiles = getLibertyDirectoryPropertyFiles(getInstallDir(project), getUserDir(project), serverDirectory);
        } catch (IOException x) {
            logger.debug("Exception reading the server property files", e);
            logger.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;
        } */

        // get existing server features from source directory
        ServerFeatureUtil servUtil = getServerFeatureUtil(true, null);
        Set generatedFiles = new HashSet();
        generatedFiles.add(GENERATED_FEATURES_FILE_NAME);

        Set existingFeatures = getServerFeatures(servUtil, generatedFiles, optimize);
        logger.debug("Existing features:" + existingFeatures);
        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);
        }
        logger.debug("Non-custom features:" + nonCustomFeatures);

        Set scannedFeatureList;
        String eeVersion = null;
        String mpVersion = null;
        try {
            Set directories = getClassesDirectories();
            if (directories.isEmpty() && (classFiles == null || classFiles.isEmpty())) {
                // log as warning and continue to call binary scanner to detect conflicts in user specified features
                logger.warn(NO_CLASS_FILES_WARNING);
            }
            eeVersion = getEEVersion(project);
            mpVersion = getMPVersion(project);

            String logLocation = project.getBuildDir().getCanonicalPath();
            String eeVersionArg = composeEEVersion(eeVersion);
            String mpVersionArg = composeMPVersion(mpVersion);
            scannedFeatureList = binaryScannerHandler.runBinaryScanner(nonCustomFeatures, classFiles, directories, logLocation, eeVersionArg, mpVersionArg, optimize);
        } catch (BinaryScannerUtil.NoRecommendationException noRecommendation) {
            throw new GradleException(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.
                logger.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
                logger.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
                logger.debug("FeatureModifiedException, combine suggestions from scanner with user features in error msg");
                throw new GradleException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE1, allAppFeatures, modifiedSet));
            }
        } catch (BinaryScannerUtil.RecommendationSetException showRecommendation) {
            if (showRecommendation.isExistingFeaturesConflict()) {
                throw new GradleException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE2, showRecommendation.getConflicts(), showRecommendation.getSuggestions()));
            }
            throw new GradleException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE1, showRecommendation.getConflicts(), showRecommendation.getSuggestions()));
        } catch (BinaryScannerUtil.FeatureUnavailableException featureUnavailable) {
            throw new GradleException(String.format(BinaryScannerUtil.BINARY_SCANNER_CONFLICT_MESSAGE5, featureUnavailable.getConflicts(), featureUnavailable.getMPLevel(),
                    featureUnavailable.getEELevel(), featureUnavailable.getUnavailableFeatures()));
        } catch (BinaryScannerUtil.IllegalTargetComboException illegalCombo) {
            throw new GradleException(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 GradleException(messages);
        } catch (PluginExecutionException x) {
            // throw an error when there is a problem not caught in runBinaryScanner()
            Object o = x.getCause();
            if (o != null) {
                logger.debug("Caused by exception:" + x.getCause().getClass().getName());
                logger.debug("Caused by exception message:" + x.getCause().getMessage());
            }
            throw new GradleException("Failed to generate a working set of features. " + x.getMessage(), x);
        }

        def 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
            Set userDefinedFeatures = optimize ? existingFeatures : new HashSet();
            if (!optimize) {
                FeaturesPlatforms fp = servUtil.getServerFeatures(server.configDirectory, server.serverXmlFile, new HashMap(), generatedFiles);
                if (fp != null) {
                    userDefinedFeatures = fp.getFeatures();
                }
            }
            logger.debug("User defined features:" + userDefinedFeatures);
            servUtil.setLowerCaseFeatures(true);
            if (!userDefinedFeatures.isEmpty()) {
                missingLibertyFeatures.removeAll(userDefinedFeatures);
            }
        }
        logger.debug("Features detected by binary scanner which are not in server.xml : " + missingLibertyFeatures);

        def newServerXmlSrc = new File(server.configDirectory, GENERATED_FEATURES_FILE_PATH);
        try {
            if (missingLibertyFeatures.size() > 0) {
                Set existingGeneratedFeatures = getGeneratedFeatures(servUtil, newServerXmlSrc);
                if (!missingLibertyFeatures.equals(existingGeneratedFeatures)) {
                    // Create specialized server.xml
                    ServerConfigXmlDocument configDocument = ServerConfigXmlDocument.newInstance();
                    configDocument.createComment(HEADER);
                    Element featureManagerElem = configDocument.createFeatureManager();
                    configDocument.createComment(featureManagerElem, GENERATED_FEATURES_COMMENT);
                    for (String missing : missingLibertyFeatures) {
                        logger.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
                    logger.lifecycle("Generated the following features: " + missingLibertyFeatures);
                    // use logger.lifecycle so that message appears without --info tag on
                    configDocument.writeXMLDocument(newServerXmlSrc);
                    logger.debug("Created file " + newServerXmlSrc);
                    // Add a reference to this new file in existing server.xml.
                    def serverXml = findConfigFile("server.xml", server.serverXmlFile);
                    def doc = getServerXmlDocFromConfig(serverXml);
                    logger.debug("Xml document we'll try to update after generate features doc=" + doc + " file=" + serverXml);
                    addGenerationCommentToConfig(doc, serverXml);
                } else {
                    logger.lifecycle("Regenerated the following features: " + missingLibertyFeatures);
                    // use logger.lifecycle so that message appears without --info tag on
                }
            } else {
                logger.lifecycle("No additional features were generated.");
                if (newServerXmlSrc.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(newServerXmlSrc);
                }
            }
        } catch (ParserConfigurationException | TransformerException | IOException e) {
            logger.debug("Exception creating the server features file", e);
            throw new GradleException("Automatic generation of features failed. Error attempting to create the " + GENERATED_FEATURES_FILE_NAME + ". Ensure your id has write permission to the server installation directory.", e);
        }
    }

    // Get the features from the server config and optionally exclude the specified config files from the search.
    private Set getServerFeatures(ServerFeatureUtil servUtil, Set generatedFiles, boolean excludeGenerated) {
        servUtil.setLowerCaseFeatures(false);
        // if optimizing, ignore generated files when passing in existing features to binary scanner
        FeaturesPlatforms fp = servUtil.getServerFeatures(server.configDirectory, server.serverXmlFile, new HashMap(), excludeGenerated ? generatedFiles : null); // pass generatedFiles to exclude them
        Set existingFeatures = fp == null ? new HashSet() : fp.getFeatures();

        servUtil.setLowerCaseFeatures(true);
        return existingFeatures;
    }

    // returns the features specified in the generated-features.xml file
    private Set getGeneratedFeatures(ServerFeatureUtil servUtil, File generatedFeaturesFile) {
        servUtil.setLowerCaseFeatures(false);
        FeaturesPlatforms fp = servUtil.getServerXmlFeatures(new FeaturesPlatforms(), server.configDirectory,
                generatedFeaturesFile, null, null);
        Set genFeatSet = fp == null ? new HashSet() : fp.getFeatures();
        servUtil.setLowerCaseFeatures(true);
        return genFeatSet;
    }

    /**
     * Gets the binary scanner jar file from the local cache.
     * Downloads it first from connected repositories such as Maven Central if a newer release is available than the cached version.
     * Note: Maven updates artifacts daily by default based on the last updated timestamp. Users should use 'mvn -U' to force updates if needed.
     *
     * @return The File object of the binary-app-scanner.jar in the local cache.
     * @throws PluginExecutionException indicates the binary-app-scanner.jar could not be found
     */
    private File getBinaryScannerJarFromRepository() throws PluginExecutionException {
        try {
            return ArtifactDownloadUtil.downloadBuildArtifact(project, BINARY_SCANNER_MAVEN_GROUP_ID, BINARY_SCANNER_MAVEN_ARTIFACT_ID, BINARY_SCANNER_MAVEN_TYPE, BINARY_SCANNER_MAVEN_VERSION);
        } catch (Exception e) {
            throw new PluginExecutionException("Could not retrieve the artifact " + BINARY_SCANNER_MAVEN_GROUP_ID + "."
                    + BINARY_SCANNER_MAVEN_ARTIFACT_ID
                    + " needed for generateFeatures. Ensure you have a connection to Maven Central or another repository that contains the "
                    + BINARY_SCANNER_MAVEN_GROUP_ID + "." + BINARY_SCANNER_MAVEN_ARTIFACT_ID
                    + ".jar configured in your build.gradle.",
                    e);
        }
    }

    /**
     * Return specificFile if it exists; otherwise check for a file with the requested name in the
     * configDirectory and return it if it exists. Null is returned if a file does not exist in 
     * either location.
     */
    private File findConfigFile(String fileName, File specificFile) {
        if (specificFile != null && specificFile.exists()) {
            return specificFile;
        }

        if (server.configDirectory == null) {
            return null;
        }
        File f = new File(server.configDirectory, fileName);
        if (f.exists()) {
            return f;
        } else {
            return null;
        }
    }

    // Convert a file into a document object
    private ServerConfigXmlDocument getServerXmlDocFromConfig(File serverXml) {
        if (serverXml == null || !serverXml.exists()) {
            return null;
        }
        try {
            return ServerConfigXmlDocument.newInstance(serverXml);
        } catch (ParserConfigurationException | SAXException | IOException e) {
            logger.debug("Exception creating server.xml object model", e);
        }
        return null;
    }

    /**
     * Add a comment to server.xml to warn them we created another file with features in it.
     */
    private void addGenerationCommentToConfig(ServerConfigXmlDocument doc, File serverXml) {
        if (doc == null) {
            return;
        }
        try {
            if (doc.createFMComment(FEATURES_FILE_MESSAGE)) {
                doc.writeXMLDocument(serverXml);
                XmlDocument.addNewlineBeforeFirstElement(serverXml);
            }
        } catch (IOException | TransformerException e) {
            log.debug("Exception adding comment to server.xml", e);
        }
        return;
    }

    private Set getClassesDirectories() {
        Set classesDirectories = new ArrayList();
        project.sourceSets.main.getOutput().getClassesDirs().each {
            if (it.exists()) {
                classesDirectories.add(it.getAbsolutePath());
            }
        }
        return classesDirectories;
    }

    /**
     * Return the latest EE major version detected in the project dependencies
     *
     * @param project
     * @return latest EE major version corresponding to the EE umbrella dependency, null if an EE umbrella dependency is not found
     */
    protected getEEVersion(Object project) {
        String eeVersion = null
        project.configurations.compileClasspath.allDependencies.each {
            dependency ->
                if ((dependency.group.equals("javax") && dependency.name.equals("javaee-api")) ||
                    (dependency.group.equals("jakarta.platform") &&
                        dependency.name.equals("jakarta.jakartaee-api"))) {
                    String newVersion = dependency.version
                    if (newVersion != null && isLatestVersion(eeVersion, newVersion)) {
                        eeVersion = newVersion
                    }
                }
        }
        return eeVersion;
    }
    /**
     * Returns the latest MicroProfile major version detected in the project dependencies
     *
     * @param project
     * @return latest MP major version corresponding to the MP umbrella dependency, null if an MP umbrella dependency is not found
     */
    protected getMPVersion(Object project) {
        String mpVersion = null
        project.configurations.compileClasspath.allDependencies.each {
            dependency ->
                if (dependency.group.equals("org.eclipse.microprofile") &&
                        dependency.name.equals("microprofile")) {
                    String newVersion = dependency.version
                    if (newVersion != null && isLatestVersion(mpVersion, newVersion)) {
                        mpVersion = newVersion;
                    }
                }
        }
        return mpVersion;
    }

    // Return true if the newVer > currentVer
    protected static boolean isLatestVersion(String currentVer, String newVer) {
        if (currentVer == null || currentVer.isEmpty())  {
            return true;
        }
        // Comparing versions: mp4 > mp3.3 > mp3.0 > mp3
        return (currentVer.compareTo(newVer) < 0);
    }

    // Define the logging functions of the binary scanner handler and make it available in this plugin
    private class BinaryScannerHandler extends BinaryScannerUtil {
        BinaryScannerHandler(File scannerFile) {
            super(scannerFile);
        }

        @Override
        public void debug(String msg) {
            logger.debug(msg);
        }

        @Override
        public void debug(String msg, Throwable t) {
            logger.debug(msg, t);
        }

        @Override
        public void error(String msg) {
            logger.error(msg);
        }

        @Override
        public void warn(String msg) {
            logger.warn(msg);
        }

        @Override
        public void info(String msg) {
            logger.lifecycle(msg);
        }

        @Override
        public boolean isDebugEnabled() {
            return logger.isEnabled(LogLevel.DEBUG);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy