de.tobiasroeser.maven.featurebuilder.FeatureBuilder.java Source code

Java tutorial

Introduction

Here is the source code for de.tobiasroeser.maven.featurebuilder.FeatureBuilder.java

Source

package de.tobiasroeser.maven.featurebuilder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.pom.x400.Model;
import org.apache.maven.pom.x400.ProjectDocument;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;

import de.tobiasroeser.maven.shared.MavenXmlSupport;
import de.tobiasroeser.maven.versionupdater.LocalArtifact;
import de.tototec.cmdoption.CmdOption;
import de.tototec.cmdoption.CmdlineParser;
import de.tototec.cmdoption.CmdlineParserException;

public class FeatureBuilder {

    private final Log log = LogFactory.getLog(FeatureBuilder.class);

    public static void main(final String[] args) {
        try {
            final int status = new FeatureBuilder().run(args);
            System.exit(status);
        } catch (final Throwable t) {
            LogFactory.getLog(FeatureBuilder.class).error("Caught an exception.", t);
            System.exit(1);
        }
    }

    public static class Config {

        @CmdOption(names = { "--help", "-h" }, description = "Show this help")
        public boolean showHelp = false;

        @CmdOption(names = "--use-pom", args = {
                "FILE" }, description = "EXPERIMENTAL: Use a maven project {0} file to read feature name, version and dependencies.")
        public String pomFile;

        @CmdOption(names = "--createFeatureXml", args = { "FILE" }, description = "Create a feature xml file {0}")
        public String createFeatureXml;

        @CmdOption(names = "--feature-id", args = { "ID" }, description = "Feature {0}")
        public String symbolicName;

        @CmdOption(names = "--feature-version", args = { "VERSION" }, description = "Feature version")
        public String version;

        @CmdOption(names = "--scan-jars", args = {
                "DIR" }, description = "Scan directory {0} for jars and extract bundle information (used to build the feature)")
        public String scanJarsAtDir;

        @CmdOption(names = "--update-feature-xml", args = { "FILE" }, description = "Update a feature xml file {0}")
        public String updateFeatureXml;

        @CmdOption(names = "--template-xml", args = {
                "FILE" }, description = "The feature.xml template {0} file (optional for --update-feature-xml)")
        public String updateFeatureXmlTemplate;

        @CmdOption(names = "--copy-jars-as-bundles", args = {
                "DIR" }, description = "Copy found jars (via --scan-jars option) with their bundle name to directory {0}")
        public String copyJarsAsBundlesTo;

        /** Map(feature-id,new-version) */
        @CmdOption(names = "--update-included-feature-version", args = { "FEATURE",
                "VERSION" }, maxCount = -1, description = "Update the version of an included feature {0} to version {1}")
        public final Map<String, String> updateIncludedFeatureVersion = new LinkedHashMap<String, String>();

        /** List(feature-id) */
        @CmdOption(names = "--update-included-feature", args = {
                "FEATURE" }, maxCount = -1, description = "Update the version of an included feature {0}. The version of feature PAR will be autodetected but requires the use of --scan-jars. Feature jars need the MANIFEST.MF entries 'FeatureBuilder-FeatureId' and 'FeatureBuilder-FeatureVersion'")
        public final List<String> updateIncludedFeature = new LinkedList<String>();

        @CmdOption(names = "--jar-feature", args = { "DIR" }, description = "Create a feature jar to directory {0}")
        public String jarFeatureTo;

        public List<String> validate() {
            final List<String> errors = new LinkedList<String>();
            int count = 0;
            if (createFeatureXml != null) {
                ++count;
            }
            if (updateFeatureXml != null) {
                ++count;
            }

            if (count == 0) {
                errors.add("No feature.xml creation/modification method given.");
            }
            if (count > 1) {
                errors.add("More that one feature.xml creation/modification methods given.");
            }

            return errors;
        }
    }

    private int run(final String[] params) {
        final Config config = new Config();
        final CmdlineParser cp = new CmdlineParser(config);
        try {
            cp.parse(params);
        } catch (final CmdlineParserException e) {
            System.err.println(e.getMessage());
            return 1;
        }
        if (config.showHelp) {
            cp.usage();
            return 0;
        }
        return run(config);
    }

    public int run(final Config config) {

        try {
            final List<Bundle> bundles = new LinkedList<Bundle>();

            if (config.pomFile != null) {
                readPom(config.pomFile);
            }

            List<Bundle> scannedBundles = null;
            if (config.scanJarsAtDir != null) {
                scannedBundles = scanBundlesAtDir(config.scanJarsAtDir);
                log.info("jar scan found the following bundles: " + scannedBundles);
                bundles.addAll(scannedBundles);

                if (config.updateIncludedFeature.size() > 0) {
                    final Map<String, String> scanFeatureVersionsAtDir = scanFeatureVersionsAtDir(
                            config.scanJarsAtDir);
                    log.info("Scanned " + scanFeatureVersionsAtDir.size() + " features in jar dir: "
                            + config.scanJarsAtDir);
                    for (final String featureId : config.updateIncludedFeature) {
                        if (scanFeatureVersionsAtDir.containsKey(featureId)) {
                            final String version = scanFeatureVersionsAtDir.get(featureId);
                            log.info("Scanned feature id '" + featureId + "' with version '" + version + "'");
                            config.updateIncludedFeatureVersion.put(featureId, version);
                        } else {
                            log.warn("Could not scan feature id: " + featureId);
                        }
                    }
                }
            } else {
                if (config.updateIncludedFeature.size() > 0) {
                    throw new IllegalArgumentException(
                            "Need option scan jars to automatically scan for included feature versions");
                }
            }

            if (config.createFeatureXml != null) {
                createFeatureXml(config.createFeatureXml, config.symbolicName, config.version, bundles);
            }

            if (config.updateFeatureXml != null) {
                updateFeatureXml(config.updateFeatureXml, config.updateFeatureXmlTemplate, config.symbolicName,
                        config.version, config.updateIncludedFeatureVersion, bundles);
            }

            if (config.copyJarsAsBundlesTo != null && scannedBundles != null) {
                copyJarsAsBundles(scannedBundles, config.copyJarsAsBundlesTo);
            }

            if (config.jarFeatureTo != null) {
                createFeatureJar(config.symbolicName, config.version,
                        config.createFeatureXml != null ? config.createFeatureXml : config.updateFeatureXml,
                        config.jarFeatureTo);
            }

        } catch (final Exception e) {
            log.error("Errors occured", e);
            return 1;
        }
        return 0;
    }

    private boolean createFeatureJar(final String featureId, final String featureVersion, final String featureXml,
            final String jarDir) {

        final File file = new File(jarDir, featureId + "_" + featureVersion + ".jar");
        file.getParentFile().mkdirs();

        try {
            final JarOutputStream jarOutputStream = new JarOutputStream(
                    new BufferedOutputStream(new FileOutputStream(file)));

            final JarEntry entry = new JarEntry("feature.xml");
            jarOutputStream.putNextEntry(entry);

            final BufferedInputStream featureXmlStream = new BufferedInputStream(new FileInputStream(featureXml));
            copy(featureXmlStream, jarOutputStream);

            jarOutputStream.close();

        } catch (final FileNotFoundException e) {
            throw new RuntimeException("Could not create Feature Jar: " + file.getAbsolutePath(), e);
        } catch (final IOException e) {
            throw new RuntimeException("Could not create Feature Jar: " + file.getAbsolutePath(), e);
        }

        return true;
    }

    protected void copy(final InputStream in, final OutputStream out) {
        try {
            final byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        } catch (final Exception e) {
            throw new RuntimeException("Error: " + e, e);
        }
    }

    private void copyJarsAsBundles(final List<Bundle> bundles, final String copyJarsAsBundlesTo) {

        final File dir = new File(copyJarsAsBundlesTo);
        if (dir.exists()) {
            if (!dir.isDirectory()) {
                log.error(dir.getAbsolutePath() + " is not a directory.");
                return;
            }
        } else {
            dir.mkdirs();
        }

        log.info("Copying " + bundles.size() + " bundles into: " + dir.getAbsolutePath());

        for (final Bundle bundle : bundles) {
            final File target = new File(dir, bundle.getSymbolicName() + "_" + bundle.getVersion() + ".jar");

            FileChannel in = null;
            FileChannel out = null;

            try {
                in = new FileInputStream(bundle.getJarLocation()).getChannel();
                out = new FileOutputStream(target).getChannel();

                // According to
                // http://www.rgagnon.com/javadetails/java-0064.html
                // we cannot copy files greater than 64 MB.
                // magic number for Windows, 64Mb - 32Kb)
                final int maxCount = (64 * 1024 * 1024) - (32 * 1024);
                final long size = in.size();
                long position = 0;
                while (position < size) {
                    position += in.transferTo(position, maxCount, out);
                }

            } catch (final IOException e) {
                log.error("Error whily copying '" + bundle.getJarLocation().getAbsolutePath() + "' to '"
                        + target.getAbsolutePath() + "'", e);
                return;
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                    if (out != null) {
                        out.close();
                    }
                } catch (final IOException e) {
                    throw new RuntimeException("Could not recover from error.", e);
                }
            }
        }
    }

    /**
     * @param scanJarsAtDir
     * @return A Map(featureId -> featureVersion)
     */
    public Map<String, String> scanFeatureVersionsAtDir(final String scanJarsAtDir) {
        final Map<String, String> featureVersions = new LinkedHashMap<String, String>();

        final File file = new File(scanJarsAtDir);
        if (!file.exists() || !file.isDirectory()) {
            log.error("Directory '" + file.getAbsolutePath() + "' does not exists.");
            return featureVersions;
        }

        for (final File jar : file.listFiles()) {
            if (jar.isFile() && jar.getName().toLowerCase().endsWith(".jar")) {
                try {
                    final JarInputStream jarStream = new JarInputStream(
                            new BufferedInputStream(new FileInputStream(jar)));
                    final Manifest manifest = jarStream.getManifest();
                    final String featureId = manifest.getMainAttributes().getValue("FeatureBuilder-FeatureId");
                    final String featureVersion = manifest.getMainAttributes()
                            .getValue("FeatureBuilder-FeatureVersion");

                    if (featureId != null && featureVersion != null) {
                        featureVersions.put(featureId, featureVersion);
                    }

                } catch (final FileNotFoundException e) {
                    log.error("Errors while reading the Mainfest of: " + jar, e);
                } catch (final IOException e) {
                    log.error("Errors while reading the Mainfest of: " + jar, e);
                }
            }
        }
        return featureVersions;
    }

    public List<Bundle> scanBundlesAtDir(final String scanJarsAtDir) {
        final File file = new File(scanJarsAtDir);

        final LinkedList<Bundle> bundles = new LinkedList<Bundle>();

        if (!file.exists() || !file.isDirectory()) {
            log.error("Directory '" + file.getAbsolutePath() + "' does not exists.");
            return bundles;
        }

        for (final File jar : file.listFiles()) {
            if (jar.isFile() && jar.getName().toLowerCase().endsWith(".jar")) {
                try {
                    final JarInputStream jarStream = new JarInputStream(
                            new BufferedInputStream(new FileInputStream(jar)));
                    final Manifest manifest = jarStream.getManifest();
                    String symbolicName = manifest.getMainAttributes().getValue("Bundle-SymbolicName");
                    final String version = manifest.getMainAttributes().getValue("Bundle-Version");
                    if (symbolicName != null && version != null) {
                        symbolicName = symbolicName.split(";")[0].trim();
                        final Bundle bundle = new Bundle(symbolicName, version, jar.length(), jar);
                        bundles.add(bundle);
                    } else {
                        log.warn("jar '" + jar.getAbsolutePath() + "' is not an OSGi bundle.");
                    }

                } catch (final FileNotFoundException e) {
                    log.error("Errors while reading the Mainfest of: " + jar, e);
                } catch (final IOException e) {
                    log.error("Errors while reading the Mainfest of: " + jar, e);
                }

            }
        }
        log.info("Found " + bundles.size() + " bundles in scanned directory: " + file.getAbsolutePath());

        Collections.sort(bundles);
        return bundles;
    }

    private void createFeatureXml(final String fileName, final String symbolicName, final String version,
            final List<Bundle> bundles) {

        if (fileName == null || symbolicName == null || version == null || bundles == null) {
            throw new IllegalArgumentException("All parameters must be not null.");
        }

        final XmlObject xml = XmlObject.Factory.newInstance();
        final XmlCursor cursor = xml.newCursor();

        cursor.toFirstContentToken();
        cursor.beginElement("feature");
        cursor.insertAttributeWithValue("id", symbolicName);
        cursor.insertAttributeWithValue("version", version);

        cursor.insertChars("\n\t");
        cursor.beginElement("description");
        cursor.insertAttributeWithValue("url", "http://www.example.com/description");
        cursor.insertChars("[Enter Feature Description here.]");
        cursor.toNextToken();

        cursor.insertChars("\n\t");
        cursor.beginElement("copyright");
        cursor.insertAttributeWithValue("url", "http://www.example.com/copyright");
        cursor.insertChars("[Enter Copyright Description here.]");
        cursor.toNextToken();

        cursor.insertChars("\n\t");
        cursor.beginElement("license");
        cursor.insertAttributeWithValue("url", "http://www.example.com/license");
        cursor.insertChars("[Enter License Description here.]");
        cursor.toNextToken();

        cursor.insertChars("\n\t");
        cursor.beginElement("url");
        cursor.beginElement("update");
        cursor.insertAttributeWithValue("label", "Update-Site name");
        cursor.insertAttributeWithValue("url", "http://www.example.com/update-site");
        cursor.toNextToken();
        cursor.toNextToken();

        for (final Bundle bundle : bundles) {
            insertBundle(cursor, bundle);
        }
        cursor.insertChars("\n");

        cursor.dispose();
        log.debug("Created document: " + xml.xmlText());

        final File file = new File(fileName);
        log.info("Writing document to: " + file.getAbsolutePath());

        try {
            xml.save(file);
        } catch (final IOException e) {
            log.error("Could not save feature file: " + file.getAbsolutePath(), e);
        }

    }

    private void insertBundle(final XmlCursor cursor, final Bundle bundle) {
        cursor.insertChars("\n\t");
        cursor.beginElement("plugin");
        cursor.insertAttributeWithValue("id", bundle.getSymbolicName());
        cursor.insertAttributeWithValue("version", bundle.getVersion());
        cursor.insertAttributeWithValue("unpack", "false");
        cursor.insertAttributeWithValue("download-size", Long.toString(bundle.getSizeInB()));
        cursor.insertAttributeWithValue("install-size", Long.toString(bundle.getSizeInB()));
        cursor.toNextToken();
    }

    private void updateFeatureXml(final String fileName, final String templateFile, final String symbolicName,
            final String version, final Map<String, String> includedFeatureVersions, final List<Bundle> bundles) {

        File tFile = null;
        if (templateFile != null) {
            tFile = new File(templateFile);
            if (!tFile.exists() || !tFile.isFile()) {
                log.error("Cannot read template feature file: " + tFile.getAbsolutePath());
            }
        }

        final File file = new File(fileName);
        if (tFile == null) {
            if (!file.exists() || !file.isFile()) {
                log.error("Cannot update not existing feature file: " + file.getAbsolutePath());
            }
            tFile = file;
        }

        try {
            final XmlObject xml = XmlObject.Factory.parse(tFile);
            XmlCursor pC = xml.newCursor();

            // remove existing plugin entries
            pC.selectPath("$this/feature/plugin");
            for (int i = 0; i < pC.getSelectionCount(); ++i) {
                pC.toSelection(i);
                pC.removeXml();
            }
            pC.dispose();

            // add new plugin entries
            pC = xml.newCursor();
            pC.selectPath("$this/feature");
            pC.toSelection(0);
            pC.toFirstContentToken();

            // new entries
            for (final Bundle bundle : bundles) {
                insertBundle(pC, bundle);
            }

            pC.dispose();

            final XmlCursor tester = xml.newCursor();
            tester.selectPath("$this/feature/plugin");
            if (tester.getSelectionCount() > bundles.size()) {
                log.warn("At least " + (tester.getSelectionCount() - bundles.size())
                        + " plugins of the given feature were not updated.");
            }
            tester.dispose();

            // Update feature id if necessary
            if (symbolicName != null) {
                final XmlCursor idCursor = xml.newCursor();
                idCursor.selectPath("$this/feature");
                if (idCursor.getSelectionCount() == 0) {
                    log.warn("Could not found feature entry.");
                } else {
                    idCursor.toSelection(0);
                    final String curId = idCursor.getAttributeText(new QName("id"));
                    if (!symbolicName.equals(curId)) {
                        idCursor.setAttributeText(new QName("id"), symbolicName);
                    }
                }
                idCursor.dispose();
            }

            // Update feature version if necessary
            if (version != null) {
                final XmlCursor versionCursor = xml.newCursor();
                versionCursor.selectPath("$this/feature");
                if (versionCursor.getSelectionCount() == 0) {
                    log.warn("Could not found feature entry.");
                } else {
                    versionCursor.toSelection(0);
                    final String curVersion = versionCursor.getAttributeText(new QName("version"));
                    if (!version.equals(curVersion)) {
                        versionCursor.setAttributeText(new QName("version"), version);
                    }
                }
                versionCursor.dispose();
            }

            // Update versions of included features
            if (includedFeatureVersions.size() > 0) {
                final XmlCursor includesCursor = xml.newCursor();
                includesCursor.selectPath("$this/feature/includes");
                if (includesCursor.getSelectionCount() < includedFeatureVersions.size()) {
                    log.warn("Could not found some of the requested included features.");
                } else {
                    for (int i = 0; i < includedFeatureVersions.size(); ++i) {
                        includesCursor.toSelection(i);
                        final String id = includesCursor.getAttributeText(new QName("id"));
                        if (includedFeatureVersions.containsKey(id)) {
                            includesCursor.setAttributeText(new QName("version"), includedFeatureVersions.get(id));
                        }
                    }
                }
            }

            log.info("Modifying feature file: " + file.getAbsolutePath());
            xml.save(file, new XmlOptions().setSavePrettyPrint().setSavePrettyPrintIndent(2));

        } catch (final XmlException e) {
            log.error("Could not modify feature description: " + file.getAbsolutePath(), e);
        } catch (final IOException e) {
            log.error("Could not modify feature description: " + file.getAbsolutePath(), e);
        }
    }

    private List<Bundle> readPom(final String pomFile) {

        final LinkedList<Bundle> bundles = new LinkedList<Bundle>();

        final File file = new File(pomFile);
        if (!file.exists() || !file.isFile()) {
            log.error("Maven project file does not exists: " + file.getAbsolutePath());
            return bundles;
        }

        try {
            final ProjectDocument o = ProjectDocument.Factory.parse(file,
                    MavenXmlSupport.instance.createXmlOptions());

            final Model project = o.getProject();
            final LocalArtifact localArtifact = MavenXmlSupport.instance.readLocalArtifactFromProject(project,
                    file);

            log.debug("Analyzing Maven project: " + localArtifact);

            // Map<String, List<Dependency>> depsAndDependants =
            // MavenXmlSupport.instance
            // .readDirectDependencyFromProject(project, file);

            // FIXME: populate bundles from dependencies

            return bundles;

        } catch (final XmlException e) {
            log.error("Could not read pom file: " + file.getAbsolutePath(), e);
            return bundles;
        } catch (final IOException e) {
            log.error("Could not read pom file: " + file.getAbsolutePath(), e);
            return bundles;
        }

    }

}