org.pentaho.osgi.platform.webjars.WebjarsURLConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.osgi.platform.webjars.WebjarsURLConnection.java

Source

/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright 2017 Pentaho Corporation. All rights reserved.
 */

package org.pentaho.osgi.platform.webjars;

import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.DiagnosticGroups;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.SourceMap;
import com.google.javascript.jscomp.WarningLevel;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.osgi.framework.Constants;
import org.pentaho.js.require.RequireJsGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;

/**
 * Created by nbaker on 9/6/14.
 */
public class WebjarsURLConnection extends URLConnection {

    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(5, r -> {
        Thread thread = Executors.defaultThreadFactory().newThread(r);
        thread.setDaemon(true);
        thread.setName("WebjarsURLConnection pool");
        return thread;
    });

    Future<Void> transform_thread;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final boolean minificationEnabled;

    public WebjarsURLConnection(URL url) {
        this(url, true);
    }

    public WebjarsURLConnection(URL url, boolean minificationEnabled) {
        super(url);

        this.minificationEnabled = minificationEnabled;
    }

    @Override
    public void connect() throws IOException {
    }

    @Override
    public InputStream getInputStream() throws IOException {
        try {
            final PipedOutputStream pipedOutputStream = new PipedOutputStream();
            PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

            // making this here allows to fail with invalid URLs
            final URLConnection urlConnection = url.openConnection();
            urlConnection.connect();
            final InputStream originalInputStream = urlConnection.getInputStream();

            transform_thread = EXECUTOR.submit(
                    new WebjarsTransformer(url, originalInputStream, pipedOutputStream, this.minificationEnabled));

            return pipedInputStream;
        } catch (Exception e) {
            logger.error(getURL().toString() + ": Error opening url");

            throw new IOException("Error opening url", e);
        }
    }

    private static class WebjarsTransformer implements Callable<Void> {
        private static final String DEBUG_MESSAGE_FAILED_WRITING = "Problem transfering Jar content, probably JarOutputStream was already closed.";

        private static final String MANIFEST_MF = "MANIFEST.MF";
        private static final String PENTAHO_RJS_LOCATION = "META-INF/js/require.json";

        private static final String WEBJARS_REQUIREJS_NAME = "webjars-requirejs.js";
        private static final Pattern MODULE_PATTERN = Pattern
                .compile("META-INF/resources/webjars/([^/]+)/([^/]+)/" + WEBJARS_REQUIREJS_NAME);

        private static final String POM_NAME = "pom.xml";
        private static final Pattern POM_PATTERN = Pattern
                .compile("META-INF/maven/org.webjars([^/]*)/([^/]+)/" + POM_NAME);

        private static final String BOWER_NAME = "bower.json";
        private static final Pattern BOWER_PATTERN = Pattern
                .compile("META-INF/resources/webjars/([^/]+)/([^/]+)/" + BOWER_NAME);

        private static final String NPM_NAME = "package.json";
        private static final Pattern NPM_PATTERN = Pattern
                .compile("META-INF/resources/webjars/([^/]+)/([^/]+)/" + NPM_NAME);

        private static final Pattern PACKAGE_FILES_PATTERN = Pattern
                .compile("META-INF/resources/webjars/([^/]+)/([^/]+)/.*");

        private static final ArrayList<String> JS_KNOWN_GLOBALS;

        private static final int BYTES_BUFFER_SIZE = 4096;
        private static final String WEBJAR_SRC_ALIAS_PREFIX = "webjar-src";
        private static final String MINIFIED_RESOURCES_OUTPUT_PATH = "META-INF/resources/dist-gen";

        static {
            JS_KNOWN_GLOBALS = new ArrayList<>();
            JS_KNOWN_GLOBALS.add("applicationCache");
            JS_KNOWN_GLOBALS.add("caches");
            JS_KNOWN_GLOBALS.add("closed");
            JS_KNOWN_GLOBALS.add("Components");
            JS_KNOWN_GLOBALS.add("console");
            JS_KNOWN_GLOBALS.add("content");
            JS_KNOWN_GLOBALS.add("_content");
            JS_KNOWN_GLOBALS.add("controllers");
            JS_KNOWN_GLOBALS.add("crypto");
            JS_KNOWN_GLOBALS.add("defaultStatus");
            JS_KNOWN_GLOBALS.add("devicePixelRatio");
            JS_KNOWN_GLOBALS.add("dialogArguments");
            JS_KNOWN_GLOBALS.add("directories");
            JS_KNOWN_GLOBALS.add("document");
            JS_KNOWN_GLOBALS.add("frameElement");
            JS_KNOWN_GLOBALS.add("frames");
            JS_KNOWN_GLOBALS.add("fullScreen");
            JS_KNOWN_GLOBALS.add("globalStorage");
            JS_KNOWN_GLOBALS.add("history");
            JS_KNOWN_GLOBALS.add("innerHeight");
            JS_KNOWN_GLOBALS.add("innerWidth");
            JS_KNOWN_GLOBALS.add("length");
            JS_KNOWN_GLOBALS.add("location");
            JS_KNOWN_GLOBALS.add("locationbar");
            JS_KNOWN_GLOBALS.add("localStorage");
            JS_KNOWN_GLOBALS.add("menubar");
            JS_KNOWN_GLOBALS.add("messageManager");
            JS_KNOWN_GLOBALS.add("name");
            JS_KNOWN_GLOBALS.add("navigator");
            JS_KNOWN_GLOBALS.add("opener");
            JS_KNOWN_GLOBALS.add("outerHeight");
            JS_KNOWN_GLOBALS.add("outerWidth");
            JS_KNOWN_GLOBALS.add("pageXOffset");
            JS_KNOWN_GLOBALS.add("pageYOffset");
            JS_KNOWN_GLOBALS.add("sessionStorage");
            JS_KNOWN_GLOBALS.add("parent");
            JS_KNOWN_GLOBALS.add("performance");
            JS_KNOWN_GLOBALS.add("personalbar");
            JS_KNOWN_GLOBALS.add("pkcs11");
            JS_KNOWN_GLOBALS.add("returnValue");
            JS_KNOWN_GLOBALS.add("screen");
            JS_KNOWN_GLOBALS.add("screenX");
            JS_KNOWN_GLOBALS.add("screenY");
            JS_KNOWN_GLOBALS.add("scrollbars");
            JS_KNOWN_GLOBALS.add("scrollMaxX");
            JS_KNOWN_GLOBALS.add("scrollMaxY");
            JS_KNOWN_GLOBALS.add("scrollX");
            JS_KNOWN_GLOBALS.add("scrollY");
            JS_KNOWN_GLOBALS.add("self");
            JS_KNOWN_GLOBALS.add("sessionStorage");
            JS_KNOWN_GLOBALS.add("sidebar");
            JS_KNOWN_GLOBALS.add("status");
            JS_KNOWN_GLOBALS.add("statusbar");
            JS_KNOWN_GLOBALS.add("toolbar");
            JS_KNOWN_GLOBALS.add("top");
            JS_KNOWN_GLOBALS.add("window");
        }

        private final Logger logger = LoggerFactory.getLogger(getClass());

        /* constructor information */
        private final URL url;
        private final InputStream inputStream;
        private final OutputStream outputStream;

        private final boolean minificationEnabled;

        //region transformation state

        /* artifact information */
        private RequireJsGenerator.ArtifactInfo artifactInfo;
        private JarOutputStream jarOutputStream;
        private boolean isClassicWebjar;
        private boolean isNpmWebjar;
        private boolean isBowerWebjar;
        // version read from pom file, used as fallback in bower webjars without version information
        private String pomProjectVersion;

        /* require config information */
        private boolean wasReadFromPom;
        private RequireJsGenerator requireConfig;

        /* AMD module information */
        private ArrayList<String> exportedGlobals;
        private boolean isAmdPackage;

        /* source map location cache */
        private String lastPrefix;
        private SourceMap.LocationMapping lastLocationMapping;

        /* other */
        private String webjarUrl;

        /* resource paths */
        private String packageNameFromResourcesPath;
        private String packageVersionFromResourcesPath;

        private String relativeResourcesPath;
        private Path absoluteResourcesPath;

        private Path absoluteTempPath;

        //endregion

        WebjarsTransformer(URL url, InputStream inputStream, PipedOutputStream outputStream,
                boolean minificationEnabled) {
            this.url = url;

            this.inputStream = inputStream;
            this.outputStream = outputStream;

            this.minificationEnabled = minificationEnabled;
        }

        @Override
        public Void call() throws Exception {
            try {
                this.transform();
            } catch (Exception e) {
                logger.error(this.url.toString() + ": Error Transforming zip", e);

                this.outputStream.close();

                throw e;
            }

            return null;
        }

        private void transform() throws IOException {
            JarInputStream jarInputStream = new JarInputStream(inputStream);

            try {
                init();

                extractArtifactInfo(this.url);

                this.jarOutputStream = new JarOutputStream(outputStream, getManifest(artifactInfo, jarInputStream));

                boolean minificationFailed = false;

                ZipEntry entry;
                while ((entry = jarInputStream.getNextJarEntry()) != null) {
                    String name = entry.getName();

                    if (name.endsWith(MANIFEST_MF)) {
                        // ignore existing manifest, we already created our own
                        jarInputStream.closeEntry();
                        continue;
                    }

                    if (name.endsWith(POM_NAME) && POM_PATTERN.matcher(name).matches()) {
                        handlePomFile(jarInputStream);

                        // the pom file will not be copied to the generated bundle,
                        // because it would be troublesome as the entry stream is already consumed
                        // and it isn't really needed anyway
                        continue;
                    }

                    boolean isPackageFile = isPackageFile(name);

                    if (!entry.isDirectory()) {
                        File temporarySourceFile = null;
                        BufferedOutputStream temporarySourceFileOutputStream = null;

                        if (isPackageFile) {
                            // only save to the temp folder resources from the package's source folder
                            temporarySourceFile = new File(absoluteTempPath.toAbsolutePath() + File.separator
                                    + FilenameUtils.separatorsToSystem(name));

                            //noinspection ResultOfMethodCallIgnored
                            temporarySourceFile.getParentFile().mkdirs();

                            temporarySourceFileOutputStream = new BufferedOutputStream(
                                    new FileOutputStream(temporarySourceFile));
                        }

                        //region Copy the file from the source jar to the generated jar
                        ZipEntry zipEntry = new ZipEntry(name);
                        jarOutputStream.putNextEntry(zipEntry);

                        byte[] bytes = new byte[BYTES_BUFFER_SIZE];
                        int read;
                        while ((read = jarInputStream.read(bytes)) != -1) {
                            if (isPackageFile) {
                                // also output to the temp source file
                                temporarySourceFileOutputStream.write(bytes, 0, read);
                            }

                            jarOutputStream.write(bytes, 0, read);
                        }

                        jarOutputStream.closeEntry();
                        // endregion

                        if (isPackageFile) {
                            temporarySourceFileOutputStream.close();

                            if (!isAmdPackage && isJsFile(name)
                                    && findAmdDefine(new FileInputStream(temporarySourceFile), exportedGlobals)) {
                                isAmdPackage = true;
                            }
                        }

                        if (requireConfig == null || (isClassicWebjar && !wasReadFromPom)) {
                            if (isClassicWebjar && name.endsWith(WEBJARS_REQUIREJS_NAME)) {
                                // next comes the module author's webjars-requirejs.js
                                handleWebjarsRequireJS(name, temporarySourceFile);
                            } else if (isNpmWebjar && name.endsWith(NPM_NAME)) {
                                // try to generate requirejs.json from package.json (Npm WebJars) or bower.json (Bower WebJars)
                                handlePackageJson(name, temporarySourceFile);
                            } else if (isBowerWebjar && name.endsWith(BOWER_NAME)) {
                                // try to generate requirejs.json from package.json (Npm WebJars) or bower.json (Bower WebJars)
                                handleBowerJson(name, temporarySourceFile);
                            }
                        }
                    }

                    jarInputStream.closeEntry();
                }

                // nothing more to do if there aren't any source files
                if (absoluteResourcesPath != null) {
                    Collection<File> scrFiles = FileUtils.listFiles(absoluteResourcesPath.toFile(),
                            TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);

                    if (this.minificationEnabled) {
                        try {
                            CompilerOptions options = initCompilationResources();

                            for (File srcFile : scrFiles) {
                                final String name = srcFile.getName();

                                if (name.endsWith(WEBJARS_REQUIREJS_NAME)) {
                                    continue;
                                }

                                final String relSrcFilePath = FilenameUtils.separatorsToUnix(
                                        absoluteResourcesPath.relativize(srcFile.toPath()).toString());
                                final String relOutFilePath = MINIFIED_RESOURCES_OUTPUT_PATH + "/" + relSrcFilePath;

                                if (isJsFile(name)) {
                                    // Check if there is a .map with the same name
                                    // if so, assume it is already minified and just copy both files
                                    File mapFile = new File(name + ".map", srcFile.getParent());
                                    if (mapFile.exists()) {
                                        copyFileToZip(jarOutputStream, relOutFilePath, srcFile);
                                        copyFileToZip(jarOutputStream, relOutFilePath + ".map", mapFile);
                                        continue;
                                    }

                                    options.setSourceMapLocationMappings(
                                            getLocationMappings(srcFile.toPath().getParent(), absoluteResourcesPath,
                                                    packageNameFromResourcesPath, packageVersionFromResourcesPath));

                                    try {
                                        CompiledScript compiledScript = new CompiledScript(srcFile, relSrcFilePath,
                                                options).invoke();

                                        addContentToZip(jarOutputStream, relOutFilePath, compiledScript.getCode());
                                        addContentToZip(jarOutputStream, relOutFilePath + ".map",
                                                compiledScript.getSourcemap());
                                    } catch (Exception failedCompilationException) {
                                        logger.warn(webjarUrl + ": error minifing " + relSrcFilePath
                                                + ", copied original version");

                                        copyFileToZip(jarOutputStream, relOutFilePath, srcFile);
                                    }
                                } else if (!isMapFile(name)) {
                                    // just copy all resources (except .map files)
                                    copyFileToZip(jarOutputStream, relOutFilePath, srcFile);
                                }
                            }
                        } catch (Exception e) {
                            minificationFailed = true;

                            logger.warn(webjarUrl + ": exception minifing, serving original files", e);
                        }
                    }

                    if (requireConfig == null) {
                        // in last resort generate requirejs config by mapping the root path
                        requireConfig = RequireJsGenerator.emptyGenerator(packageNameFromResourcesPath,
                                packageVersionFromResourcesPath);

                        logger.warn(webjarUrl + ": malformed webjar deployed using root path mapping");
                    }

                    if (requireConfig != null) {
                        try {
                            final String exports = !isAmdPackage && !exportedGlobals.isEmpty()
                                    ? exportedGlobals.get(0)
                                    : null;
                            final RequireJsGenerator.ModuleInfo moduleInfo = requireConfig
                                    .getConvertedConfig(artifactInfo, isAmdPackage, exports);

                            addContentToZip(jarOutputStream, PENTAHO_RJS_LOCATION, moduleInfo.exportRequireJs());

                            try {
                                String blueprintTemplate;
                                if (!this.minificationEnabled || minificationFailed) {
                                    blueprintTemplate = generateBlueprintWithoutMinifiedResources(
                                            relativeResourcesPath, moduleInfo);
                                } else {
                                    blueprintTemplate = generateBlueprint(relativeResourcesPath, moduleInfo);
                                }

                                addContentToZip(jarOutputStream, "OSGI-INF/blueprint/blueprint.xml",
                                        blueprintTemplate);
                            } catch (Exception e) {
                                logger.error(webjarUrl + ": error saving OSGI-INF/blueprint/blueprint.xml - "
                                        + e.getMessage());
                            }
                        } catch (Exception e) {
                            logger.error(
                                    webjarUrl + ": error saving " + PENTAHO_RJS_LOCATION + " - " + e.getMessage());
                        }
                    }
                }

                try {
                    jarOutputStream.closeEntry();

                    outputStream.flush();

                    jarOutputStream.close();
                } catch (IOException ioexception) {
                    logger.debug(webjarUrl + ": " + DEBUG_MESSAGE_FAILED_WRITING, ioexception);
                }
            } catch (IOException e) {
                logger.debug(webjarUrl + ": Pipe is closed, no need to continue.");
            } finally {
                try {
                    FileUtils.deleteDirectory(absoluteTempPath.toFile());
                } catch (IOException ignored) {
                    // ignored
                }

                try {
                    jarInputStream.close();
                } catch (IOException ioexception) {
                    logger.debug(webjarUrl + ": Tried to close JarInputStream, but it was already closed.",
                            ioexception);
                }
            }
        }

        private void init() throws IOException {
            this.wasReadFromPom = false;
            this.requireConfig = null;

            this.exportedGlobals = new ArrayList<>();
            this.isAmdPackage = false;

            this.pomProjectVersion = null;

            this.packageNameFromResourcesPath = null;
            this.packageVersionFromResourcesPath = null;
            this.relativeResourcesPath = null;
            this.absoluteResourcesPath = null;

            this.absoluteTempPath = Files.createTempDirectory("WebjarsURLConnection");
        }

        private void extractArtifactInfo(URL url) {
            this.artifactInfo = new RequireJsGenerator.ArtifactInfo(url);

            this.isClassicWebjar = artifactInfo.getGroup().equals("org.webjars");

            this.isNpmWebjar = artifactInfo.getGroup().equals("org.webjars.npm");
            this.isBowerWebjar = artifactInfo.getGroup().equals("org.webjars.bower");

            this.webjarUrl = url.toString();
        }

        private Manifest getManifest(RequireJsGenerator.ArtifactInfo artifactInfo, JarInputStream jarInputStream) {
            Manifest manifest = jarInputStream.getManifest();
            if (manifest == null) {
                manifest = new Manifest();
                manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
            }
            manifest.getMainAttributes().put(new Attributes.Name(Constants.BUNDLE_SYMBOLICNAME),
                    "pentaho-webjars-" + artifactInfo.getArtifactId());
            manifest.getMainAttributes().put(new Attributes.Name(Constants.IMPORT_PACKAGE),
                    "org.osgi.service.http,org.apache.felix.http.api,org.ops4j.pax.web.extender.whiteboard.runtime,"
                            + "org.ops4j.pax.web.extender.whiteboard");

            manifest.getMainAttributes().put(new Attributes.Name(Constants.BUNDLE_VERSION),
                    artifactInfo.getOsgiCompatibleVersion());
            return manifest;
        }

        private void handlePomFile(JarInputStream jarInputStream) {
            if (isClassicWebjar) {
                // handcrafted requirejs configuration on pom.xml has top prioriy in Classic WebJars
                try {
                    requireConfig = RequireJsGenerator.parsePom(jarInputStream);
                    wasReadFromPom = true;

                    logger.debug(webjarUrl + ": Classic WebJar -> requirejs configuration from pom.xml");
                } catch (Exception ignored) {
                    // ignored
                }
            } else if (isBowerWebjar) {
                // collect from the pom file just the version information, for fallback in bower webjars
                try {
                    pomProjectVersion = RequireJsGenerator.getWebjarVersionFromPom(jarInputStream);

                    // fill in the version if bower.json was already processed
                    if (requireConfig != null && requireConfig.getModuleInfo() != null
                            && requireConfig.getModuleInfo().getVersion() == null) {
                        requireConfig.getModuleInfo().setVersion(pomProjectVersion);
                    }
                } catch (Exception ignored) {
                    // ignored
                }
            }
        }

        private void handleWebjarsRequireJS(String name, File temporarySourceFile) {
            Matcher matcher = MODULE_PATTERN.matcher(name);
            if (matcher.matches()) {
                try {
                    requireConfig = RequireJsGenerator.processJsScript(matcher.group(1), matcher.group(2),
                            new FileInputStream(temporarySourceFile));

                    logger.debug(webjarUrl
                            + ": Classic WebJar -> read requirejs configuration from webjars-requirejs.js");
                } catch (Exception ignored) {
                    // ignored
                }
            }
        }

        private void handlePackageJson(String name, File temporarySourceFile) {
            if (NPM_PATTERN.matcher(name).matches()) {
                try {
                    requireConfig = RequireJsGenerator.parseJsonPackage(new FileInputStream(temporarySourceFile));

                    logger.debug(webjarUrl + ": NPM WebJar -> read requirejs configuration from package.json");
                } catch (Exception ignored) {
                    // ignored
                }
            }
        }

        private void handleBowerJson(String name, File temporarySourceFile) {
            if (BOWER_PATTERN.matcher(name).matches()) {
                try {
                    requireConfig = RequireJsGenerator.parseJsonPackage(new FileInputStream(temporarySourceFile));

                    // on bower webjars, check if the version information is present
                    // if not fill it with the version extracted from pom, if already gathered
                    if (pomProjectVersion != null && requireConfig != null && requireConfig.getModuleInfo() != null
                            && requireConfig.getModuleInfo().getVersion() == null) {
                        requireConfig.getModuleInfo().setVersion(pomProjectVersion);
                    }

                    logger.debug(webjarUrl + ": Bower WebJar -> read requirejs configuration from bower.json");
                } catch (Exception ignored) {
                    // ignored
                }
            }
        }

        private boolean isPackageFile(String name) {
            boolean isPackageFile = false;

            // store the path of the first file that fits de expected folder structure,
            // for physical path mapping and also fallback on malformed webjars
            if (packageNameFromResourcesPath == null) {
                Matcher matcher = PACKAGE_FILES_PATTERN.matcher(name);

                if (matcher.matches()) {
                    packageNameFromResourcesPath = matcher.group(1);
                    packageVersionFromResourcesPath = matcher.group(2);

                    relativeResourcesPath = "META-INF/resources/webjars/" + packageNameFromResourcesPath + "/"
                            + packageVersionFromResourcesPath;
                    absoluteResourcesPath = absoluteTempPath
                            .resolve(FilenameUtils.separatorsToSystem(relativeResourcesPath));

                    isPackageFile = true;
                }
            } else {
                isPackageFile = name.contains(packageNameFromResourcesPath + "/" + packageVersionFromResourcesPath);
            }

            return isPackageFile;
        }

        private String generateBlueprint(String relSrcPath, RequireJsGenerator.ModuleInfo moduleInfo)
                throws IOException {
            String blueprintTemplate;
            blueprintTemplate = IOUtils.toString(getClass()
                    .getResourceAsStream("/org/pentaho/osgi/platform/webjars/blueprint-minified-template.xml"));

            blueprintTemplate = blueprintTemplate.replace("{src_path}", relSrcPath);
            blueprintTemplate = blueprintTemplate.replace("{src_alias}",
                    WEBJAR_SRC_ALIAS_PREFIX + "/" + moduleInfo.getVersionedPath());

            blueprintTemplate = blueprintTemplate.replace("{dist_path}", MINIFIED_RESOURCES_OUTPUT_PATH);
            blueprintTemplate = blueprintTemplate.replace("{dist_alias}", moduleInfo.getVersionedPath());
            return blueprintTemplate;
        }

        private String generateBlueprintWithoutMinifiedResources(String relSrcPath,
                RequireJsGenerator.ModuleInfo moduleInfo) throws IOException {
            String blueprintTemplate;
            blueprintTemplate = IOUtils.toString(
                    getClass().getResourceAsStream("/org/pentaho/osgi/platform/webjars/blueprint-template.xml"));

            blueprintTemplate = blueprintTemplate.replace("{dist_path}", relSrcPath);
            blueprintTemplate = blueprintTemplate.replace("{dist_alias}", moduleInfo.getVersionedPath());
            return blueprintTemplate;
        }

        private boolean isJsFile(String name) {
            return name.endsWith(".js");
        }

        private boolean isMapFile(String name) {
            return name.endsWith(".map");
        }

        private void copyFileToZip(JarOutputStream zip, String entry, File file) throws IOException {
            int bytesIn;
            byte[] readBuffer = new byte[BYTES_BUFFER_SIZE];

            FileInputStream inputStream = null;
            try {
                inputStream = new FileInputStream(file);

                ZipEntry zipEntry = new ZipEntry(entry);
                zip.putNextEntry(zipEntry);

                bytesIn = inputStream.read(readBuffer);
                while (bytesIn != -1) {
                    zip.write(readBuffer, 0, bytesIn);
                    bytesIn = inputStream.read(readBuffer);
                }
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                } catch (IOException ignored) {
                    // ignored
                }
            }
        }

        private void addContentToZip(JarOutputStream zip, String entry, String content) throws IOException {
            ZipEntry zipEntry = new ZipEntry(entry);
            zip.putNextEntry(zipEntry);
            zip.write(content.getBytes("UTF-8"));
            zip.closeEntry();
        }

        private boolean findAmdDefine(InputStream is, ArrayList<String> exports) {
            final Pattern definePattern = Pattern
                    .compile("\bdefine\b(\\s*)\\(((\\s*)\"[^\"]+\"(\\s*),)?((\\s*)\\[((\\s*)\"[^\"]+\""
                            + "(\\s*),?)+(\\s*)\\](\\s*),)?((\\s*)function)");

            final Pattern globalPattern = Pattern.compile(
                    "(\\bwindow\\b|\\bexports\\b)\\.(([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*)"
                            + "\\s*=\\s*[\\w${][^,;]+");

            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            String line;
            try {
                while ((line = br.readLine()) != null) {
                    Matcher matcher = definePattern.matcher(line);
                    if (matcher.find()) {
                        return true;
                    }

                    matcher = globalPattern.matcher(line);
                    if (matcher.find()) {
                        final String var = matcher.group(2);
                        final String varSegment = var.split("\\.", 2)[0];
                        if (!varSegment.startsWith("on") && !JS_KNOWN_GLOBALS.contains(varSegment)
                                && !exports.contains(var)) {
                            exports.add(var);
                        }
                    }
                }
            } catch (IOException ignored) {
                // ignored
            }

            return false;
        }

        private CompilerOptions initCompilationResources() {
            CompilerOptions options = new CompilerOptions();
            CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel(options);

            options.setSourceMapOutputPath(".");

            WarningLevel.QUIET.setOptionsForWarningLevel(options);
            options.setWarningLevel(DiagnosticGroups.LINT_CHECKS, CheckLevel.OFF);

            options.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT5);

            // make sure these are clear
            this.lastPrefix = null;
            this.lastLocationMapping = null;

            return options;
        }

        private List<SourceMap.LocationMapping> getLocationMappings(Path srcFileFolder, Path absSrcPath,
                String packageName, String packageVersion) {
            // reuses the lastLocationMapping if the script's folder is the same than the previous processed script

            final String prefix = FilenameUtils.separatorsToUnix(srcFileFolder.toString());
            if (lastPrefix == null || !lastPrefix.equals(prefix)) {
                String relPath = FilenameUtils.separatorsToUnix(absSrcPath.relativize(srcFileFolder).toString());
                if (!relPath.isEmpty()) {
                    relPath = "/" + relPath;
                }

                String reverseRelPath = FilenameUtils
                        .separatorsToUnix(srcFileFolder.relativize(absSrcPath).toString());
                if (!reverseRelPath.isEmpty()) {
                    reverseRelPath += "/";
                }

                final String replacement = "../../" + reverseRelPath + WEBJAR_SRC_ALIAS_PREFIX + "/" + packageName
                        + "/" + packageVersion + relPath;

                lastLocationMapping = new SourceMap.LocationMapping(prefix, replacement);

                lastPrefix = prefix;
            }

            List<SourceMap.LocationMapping> lms = new ArrayList<>();
            lms.add(lastLocationMapping);
            return lms;
        }

        private static class CompiledScript {
            private static final SourceFile EMPTY_EXTERNS_SOURCE_FILE = SourceFile.fromCode("externs.js", "");

            private final File srcFile;
            private final String relSrcFilePath;
            private final CompilerOptions options;

            private StringBuilder code;
            private StringBuilder sourcemap;

            CompiledScript(File srcFile, String relSrcFilePath, CompilerOptions options) {
                this.srcFile = srcFile;
                this.relSrcFilePath = relSrcFilePath;
                this.options = options;
            }

            String getCode() {
                return code.toString();
            }

            String getSourcemap() {
                return sourcemap.toString();
            }

            CompiledScript invoke() throws Exception {
                Compiler compiler = new Compiler();

                compiler.setLoggingLevel(Level.OFF);

                SourceFile input = SourceFile.fromFile(srcFile);
                input.setOriginalPath(relSrcFilePath);

                Result res = compiler.compile(EMPTY_EXTERNS_SOURCE_FILE, input, options);
                if (res.success) {
                    String name = srcFile.getName();

                    code = new StringBuilder(compiler.toSource());
                    code.append("\n//# sourceMappingURL=").append(name).append(".map");

                    sourcemap = new StringBuilder();
                    SourceMap sm = compiler.getSourceMap();
                    sm.appendTo(sourcemap, name);

                    return this;
                }

                throw new Exception("Failed script compilation");
            }
        }
    }
}