org.wisdom.maven.node.NPM.java Source code

Java tutorial

Introduction

Here is the source code for org.wisdom.maven.node.NPM.java

Source

/*
 * #%L
 * Wisdom-Framework
 * %%
 * Copyright (C) 2013 - 2014 Wisdom Framework
 * %%
 * 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.
 * #L%
 */
package org.wisdom.maven.node;

import com.github.eirslett.maven.plugins.frontend.lib.TaskRunnerException;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.exec.environment.EnvironmentUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.wisdom.maven.mojos.AbstractWisdomMojo;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;

/**
 * Manages an execution of NPM.
 */
public final class NPM {

    /**
     * The 'package.json' constant.
     */
    public static final String PACKAGE_JSON = "package.json";
    private final String npmName;
    private final String npmVersion;

    private final NodeManager node;
    private final Log log;
    private final String[] installArguments;

    private boolean handleQuoting = true;
    private LoggedOutputStream errorStreamFromLastExecution;
    private LoggedOutputStream outputStreamFromLastExecution;
    private boolean registerOutputStream = false;

    /**
     * Constructor used to install an NPM.
     *
     * @param log     the logger
     * @param manager the node manager
     * @param name    the NPM name
     * @param version the NPM version
     * @param args    additional installation arguments.
     */
    private NPM(Log log, NodeManager manager, String name, String version, String... args) {
        this.node = manager;
        this.npmName = name;
        this.npmVersion = version;
        this.log = log;
        this.handleQuoting = false;
        this.installArguments = args;
        ensureNodeInstalled();
    }

    /**
     * Let Wisdom handle the quoting of the arguments passed to an NPM execution.
     * Notice that generally, NPM executables handle the quoting by themselves, meaning that you should be careful
     * before using this method.
     *
     * @return the current NPM
     */
    public NPM handleQuoting() {
        handleQuoting = true;
        return this;
    }

    private void ensureNodeInstalled() {
        try {
            node.installIfNotInstalled();
        } catch (IOException e) {
            log.error("Cannot install node", e);
        }
    }

    /**
     * Executes the current NPM.
     * NPM can have several executable attached to them, so the 'binary' argument specifies which
     * one has to be executed. Check the 'bin' entry of the package.json file to determine which
     * one you need. 'Binary' is the key associated with the executable to invoke. For example, in
     * <code>
     * <pre>
     *      "bin": {
     *           "coffee": "./bin/coffee",
     *           "cake": "./bin/cake"
     *      },
     *     </pre>
     * </code>
     * <p/>
     * we have two alternatives: 'coffee' and 'cake'.
     *
     * @param binary the key of the binary to invoke
     * @param args   the arguments
     * @return the execution exit status
     * @throws MojoExecutionException if the execution failed
     */
    public int execute(String binary, String... args) throws MojoExecutionException {
        File destination = getNPMDirectory();
        if (!destination.isDirectory()) {
            throw new IllegalStateException("The npm module " + this.npmName + " is not installed");
        }

        CommandLine cmdLine = new CommandLine(node.getNodeExecutable());
        File npmExec = null;
        try {
            npmExec = findExecutable(binary);
        } catch (IOException | ParseException e) { //NOSONAR
            log.error(e);
        }
        if (npmExec == null) {
            throw new IllegalStateException(
                    "Cannot execute NPM " + this.npmName + " - cannot find the JavaScript file " + "matching "
                            + binary + " in the " + PACKAGE_JSON + " file");
        }

        // NPM is launched using the main file.
        cmdLine.addArgument(npmExec.getAbsolutePath(), false);
        for (String arg : args) {
            cmdLine.addArgument(arg, this.handleQuoting);
        }

        DefaultExecutor executor = new DefaultExecutor();

        executor.setExitValue(0);

        errorStreamFromLastExecution = new LoggedOutputStream(log, true, true);
        outputStreamFromLastExecution = new LoggedOutputStream(log, false, registerOutputStream);
        PumpStreamHandler streamHandler = new PumpStreamHandler(outputStreamFromLastExecution,
                errorStreamFromLastExecution);

        executor.setStreamHandler(streamHandler);
        executor.setWorkingDirectory(node.getWorkDir());
        log.info("Executing " + cmdLine.toString() + " from " + executor.getWorkingDirectory().getAbsolutePath());

        try {
            return executor.execute(cmdLine, extendEnvironmentWithNodeInPath(node));
        } catch (IOException e) {
            throw new MojoExecutionException("Error during the execution of the NPM " + npmName, e);
        }

    }

    private static Map<String, String> extendEnvironmentWithNodeInPath(NodeManager node) throws IOException {
        Map<String, String> env = EnvironmentUtils.getProcEnvironment();
        if (env.containsKey("PATH")) {
            String path = env.get("PATH");
            env.put("PATH", node.getNodeExecutable().getParent() + File.pathSeparator + path);
        } else {
            env.put("PATH", node.getNodeExecutable().getParent());
        }
        return env;
    }

    /**
     * Executes the current NPM using the given binary file.
     *
     * @param binary the program to run
     * @param args   the arguments
     * @return the execution exit status
     * @throws MojoExecutionException if the execution failed
     */
    public int execute(File binary, String... args) throws MojoExecutionException {
        File destination = getNPMDirectory();
        if (!destination.isDirectory()) {
            throw new IllegalStateException("NPM " + this.npmName + " not installed");
        }

        CommandLine cmdLine = new CommandLine(node.getNodeExecutable());

        if (binary == null) {
            throw new IllegalStateException(
                    "Cannot execute NPM " + this.npmName + " - the given binary is 'null'.");
        }

        if (!binary.isFile()) {
            throw new IllegalStateException("Cannot execute NPM " + this.npmName + " - the given binary does not "
                    + "exist: " + binary.getAbsoluteFile() + ".");
        }

        // NPM is launched using the main file.
        cmdLine.addArgument(binary.getAbsolutePath(), false);
        for (String arg : args) {
            cmdLine.addArgument(arg, this.handleQuoting);
        }

        DefaultExecutor executor = new DefaultExecutor();

        executor.setExitValue(0);

        errorStreamFromLastExecution = new LoggedOutputStream(log, true, true);
        outputStreamFromLastExecution = new LoggedOutputStream(log, false, registerOutputStream);

        PumpStreamHandler streamHandler = new PumpStreamHandler(outputStreamFromLastExecution,
                errorStreamFromLastExecution);

        executor.setStreamHandler(streamHandler);
        executor.setWorkingDirectory(node.getWorkDir());
        log.info("Executing " + cmdLine.toString() + " from " + executor.getWorkingDirectory().getAbsolutePath());

        try {
            return executor.execute(cmdLine, extendEnvironmentWithNodeInPath(node));
        } catch (IOException e) {
            throw new MojoExecutionException("Error during the execution of the NPM " + npmName, e);
        }

    }

    /**
     * Gets the error stream from the last NPM execution.
     *
     * @return the error stream.
     */
    public String getLastErrorStream() {
        if (errorStreamFromLastExecution != null) {
            return errorStreamFromLastExecution.getOutput();
        } else {
            return null;
        }
    }

    /**
     * Gets the output stream from the last NPM execution. The output stream must have been explicitly recorded
     * using {@link #registerOutputStream(boolean)}.
     *
     * @return the output stream.
     */
    public String getLastOutputStream() {
        if (outputStreamFromLastExecution != null) {
            return outputStreamFromLastExecution.getOutput();
        } else {
            return null;
        }
    }

    /**
     * Tries to find the main JS file.
     * This search is based on the `package.json` file and it's `bin` entry.
     * If there is an entry in the `bin` object matching `binary`, it uses this javascript file.
     * If the search failed, `null` is returned
     *
     * @return the JavaScript file to execute, null if not found
     */
    public File findExecutable(String binary) throws IOException, ParseException {
        File npmDirectory = getNPMDirectory();
        File packageFile = new File(npmDirectory, PACKAGE_JSON);
        if (!packageFile.isFile()) {
            throw new IllegalStateException(
                    "Invalid NPM " + npmName + " - " + packageFile.getAbsolutePath() + " does not" + " exist");
        }
        FileReader reader = null;
        try {
            reader = new FileReader(packageFile);
            JSONObject json = (JSONObject) JSONValue.parseWithException(reader);
            JSONObject bin = (JSONObject) json.get("bin");
            if (bin == null) {
                log.error("No `bin` object in " + packageFile.getAbsolutePath());
                return null;
            } else {
                String exec = (String) bin.get(binary);
                if (exec == null) {
                    log.error(
                            "No `" + binary + "` object in the `bin` object from " + packageFile.getAbsolutePath());
                    return null;
                }
                File file = new File(npmDirectory, exec);
                if (!file.isFile()) {
                    log.error(
                            "To execute " + npmName + ", an entry was found for " + binary + " in 'package.json', "
                                    + "but the specified file does not exist - " + file.getAbsolutePath());
                    return null;
                }
                return file;
            }
        } finally {
            IOUtils.closeQuietly(reader);
        }
    }

    private File getNPMDirectory() {
        return new File(node.getNodeModulesDirectory(), npmName);
    }

    /**
     * Installs the NPM. The NPM is installed using {@code npm install npm@version}, without the {@code -g} option.
     * The installation is executed from the installation directory of node.
     */
    private void install() {
        File directory = getNPMDirectory();
        if (directory.isDirectory()) {
            // Check the version
            String version = getVersionFromNPM(directory, log);
            // Are we looking for a specific version ?
            if (npmVersion != null) {
                // Yes
                if (!npmVersion.equals(version)) {
                    log.info("The NPM " + npmName + " is already installed but not in the requested version"
                            + " (requested: " + npmVersion + " - current: " + version + ") - uninstall it");
                    try {
                        FileUtils.deleteDirectory(directory);
                    } catch (IOException e) { //NOSONAR
                        // ignore it.
                    }
                } else {
                    log.debug("NPM " + npmName + " already installed in " + directory.getAbsolutePath() + " ("
                            + version + ")");
                    return;
                }
            } else {
                // No
                log.debug("NPM " + npmName + " already installed in " + directory.getAbsolutePath() + " " + "("
                        + version + ")");
                return;
            }
        }

        StringBuilder command = new StringBuilder();
        command.append("install ");
        if (installArguments != null) {
            for (String s : installArguments) {
                command.append(s);
                command.append(" ");
            }
        }

        if (npmVersion != null) {
            command.append(npmName).append("@").append(npmVersion);
        } else {
            command.append(npmName);
        }

        try {
            node.factoryForNPMInstallation().getNpmRunner(node.proxy()).execute(command.toString());
        } catch (TaskRunnerException e) {
            log.error("Error during the installation of the NPM " + npmName + " - check log", e);
        }
    }

    /**
     * Utility method to extract the version from a NPM by reading its 'package.json' file.
     *
     * @param npmDirectory the directory in which the NPM is installed
     * @param log          the logger object
     * @return the read version, "0.0.0" if there are not 'package.json' file, {@code null} if this file cannot be
     * read or does not contain the "version" metadata
     */
    public static String getVersionFromNPM(File npmDirectory, Log log) {
        File packageFile = new File(npmDirectory, PACKAGE_JSON);
        if (!packageFile.isFile()) {
            return "0.0.0";
        }

        FileReader reader = null;
        try {
            reader = new FileReader(packageFile); //NOSONAR
            JSONObject json = (JSONObject) JSONValue.parseWithException(reader);
            return (String) json.get("version");
        } catch (IOException | ParseException e) {
            log.error("Cannot extract version from " + packageFile.getAbsolutePath(), e);
        } finally {
            IOUtils.closeQuietly(reader);
        }

        return null;
    }

    /**
     * Creates an NPM object based on the NPM's name and version (or tag).
     * If the NPM is not installed, it installs it.
     * There returned NPM let you execute it.
     *
     * @param mojo    the Wisdom Mojo
     * @param name    the NPM name
     * @param version the NPM version or tag
     * @param args    additional arguments (use with precautions)
     * @return the NPM object. The NPM may have been installed if it was not installed or installed in another version.
     */
    public static NPM npm(AbstractWisdomMojo mojo, String name, String version, String... args) {
        NPM npm = new NPM(mojo.getLog(), mojo.getNodeManager(), name, version, args);
        npm.install();
        return npm;
    }

    /**
     * Configures the NPM registry location.
     *
     * @param node           the node manager
     * @param log            the logger
     * @param npmRegistryUrl the registry url
     */
    public static void configureRegistry(NodeManager node, Log log, String npmRegistryUrl) {
        try {
            node.factory().getNpmRunner(node.proxy()).execute("config set registry " + npmRegistryUrl);
        } catch (TaskRunnerException e) {
            log.error("Error during the configuration of NPM registry with the url " + npmRegistryUrl
                    + " - check log", e);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        NPM npm = (NPM) o;

        return npmName.equals(npm.npmName)
                && !(npmVersion != null ? !npmVersion.equals(npm.npmVersion) : npm.npmVersion != null);

    }

    @Override
    public int hashCode() {
        int result = npmName.hashCode();
        result = 31 * result + (npmVersion != null ? npmVersion.hashCode() : 0);
        return result;
    }

    /**
     * Enables the recoding ot the output stream when the current NPM is executed.
     *
     * @param register whether or not the output stream must be recorded
     */
    public void registerOutputStream(boolean register) {
        registerOutputStream = register;
    }
}