/*
* 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.fileLevel;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import milk.jpatch.Util;
/**
* Patches all a directory structure.
* @author Thomas Kerber
* @version 1.1.2
*/
public class FilesPatch{
/**
* The component file patches.
*/
private Map<String, FilePatch> filePatches;
/**
* Creates.
* @param filePatches The component file patches.
*/
public FilesPatch(Map<String, FilePatch> filePatches){
this.filePatches = filePatches;
}
/**
* "Deserializes"
* @param root The root directory to load patches from.
* @throws IOException If a read error occurs.
*/
public FilesPatch(File root) throws IOException{
filePatches = new HashMap<String, FilePatch>();
loadFromDir(root, root);
}
/**
* Generates.
* @param rootOrig The root of the original.
* @param rootMod The root of the modification.
* @return The files patch.
* @throws IOException If a read error occurrs.
*/
public static FilesPatch generate(File rootOrig, File rootMod)
throws IOException{
Set<String> names = new HashSet<String>();
names.addAll(Util.listNamesIn(rootOrig));
names.addAll(Util.listNamesIn(rootMod));
Map<String, FilePatch> patches = new HashMap<String, FilePatch>();
for(String name : names){
patches.put(name, FilePatch.generate(rootOrig, rootMod, name));
}
return new FilesPatch(patches);
}
/**
* Applies a series of FilesPatches.
* @param rootIn The input root.
* @param rootOut The output root.
* @param patches The patches.
* @throws IOException
*/
public static void patchAll(File rootIn, File rootOut,
FilesPatch[] patches) throws IOException{
Set<String> names = new HashSet<String>(Util.listNamesIn(rootIn));
for(FilesPatch fp : patches)
names.addAll(fp.filePatches.keySet());
for(String name : names){
File inFile = new File(rootIn, name);
File outFile = new File(rootOut, name);
outFile.getParentFile().mkdirs();
try{
patchName(name, inFile, outFile, patches, patches.length - 1);
}
catch(Exception e){
Util.logger.log(Level.SEVERE,
"Patch of " + name + " failed. Falling back to ID.", e);
new FileID(name).patch(inFile, outFile);
}
}
}
/**
* Applies a series of patches to a certain name.
* @param name The name to apply to.
* @param inFile The input file.
* @param outFile The output file.
* @param patches The patches to apply.
* @param lastIndex The index of the last patch to apply.
* @throws IOException
*/
private static void patchName(String name, File inFile, File outFile,
FilesPatch[] patches, int lastIndex) throws IOException{
// So that sanity may be kept.
if(lastIndex == -1){
new FileID(name).patch(inFile, outFile);
return;
}
FilePatch fp = patches[lastIndex].getPatchForName(name);
if(fp instanceof FileID){
// Do nothing and continue to the previous patch.
patchName(name, inFile, outFile, patches, lastIndex - 1);
}
else if(fp instanceof FileRemove){
// Just do nothing.
}
else if(fp instanceof FileReplace){
// Apply the patch. And then do nothing.
fp.patch(inFile, outFile);
}
else if(fp instanceof ClassPatch){
// YAY! This is special.
JavaClass c = patchClassName(name, inFile, patches, lastIndex);
c.dump(outFile);
}
}
/**
* Applies a series of patches to a class.
* @param name The name to patch.
* @param inFile The input file.
* @param patches The patches to apply.
* @param lastIndex The index of the last patch to apply.
* @return The patched class.
* @throws IOException
*/
private static JavaClass patchClassName(String name, File inFile,
FilesPatch[] patches, int lastIndex) throws IOException{
if(lastIndex == -1){
return new ClassParser(inFile.getAbsolutePath()).parse();
}
FilePatch fp = patches[lastIndex].getPatchForName(name);
if(fp instanceof ClassPatch){
JavaClass prev = patchClassName(name, inFile, patches,
lastIndex - 1);
return ((ClassPatch)fp).patch(prev);
}
else if(fp instanceof FileID)
return patchClassName(name, inFile, patches, lastIndex - 1);
else{
File prev = File.createTempFile("milk", ".class");
patchName(name, inFile, prev, patches, lastIndex);
return new ClassParser(prev.getAbsolutePath()).parse();
}
}
/**
* Patches all files in a directory.
* @param rootIn The input dir.
* @param rootOut The output dir.
* @throws IOException
*/
public void patchAll(File rootIn, File rootOut) throws IOException{
FilesPatch.patchAll(rootIn, rootOut, new FilesPatch[]{this});
}
/**
* Gets patch for a certain file.
*
* If none is found, an ID patch is generated.
* @param name The filename.
* @return The file patch.
*/
public FilePatch getPatchForName(String name){
if(!filePatches.containsKey(name)){
// Yes, we hate manifest files.
// TODO: option to turn this off.
if(name.endsWith("MANIFEST.MF"))
filePatches.put(name, new FileRemove(name));
else
filePatches.put(name, new FileID(name));
}
return filePatches.get(name);
}
/**
* Deserializes recursively in a directory.
* @param root The root directory.
* @param dirAt The currently deserializing directory.
* @throws IOException
*/
private void loadFromDir(File root, File dirAt) throws IOException{
for(File f : dirAt.listFiles()){
if(f.isDirectory())
loadFromDir(root, f);
else if(f.isFile()){
FilePatch fp = null;
if(ClassPatch.canDeserializeAt(f)){
fp = ClassPatch.deserializeAt(root, f);
}
else if(FileRemove.canDeserializeAt(f)){
fp = FileRemove.deserializeAt(root, f);
}
else if(FileReplace.canDeserializeAt(f)){
fp = FileReplace.deserializeAt(root, f);
}
// Note: FileID is redundant, as it doesn't do anything.
filePatches.put(fp.name, fp);
}
}
}
/**
* Dumps to directory.
* @param dir The directory to dump to.
* @throws IOException If a write error occurs.
*/
public void serializeToDir(File dir) throws IOException{
for(FilePatch fp : filePatches.values()){
fp.serialize(dir);
}
}
/**
* Dumps to a zip file.
* @param zip The zip file.
* @throws IOException If a read/write error occurs.
*/
public void serializeToZip(File zip) throws IOException{
// TODO: possibly stream directly to zip?
File dir = Util.getTempDir();
serializeToDir(dir);
Util.packZip(zip, dir);
Util.remDir(dir);
}
}