/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.zeppelin.helium;

import com.github.eirslett.maven.plugins.frontend.lib.FrontendPluginFactory;
import com.github.eirslett.maven.plugins.frontend.lib.InstallationException;
import com.github.eirslett.maven.plugins.frontend.lib.NPMInstaller;
import com.github.eirslett.maven.plugins.frontend.lib.NodeInstaller;
import com.github.eirslett.maven.plugins.frontend.lib.NpmRunner;
import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig;
import com.github.eirslett.maven.plugins.frontend.lib.TaskRunnerException;
import com.github.eirslett.maven.plugins.frontend.lib.YarnInstaller;
import com.github.eirslett.maven.plugins.frontend.lib.YarnRunner;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import jakarta.inject.Inject;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Appender;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.WriterAppender;
import org.apache.log4j.spi.Filter;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Load helium visualization & spell
 */
public class HeliumBundleFactory {
  private static final Logger LOGGER = LoggerFactory.getLogger(HeliumBundleFactory.class);
  private static final String NODE_VERSION = "v16.16.0";
  private static final String NPM_VERSION = "8.11.0";
  private static final String YARN_VERSION = "v1.22.0";
  private static final String NPM_PACKAGE_NAME = "npm";
  protected static final String HELIUM_LOCAL_REPO = "helium-bundle";
  private static final String HELIUM_BUNDLES_DIR = "bundles";
  private static final String HELIUM_LOCAL_MODULE_DIR = "local_modules";
  private static final String HELIUM_BUNDLES_SRC_DIR = "src";
  private static final String HELIUM_BUNDLES_SRC = "load.js";
  private static final String YARN_CACHE_DIR = "yarn-cache";
  private static final String PACKAGE_JSON = "package.json";
  private static final String HELIUM_BUNDLE_CACHE = "helium.bundle.cache.js";
  private static final String HELIUM_BUNDLE = "helium.bundle.js";
  private static final String HELIUM_BUNDLES_VAR = "window._heliumBundles";
  private static final int FETCH_RETRY_COUNT = 2;
  private static final int FETCH_RETRY_FACTOR_COUNT = 1;
  private static final int FETCH_RETRY_MIN_TIMEOUT = 5000; // Milliseconds

  private final FrontendPluginFactory frontEndPluginFactory;
  private final File nodeInstallationDirectory;
  private final File heliumLocalRepoDirectory;
  private final File heliumBundleDirectory;
  private final File heliumLocalModuleDirectory;
  private final File yarnCacheDir;
  private File tabledataModulePath;
  private File visualizationModulePath;
  private File spellModulePath;
  private String defaultNodeInstallerUrl;
  private String defaultNpmInstallerUrl;
  private String defaultYarnInstallerUrl;
  private Gson gson;
  private boolean nodeAndNpmInstalled = false;

  private ByteArrayOutputStream out  = new ByteArrayOutputStream();

  @Inject
  public HeliumBundleFactory(ZeppelinConfiguration zConf) {
    this.heliumLocalRepoDirectory =
        new File(zConf.getAbsoluteDir(ConfVars.ZEPPELIN_DEP_LOCALREPO), HELIUM_LOCAL_REPO);
    this.heliumBundleDirectory = new File(heliumLocalRepoDirectory, HELIUM_BUNDLES_DIR);
    this.heliumLocalModuleDirectory = new File(heliumLocalRepoDirectory, HELIUM_LOCAL_MODULE_DIR);
    this.yarnCacheDir = new File(heliumLocalRepoDirectory, YARN_CACHE_DIR);
    this.defaultNodeInstallerUrl = zConf.getHeliumNodeInstallerUrl();
    this.defaultNpmInstallerUrl = zConf.getHeliumNpmInstallerUrl();
    this.defaultYarnInstallerUrl = zConf.getHeliumYarnInstallerUrl();
    this.nodeInstallationDirectory = this.heliumLocalRepoDirectory;

    this.frontEndPluginFactory =
        new FrontendPluginFactory(heliumLocalRepoDirectory, nodeInstallationDirectory);

    this.gson = new Gson();

    File zeppelinWebPath = new File(zConf.getAbsoluteDir("zeppelin-web"));
    if (!zeppelinWebPath.isDirectory()) {
      this.tabledataModulePath =
          new File(zConf.getAbsoluteDir("lib/node_modules/zeppelin-tabledata"));
      this.visualizationModulePath = new File(zConf.getAbsoluteDir("lib/node_modules/zeppelin-vis"));
      this.spellModulePath = new File(zConf.getAbsoluteDir("lib/node_modules/zeppelin-spell"));
    } else {
      this.tabledataModulePath = new File(zConf.getAbsoluteDir("zeppelin-web/src/app/tabledata"));
      this.visualizationModulePath =
          new File(zConf.getAbsoluteDir("zeppelin-web/src/app/visualization"));
      this.spellModulePath = new File(zConf.getAbsoluteDir("zeppelin-web/src/app/spell"));
    }
  }

  void installNodeAndNpm() throws TaskRunnerException {
    if (nodeAndNpmInstalled) {
      return;
    }
    try {
      NodeInstaller nodeInstaller = frontEndPluginFactory
              .getNodeInstaller(getProxyConfig(isSecure(defaultNodeInstallerUrl)));
      nodeInstaller.setNodeVersion(NODE_VERSION);
      nodeInstaller.setNodeDownloadRoot(defaultNodeInstallerUrl);
      nodeInstaller.install();

      NPMInstaller npmInstaller = frontEndPluginFactory
              .getNPMInstaller(getProxyConfig(isSecure(defaultNpmInstallerUrl)));
      npmInstaller.setNpmVersion(NPM_VERSION);
      npmInstaller.setNpmDownloadRoot(defaultNpmInstallerUrl + "/" + NPM_PACKAGE_NAME + "/-/");
      npmInstaller.install();

      YarnInstaller yarnInstaller = frontEndPluginFactory
              .getYarnInstaller(getProxyConfig(isSecure(defaultYarnInstallerUrl)));
      yarnInstaller.setYarnVersion(YARN_VERSION);
      yarnInstaller.setYarnDownloadRoot(defaultYarnInstallerUrl);
      yarnInstaller.install();
      yarnCacheDir.mkdirs();
      String yarnCacheDirPath = yarnCacheDir.getAbsolutePath();
      yarnCommand(frontEndPluginFactory, "config set cache-folder " + yarnCacheDirPath);

      configureLogger();
      nodeAndNpmInstalled = true;
    } catch (InstallationException e) {
      LOGGER.error(e.getMessage(), e);
    }
  }

  private ProxyConfig getProxyConfig(boolean isSecure) {
    List<ProxyConfig.Proxy> proxies = new LinkedList<>();

    String httpProxy = StringUtils.isBlank(System.getenv("http_proxy")) ?
            System.getenv("HTTP_PROXY") : System.getenv("http_proxy");

    String httpsProxy = StringUtils.isBlank(System.getenv("https_proxy")) ?
            System.getenv("HTTPS_PROXY") : System.getenv("https_proxy");

    try {
      if (isSecure && StringUtils.isNotBlank(httpsProxy))
        proxies.add(generateProxy("secure", new URI(httpsProxy)));
      else if (!isSecure && StringUtils.isNotBlank(httpProxy))
        proxies.add(generateProxy("insecure", new URI(httpProxy)));
    } catch (Exception ex) {
      LOGGER.error(ex.getMessage(), ex);
    }
    return new ProxyConfig(proxies);
  }

  private ProxyConfig.Proxy generateProxy(String proxyId, URI uri) {

    String protocol = uri.getScheme();
    String host = uri.getHost();
    int port = uri.getPort() <= 0 ? 80 : uri.getPort();

    String username = null;
    String password = null;
    if (uri.getUserInfo() != null) {
      String[] authority = uri.getUserInfo().split(":");
      if (authority.length == 2) {
        username = authority[0];
        password = authority[1];
      } else if (authority.length == 1) {
        username = authority[0];
      }
    }
    String nonProxyHosts = StringUtils.isBlank(System.getenv("no_proxy")) ?
            System.getenv("NO_PROXY") : System.getenv("no_proxy");
    return new ProxyConfig.Proxy(proxyId, protocol, host, port, username, password, nonProxyHosts);
  }

  private boolean isSecure(String url) {
    return url.toLowerCase().startsWith("https");
  }

  public void buildAllPackages(List<HeliumPackage> pkgs) throws IOException {
    buildAllPackages(pkgs, false);
  }

  private File getHeliumPackageDirectory(String pkgName) {
    return new File(heliumBundleDirectory, pkgName);
  }

  private File getHeliumPackageSourceDirectory(String pkgName) {
    return new File(heliumBundleDirectory, pkgName + "/" + HELIUM_BUNDLES_SRC_DIR);
  }

  private File getHeliumPackageBundleCache(String pkgName) {
    return new File(heliumBundleDirectory, pkgName + "/" + HELIUM_BUNDLE_CACHE);
  }

  private static List<String> unTgz(File tarFile, File directory) throws IOException {
    List<String> result = new ArrayList<>();
    try (TarArchiveInputStream in = new TarArchiveInputStream(
            new GzipCompressorInputStream(new FileInputStream(tarFile)))) {
      TarArchiveEntry entry = in.getNextTarEntry();
      while (entry != null) {
        if (entry.isDirectory()) {
          entry = in.getNextTarEntry();
          continue;
        }
        File curfile = new File(directory, entry.getName());
        File parent = curfile.getParentFile();
        if (!parent.exists()) {
          parent.mkdirs();
        }
        try (OutputStream out = new FileOutputStream(curfile)) {
          IOUtils.copy(in, out);
        }
        result.add(entry.getName());
        entry = in.getNextTarEntry();
      }
    }
    return result;
  }

  /**
   * @return main file name of this helium package (relative path)
   */
  private String downloadPackage(HeliumPackage pkg, String[] nameAndVersion, File bundleDir,
                                String templateWebpackConfig, String templatePackageJson,
                                FrontendPluginFactory fpf) throws IOException, TaskRunnerException {
    if (bundleDir.exists()) {
      FileUtils.deleteQuietly(bundleDir);
    }
    FileUtils.forceMkdir(bundleDir);

    FileFilter copyFilter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        String fileName = pathname.getName();
        if (fileName.startsWith(".") || fileName.startsWith("#") || fileName.startsWith("~")) {
          return false;
        } else {
          return true;
        }
      }
    };

    if (isLocalPackage(pkg)) {
      FileUtils.copyDirectory(
              new File(pkg.getArtifact()),
              bundleDir,
              copyFilter);
    } else {
      // if online package
      String version = nameAndVersion[1];
      File tgz = new File(heliumLocalRepoDirectory, pkg.getName() + "-" + version + ".tgz");
      FileUtils.deleteQuietly(tgz);

      // wget, extract and move dir to `bundles/${pkg.getName()}`, and remove tgz
      npmCommand(fpf, "pack " + pkg.getArtifact());
      File extracted = new File(heliumBundleDirectory, "package");
      FileUtils.deleteDirectory(extracted);
      List<String> entries = unTgz(tgz, heliumBundleDirectory);
      for (String entry : entries) {
        LOGGER.debug("Extracted {}", entry);
      }
      FileUtils.deleteQuietly(tgz);
      FileUtils.copyDirectory(extracted, bundleDir);
      FileUtils.deleteDirectory(extracted);
    }

    // 1. setup package.json
    File existingPackageJson = new File(bundleDir, "package.json");
    JsonReader reader = new JsonReader(new FileReader(existingPackageJson));
    Map<String, Object> packageJson = gson.fromJson(reader,
            new TypeToken<Map<String, Object>>(){}.getType());
    Map<String, String> existingDeps = (Map<String, String>) packageJson.get("dependencies");
    String mainFileName = (String) packageJson.get("main");

    StringBuilder dependencies = new StringBuilder();
    int index = 0;
    for (Map.Entry<String, String> e: existingDeps.entrySet()) {
      dependencies.append("    \"").append(e.getKey()).append("\": ");
      if (e.getKey().equals("zeppelin-vis") ||
          e.getKey().equals("zeppelin-tabledata") ||
          e.getKey().equals("zeppelin-spell")) {
        dependencies.append("\"file:../../" + HELIUM_LOCAL_MODULE_DIR + "/")
                .append(e.getKey()).append("\"");
      } else {
        dependencies.append("\"").append(e.getValue()).append("\"");
      }

      if (index < existingDeps.size() - 1) {
        dependencies.append(",\n");
      }
      index = index + 1;
    }

    FileUtils.deleteQuietly(new File(bundleDir, PACKAGE_JSON));
    templatePackageJson = templatePackageJson.replaceFirst("PACKAGE_NAME", pkg.getName());
    templatePackageJson = templatePackageJson.replaceFirst("MAIN_FILE", mainFileName);
    templatePackageJson = templatePackageJson.replaceFirst("DEPENDENCIES", dependencies.toString());
    FileUtils.write(new File(bundleDir, PACKAGE_JSON), templatePackageJson, StandardCharsets.UTF_8);

    // 2. setup webpack.config
    FileUtils.write(new File(bundleDir, "webpack.config.js"), templateWebpackConfig, StandardCharsets.UTF_8);

    return mainFileName;
  }

  private void prepareSource(HeliumPackage pkg, String[] moduleNameVersion,
                            String mainFileName) throws IOException {
    StringBuilder loadJsImport = new StringBuilder();
    StringBuilder loadJsRegister = new StringBuilder();
    String className = "bundles" + pkg.getName().replaceAll("[-_]", "");

    // remove postfix `.js` for ES6 import
    if (mainFileName.endsWith(".js")) {
      mainFileName = mainFileName.substring(0, mainFileName.length() - 3);
    }

    loadJsImport
        .append("import ")
        .append(className)
        .append(" from \"../" + mainFileName + "\"\n");

    loadJsRegister.append(HELIUM_BUNDLES_VAR).append(".push({\n");
    loadJsRegister.append("id: \"").append(moduleNameVersion[0]).append("\",\n");
    loadJsRegister.append("name: \"").append(pkg.getName()).append("\",\n");
    loadJsRegister.append("icon: ").append(gson.toJson(pkg.getIcon())).append(",\n");
    loadJsRegister.append("type: \"").append(pkg.getType()).append("\",\n");
    loadJsRegister.append("class: ").append(className).append("\n");
    loadJsRegister.append("})\n");

    File srcDir = getHeliumPackageSourceDirectory(pkg.getName());
    FileUtils.forceMkdir(srcDir);
    FileUtils.write(new File(srcDir, HELIUM_BUNDLES_SRC),
            loadJsImport.append(loadJsRegister).toString(), StandardCharsets.UTF_8);
  }

  private synchronized void installNodeModules(FrontendPluginFactory fpf) throws IOException {
    try {
      out.reset();
      String commandForNpmInstall =
              String.format("install --fetch-retries=%d --fetch-retry-factor=%d " +
                              "--fetch-retry-mintimeout=%d",
                      FETCH_RETRY_COUNT, FETCH_RETRY_FACTOR_COUNT, FETCH_RETRY_MIN_TIMEOUT);
      LOGGER.info("Installing required node modules");
      yarnCommand(fpf, commandForNpmInstall);
      LOGGER.info("Installed required node modules");
    } catch (TaskRunnerException e) {
      throw new IOException(e);
    }
  }

  private synchronized File bundleHeliumPackage(FrontendPluginFactory fpf,
                                               File bundleDir) throws IOException {
    try {
      out.reset();
      LOGGER.info("Bundling helium packages");
      yarnCommand(fpf, "run bundle");
      LOGGER.info("Bundled helium packages");
    } catch (TaskRunnerException e) {
      throw new IOException(new String(out.toByteArray()));
    }

    String bundleStdoutResult = new String(out.toByteArray());
    File heliumBundle = new File(bundleDir, HELIUM_BUNDLE);
    if (!heliumBundle.isFile()) {
      throw new IOException(
              "Can't create bundle: \n" + bundleStdoutResult);
    }

    WebpackResult result = getWebpackResultFromOutput(bundleStdoutResult);
    if (result.errors.length > 0) {
      FileUtils.deleteQuietly(heliumBundle);
      throw new IOException(result.errors[0]);
    }

    return heliumBundle;
  }

  public synchronized File buildPackage(HeliumPackage pkg,
                                        boolean rebuild,
                                        boolean recopyLocalModule) throws IOException {
    if (pkg == null) {
      return null;
    }

    String[] moduleNameVersion = getNpmModuleNameAndVersion(pkg);
    if (moduleNameVersion == null) {
      LOGGER.warn("Can't get module name and version of package {}", pkg.getName());
      return null;
    }

    String pkgName = pkg.getName();
    File bundleDir = getHeliumPackageDirectory(pkgName);
    File bundleCache = getHeliumPackageBundleCache(pkgName);

    if (!rebuild && bundleCache.exists() && !bundleCache.isDirectory()) {
      return bundleCache;
    }

    // 0. install node, npm (should be called before `downloadPackage`
    try {
      installNodeAndNpm();
    } catch (TaskRunnerException e) {
      throw new IOException(e);
    }

    // 1. prepare directories
    if (!heliumLocalRepoDirectory.exists() || !heliumLocalRepoDirectory.isDirectory()) {
      FileUtils.deleteQuietly(heliumLocalRepoDirectory);
      FileUtils.forceMkdir(heliumLocalRepoDirectory);
    }
    FrontendPluginFactory fpf = new FrontendPluginFactory(
            bundleDir, nodeInstallationDirectory);

    // resources: webpack.js, package.json
    String templateWebpackConfig = Resources.toString(
        Resources.getResource("helium/webpack.config.js"), StandardCharsets.UTF_8);
    String templatePackageJson = Resources.toString(
        Resources.getResource("helium/" + PACKAGE_JSON), StandardCharsets.UTF_8);

    // 2. download helium package using `npm pack`
    String mainFileName = null;
    try {
      mainFileName = downloadPackage(pkg, moduleNameVersion, bundleDir,
              templateWebpackConfig, templatePackageJson, fpf);
    } catch (TaskRunnerException e) {
      throw new IOException(e);
    }

    // 3. prepare bundle source
    prepareSource(pkg, moduleNameVersion, mainFileName);

    // 4. install node and local modules for a bundle
    copyFrameworkModulesToInstallPath(recopyLocalModule); // should copy local modules first
    installNodeModules(fpf);

    // 5. let's bundle and update cache
    File heliumBundle = bundleHeliumPackage(fpf, bundleDir);
    bundleCache.delete();
    FileUtils.moveFile(heliumBundle, bundleCache);

    return bundleCache;
  }

  private synchronized void buildAllPackages(List<HeliumPackage> pkgs, boolean rebuild)
      throws IOException {

    if (pkgs == null || pkgs.isEmpty()) {
      return;
    }

    // DON't recopy local modules when build all packages to avoid duplicated copies.
    boolean recopyLocalModules = false;

    for (HeliumPackage pkg : pkgs) {
      try {
        buildPackage(pkg, rebuild, recopyLocalModules);
      } catch (IOException e) {
        LOGGER.error("Failed to build helium package: {}", pkg.getArtifact(), e);
      }
    }
  }

  private void copyFrameworkModule(boolean recopy, FileFilter filter,
                           File src, File dest) throws IOException {
    if (src != null) {
      if (recopy && dest.exists()) {
        FileUtils.deleteDirectory(dest);
      }

      if (!dest.exists()) {
        FileUtils.copyDirectory(
            src,
            dest,
            filter);
      }
    }
  }

  private void deleteYarnCache() {
    FilenameFilter filter = new FilenameFilter() {
      @Override
      public boolean accept(File dir, String name) {
        if ((name.startsWith("npm-zeppelin-vis-") ||
            name.startsWith("npm-zeppelin-tabledata-") ||
            name.startsWith("npm-zeppelin-spell-")) &&
            dir.isDirectory()) {
          return true;
        }

        return false;
      }
    };

    File[] localModuleCaches = yarnCacheDir.listFiles(filter);
    if (localModuleCaches != null) {
      for (File f : localModuleCaches) {
        FileUtils.deleteQuietly(f);
      }
    }
  }

  void copyFrameworkModulesToInstallPath(boolean recopy)
      throws IOException {

    FileFilter npmPackageCopyFilter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        String fileName = pathname.getName();
        if (fileName.startsWith(".") || fileName.startsWith("#") || fileName.startsWith("~")) {
          return false;
        } else {
          return true;
        }
      }
    };

    FileUtils.forceMkdir(heliumLocalModuleDirectory);
    // should delete yarn caches for local modules since they might be updated
    deleteYarnCache();

    // install tabledata module
    File tabledataModuleInstallPath = new File(heliumLocalModuleDirectory,
        "zeppelin-tabledata");
    copyFrameworkModule(recopy, npmPackageCopyFilter,
        tabledataModulePath, tabledataModuleInstallPath);

    // install visualization module
    File visModuleInstallPath = new File(heliumLocalModuleDirectory,
        "zeppelin-vis");
    copyFrameworkModule(recopy, npmPackageCopyFilter,
        visualizationModulePath, visModuleInstallPath);

    // install spell module
    File spellModuleInstallPath = new File(heliumLocalModuleDirectory,
        "zeppelin-spell");
    copyFrameworkModule(recopy, npmPackageCopyFilter,
        spellModulePath, spellModuleInstallPath);
  }

  private WebpackResult getWebpackResultFromOutput(String output) {
    BufferedReader reader = new BufferedReader(new StringReader(output));

    boolean webpackRunDetected = false;
    boolean resultJsonDetected = false;
    StringBuilder sb = new StringBuilder();
    try {
      String next, line = reader.readLine();
      for (boolean last = (line == null); !last; line = next) {
        last = ((next = reader.readLine()) == null);

        if (!webpackRunDetected) {
          String trimed = line.trim();
          if (trimed.contains("webpack")
              && trimed.endsWith("--json --registry=https://registry.npmjs.org/")) {
            webpackRunDetected = true;
          }
          continue;
        }

        if (!resultJsonDetected) {
          if (line.trim().equals("{")) {
            sb.append(line);
            resultJsonDetected = true;
          }
          continue;
        }

        if (resultJsonDetected && webpackRunDetected) {
          // yarn command always ends with `Done in ... seconds `
          if (!last) {
            sb.append(line);
          }
        }
      }
      return WebpackResult.fromJson(sb.toString());
    } catch (IOException e) {
      LOGGER.error(e.getMessage(), e);
      return new WebpackResult();
    }
  }

  private boolean isLocalPackage(HeliumPackage pkg) {
    return (pkg.getArtifact().startsWith(".") || pkg.getArtifact().startsWith("/"));
  }

  private String[] getNpmModuleNameAndVersion(HeliumPackage pkg) {
    String artifact = pkg.getArtifact();

    if (isLocalPackage(pkg)) {
      File packageJson = new File(artifact, "package.json");
      if (!packageJson.isFile()) {
        return null;
      }
      try {
        NpmPackage npmPackage = NpmPackage.fromJson(
            FileUtils.readFileToString(packageJson, StandardCharsets.UTF_8));

        String[] nameVersion = new String[2];
        nameVersion[0] = npmPackage.name;
        nameVersion[1] = npmPackage.version;
        return nameVersion;
      } catch (IOException e) {
        LOGGER.error(e.getMessage(), e);
        return null;
      }
    } else {
      String[] nameVersion = new String[2];

      int pos;
      if ((pos = artifact.indexOf('@')) > 0) {
        nameVersion[0] = artifact.substring(0, pos);
        nameVersion[1] = artifact.substring(pos + 1);
      } else if (
          (pos = artifact.indexOf('^')) > 0 ||
              (pos = artifact.indexOf('~')) > 0) {
        nameVersion[0] = artifact.substring(0, pos);
        nameVersion[1] = artifact.substring(pos);
      } else {
        nameVersion[0] = artifact;
        nameVersion[1] = "";
      }
      return nameVersion;
    }
  }

  synchronized void install(HeliumPackage pkg) throws TaskRunnerException {
    String commandForNpmInstallArtifact =
        String.format("install %s --fetch-retries=%d --fetch-retry-factor=%d " +
                        "--fetch-retry-mintimeout=%d", pkg.getArtifact(),
                FETCH_RETRY_COUNT, FETCH_RETRY_FACTOR_COUNT, FETCH_RETRY_MIN_TIMEOUT);
    npmCommand(commandForNpmInstallArtifact);
  }

  private void npmCommand(String args) throws TaskRunnerException {
    npmCommand(args, new HashMap<>());
  }

  private void npmCommand(String args, Map<String, String> env) throws TaskRunnerException {
    NpmRunner npm = frontEndPluginFactory.getNpmRunner(
            getProxyConfig(isSecure(defaultNpmInstallerUrl)), defaultNpmInstallerUrl);
    npm.execute(args, env);
  }

  private void npmCommand(FrontendPluginFactory fpf, String args) throws TaskRunnerException {
    npmCommand(args, new HashMap<>());
  }

  private void yarnCommand(FrontendPluginFactory fpf, String args) throws TaskRunnerException {
    yarnCommand(fpf, args, new HashMap<>());
  }

  private void yarnCommand(FrontendPluginFactory fpf,
                           String args, Map<String, String> env) throws TaskRunnerException {
    YarnRunner yarn = fpf.getYarnRunner(
            getProxyConfig(isSecure(defaultNpmInstallerUrl)), defaultNpmInstallerUrl);
    yarn.execute(args, env);
  }

  private synchronized void configureLogger() {
    org.apache.log4j.Logger npmLogger = org.apache.log4j.Logger.getLogger(
        "com.github.eirslett.maven.plugins.frontend.lib.DefaultYarnRunner");
    Enumeration appenders = org.apache.log4j.Logger.getRootLogger().getAllAppenders();

    if (appenders != null) {
      while (appenders.hasMoreElements()) {
        Appender appender = (Appender) appenders.nextElement();
        appender.addFilter(new Filter() {

          @Override
          public int decide(LoggingEvent loggingEvent) {
            if (loggingEvent.getLoggerName().contains("DefaultYarnRunner")) {
              return DENY;
            } else {
              return NEUTRAL;
            }
          }
        });
      }
    }
    npmLogger.addAppender(new WriterAppender(
        new PatternLayout("%m%n"),
        out
    ));
  }
}
