package nl.helixsoft.zipper; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.io.IOUtils; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; import nl.helixsoft.util.HFileUtils; import nl.helixsoft.util.HStringUtils; class Main { static final String HELP = "Zipper is a tool that creates zip / tar.gz bundles " + "according to instructions\nfrom an ini-style configuration file.\n" + "\n" + "Zipper lets you:\n" + "* define the name of the top-level zip entry\n" + "* select files to include using glob patterns\n" + "* create multiple bundles from a single config file\n" + "* put zip entries in a different relative location\n" + "* generate precisely the same tar.gz and zip bundles\n" + "* use macro variables using C-style #include and #define\n" + "\n"; public static class Mapping { public String srcGlob; public String dest; // not parsed but managed during processing public List<File> _files = new ArrayList<File>(); } public static class Config { public String suffix; public String section; public List<Mapping> mappings = new ArrayList<Mapping>(); // not parsed but managed during processing public File _destZip; public File _destTgz; } public static class Project { public String baseName; public String version; public String baseDir; public List<Config> configurations = new ArrayList<Config>(); } private void readInclude(File inFile, Map<String, String> props) throws IOException { BufferedReader reader = new BufferedReader (new FileReader(inFile)); String raw; while ((raw = reader.readLine()) != null) { Pattern patDefine = Pattern.compile("^#define\\s+(\\w+)\\s+(.*)"); String line = raw.trim(); if ("".equals(line)) continue; Matcher m0 = patDefine.matcher(line); if (m0.matches()) { String key = m0.group(1); String value = HStringUtils.removeOptionalQuotes(m0.group(2)); props.put(key, value); } } reader.close(); } private String substProps(String in, Map<String, String> props) { String result = in; for (Map.Entry<String, String> entry : props.entrySet()) { String var = "${" + entry.getKey() + "}"; result = Pattern.compile(var, Pattern.LITERAL).matcher(result).replaceAll(entry.getValue()); } return result; } private Project parseConfig(File inFile) throws IOException { BufferedReader reader = new BufferedReader (new FileReader(inFile)); try { Config currentConfig = null; Map<String, String> props = new HashMap<String, String>(); Project result = new Project(); Pattern patSection = Pattern.compile("\\[(.*)\\]"); Pattern patProperty = Pattern.compile("(.*\\S)\\s*=\\s*(.*)"); Pattern patInclude = Pattern.compile("^#include\\s+'(.*)'"); Pattern patComment = Pattern.compile("^#.*"); Pattern patMapping = Pattern.compile("(.*\\S)\\s*->\\s*(.*)"); String raw; int lineno = 0; while ((raw = reader.readLine()) != null) { lineno++; String line = raw.trim(); if ("".equals(line)) continue; Matcher m0 = patInclude.matcher(line); if (m0.matches()) { readInclude(new File(inFile.getParentFile(), m0.group(1)), props); continue; } if (patComment.matcher(line).matches()) continue; Matcher m1 = patSection.matcher(line); if (m1.matches()) { currentConfig = new Config(); currentConfig.section = m1.group(1); result.configurations.add(currentConfig); continue; } Matcher m2 = patProperty.matcher(line); if (m2.matches()) { String key = m2.group(1); String value = substProps(m2.group(2), props); if ("version".equals(key)) { result.version = value; } else if ("basename".equals(key)) { result.baseName = value; } else if ("basedir".equals(key)) { result.baseDir = value; } else if ("suffix".equals(key)) { if (currentConfig == null) { throw new IllegalStateException("suffix must not be in main section: " + lineno); } currentConfig.suffix = value; } else { throw new IllegalStateException("Invalid key found in config file: " + key + ", in line " + lineno); } continue; } if (currentConfig == null) { throw new IllegalStateException("Found pattern in main section in line " + lineno); } Mapping mapping = new Mapping(); Matcher m3 = patMapping.matcher(line); if (m3.matches()) { mapping.dest = m3.group(2); mapping.srcGlob = m3.group(1); } else { mapping.dest = null; // use default. mapping.srcGlob = line; } currentConfig.mappings.add(mapping); } return result; } finally { reader.close(); } } /** Command-line options */ public static class Options { @Option(name="-c", usage="Configuration file") File configFile = new File ("zipper.conf"); @Option(name="--help", aliases="-h", usage="Show usage") boolean help = false; @Option(name="--version", aliases="-v", usage="Print version and quit") boolean version = false; @Option(name="--format", aliases="-f", usage="which formats to produce. Valid values: tgz, zip or both. Default: both") String format = "both"; @Argument(usage="run selected sections only") List<String> sections; } Options opts = new Options(); boolean formatTgz = true; boolean formatZip = true; private void checkFormat(String fmt) throws CmdLineException { if ("both".equals(fmt)) { formatTgz = true; formatZip = true; } else if ("tgz".equals(fmt)) { formatTgz = true; formatZip = false; } else if ("zip".equals(fmt)) { formatTgz = false; formatZip = true; } else { throw new CmdLineException("Unknown format selected: " + fmt + " must be one of 'tgz', 'zip' or 'both'"); } } public void run(String[] args) throws IOException { CmdLineParser parser = new CmdLineParser(opts); try { parser.parseArgument(args); if (opts.help) throw new CmdLineException (parser, "Help requested"); if (opts.version) { InputStream is = this.getClass().getResourceAsStream("/META-INF/MANIFEST.MF"); if (is != null) { Manifest manifest = new Manifest (is); System.out.println("buildDate: " + manifest.getMainAttributes().getValue("Build-Date")); System.out.println("gitHash: " + manifest.getMainAttributes().getValue("Git-Hash")); } return; } checkFormat(opts.format); if (!opts.configFile.exists()) throw new CmdLineException("Could not find configuration file " + opts.configFile); } catch (CmdLineException ex) { System.err.println(ex.getMessage()); System.out.println(HELP); parser.printUsage(System.out); return; } File inFile = new File ("zipper.conf"); // step 1: read configuration file. Project project = parseConfig(inFile); doProject(project); } private boolean isValidSection(Config config) { return (opts.sections == null || opts.sections.contains(config.section)); } private void doProject(Project project) throws IOException { for (Config config : project.configurations) { if (isValidSection(config)) processConfig(project, config); } for (Config config : project.configurations) { if (isValidSection(config)) { if (formatZip) doConfigZip(project, config); if (formatTgz) doConfigTgz(project, config); } } } private void processConfig(Project project, Config config) { // create Zip archive... config._destZip = new File (project.baseName + "-" + config.suffix + "-" + project.version + ".zip"); config._destTgz = new File (project.baseName + "-" + config.suffix + "-" + project.version + ".tar.gz"); for (Mapping pat : config.mappings) { pat._files = HFileUtils.expandGlob(pat.srcGlob); if (pat._files.size() == 0) throw new IllegalStateException ("Glob " + pat.srcGlob + " matches 0 files"); if (pat.dest == null) { File globAsFile = new File(pat.srcGlob).getParentFile(); if (globAsFile != null) { pat.dest = project.baseDir + File.separator + globAsFile; } else { pat.dest = project.baseDir; } } } } private void doConfigZip(Project project, Config config) throws IOException { ensureParentDirExists(config._destZip); System.out.println("Writing " + config._destZip); ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(config._destZip)); for (Mapping pat : config.mappings) { for (File f : pat._files) { // System.out.println ("Adding " + f.getName() + " as " + pat.dest + "/" + f.getName()); ZipEntry ze = new ZipEntry(pat.dest + "/" + f.getName()); zos.putNextEntry(ze); FileInputStream fis = new FileInputStream(f); IOUtils.copy(fis, zos); zos.closeEntry();; } } zos.close(); } private void ensureParentDirExists(File destFile) { File parentFile = destFile.getParentFile(); if (parentFile != null) { parentFile.mkdirs(); } } private void doConfigTgz(Project project, Config config) throws IOException { ensureParentDirExists(config._destTgz); FileOutputStream fos = new FileOutputStream(config._destTgz); TarArchiveOutputStream zos = new TarArchiveOutputStream(new GZIPOutputStream (fos)); System.out.println("Writing " + config._destTgz); for (Mapping pat : config.mappings) { for (File f : pat._files) { // System.out.println ("Adding " + f.getName() + " as " + pat.dest + "/" + f.getName()); TarArchiveEntry ze = new TarArchiveEntry(f, pat.dest + "/" + f.getName()); if (f.canExecute()) { ze.setMode(0755); } zos.putArchiveEntry(ze); FileInputStream fis = new FileInputStream(f); try { IOUtils.copy(fis, zos); } finally { fis.close(); } zos.closeArchiveEntry(); } } zos.close(); fos.close(); } public static void main(String[] args) throws IOException { new Main().run(args); } }