/*
* Copyright 2012, Thomas Kerber
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package milk.jpatch;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import milk.jpatch.fileLevel.ClassPatch;
import milk.jpatch.fileLevel.FilesPatch;
/**
* Manages command-line and API calls.
*
* @author Thomas Kerber
* @version 1.1.1
*/
public class JPatch{
/**
* The version info as printed by the --version flag.
*/
public static final String VERSION_INFO =
"JPatch 1.0.0-a1\n\n" +
"Licenced under the Apache Licence Version 2.0.\n" +
"Copyright 2012 Thomas Kerber";
/**
* The usage syntax as printed by the --help flag.
*/
public static final String SYNTAX =
"java -jar jpatch.jar [options] -{cap} [files...]\n";
/**
* The JPatch logger. Reference to the Util logger.
*/
public static final Logger logger = Util.logger;
/**
* Creates and immediately applies a patch.
* @param orig The original file.
* @param mod The modified file.
* @param in The file to patch.
* @param out Where to output the patched file.
* @throws IOException
*/
public static void generateAndApply(String orig, String mod, String in,
String out) throws IOException{
// The original file is considered determining whether zip/jar patch
// is used or class patch is used.
String lowerOrig = orig.toLowerCase();
if(lowerOrig.endsWith(".jar") || lowerOrig.endsWith(".zip"))
generateAndApplyJarp(orig, mod, in, out);
else if(lowerOrig.endsWith(".class"))
generateAndApplyClass(orig, mod, in, out);
else
throw new IOException(
"Could not determine patch type to use (class or jar).");
}
/**
* Creates and immediately applies a JCP.
* @param origClass The original class.
* @param modClass The modified class.
* @param inClass The class to patch.
* @param outClass Where to output the patched class.
* @throws IOException
*/
public static void generateAndApplyClass(String origClass, String modClass,
String inClass, String outClass) throws IOException{
ClassPatch.generate(new File(origClass), new File(modClass), "-").
patch(new File(inClass), new File(outClass));
}
/**
* Creates and immediately applies a jarp.
* @param origZip The original jar.
* @param modZip The modified jar to diff.
* @param inZip The jar to patch.
* @param outZip Where to output the patched jar.
* @throws IOException
*/
public static void generateAndApplyJarp(String origZip, String modZip,
String inZip, String outZip) throws IOException{
File rootOrig = Util.extractZip(new File(origZip));
File rootMod = Util.extractZip(new File(modZip));
FilesPatch fp = FilesPatch.generate(rootOrig, rootMod);
File rootIn = Util.extractZip(new File(inZip));
File rootOut = Util.getTempDir();
fp.patchAll(rootIn, rootOut);
Util.packZip(new File(outZip), rootOut);
Util.remDir(rootOrig);
Util.remDir(rootMod);
Util.remDir(rootIn);
Util.remDir(rootOut);
}
/**
* Applies a series of patches to a file.
* @param in The input file.
* @param out Where to output the finished file.
* @param mods The mods to apply in order.
* @throws IOException
*/
public static void apply(String in, String out, String[] mods)
throws IOException{
// The input file is considered determining whether zip/jar or class
// patch is used.
String lowerIn = in.toLowerCase();
if(lowerIn.endsWith(".jar") || lowerIn.endsWith(".zip"))
applyJarps(in, out, mods);
else if(lowerIn.endsWith(".class"))
applyClasses(in, out, mods);
else
throw new IOException(
"Could not determine patch type (jarp or jcp)");
}
/**
* Applies JCPs to an input classfile.
* @param inClass The input classfile.
* @param outClass Where to output the patched classfile.
* @param modJCPs The JCPs to apply in order.
* @throws IOException
*/
public static void applyClasses(String inClass, String outClass,
String[] modJCPs) throws IOException{
File tmpOld = new File(inClass);
for(int i = 0; i < modJCPs.length; i++){
File tmpNew = i == modJCPs.length - 1 ?
new File(outClass) :
File.createTempFile("milk", ".class");
ClassPatch.deserializeAt(null, new File(modJCPs[i])).
patch(tmpOld, tmpNew);
if(i != 0)
tmpOld.delete();
tmpOld = tmpNew;
}
}
/**
* Applies jarps to an input jar.
* @param inZip The input jar.
* @param outZip Where to output the patches jar.
* @param modJarps The jarps to apply in order.
* @throws IOException
*/
public static void applyJarps(String inZip, String outZip,
String[] modJarps) throws IOException{
File[] modRoots = new File[modJarps.length];
for(int i = 0; i < modJarps.length; i++)
modRoots[i] = Util.extractZip(new File(modJarps[i]));
FilesPatch[] fps = new FilesPatch[modRoots.length];
for(int i = 0; i < modRoots.length; i++)
fps[i] = new FilesPatch(modRoots[i]);
File rootIn = Util.extractZip(new File(inZip));
File rootOut = Util.getTempDir();
FilesPatch.patchAll(rootIn, rootOut, fps);
Util.packZip(new File(outZip), rootOut);
for(File f : modRoots)
Util.remDir(f);
Util.remDir(rootIn);
Util.remDir(rootOut);
}
/**
* Creates a patch.
* @param orig The original file.
* @param mod The modified file.
* @param out Where to output the patch.
* @throws IOException
*/
public static void create(String orig, String mod, String out)
throws IOException{
String lowerOrig = orig.toLowerCase();
if(lowerOrig.endsWith(".jar") || lowerOrig.endsWith(".zip"))
createJarp(orig, mod, out);
else if(lowerOrig.endsWith(".class"))
createJCP(orig, mod, out);
else
throw new IOException(
"Could not determine patch type to use (class or jar).");
}
/**
* Creates a JCP.
* @param origClass The original class.
* @param modClass The modified class.
* @param outClass Where to output the JCP.
* @throws IOException
*/
public static void createJCP(String origClass, String modClass,
String outClass) throws IOException{
ClassPatch.generate(new File(origClass), new File(modClass), "-").
dump(outClass);
}
/**
* Creates a JARP
* @param origJar The original jar.
* @param modJar The modified jar.
* @param outJar Where to output the JARP.
* @throws IOException
*/
public static void createJarp(String origJar, String modJar,
String outJar) throws IOException{
File rootOrig = Util.extractZip(new File(origJar));
File rootMod = Util.extractZip(new File(modJar));
FilesPatch fp = FilesPatch.generate(rootOrig, rootMod);
fp.serializeToZip(new File(outJar));
Util.remDir(rootOrig);
Util.remDir(rootMod);
}
/**
* Creates a JARP, and optionally leaves it as a directory.
* @param origJar The original jar.
* @param modJar The modified jar.
* @param out Where to output the JARP.
* @param outputAsDir Whether or not the output should be a directory.
* @throws IOException
*/
public static void createJarp(String origJar, String modJar, String out,
boolean outputAsDir) throws IOException{
File rootOrig = Util.extractZip(new File(origJar));
File rootMod = Util.extractZip(new File(modJar));
FilesPatch fp = FilesPatch.generate(rootOrig, rootMod);
if(outputAsDir)
fp.serializeToDir(new File(out));
else
fp.serializeToZip(new File(out));
Util.remDir(rootOrig);
Util.remDir(rootMod);
}
/**
* Runs. 'Nuff said.
*
* @param args Command line args.
*/
public static void main(String[] args){
CommandLineParser clp = new PosixParser();
Options options = new Options();
OptionGroup force = new OptionGroup();
force.setRequired(false);
Option forceClass = new Option(null, "force-class", false,
"Forces interpretation as a class patch.");
Option forceJar = new Option(null, "force-jar", false,
"Forces interpretation as a jar patch");
force.addOption(forceClass);
force.addOption(forceJar);
options.addOptionGroup(force);
OptionGroup actions = new OptionGroup();
actions.setRequired(true);
Option create = new Option("c", "create", false,
"Creates a new patch file. The filenames for the original, " +
"modified and output files must be passed in that order.");
Option apply = new Option("a", "apply", false,
"Applys a patch. The filenames the orginial, output and all " +
"patch files must be passed in that order. The patch files " +
"must be passed in the order they are to be applied.");
Option patch = new Option("p", "patch", false,
"Creates and immediately applies a patch. The filenames for " +
"the original, modified, input and output files must be " +
"passed in that order.");
actions.addOption(create);
actions.addOption(apply);
actions.addOption(patch);
options.addOptionGroup(actions);
Option help = new Option("h", "help", false,
"Prints usage information.");
options.addOption(help);
Option version = new Option(null, "version", false,
"Print version information.");
options.addOption(version);
try{
CommandLine cl = clp.parse(options, args);
if(cl.hasOption("h")){
throw new ParseException("Dummy to generate help message.");
}
if(cl.hasOption("version")){
System.out.println(VERSION_INFO);
System.exit(0);
}
String[] left = cl.getArgs();
if(cl.hasOption("c")){
if(left.length < 3)
throw new ParseException("Not enough arguments.");
if(cl.hasOption("force-class"))
createJCP(left[0], left[1], left[2]);
else if(cl.hasOption("froce-jar"))
createJarp(left[0], left[1], left[2]);
else
create(left[0], left[1], left[2]);
}
else if(cl.hasOption("a")){
if(left.length < 3)
throw new ParseException("Not enough arguments.");
String[] mods = Arrays.copyOfRange(left, 2, left.length);
if(cl.hasOption("force-class"))
applyClasses(left[0], left[1], mods);
else if(cl.hasOption("force-jar"))
applyJarps(left[0], left[1], mods);
else
apply(left[0], left[1], mods);
}
else{ // if(cl.hasOption("p"))
if(left.length < 4)
throw new ParseException("Not enough arguments.");
if(cl.hasOption("force-class"))
generateAndApplyClass(left[0], left[1], left[2], left[3]);
else if(cl.hasOption("force-jar"))
generateAndApplyJarp(left[0], left[1], left[2], left[3]);
else
generateAndApply(left[0], left[1], left[2], left[3]);
}
}
catch(ParseException e){
new HelpFormatter().printHelp(SYNTAX, options);
}
catch(IOException e){
logger.log(Level.SEVERE, "Top-level IOException:", e);
System.out.println("Uh-oh... Something went horribly wrong " +
"(or you just made a silly mistake). Check your log for " +
"details.");
System.exit(1);
}
}
}