InitCommand.java

package org.docascode.api;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.docascode.api.core.errors.DocAsCodeException;
import org.docascode.api.core.DocAsCodeRepository;
import org.docascode.api.core.errors.NotADocAsCodeRepository;
import org.docascode.api.event.Event;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;

import javax.xml.XMLConstants;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class InitCommand extends DocAsCodeCommand<DocAsCode> {
    private static final String DOCASCODE_DOCX = "docascode-docx";
    private boolean force = false;

    public InitCommand setForce(boolean force){
        this.force = force;
        return this;
    }

    private File directory;

    public InitCommand setDirectory(File directory){
        this.directory = directory;
        return this;
    }

    @Override
    public DocAsCode call() throws DocAsCodeException {
        Git git;
        try {
            git = Git.init().setDirectory(directory).call();
        } catch (GitAPIException e) {
            throw new DocAsCodeException(
                    String.format("Unable to initialize a DocAsCode repository in '%s'",
                            directory), e);
        }
        try {
            Status s = git.status().call();
            if(s.hasUncommittedChanges() || !force){
                throw new DocAsCodeException("Working tree contains unstaged changes. Aborting.");
            }
        } catch (GitAPIException e) {
            throw new DocAsCodeException("Unable to get status of git repository " + directory.getAbsolutePath(), e);
        }
        DocAsCodeRepository repo;
        try {
            repo = new DocAsCodeRepository(git.getRepository());
        } catch (IOException e) {
            throw new DocAsCodeException("Unable to handle DocAsCode repository.",e);
        }
        String versionString = repo.getVersion();
        if (versionString == null) {
            initialize(repo);
        } else {
            DefaultArtifactVersion version = new DefaultArtifactVersion(versionString);
            switch (version.getMajorVersion()){
                case 3:
                    upgradeChronoXML(repo);
                    try {
                        Files.delete(Paths.get(
                                String.format("%s/%s",
                                        repo.getWorkTree(),
                                        ".docascode/docascode.xml")));
                    } catch (IOException e) {
                        throw new DocAsCodeException("Unable to delete .docascode/docascode.xml",e);
                    }
                    break;
                case 4:
                    break;
                default:
                    throw new DocAsCodeException(
                            String.format("Unhandled version ('%s')of DocAsCode repository...",versionString));
            }
        }
        new ConfigCommand(repo)
                .setAction(ConfigCommand.Action.SET)
                .setLevel(ConfigCommand.Level.PROJECT)
                .setSection("docascode")
                .setSubsection(null)
                .setName("version")
                .setValue(DocAsCode.getVersion())
                .call();
        try {
            return new DocAsCode(repo);
        } catch (NotADocAsCodeRepository notADocAsCodeRepository) {
            throw new DocAsCodeException("Something went wrong when initialized repository",notADocAsCodeRepository);
        }
    }

    private void initialize(DocAsCodeRepository repo) throws DocAsCodeException {
        copyResource("org/docascode/init/config",repo.git().getProjectConfigFile());
        copyResource("org/docascode/init/delivery/chrono.xml",repo.getChronoXML());
        copyResource("org/docascode/init/delivery/delivery.xml",repo.getDeliveryXML());
        copyResource("org/docascode/init/delivery/delivery.properties",repo.getDeliveryProperties());
        copyHook("org/docascode/init/hooks/pre-commit",repo.git().getPreCommit());
        copyHook("org/docascode/init/hooks/post-commit",repo.git().getPostCommit());
        ConfigCommand config = new ConfigCommand(repo);
        config.setAction(ConfigCommand.Action.ADD)
                .setSection("include")
                .setSubsection(null)
                .setName("path")
                .setValue("../.docascode/config")
                .setLevel(ConfigCommand.Level.LOCAL)
                .call();
        config.setAction(ConfigCommand.Action.SET)
                .setSection("merge")
                .setSubsection(DOCASCODE_DOCX)
                .setName("driver")
                .setValue("docascode merge --ancestor %O --current %A --other %B --placeholder %P --format docx")
                .setLevel(ConfigCommand.Level.LOCAL)
                .call();
        config.setAction(ConfigCommand.Action.SET)
                .setSection("merge")
                .setSubsection("keep-mine")
                .setName("driver")
                .setValue("true")
                .setLevel(ConfigCommand.Level.LOCAL)
                .call();
        config.setAction(ConfigCommand.Action.SET)
                .setSection("diff")
                .setSubsection(DOCASCODE_DOCX)
                .setName("textconv")
                .setValue("pandoc --to=markdown")
                .setLevel(ConfigCommand.Level.LOCAL)
                .call();
        config.setAction(ConfigCommand.Action.SET)
                .setSection("diff")
                .setSubsection(DOCASCODE_DOCX)
                .setName("prompt")
                .setValue("false")
                .setLevel(ConfigCommand.Level.LOCAL)
                .call();
        File gitattributes = new File(repo.getWorkTree(),".gitattributes");
        if (!gitattributes.exists()){
            try {
                Files.createFile(Paths.get(gitattributes.getAbsolutePath()));
            } catch (IOException e) {
                throw new DocAsCodeException("Unable to create .gitattributes file.",e);
            }
        }
        replace(gitattributes,
                "\\*.docx merge=*",
                "*.docx merge=docascode-docx diff=docascode-docx");
        replace(gitattributes,
                "\\.docascode/\\*\\*/\\*.md merge=",
                ".docascode/**/*.md merge=keep-mine");
        mergeGitIgnore(new File(repo.getWorkTree().getAbsolutePath()+"/.gitignore"));
        log(
                String.format("Successfully initialized DocAsCode Repository in '%s'.",
                        directory), Event.Level.SUCESS);
    }

    private void copyResource(String resourcePath, File destFile) throws DocAsCodeException {
        InputStream src = getClass().getClassLoader().getResourceAsStream(resourcePath);
        try {
            copyInputStreamToFile(src,destFile);
        } catch (IOException e) {
            throw new DocAsCodeException(
                String.format("Unable to copy resource '%s' to '%s'.",
                    resourcePath,
                    destFile),e);
        }
    }

    private void copyHook(String resourcePath, File destHook) throws DocAsCodeException {
        if (destHook.exists()){
            InputStream src = getClass().getClassLoader().getResourceAsStream(resourcePath);
            if (src != null) {
                String content;
                try {
                    content = IOUtils.toString(src, StandardCharsets.UTF_8);
                } catch (IOException e) {
                    throw new DocAsCodeException(
                        "Unable to read hook content", e);
                }
                log(String.format(
                        "Hook '%s' already exists. Please check that it contains the following content:%n%n%s%n",
                        destHook.getPath(),
                        content),
                    Event.Level.WARN);
            }
        } else {
            copyResource(resourcePath,destHook);
            if (!destHook.setExecutable(true)) {
                throw new DocAsCodeException(
                        String.format(
                                "Unable to make '%s' executable.",
                                getRepository().git().getPreCommit()
                        )
                );
            }
        }
    }

    private void copyInputStreamToFile(InputStream source, File destination) throws IOException {
        if (!destination.exists()){
            FileUtils.copyInputStreamToFile(source,destination);
        }
    }

    private void replace(File file, String pattern, String replace) throws DocAsCodeException {
        List<String> result = new ArrayList<>();
        Pattern regexp = Pattern.compile(pattern);
        boolean missing = true;
        Matcher matcher = regexp.matcher("");
        try (
                BufferedReader reader = Files.newBufferedReader(file.toPath(),
                        StandardCharsets.UTF_8);
                LineNumberReader lineReader = new LineNumberReader(reader)
        ){
            String line;
            while ((line = lineReader.readLine()) != null) {
                matcher.reset(line); //reset the input
                if (matcher.find()) {
                    line = replace;
                    missing = false;
                }
                result.add(line);
            }
            if (missing){
                result.add(replace);
            }
            try (FileOutputStream fos=new FileOutputStream(file,false)) {
                for (String s : result) {
                    fos.write(s.getBytes());
                    fos.write("\n".getBytes());
                }
            }
        }
        catch (IOException e){
            throw new DocAsCodeException("",e);
        }
    }

    private void mergeGitIgnore(File gitignore) throws DocAsCodeException {
        List<String> lines = new ArrayList<>();
        String line;
        if (!gitignore.exists()) {
            try {
                Files.createFile(Paths.get(gitignore.getAbsolutePath()));
            } catch (IOException e) {
                throw new DocAsCodeException("Unable to create .gitignore file.", e);
            }
        }
        try (BufferedReader reader = Files.newBufferedReader(gitignore.toPath(),
                StandardCharsets.UTF_8);
             LineNumberReader lineReader = new LineNumberReader(reader)){

            while ((line = lineReader.readLine()) != null) {
                if(!lines.contains(line))
                {
                    lines.add(line);
                }
            }
        } catch (IOException e) {
            throw new DocAsCodeException("Unable to read .gitignore file.",e);
        }
        InputStream gitIgnoreResource = getClass().getClassLoader().getResourceAsStream("org/docascode/init/gitignore");
        if (gitIgnoreResource == null){
            throw new DocAsCodeException("Unable to read .gitignore resource file.");
        }
        try(FileOutputStream fos=new FileOutputStream(gitignore,false)) {
            BufferedReader gitIgnoreResourceReader = new BufferedReader(new InputStreamReader(gitIgnoreResource));
            while ((line = gitIgnoreResourceReader.readLine()) != null) {
                if(!lines.contains(line))
                {
                    lines.add(line);
                }
            }
            for (String s : lines) {
                fos.write(s.getBytes());
                fos.write("\n".getBytes());
            }
        } catch (IOException e) {
            throw new DocAsCodeException("Unable to merge local .gitignore with DocAsCode .gitignore");
        }
    }

    private void upgradeChronoXML(DocAsCodeRepository repository) throws DocAsCodeException{
        try {
            TransformerFactory factory = TransformerFactory.newInstance();
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
            InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream("org/docascode/api/core/delivery-1.0-to-2.0.xsl");
            StreamSource xslt = new StreamSource(resourceAsStream);
            Transformer transformer = factory.newTransformer(xslt);
            File oldChrono =
                    new File(repository.getWorkTree(),"chrono.xml");
            StreamSource source = new StreamSource(oldChrono);
            StreamResult result = new StreamResult(repository.getChronoXML());
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.transform(source, result);
            Files.delete(Paths.get(oldChrono.getAbsolutePath()));
            log("Successfully upgraded chrono.xml", Event.Level.SUCESS);
        } catch (TransformerException | IOException e) {
            throw new DocAsCodeException("Unable to upgrade chrono.xml.",e);
        }
    }

}