org.jahia.services.templates.ModuleBuildHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.templates.ModuleBuildHelper.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.templates;

import name.fraser.neil.plaintext.DiffMatchPatch;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.DirectoryWalker;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.HiddenFileFilter;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.model.Model;
import org.apache.maven.model.Scm;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.dom4j.DocumentException;
import org.jahia.api.Constants;
import org.jahia.commons.Version;
import org.jahia.data.templates.JahiaTemplatesPackage;
import org.jahia.data.templates.ModuleReleaseInfo;
import org.jahia.exceptions.JahiaRuntimeException;
import org.jahia.osgi.BundleLifecycleUtils;
import org.jahia.osgi.BundleUtils;
import org.jahia.security.license.LicenseCheckException;
import org.jahia.services.content.JCRContentUtils;
import org.jahia.services.content.JCRNodeWrapper;
import org.jahia.services.content.JCRSessionWrapper;
import org.jahia.services.content.JCRValueWrapper;
import org.jahia.services.content.nodetypes.ValueImpl;
import org.jahia.services.importexport.ImportExportBaseService;
import org.jahia.services.importexport.ImportExportService;
import org.jahia.services.modulemanager.BundleInfo;
import org.jahia.services.modulemanager.ModuleManager;
import org.jahia.services.modulemanager.OperationResult;
import org.jahia.services.modulemanager.util.ModuleUtils;
import org.jahia.services.notification.ToolbarWarningsService;
import org.jahia.settings.SettingsBean;
import org.jahia.utils.PomUtils;
import org.jahia.utils.ProcessHelper;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.FileSystemResource;
import org.xml.sax.SAXException;

import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.xml.transform.TransformerException;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Utility class for module compilation and build.
 *
 * @author Sergiy Shyrkov
 */
public class ModuleBuildHelper implements InitializingBean {

    private static final Logger logger = LoggerFactory.getLogger(ModuleBuildHelper.class);
    private static final Pattern UNICODE_PATTERN = Pattern.compile("\\\\u([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})");

    private String mavenExecutable;
    private String ignoreSnapshots;
    private boolean ignoreSnapshotsFlag;
    private String mavenArchetypePlugin;
    private String mavenArchetypeCatalog;
    private String mavenArchetypeVersion;
    private String mavenMinRequiredVersion;
    private String mavenReleasePlugin;
    private String mavenWarnIfVersionIsOlderThan;
    private ModuleManager moduleManager;
    private int moduleStartLevel;
    private SourceControlHelper scmHelper;
    private SettingsBean settingsBean;
    private TemplatePackageRegistry templatePackageRegistry;
    private ToolbarWarningsService toolbarWarningsService;

    private static void warnOldMavenVersion(String mvnVersionString) {
        logger.warn("");
        logger.warn("************************* DEPRECATION *************************");
        logger.warn("*                                                             *");
        logger.warn("* The version of Maven ({}), you are using, is deprecated. *", mvnVersionString);
        logger.warn("* Please, switch to a more recent one (e.g. 3.3.x).           *");
        logger.warn("*                                                             *");
        logger.warn("***************************************************************");
        logger.warn("");
    }

    public JahiaTemplatesPackage compileAndDeploy(final String moduleId, File sources, JCRSessionWrapper session)
            throws RepositoryException, IOException, BundleException {

        CompiledModuleInfo moduleInfo = compileModule(sources);
        Bundle bundle = BundleUtils.getBundle(moduleId, moduleInfo.getVersion());
        if (bundle != null) {
            // we deal with an existing bundle
            FileInputStream is = new FileInputStream(moduleInfo.getFile());
            try {
                bundle.update(ModuleUtils.addModuleDependencies(is));

                // start the bundle to make sure its template is available to be displayed
                bundle.start();

                // refresh wirings
                BundleLifecycleUtils.refreshBundle(bundle);
            } finally {
                IOUtils.closeQuietly(is);
            }
            if (BundleUtils.getContextStartException(bundle.getSymbolicName()) != null && BundleUtils
                    .getContextStartException(bundle.getSymbolicName()) instanceof LicenseCheckException) {
                throw new IOException(
                        BundleUtils.getContextStartException(bundle.getSymbolicName()).getLocalizedMessage());
            }
            return templatePackageRegistry.lookupByIdAndVersion(moduleInfo.getModuleName(),
                    new ModuleVersion(moduleInfo.getVersion()));
        } else {
            // No existing bundle found, deploy new one
            OperationResult operationResult = moduleManager.install(new FileSystemResource(moduleInfo.getFile()),
                    null, true);
            BundleInfo bundleInfo = operationResult.getBundleInfos().get(0);
            bundle = BundleUtils.getBundleBySymbolicName(bundleInfo.getSymbolicName(), bundleInfo.getVersion());

            JahiaTemplatesPackage pkg = BundleUtils.getModule(bundle);
            if (pkg == null) {
                throw new IOException("Cannot deploy module");
            }
            if (BundleUtils.getContextStartException(bundle.getSymbolicName()) != null && BundleUtils
                    .getContextStartException(bundle.getSymbolicName()) instanceof LicenseCheckException) {
                throw new IOException(
                        BundleUtils.getContextStartException(bundle.getSymbolicName()).getLocalizedMessage());
            }
            return templatePackageRegistry.lookupByIdAndVersion(moduleInfo.getModuleName(),
                    new ModuleVersion(moduleInfo.getVersion()));
        }
    }

    public CompiledModuleInfo compileModule(File sources) throws IOException {

        if (!isMavenConfigured()) {
            throw new JahiaRuntimeException("Cannot compile module, either current instance is not "
                    + "in development mode or maven configuration is not good");
        }

        File pom = new File(sources, "pom.xml");
        try {
            Model model = PomUtils.read(pom);
            String artifactId = model.getArtifactId();
            String version = PomUtils.getVersion(model);
            if (StringUtils.isEmpty(version)) {
                throw new IOException("No version found in pom.xml file " + pom);
            }

            StringBuilder out = new StringBuilder();
            int r = 0;
            try {
                r = ProcessHelper.execute(mavenExecutable,
                        new String[] { "clean", "install", "-Dmaven.test.skip=true" }, null, sources, out, null);
            } catch (JahiaRuntimeException e) {
                logger.error(e.getCause().getMessage(), e.getCause());
                throw e;
            }

            if (r > 0) {
                logger.error("Compilation error, returned status " + r);
                logger.error("Maven out : " + out);
                throw new IOException(out.toString());
            }
            File file = new File(sources.getPath() + "/target/" + artifactId + "-" + version + ".jar");
            if (file.exists()) {
                return new CompiledModuleInfo(file, artifactId, version);
            } else {
                throw new IOException("Cannot find a module archive to deploy in folder "
                        + file.getParentFile().getAbsolutePath());
            }
        } catch (XmlPullParserException e) {
            logger.error("Error parsing pom.xml file at " + pom, e);
            throw new IOException("Cannot parse pom.xml file " + pom, e);
        }
    }

    public JCRNodeWrapper createModule(final String moduleName, String artifactId, final String groupId,
            final String moduleType, final File moduleSources, final JCRSessionWrapper session)
            throws IOException, RepositoryException, BundleException {

        if (!isMavenConfigured()) {
            throw new JahiaRuntimeException("Cannot create module, either current instance is not "
                    + "in development mode or maven configuration is not good");
        }
        if (StringUtils.isBlank(moduleName)) {
            throw new RepositoryException("Cannot create module because no module name has been specified");
        }
        if (StringUtils.isBlank(artifactId)) {
            artifactId = JCRContentUtils.generateNodeName(moduleName);
        }
        if (templatePackageRegistry.containsId(artifactId)) {
            throw new RepositoryException("Cannot create module " + artifactId
                    + " because another module with the same artifactId exists");
        }

        File sources = moduleSources;
        if (sources == null) {
            sources = new File(SettingsBean.getInstance().getModulesSourcesDiskPath());
            if (!sources.exists() && !sources.mkdirs()) {
                throw new IOException("Unable to create path for: " + sources);
            }
        }

        String finalFolderName = null;

        if (!sources.exists()) {
            finalFolderName = sources.getName();
            sources = sources.getParentFile();
            if (sources == null) {
                sources = new File(SettingsBean.getInstance().getModulesSourcesDiskPath());
            }
            if (!sources.exists() && !sources.mkdirs()) {
                throw new IOException("Unable to create path for: " + sources);
            }
        }

        List<String> archetypeParams = new ArrayList<String>();
        archetypeParams.add(mavenArchetypePlugin + ":generate");
        archetypeParams.add("-DarchetypeCatalog=" + mavenArchetypeCatalog + ",local");
        archetypeParams.add("-DarchetypeGroupId=org.jahia.archetypes");
        archetypeParams.add("-DarchetypeArtifactId=jahia-" + moduleType + "-archetype");
        archetypeParams.add("-DarchetypeVersion=" + mavenArchetypeVersion);
        archetypeParams.add("-Dversion=1.0-SNAPSHOT");
        archetypeParams.add("\"-DmoduleName=" + moduleName + "\"");
        archetypeParams.add("-DartifactId=" + artifactId);
        if (StringUtils.isNotBlank(groupId)) {
            archetypeParams.add("-DgroupId=" + groupId);
        }
        archetypeParams.add("-DdigitalFactoryVersion=" + Constants.JAHIA_PROJECT_VERSION);
        archetypeParams.add("-DinteractiveMode=false");

        StringBuilder out = new StringBuilder();
        int ret = ProcessHelper.execute(mavenExecutable,
                archetypeParams.toArray(new String[archetypeParams.size()]), null, sources, out, out);

        if (ret > 0) {
            logger.error("Maven archetype call returned " + ret);
            logger.error("Maven out : " + out);
            return null;
        }

        File path = new File(sources, artifactId);
        if (finalFolderName != null && !path.getName().equals(finalFolderName)) {
            try {
                File newPath = new File(sources, finalFolderName);
                FileUtils.moveDirectory(path, newPath);
                path = newPath;
            } catch (IOException e) {
                logger.error("Cannot rename folder", e);
            }
        }

        JahiaTemplatesPackage pack = compileAndDeploy(artifactId, path, session);

        JCRNodeWrapper node = session.getNode("/modules/" + pack.getIdWithVersion());
        scmHelper.setSourcesFolderInPackageAndNode(pack, path, node);
        session.save();

        return node;
    }

    public void deployToMaven(String groupId, String artifactId, ModuleReleaseInfo releaseInfo, File generatedJar)
            throws IOException {

        if (!isMavenConfigured()) {
            throw new JahiaRuntimeException("Cannot deploy module to maven, either current instance is not "
                    + "in development mode or maven configuration is not good");
        }

        File settings = null;
        File pomFile = null;
        try {
            if (!StringUtils.isEmpty(releaseInfo.getUsername())
                    && !StringUtils.isEmpty(releaseInfo.getPassword())) {
                settings = File.createTempFile("settings", ".xml");
                BufferedWriter w = new BufferedWriter(new FileWriter(settings));
                w.write("<settings><servers><server><id>" + releaseInfo.getRepositoryId() + "</id><username>");
                w.write(releaseInfo.getUsername());
                w.write("</username><password>");
                w.write(releaseInfo.getPassword());
                w.write("</password></server></servers></settings>");
                w.close();
            }
            JarFile jar = new JarFile(generatedJar);
            pomFile = PomUtils.extractPomFromJar(jar, groupId, artifactId);
            jar.close();

            Model pom;
            try {
                pom = PomUtils.read(pomFile);
            } catch (XmlPullParserException e) {
                throw new IOException(e);
            }
            String version = pom.getVersion();
            if (version == null) {
                version = pom.getParent().getVersion();
            }
            if (version == null) {
                throw new IOException("unable to read project version");
            }
            String[] deployParams = { "deploy:deploy-file", "-Dfile=" + generatedJar,
                    "-DrepositoryId=" + releaseInfo.getRepositoryId(), "-Durl=" + releaseInfo.getRepositoryUrl(),
                    "-DpomFile=" + pomFile.getPath(),
                    "-Dpackaging=" + StringUtils.substringAfterLast(generatedJar.getName(), "."),
                    "-DgroupId=" + PomUtils.getGroupId(pom), "-DartifactId=" + pom.getArtifactId(),
                    "-Dversion=" + version };
            if (settings != null) {
                deployParams = (String[]) ArrayUtils.addAll(deployParams,
                        new String[] { "--settings", settings.getPath() });
            }
            StringBuilder out = new StringBuilder();
            int ret = ProcessHelper.execute(mavenExecutable, deployParams, null, generatedJar.getParentFile(), out,
                    out);

            if (ret > 0) {
                String s = getMavenError(out.toString());
                logger.error("Maven archetype call returned " + ret);
                logger.error("Maven out : " + out);
                throw new IOException("Maven invocation failed\n" + s);
            }
        } finally {
            FileUtils.deleteQuietly(settings);
            FileUtils.deleteQuietly(pomFile);
        }
    }

    private String getMavenError(String out) {
        Matcher m = Pattern.compile("^\\[ERROR\\](.*)$", Pattern.MULTILINE).matcher(out);
        StringBuilder s = new StringBuilder();
        while (m.find()) {
            s.append(m.group(1)).append("\n");
        }
        return s.toString();
    }

    private String getMavenHome() throws IOException {
        String home = System.getenv().get("M2_HOME") != null ? System.getenv().get("M2_HOME") : "/usr/share/maven";
        if (!new File(home).exists()) {
            throw new IOException("Maven home not found, please set your M2_HOME environment variable");
        }
        return home;
    }

    protected File releaseModuleInternal(Model model, String lastVersion, String releaseVersion,
            ModuleReleaseInfo releaseInfo, File sources, String scmUrl) throws IOException, XmlPullParserException {

        if (!isMavenConfigured()) {
            throw new JahiaRuntimeException("Cannot release module, either current instance is not "
                    + "in development mode or maven configuration is not good");
        }

        String nextVersion = releaseInfo.getNextVersion();
        String artifactId = model.getArtifactId();
        File pom = new File(sources, "pom.xml");
        File generatedJar;

        if (scmUrl != null) {
            // release using maven-release-plugin
            String tag = StringUtils.replace(releaseVersion, ".", "_");

            int ret;

            File tmpRepo = new File(System.getProperty("java.io.tmpdir"), "repo");
            tmpRepo.mkdir();
            String[] installParams = new String[] { mavenReleasePlugin + ":prepare", mavenReleasePlugin + ":stage",
                    mavenReleasePlugin + ":clean", "-Dmaven.home=" + getMavenHome(), "-Dtag=" + tag,
                    "-DreleaseVersion=" + releaseVersion, "-DdevelopmentVersion=" + nextVersion,
                    "-DignoreSnapshots=" + ignoreSnapshotsFlag,
                    "-DstagingRepository=tmp::default::" + tmpRepo.toURI().toString(), "--batch-mode" };
            StringBuilder out = new StringBuilder();
            ret = ProcessHelper.execute(mavenExecutable, installParams, null, sources, out, out);

            FileUtils.deleteDirectory(tmpRepo);

            if (ret > 0) {
                String s = getMavenError(out.toString());
                logger.error("Maven release call returnedError release, maven out : " + out);
                logger.error("Error when releasing, maven out : " + out);
                ProcessHelper.execute(mavenExecutable, new String[] { mavenReleasePlugin + ":rollback" }, null,
                        sources, out, out);
                logger.error("Rollback release : " + out);
                throw new IOException("Maven invocation failed\n" + s);
            }

            File oldJar = new File(settingsBean.getJahiaModulesDiskPath(), artifactId + "-" + lastVersion + ".jar");
            if (oldJar.exists()) {
                oldJar.delete();
            }

            generatedJar = new File(
                    sources.getPath() + "/target/checkout/target/" + artifactId + "-" + releaseVersion + ".jar");
        } else {
            // modify the version in the pom.xml and compile/install module
            PomUtils.updateVersion(pom, releaseVersion);

            generatedJar = compileModule(sources).getFile();

            PomUtils.updateVersion(pom, nextVersion);
        }
        return generatedJar;
    }

    public void setIgnoreSnapshots(String ignoreSnapshots) {
        this.ignoreSnapshots = ignoreSnapshots;
    }

    public void setMavenArchetypeCatalog(String mavenArchetypeCatalog) {
        this.mavenArchetypeCatalog = mavenArchetypeCatalog;
    }

    public void setSettingsBean(SettingsBean settingsBean) {
        this.settingsBean = settingsBean;
    }

    public void setSourceControlHelper(SourceControlHelper scmHelper) {
        this.scmHelper = scmHelper;
    }

    public void setTemplatePackageRegistry(TemplatePackageRegistry registry) {
        templatePackageRegistry = registry;
    }

    private boolean isMavenConfigured() {
        return settingsBean.isDevelopmentMode() && settingsBean.isMavenExecutableSet();
    }

    private void checkMavenExecutable() {

        if (settingsBean.isDevelopmentMode()) {
            settingsBean.setMavenExecutableSet(false);

            String mavenExecutable = this.mavenExecutable;
            StringBuilder resultOut = new StringBuilder();
            try {
                String[] args = new String[] { "-version" };
                int res = 0;
                if (System.getProperty("os.name").toLowerCase().startsWith("windows")
                        && !mavenExecutable.endsWith(".bat") && !mavenExecutable.endsWith(".cmd")) {
                    // check for Maven 3.3.x
                    try {
                        res = ProcessHelper.execute(mavenExecutable + ".cmd", args, null, null, resultOut, null);
                        if (res == 0) {
                            // we are dealing with Maven 3.3.x
                            mavenExecutable = mavenExecutable + ".cmd";
                        }
                    } catch (JahiaRuntimeException e) {
                        // assume Maven < 3.3.x
                        mavenExecutable = mavenExecutable + ".bat";
                    }
                }
                if (res > 0 || resultOut.length() == 0) {
                    res = ProcessHelper.execute(mavenExecutable, args, null, null, resultOut, null);
                }
                if (res > 0) {
                    toolbarWarningsService.addMessage("warning.maven.missing");
                    logger.error("Cannot set maven executable to " + mavenExecutable
                            + ", please check your configuration");
                    return;
                }
                String mvnVersionString = StringUtils.substringBefore(
                        StringUtils.substringBetween(resultOut.toString(), "Apache Maven ", "\n"), " ");
                String[] mvnVersion = StringUtils.split(mvnVersionString, ".");
                String[] requiredVersion = StringUtils.split(mavenMinRequiredVersion, ".");
                boolean isValid = true;
                for (int i = 0; i < mvnVersion.length; i++) {
                    isValid = Integer.parseInt(mvnVersion[i]) >= Integer.parseInt(requiredVersion[i]);
                    if (!isValid || i == requiredVersion.length - 1) {
                        break;
                    }
                }

                if (isValid) {
                    this.mavenExecutable = mavenExecutable;
                    settingsBean.setMavenExecutableSet(true);
                    if (new Version(mvnVersionString).compareTo(new Version(mavenWarnIfVersionIsOlderThan)) < 0) {
                        warnOldMavenVersion(mvnVersionString);
                    }
                } else {
                    toolbarWarningsService.addMessage("warning.maven.wrong.version");
                    logger.error("Detected Maven Version: " + StringUtils.join(mvnVersion, ".")
                            + " do not match the minimum required version " + mavenMinRequiredVersion);
                }
            } catch (Exception e) {
                toolbarWarningsService.addMessage("warning.maven.missing");
                logger.error(
                        "Cannot set maven executable to " + mavenExecutable + ", please check your configuration",
                        e);
            }
            if (!settingsBean.isMavenExecutableSet()) {
                logger.error("Until maven executable is correctly set, the studio will not be available");
            }
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        checkMavenExecutable();

        if (mavenArchetypeCatalog == null || mavenArchetypeCatalog.length() == 0) {
            mavenArchetypeCatalog = mavenArchetypeVersion != null && mavenArchetypeVersion.contains("-SNAPSHOT")
                    ? "https://devtools.jahia.com/nexus/content/repositories/jahia-snapshots/archetype-catalog.xml"
                    : "https://devtools.jahia.com/nexus/content/repositories/jahia-releases/archetype-catalog.xml";
        }

        logger.info("Using version {} for the module archetypes from catalog {}", mavenArchetypeVersion,
                mavenArchetypeCatalog);

        if (ignoreSnapshots == null || ignoreSnapshots.length() == 0) {
            ignoreSnapshotsFlag = Constants.JAHIA_PROJECT_VERSION.contains("-SNAPSHOT");
        } else {
            ignoreSnapshotsFlag = Boolean.valueOf(ignoreSnapshots.trim());
        }
        if (mavenArchetypePlugin == null || mavenArchetypePlugin.length() == 0) {
            mavenArchetypePlugin = "archetype";
        }
        if (mavenReleasePlugin == null || mavenReleasePlugin.length() == 0) {
            mavenReleasePlugin = "release";
        }
    }

    public void setMavenReleasePlugin(String mavenReleasePlugin) {
        this.mavenReleasePlugin = mavenReleasePlugin;
    }

    public void setMavenMinRequiredVersion(String mavenRequiredVersion) {
        this.mavenMinRequiredVersion = mavenRequiredVersion;
    }

    public void setMavenExecutable(String mavenExecutable) {
        this.mavenExecutable = mavenExecutable;
    }

    public void setToolbarWarningsService(ToolbarWarningsService toolbarWarningsService) {
        this.toolbarWarningsService = toolbarWarningsService;
    }

    public JahiaTemplatesPackage duplicateModule(String dstModuleName, String dstModuleId, String dstGroupId,
            String srcPath, String scmURI, String branchOrTag, String srcModuleId, String srcModuleVersion,
            boolean uninstallSrcModule, String dstPath, boolean deleteSrcFolder, JCRSessionWrapper session)
            throws IOException, RepositoryException, BundleException {

        if (StringUtils.isBlank(dstModuleName)) {
            throw new SourceControlException("Cannot create module because no module name has been specified");
        }
        if (StringUtils.isBlank(dstModuleId)) {
            dstModuleId = JCRContentUtils.generateNodeName(dstModuleName);
        }
        if (StringUtils.isBlank(dstGroupId)) {
            dstGroupId = "org.jahia.modules";
        }
        if (templatePackageRegistry.containsId(dstModuleId)) {
            throw new ScmUnavailableModuleIdException("Cannot create module " + dstModuleId
                    + " because another module with the same artifactId exists");
        }

        if (StringUtils.isBlank(dstPath)) {
            dstPath = SettingsBean.getInstance().getModulesSourcesDiskPath();
        }
        File parentDir = new File(dstPath);
        if (!parentDir.exists() && !parentDir.mkdirs()) {
            throw new SourceControlException("Unable to create path for: " + parentDir);
        }

        File dstFolder = new File(parentDir, dstModuleId);
        int i = 0;
        while (dstFolder.exists()) {
            dstFolder = new File(parentDir, dstModuleId + "_" + (++i));
        }

        File srcFolder;
        if (srcPath == null) {
            try {
                srcFolder = scmHelper.checkoutTmpModule(srcModuleId, srcModuleVersion, scmURI, branchOrTag);
            } catch (XmlPullParserException e) {
                throw new IOException(e);
            } catch (DocumentException e) {
                throw new IOException(e);
            }
            deleteSrcFolder = true;
        } else {
            srcFolder = new File(srcPath);
        }

        FileUtils.copyDirectory(srcFolder, dstFolder);
        if (deleteSrcFolder) {
            FileUtils.deleteQuietly(srcFolder);
        }

        CompiledModuleInfo compiledModuleInfo;
        try {

            String dstVersion = "1.0-SNAPSHOT";
            updateDuplicatedPom(dstModuleName, dstModuleId, dstGroupId, dstVersion, dstFolder);

            cleanScmFiles(dstFolder);

            JahiaTemplatesPackage srcModule = templatePackageRegistry.lookupByIdAndVersion(srcModuleId,
                    new ModuleVersion(srcModuleVersion));
            updateDuplicatedImportFiles(srcModule, dstModuleName, dstModuleId, dstVersion, dstFolder, session);

            renameDuplicatedResourceBundle(srcModule, dstModuleId, dstFolder);

            compiledModuleInfo = compileModule(dstFolder);

            if (uninstallSrcModule) {
                undeployAllModuleVersions(srcModuleId);
            }

        } catch (IOException e) {
            FileUtils.deleteQuietly(dstFolder);
            throw e;
        } catch (RepositoryException e) {
            FileUtils.deleteQuietly(dstFolder);
            throw e;
        } catch (BundleException e) {
            FileUtils.deleteQuietly(dstFolder);
            throw e;
        } catch (RuntimeException e) {
            FileUtils.deleteQuietly(dstFolder);
            throw e;
        }

        OperationResult operationResult = moduleManager
                .install(new FileSystemResource(compiledModuleInfo.getFile()), null, true);
        BundleInfo bundleInfo = operationResult.getBundleInfos().get(0);
        Bundle bundle = BundleUtils.getBundleBySymbolicName(bundleInfo.getSymbolicName(), bundleInfo.getVersion());

        JahiaTemplatesPackage pkg = BundleUtils.getModule(bundle);
        if (pkg == null) {
            FileUtils.deleteQuietly(dstFolder);
            throw new IOException("Cannot deploy module");
        }
        if (BundleUtils.getContextStartException(bundle.getSymbolicName()) != null && BundleUtils
                .getContextStartException(bundle.getSymbolicName()) instanceof LicenseCheckException) {
            throw new BundleException(
                    BundleUtils.getContextStartException(bundle.getSymbolicName()).getLocalizedMessage());
        }

        return templatePackageRegistry.lookupByIdAndVersion(compiledModuleInfo.getModuleName(),
                new ModuleVersion(compiledModuleInfo.getVersion()));
    }

    private void undeployAllModuleVersions(String srcModuleId) throws BundleException {
        Set<ModuleVersion> availableVersionsForModule = templatePackageRegistry
                .getAvailableVersionsForModule(srcModuleId);
        ModuleVersion[] versions = availableVersionsForModule
                .toArray(new ModuleVersion[availableVersionsForModule.size()]);
        for (int j = 0; j < versions.length; j++) {
            Bundle bundle = templatePackageRegistry.lookupByIdAndVersion(srcModuleId, versions[j]).getBundle();
            int state = bundle.getState();
            if (state == Bundle.ACTIVE || state == Bundle.STARTING) {
                bundle.stop();
            }
            bundle.uninstall();
        }
    }

    private void renameDuplicatedResourceBundle(JahiaTemplatesPackage srcModule, String dstModuleId,
            File dstFolder) {
        File rbFolder = new File(dstFolder, "src/main/resources/resources");
        if (rbFolder.exists()) {
            Pattern rbPattern = Pattern.compile("(" + srcModule.getId() + "|"
                    + StringUtils.replace(srcModule.getName(), " ", "") + "|"
                    + StringUtils.replace(srcModule.getName(), " ", "_") + ")(_[a-z]{2}(-[A-Z]{2})?)?.properties");
            File[] files = rbFolder.listFiles();
            if (files != null) {
                for (File f : files) {
                    Matcher m = rbPattern.matcher(f.getName());
                    if (m.matches()) {
                        if (m.group(2) == null) {
                            f.renameTo(new File(rbFolder, dstModuleId + ".properties"));
                        } else {
                            f.renameTo(new File(rbFolder, dstModuleId + m.group(2) + ".properties"));
                        }
                    }
                }
            }
        }
    }

    private void updateDuplicatedImportFiles(JahiaTemplatesPackage srcModule, String dstModuleName,
            String dstModuleId, String dstVersion, File dstFolder, JCRSessionWrapper session)
            throws RepositoryException, IOException {

        JCRNodeWrapper srcModuleNode = session.getNode("/modules/" + srcModule.getIdWithVersion());
        JCRNodeWrapper dstModuleNode = session.getNode("/modules");
        if (dstModuleNode.hasNode(dstModuleId)) {
            dstModuleNode = dstModuleNode.getNode(dstModuleId);
        } else {
            dstModuleNode = dstModuleNode.addNode(dstModuleId, "jnt:module");
        }
        if (dstModuleNode.hasNode(dstVersion)) {
            dstModuleNode = dstModuleNode.getNode(dstVersion);
        } else {
            dstModuleNode = dstModuleNode.addNode(dstVersion, "jnt:moduleVersion");
        }
        for (JCRNodeWrapper node : srcModuleNode.getNodes()) {
            if (!node.isNodeType("jnt:moduleVersionFolder") && !node.isNodeType("jnt:versionInfo")) {
                node.copy(dstModuleNode.getPath());
            }
        }
        dstModuleNode.setProperty("j:title", dstModuleName);
        if (srcModuleNode.hasProperty("j:installedModules")) {
            List<Value> newValues = new ArrayList<Value>();
            for (JCRValueWrapper value : srcModuleNode.getProperty("j:installedModules").getValues()) {
                if (srcModule.getId().equals(value.getString())) {
                    newValues.add(new ValueImpl(dstModuleId));
                } else {
                    newValues.add(value);
                }
            }
            dstModuleNode.setProperty("j:installedModules", newValues.toArray(new Value[newValues.size()]));
        }
        session.save();

        FileUtils.deleteQuietly(new File(dstFolder, "src/main/import/content/modules/" + srcModule.getId()));
        try {
            regenerateImportFile(session, new ArrayList<File>(), dstFolder, dstModuleId,
                    dstModuleId + "/" + dstVersion);
        } catch (SAXException | TransformerException e) {
            throw new IOException("Unable to generate import files in " + dstFolder);
        } finally {
            // clean up dest repository
            session.getNode("/modules/" + dstModuleId).remove();
            session.save();
        }
    }

    private void cleanScmFiles(File dstFolder) throws IOException {
        FileUtils.deleteQuietly(new File(dstFolder, ".git"));
        FileUtils.deleteQuietly(new File(dstFolder, ".gitignore"));
        new SvnCleaner().clean(dstFolder);
    }

    private void updateDuplicatedPom(String moduleName, String artifactId, String groupId, String dstVersion,
            File dstFolder) throws IOException {
        Model pom = null;
        try {
            pom = PomUtils.read(new File(dstFolder, "pom.xml"));
        } catch (XmlPullParserException e) {
            throw new IOException(e);
        }
        if (!"bundle".equals(pom.getPackaging())) {
            throw new IOException("This module is not compatible with the current version of Jahia.");
        }
        pom.setArtifactId(artifactId);
        pom.setGroupId(groupId);
        pom.setVersion(dstVersion);
        pom.setName(moduleName);
        Scm scm = new Scm();
        scm.setConnection(Constants.SCM_DUMMY_URI);
        scm.setDeveloperConnection(Constants.SCM_DUMMY_URI);
        pom.setScm(scm);
        pom.setDistributionManagement(null);
        pom.getProperties().remove("jahia-private-app-store");
        PomUtils.write(pom, new File(dstFolder, "pom.xml"));
    }

    public void regenerateImportFile(JCRSessionWrapper session, List<File> modifiedFiles, File sources,
            String moduleId, String moduleIdWithVersion)
            throws RepositoryException, SAXException, IOException, TransformerException {
        File f = File.createTempFile("import", null);

        Map<String, Object> params = new HashMap<String, Object>();
        params.put(ImportExportService.XSL_PATH,
                SettingsBean.getInstance().getJahiaEtcDiskPath() + "/repository/export/templatesCleanup.xsl");
        FileOutputStream out = new FileOutputStream(f);
        try {
            ImportExportBaseService.getInstance().exportZip(session.getNode("/modules/" + moduleIdWithVersion),
                    session.getRootNode(), out, params);
        } finally {
            IOUtils.closeQuietly(out);
        }

        final String importFileBasePath = "content/modules/" + moduleId + "/";
        String filesNodePath = "/modules/" + moduleIdWithVersion;
        JCRNodeWrapper filesNode = null;
        if (session.nodeExists(filesNodePath)) {
            filesNode = session.getNode(filesNodePath);
        }
        // clean up files folder before unziping in it
        File sourcesImportFolder = new File(sources, "src/main/import");
        sourcesImportFolder.mkdirs();
        File filesDirectory = new File(sourcesImportFolder.getPath() + "/" + importFileBasePath);
        Collection<File> files = null;
        if (filesDirectory.exists()) {
            files = FileUtils.listFiles(filesDirectory, null, true);
        } else {
            files = new ArrayList<File>();
        }
        ZipInputStream zis = null;
        try {
            zis = new ZipInputStream(new FileInputStream(f));
            ZipEntry zipentry;
            while ((zipentry = zis.getNextEntry()) != null) {
                if (!zipentry.isDirectory()) {
                    try {
                        String name = zipentry.getName();
                        name = name.replace(moduleIdWithVersion, moduleId);
                        File sourceFile = new File(sourcesImportFolder, name);
                        boolean nodeMoreRecentThanSourceFile = true;
                        if (sourceFile.exists() && name.startsWith(importFileBasePath)) {
                            String relPath = name.substring(importFileBasePath.length());
                            if (relPath.endsWith(sourceFile.getName() + "/" + sourceFile.getName())) {
                                relPath = StringUtils.substringBeforeLast(relPath, "/");
                            }
                            if (filesNode != null && filesNode.hasNode(relPath)) {
                                JCRNodeWrapper node = filesNode.getNode(relPath);
                                if (node.hasProperty("jcr:lastModified")) {
                                    nodeMoreRecentThanSourceFile = node.getProperty("jcr:lastModified").getDate()
                                            .getTimeInMillis() > sourceFile.lastModified();
                                }
                            }
                        }
                        if (nodeMoreRecentThanSourceFile && saveFile(zis, sourceFile)) {
                            modifiedFiles.add(sourceFile);
                        }

                        files.remove(sourceFile);
                    } catch (IOException e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
            for (File file : files) {
                try {
                    deleteFileAndEmptyParents(file, sourcesImportFolder.getPath());
                } catch (Exception e) {
                    logger.error("Cannot delete file " + file, e);
                }
            }
        } catch (Exception e) {
            logger.error("Cannot patch import file", e);
        } finally {
            if (zis != null) {
                IOUtils.closeQuietly(zis);
            }
        }
    }

    private boolean saveFile(InputStream source, File target) throws IOException {
        Charset transCodeTarget = null;
        if (target.getParentFile().getName().equals("resources") && target.getName().endsWith(".properties")) {
            transCodeTarget = Charsets.ISO_8859_1;
        }

        if (!target.exists()) {
            if (!target.getParentFile().exists() && !target.getParentFile().mkdirs()) {
                throw new IOException("Unable to create path for: " + target.getParentFile());
            }
            if (target.getParentFile().isFile()) {
                target.getParentFile().delete();
                target.getParentFile().mkdirs();
            }
            if (transCodeTarget != null) {
                FileUtils.writeLines(target, transCodeTarget.name(),
                        convertToNativeEncoding(IOUtils.readLines(source, Charsets.UTF_8), transCodeTarget), "\n");
            } else {
                FileOutputStream output = FileUtils.openOutputStream(target);
                try {
                    IOUtils.copy(source, output);
                    output.close();
                } finally {
                    IOUtils.closeQuietly(output);
                }
            }

            // Save repository.xml file after first generation
            if (target.getName().equals("repository.xml")) {
                FileUtils.copyFile(target, new File(target.getPath() + ".generated"));
            }

            return true;
        } else {
            List<String> targetContent = FileUtils.readLines(target,
                    transCodeTarget != null ? transCodeTarget : Charsets.UTF_8);
            if (!isBinary(targetContent)) {
                File previouslyGenerated = new File(target.getPath() + ".generated");
                List<String> previouslyGeneratedContent = targetContent;
                if (previouslyGenerated.exists()) {
                    previouslyGeneratedContent = FileUtils.readLines(previouslyGenerated,
                            transCodeTarget != null ? transCodeTarget : Charsets.UTF_8);
                }
                DiffMatchPatch dmp = new DiffMatchPatch();
                List<String> sourceContent = IOUtils.readLines(source, Charsets.UTF_8);
                if (transCodeTarget != null) {
                    sourceContent = convertToNativeEncoding(sourceContent, transCodeTarget);
                }

                LinkedList<DiffMatchPatch.Patch> l = dmp.patch_make(
                        StringUtils.join(previouslyGeneratedContent, "\n"), StringUtils.join(sourceContent, "\n"));

                if (target.getName().equals("repository.xml")) {
                    // Keep generated file uptodate
                    FileUtils.writeLines(new File(target.getPath() + ".generated"),
                            transCodeTarget != null ? transCodeTarget.name() : "UTF-8", sourceContent);
                }

                if (!l.isEmpty()) {
                    Object[] objects = dmp.patch_apply(l, StringUtils.join(targetContent, "\n"));

                    for (boolean b : ((boolean[]) objects[1])) {
                        if (!b) {
                            throw new IOException("Cannot apply modification on " + target.getName()
                                    + ", check generated file at : " + target.getPath() + ".generated");
                        }
                    }
                    FileUtils.write(target, (CharSequence) objects[0],
                            transCodeTarget != null ? transCodeTarget.name() : "UTF-8");
                    return true;
                }
            } else {
                byte[] sourceArray = IOUtils.toByteArray(source);
                FileInputStream input = new FileInputStream(target);
                FileOutputStream output = null;
                try {
                    byte[] targetArray = IOUtils.toByteArray(input);
                    if (!Arrays.equals(sourceArray, targetArray)) {
                        output = new FileOutputStream(target);
                        IOUtils.write(sourceArray, output);
                        return true;
                    }
                } finally {
                    IOUtils.closeQuietly(input);
                    IOUtils.closeQuietly(output);
                }
            }
        }
        return false;
    }

    private void deleteFileAndEmptyParents(File file, final String rootPath) throws IOException {
        if (rootPath.equals(file.getPath())) {
            return;
        }
        FileUtils.forceDelete(file);
        File parentFile = file.getParentFile();
        File[] files = parentFile.listFiles((FileFilter) HiddenFileFilter.VISIBLE);
        if (files == null || files.length == 0) {
            deleteFileAndEmptyParents(parentFile, rootPath);
        }
    }

    private List<String> convertToNativeEncoding(List<String> sourceContent, Charset charset)
            throws UnsupportedEncodingException {
        List<String> targetContent = new ArrayList<String>();
        for (String s : sourceContent) {
            Matcher m = UNICODE_PATTERN.matcher(s);
            int start = 0;
            while (m.find(start)) {
                String replacement = new String(new byte[] { (byte) Integer.parseInt(m.group(1), 16),
                        (byte) Integer.parseInt(m.group(2), 16) }, "UTF-16");
                if (charset.decode(charset.encode(replacement)).toString().equals(replacement)) {
                    s = m.replaceFirst(replacement);
                }
                start = m.start() + 1;
                m = UNICODE_PATTERN.matcher(s);
            }
            targetContent.add(s);
        }
        return targetContent;
    }

    private boolean isBinary(List<String> text) {
        for (String s : text) {
            if (s.contains("\u0000")) {
                return true;
            }
        }
        return false;
    }

    static class CompiledModuleInfo {

        private final File file;
        private final String moduleName;
        private final String version;

        public CompiledModuleInfo(File file, String moduleName, String version) {
            this.file = file;
            this.moduleName = moduleName;
            this.version = version;
        }

        public File getFile() {
            return file;
        }

        public String getModuleName() {
            return moduleName;
        }

        public String getVersion() {
            return version;
        }
    }

    static public class SvnCleaner extends DirectoryWalker<File> {

        public List<File> clean(File startDirectory) throws IOException {
            ArrayList<File> results = new ArrayList<File>();
            walk(startDirectory, results);
            return results;
        }

        @Override
        protected boolean handleDirectory(File directory, int depth, Collection<File> results) {
            if (".svn".equals(directory.getName())) {
                FileUtils.deleteQuietly(directory);
                results.add(directory);
                return false;
            } else {
                return true;
            }

        }
    }

    /**
     * Supplies the exact version of the Maven archetypes to use when creating a module.
     *
     * @param mavenArchetypeVersion
     *            the exact version of the Maven archetypes to use when creating a module
     */
    public void setMavenArchetypeVersion(String mavenArchetypeVersion) {
        this.mavenArchetypeVersion = mavenArchetypeVersion;
    }

    public int getModuleStartLevel() {
        return moduleStartLevel;
    }

    public void setModuleStartLevel(int moduleStartLevel) {
        this.moduleStartLevel = moduleStartLevel;
    }

    public void setModuleManager(ModuleManager moduleManager) {
        this.moduleManager = moduleManager;
    }

    public void setMavenWarnIfVersionIsOlderThan(String mavenWarnIfVersionIsOlderThan) {
        this.mavenWarnIfVersionIsOlderThan = mavenWarnIfVersionIsOlderThan;
    }

    public void setMavenArchetypePlugin(String mavenArchetypePlugin) {
        this.mavenArchetypePlugin = mavenArchetypePlugin;
    }
}