com.mastfrog.maven.merge.configuration.MergeConfigurationMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.mastfrog.maven.merge.configuration.MergeConfigurationMojo.java

Source

/*
 * The MIT License
 *
 * Copyright 2013 Tim Boudreau.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.maven.merge.configuration;

import com.google.common.base.Objects;
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.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.zip.ZipException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.DefaultDependencyResolutionRequest;
import org.apache.maven.project.DependencyResolutionException;
import org.apache.maven.project.DependencyResolutionResult;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectDependenciesResolver;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.graph.Dependency;

/**
 * A Maven plugin which, on good days, merges together all properties files on
 * the classpath whicha live in target/classes/META-INF/settings into a single
 * file in the classes output dir.
 * <p/>
 * Use this when you want to merge multiple JARs using the default namespace for
 * settings into one big JAR without files randomly clobbering each other.
 *
 * @author Tim Boudreau
 */
@org.apache.maven.plugins.annotations.Mojo(defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.COMPILE, name = "merge-configuration", threadSafe = false)
public class MergeConfigurationMojo extends AbstractMojo {

    // FOR ANYONE UNDER THE ILLUSION THAT WHAT WE DO IS COMPUTER SCIENCE:
    //
    // The following two unused fields are magical.  Remove them and you get
    // a plugin which contains no mojo.
    @Parameter(defaultValue = "${localRepository}")
    private org.apache.maven.artifact.repository.ArtifactRepository localRepository;
    @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true)
    private java.util.List remoteRepositories;
    @Parameter(defaultValue = "${repositorySystemSession}")
    private RepositorySystemSession repoSession;
    @Parameter(property = "mainClass", defaultValue = "none")
    private String mainClass;
    @Parameter(property = "jarName", defaultValue = "none")
    private String jarName;
    @Parameter(property = "exclude", defaultValue = "")
    private String exclude = "";
    private static final Pattern PAT = Pattern.compile("META-INF\\/settings\\/[^\\/]*\\.properties");
    private static final Pattern SERVICES = Pattern.compile("META-INF\\/services\\/\\S[^\\/]*\\.*");

    private static final Pattern SIG1 = Pattern.compile("META-INF\\/[^\\/]*\\.SF");
    private static final Pattern SIG2 = Pattern.compile("META-INF\\/[^\\/]*\\.DSA");
    private static final Pattern SIG3 = Pattern.compile("META-INF\\/[^\\/]*\\.RSA");

    @Component
    private ProjectDependenciesResolver resolver;
    @Component
    MavenProject project;

    private List<String> readLines(InputStream in) throws IOException {
        List<String> result = new LinkedList<>();
        InputStreamReader isr = new InputStreamReader(in);
        BufferedReader br = new BufferedReader(isr);
        String line;
        while ((line = br.readLine()) != null) {
            if (!line.trim().isEmpty()) {
                result.add(line);
            }
        }
        return result;
    }

    private static int copy(final InputStream in, final OutputStream out) throws IOException {
        final byte[] buffer = new byte[4096];
        int bytesCopied = 0;
        for (;;) {
            int byteCount = in.read(buffer, 0, buffer.length);
            if (byteCount <= 0) {
                break;
            } else {
                out.write(buffer, 0, byteCount);
                bytesCopied += byteCount;
            }
        }
        return bytesCopied;
    }

    private String strip(String name) {
        int ix = name.lastIndexOf(".");
        if (ix >= 0 && ix != name.length() - 1) {
            name = name.substring(ix + 1);
        }
        return name;
    }

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        // XXX a LOT of duplicate code here
        Log log = super.getLog();
        log.info("Merging properties files");
        if (repoSession == null) {
            throw new MojoFailureException("RepositorySystemSession is null");
        }
        List<File> jars = new ArrayList<>();
        List<String> exclude = new LinkedList<>();
        for (String ex : this.exclude.split(",")) {
            ex = ex.trim();
            ex = ex.replace('.', '/');
            exclude.add(ex);
        }
        try {
            DependencyResolutionResult result = resolver
                    .resolve(new DefaultDependencyResolutionRequest(project, repoSession));
            log.info("FOUND " + result.getDependencies().size() + " dependencies");
            for (Dependency d : result.getDependencies()) {
                switch (d.getScope()) {
                case "test":
                case "provided":
                    break;
                default:
                    File f = d.getArtifact().getFile();
                    if (f.getName().endsWith(".jar") && f.isFile() && f.canRead()) {
                        jars.add(f);
                    }
                }
            }
        } catch (DependencyResolutionException ex) {
            throw new MojoExecutionException("Collecting dependencies failed", ex);
        }

        Map<String, Properties> m = new LinkedHashMap<>();

        Map<String, Set<String>> linesForName = new LinkedHashMap<>();
        Map<String, Integer> fileCountForName = new HashMap<>();

        boolean buildMergedJar = mainClass != null && !"none".equals(mainClass);
        JarOutputStream jarOut = null;
        Set<String> seen = new HashSet<>();

        try {
            if (buildMergedJar) {
                try {
                    File outDir = new File(project.getBuild().getOutputDirectory()).getParentFile();
                    File jar = new File(outDir, project.getBuild().getFinalName() + ".jar");
                    if (!jar.exists()) {
                        throw new MojoExecutionException("Could not find jar " + jar);
                    }
                    try (JarFile jf = new JarFile(jar)) {
                        Manifest manifest = new Manifest(jf.getManifest());
                        if (mainClass != null) {
                            manifest.getMainAttributes().putValue("Main-Class", mainClass);
                        }
                        String jn = jarName == null || "none".equals(jarName) ? strip(mainClass) : jarName;
                        File outJar = new File(outDir, jn + ".jar");
                        log.info("Will build merged JAR " + outJar);
                        if (outJar.equals(jar)) {
                            throw new MojoExecutionException(
                                    "Merged jar and output jar are the same file: " + outJar);
                        }
                        if (!outJar.exists()) {
                            outJar.createNewFile();
                        }
                        jarOut = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(outJar)),
                                manifest);
                        jarOut.setLevel(9);
                        jarOut.setComment("Merged jar created by " + getClass().getName());
                        Enumeration<JarEntry> en = jf.entries();
                        while (en.hasMoreElements()) {
                            JarEntry e = en.nextElement();
                            String name = e.getName();
                            for (String s : exclude) {
                                if (name.startsWith(s)) {
                                    continue;
                                }
                            }
                            //                            if (!seen.contains(name)) {
                            switch (name) {
                            case "META-INF/MANIFEST.MF":
                            case "META-INF/":
                                break;
                            case "META-INF/LICENSE":
                            case "META-INF/LICENSE.txt":
                            case "META-INF/http/pages.list":
                            case "META-INF/http/modules.list":
                            case "META-INF/http/numble.list":
                            case "META-INF/settings/namespaces.list":
                                Set<String> s = linesForName.get(name);
                                if (s == null) {
                                    s = new LinkedHashSet<>();
                                    linesForName.put(name, s);
                                }
                                Integer ct = fileCountForName.get(name);
                                if (ct == null) {
                                    ct = 1;
                                }
                                fileCountForName.put(name, ct);
                                try (InputStream in = jf.getInputStream(e)) {
                                    s.addAll(readLines(in));
                                }
                                break;
                            default:
                                if (name.startsWith("META-INF/services/") && !name.endsWith("/")) {
                                    Set<String> s2 = linesForName.get(name);
                                    if (s2 == null) {
                                        s2 = new HashSet<>();
                                        linesForName.put(name, s2);
                                    }
                                    Integer ct2 = fileCountForName.get(name);
                                    if (ct2 == null) {
                                        ct2 = 1;
                                    }
                                    fileCountForName.put(name, ct2);
                                    try (InputStream in = jf.getInputStream(e)) {
                                        s2.addAll(readLines(in));
                                    }
                                    seen.add(name);
                                } else if (PAT.matcher(name).matches()) {
                                    log.info("Include " + name);
                                    Properties p = new Properties();
                                    try (InputStream in = jf.getInputStream(e)) {
                                        p.load(in);
                                    }
                                    Properties all = m.get(name);
                                    if (all == null) {
                                        all = p;
                                        m.put(name, p);
                                    } else {
                                        for (String key : p.stringPropertyNames()) {
                                            if (all.containsKey(key)) {
                                                Object old = all.get(key);
                                                Object nue = p.get(key);
                                                if (!Objects.equal(old, nue)) {
                                                    log.warn(key + '=' + nue + " in " + jar + '!' + name
                                                            + " overrides " + key + '=' + old);
                                                }
                                            }
                                        }
                                        all.putAll(p);
                                    }
                                } else if (!seen.contains(name) && !SIG1.matcher(name).find()
                                        && !SIG2.matcher(name).find() && !SIG3.matcher(name).find()) {
                                    log.info("Bundle " + name);
                                    JarEntry je = new JarEntry(name);
                                    je.setTime(e.getTime());
                                    try {
                                        jarOut.putNextEntry(je);
                                    } catch (ZipException ex) {
                                        throw new MojoExecutionException("Exception putting zip entry " + name, ex);
                                    }
                                    try (InputStream in = jf.getInputStream(e)) {
                                        copy(in, jarOut);
                                    }
                                    jarOut.closeEntry();
                                    seen.add(name);
                                } else {
                                    System.err.println("Skip " + name);
                                }
                            }
                            //                            }
                            seen.add(e.getName());
                        }
                    }
                } catch (IOException ex) {
                    throw new MojoExecutionException("Failed to create merged jar", ex);
                }
            }

            for (File f : jars) {
                log.info("Merge JAR " + f);
                try (JarFile jar = new JarFile(f)) {
                    Enumeration<JarEntry> en = jar.entries();
                    while (en.hasMoreElements()) {
                        JarEntry entry = en.nextElement();
                        String name = entry.getName();
                        for (String s : exclude) {
                            if (name.startsWith(s)) {
                                continue;
                            }
                        }
                        if (PAT.matcher(name).matches()) {
                            log.info("Include " + name + " in " + f);
                            Properties p = new Properties();
                            try (InputStream in = jar.getInputStream(entry)) {
                                p.load(in);
                            }
                            Properties all = m.get(name);
                            if (all == null) {
                                all = p;
                                m.put(name, p);
                            } else {
                                for (String key : p.stringPropertyNames()) {
                                    if (all.containsKey(key)) {
                                        Object old = all.get(key);
                                        Object nue = p.get(key);
                                        if (!Objects.equal(old, nue)) {
                                            log.warn(key + '=' + nue + " in " + f + '!' + name + " overrides " + key
                                                    + '=' + old);
                                        }
                                    }
                                }
                                all.putAll(p);
                            }
                        } else if (SERVICES.matcher(name).matches()
                                || "META-INF/settings/namespaces.list".equals(name)
                                || "META-INF/http/pages.list".equals(name)
                                || "META-INF/http/modules.list".equals(name)
                                || "META-INF/http/numble.list".equals(name)) {
                            log.info("Include " + name + " in " + f);
                            try (InputStream in = jar.getInputStream(entry)) {
                                List<String> lines = readLines(in);
                                Set<String> all = linesForName.get(name);
                                if (all == null) {
                                    all = new LinkedHashSet<>();
                                    linesForName.put(name, all);
                                }
                                all.addAll(lines);
                            }
                            Integer ct = fileCountForName.get(name);
                            if (ct == null) {
                                ct = 1;
                            } else {
                                ct++;
                            }
                            fileCountForName.put(name, ct);
                        } else if (jarOut != null) {
                            //                            if (!seen.contains(name)) {
                            switch (name) {
                            case "META-INF/MANIFEST.MF":
                            case "META-INF/":
                                break;
                            case "META-INF/LICENSE":
                            case "META-INF/LICENSE.txt":
                            case "META-INF/settings/namespaces.list":
                            case "META-INF/http/pages.list":
                            case "META-INF/http/numble.list":
                            case "META-INF/http/modules.list":
                                Set<String> s = linesForName.get(name);
                                if (s == null) {
                                    s = new LinkedHashSet<>();
                                    linesForName.put(name, s);
                                }
                                Integer ct = fileCountForName.get(name);
                                if (ct == null) {
                                    ct = 1;
                                }
                                fileCountForName.put(name, ct);
                                try (InputStream in = jar.getInputStream(entry)) {
                                    s.addAll(readLines(in));
                                }
                                break;
                            default:
                                if (!seen.contains(name)) {
                                    if (!SIG1.matcher(name).find() && !SIG2.matcher(name).find()
                                            && !SIG3.matcher(name).find()) {
                                        JarEntry je = new JarEntry(name);
                                        je.setTime(entry.getTime());
                                        try {
                                            jarOut.putNextEntry(je);
                                        } catch (ZipException ex) {
                                            throw new MojoExecutionException("Exception putting zip entry " + name,
                                                    ex);
                                        }
                                        try (InputStream in = jar.getInputStream(entry)) {
                                            copy(in, jarOut);
                                        }
                                        jarOut.closeEntry();
                                    }
                                } else {
                                    if (!name.endsWith("/") && !name.startsWith("META-INF")) {
                                        log.warn("Saw more than one " + name + ".  One will clobber the other.");
                                    }
                                }
                            }
                            //                            } else {
                            //                                if (!name.endsWith("/") && !name.startsWith("META-INF")) {
                            //                                    log.warn("Saw more than one " + name + ".  One will clobber the other.");
                            //                                }
                            //                            }
                            seen.add(name);
                        }
                    }
                } catch (IOException ex) {
                    throw new MojoExecutionException("Error opening " + f, ex);
                }
            }
            if (!m.isEmpty()) {
                log.warn("Writing merged files: " + m.keySet());
            } else {
                return;
            }
            String outDir = project.getBuild().getOutputDirectory();
            File dir = new File(outDir);
            // Don't bother rewriting META-INF/services files of which there is
            // only one
            //            for (Map.Entry<String, Integer> e : fileCountForName.entrySet()) {
            //                if (e.getValue() == 1) {
            //                    linesForName.remove(e.getKey());
            //                }
            //            }
            for (Map.Entry<String, Set<String>> e : linesForName.entrySet()) {
                File outFile = new File(dir, e.getKey());
                log.info("Merge configurating rewriting " + outFile);
                Set<String> lines = e.getValue();
                if (!outFile.exists()) {
                    try {
                        Path path = outFile.toPath();
                        if (!Files.exists(path.getParent())) {
                            Files.createDirectories(path.getParent());
                        }
                        path = Files.createFile(path);
                        outFile = path.toFile();
                    } catch (IOException ex) {
                        throw new MojoFailureException("Could not create " + outFile, ex);
                    }
                }
                if (!outFile.isDirectory()) {
                    try (FileOutputStream out = new FileOutputStream(outFile)) {
                        try (PrintStream ps = new PrintStream(out)) {
                            for (String line : lines) {
                                ps.println(line);
                            }
                        }
                    } catch (IOException ex) {
                        throw new MojoFailureException("Exception writing " + outFile, ex);
                    }
                }
                if (jarOut != null) {
                    log.warn("Concatenating " + fileCountForName.get(e.getKey()) + " copies of " + e.getKey());
                    JarEntry je = new JarEntry(e.getKey());
                    try {
                        jarOut.putNextEntry(je);
                        PrintStream ps = new PrintStream(jarOut);
                        for (String line : lines) {
                            ps.println(line);
                        }
                        jarOut.closeEntry();
                    } catch (IOException ex) {
                        throw new MojoFailureException("Exception writing " + outFile, ex);
                    }
                }
            }
            for (Map.Entry<String, Properties> e : m.entrySet()) {
                File outFile = new File(dir, e.getKey());
                Properties local = new Properties();
                if (outFile.exists()) {
                    try {
                        try (InputStream in = new FileInputStream(outFile)) {
                            local.load(in);
                        }
                    } catch (IOException ioe) {
                        throw new MojoExecutionException("Could not read " + outFile, ioe);
                    }
                } else {
                    try {
                        Path path = outFile.toPath();
                        if (!Files.exists(path.getParent())) {
                            Files.createDirectories(path.getParent());
                        }
                        path = Files.createFile(path);
                        outFile = path.toFile();
                    } catch (IOException ex) {
                        throw new MojoFailureException("Could not create " + outFile, ex);
                    }
                }
                Properties merged = e.getValue();
                for (String key : local.stringPropertyNames()) {
                    if (merged.containsKey(key) && !Objects.equal(local.get(key), merged.get(key))) {
                        log.warn("Overriding key=" + merged.get(key) + " with locally defined key="
                                + local.get(key));
                    }
                }
                merged.putAll(local);
                try {
                    log.info("Saving merged properties to " + outFile);
                    try (FileOutputStream out = new FileOutputStream(outFile)) {
                        merged.store(out, getClass().getName());
                    }
                } catch (IOException ex) {
                    throw new MojoExecutionException("Failed to write " + outFile, ex);
                }
                if (jarOut != null) {
                    JarEntry props = new JarEntry(e.getKey());
                    try {
                        jarOut.putNextEntry(props);
                        merged.store(jarOut, getClass().getName() + " merged " + e.getKey());
                        jarOut.closeEntry();
                    } catch (IOException ex) {
                        throw new MojoExecutionException("Failed to write jar entry " + e.getKey(), ex);
                    }
                }
                File copyTo = new File(dir.getParentFile(), "settings");
                if (!copyTo.exists()) {
                    copyTo.mkdirs();
                }
                File toFile = new File(copyTo, outFile.getName());
                try {
                    Files.copy(outFile.toPath(), toFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
                } catch (IOException ex) {
                    throw new MojoExecutionException("Failed to copy " + outFile + " to " + toFile, ex);
                }
            }
        } finally {
            if (jarOut != null) {
                try {
                    jarOut.close();
                } catch (IOException ex) {
                    throw new MojoExecutionException("Failed to close Jar", ex);
                }
            }
        }
    }
}