/*
* 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 static milk.jpatch.Util.logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import milk.jpatch.CPoolMap;
import milk.jpatch.Patch;
import milk.jpatch.Util;
import milk.jpatch.access.AccessFlagsPatch;
import milk.jpatch.attribs.AttributesPatch;
import milk.jpatch.classLevel.FieldsPatch;
import milk.jpatch.classLevel.MethodsPatch;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Attribute;
/**
* Patch for a java .class file.
* @author Thomas Kerber
* @version 1.0.3
*/
// TODO: Check if this deserialized properly now.
public class ClassPatch extends FilePatch implements Patch<JavaClass>{
private static final long serialVersionUID = -8012147988738144393L;
/**
* The postfix of the classpatch file (i.e. file extension)
*/
public static final String POSTFIX = ".milk.jpatch.jcp";
// TODO: maybe issue warnings when these don't match or summit *shrug*
/**
* The new files major version.
*/
public final int majorVersion;
/**
* The new files minor version.
*/
public final int minorVersion;
/**
* The new cpool.
*/
private final ConstantPool cpool;
/**
* The component flags patch.
*/
private final AccessFlagsPatch accessPatch;
/**
* The superclass patch. Null means ID patch, anything else a replacement
* patch.
*/
public final String superClass;
/**
* Interfaces which were added.
*/
private final String[] interfacesAdded;
/**
* Interfaces which were removed.
*/
private final String[] interfacesRemoved;
/**
* The component fields patch.
*/
private final FieldsPatch fieldsPatch;
/**
* The component methods patch.
*/
private final MethodsPatch methodsPatch;
/**
* The component attributes patch.
*/
private final AttributesPatch attributesPatch;
/**
* Creates.
* @param name The file name.
* @param majorVersion The classes major version.
* @param minorVersion The classes minor version.
* @param cpool The constant pool to add.
* @param accessPatch The component access patch.
* @param superClass The superclass patch. Null means ID patch, anything
* a replacement patch.
* @param interfacesAdded The interface names added.
* @param interfacesRemoved The interface names removed.
* @param fieldsPatch The component fields patch.
* @param methodsPatch The component methods patch.
* @param attributesPatch The component attributes patch.
*/
public ClassPatch(
String name,
int majorVersion,
int minorVersion,
ConstantPool cpool,
AccessFlagsPatch accessPatch,
String superClass,
String[] interfacesAdded,
String[] interfacesRemoved,
FieldsPatch fieldsPatch,
MethodsPatch methodsPatch,
AttributesPatch attributesPatch){
super(name);
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
this.cpool = cpool;
this.accessPatch = accessPatch;
this.superClass = superClass;
this.interfacesAdded = interfacesAdded;
this.interfacesRemoved = interfacesRemoved;
this.fieldsPatch = fieldsPatch;
this.methodsPatch = methodsPatch;
this.attributesPatch = attributesPatch;
}
/**
* Creates.
* @param name The file name.
* @param old The old class.
* @param modif The modified class.
*/
public ClassPatch(String name, JavaClass old, JavaClass modif){
super(name);
this.majorVersion = modif.getMajor();
this.minorVersion = modif.getMinor();
this.cpool = modif.getConstantPool();
this.accessPatch = AccessFlagsPatch.generate(old.getAccessFlags(),
modif.getAccessFlags(), AccessFlagsPatch.AccessLevel.CLASS);
if(old.getSuperclassName().equals(modif.getSuperclassName()))
this.superClass = null;
else
this.superClass = modif.getSuperclassName();
String[] oldInterfaces = Util.getInterfaceNamesProper(old);
String[] modInterfaces = Util.getInterfaceNamesProper(modif);
List<String> addedInterfacesL = new ArrayList<String>();
List<String> removedInterfacesL = new ArrayList<String>();
outer: for(String oInt : oldInterfaces){
for(String mInt : modInterfaces){
if(oInt.equals(mInt))
continue outer;
}
removedInterfacesL.add(oInt);
}
outer: for(String mInt : modInterfaces){
for(String oInt : oldInterfaces){
if(oInt.equals(mInt))
continue outer;
}
addedInterfacesL.add(mInt);
}
this.interfacesAdded = addedInterfacesL.toArray(
new String[addedInterfacesL.size()]);
this.interfacesRemoved = removedInterfacesL.toArray(
new String[removedInterfacesL.size()]);
// TODO: Switch these around, and use the new cpool instead.
CPoolMap map = CPoolMap.generate(old.getConstantPool(),
modif.getConstantPool());
this.fieldsPatch =
FieldsPatch.generate(old.getFields(), modif.getFields(), map);
this.methodsPatch =
MethodsPatch.generate(old.getMethods(), modif.getMethods(),
map);
this.attributesPatch = AttributesPatch.generate(old.getAttributes(),
modif.getAttributes(), map);
}
/**
* Generates.
* @param orig The original file.
* @param mod The modded file.
* @param name The file name.
* @return The patch.
* @throws IOException If files cannot be read or parsed.
*/
public static ClassPatch generate(File orig, File mod, String name)
throws IOException{
return new ClassPatch(
name,
new ClassParser(orig.getAbsolutePath()).parse(),
new ClassParser(orig.getAbsolutePath()).parse());
}
/**
*
* @return The patches constant pool.
*/
public ConstantPool getCPool(){
return cpool;
}
/**
*
* @return The component flags patch.
*/
public AccessFlagsPatch getFlagsPatch(){
return accessPatch;
}
/**
*
* @return The interfaces added by the patch.
*/
public String[] getAddedInterfaces(){
return interfacesAdded;
}
/**
*
* @return The interfaces removed by the patch.
*/
public String[] getRemovedInterfaces(){
return interfacesRemoved;
}
/**
*
* @return The component fields patch.
*/
public FieldsPatch getFieldsPatch(){
return fieldsPatch;
}
/**
*
* @return The component methods patch.
*/
public MethodsPatch getMethodsPatch(){
return methodsPatch;
}
/**
*
* @return The component attributes patch.
*/
public AttributesPatch getAttributesPatch(){
return attributesPatch;
}
/**
* Dumps as .jcp
* @param file The file to dump to.
* @throws IOException If an output error occured.
*/
public void dump(String file) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(file));
oos.writeObject(this);
oos.close();
}
@Override
public JavaClass patch(JavaClass j, CPoolMap map){
int flags = accessPatch.patch(j.getAccessFlags());
ConstantPool cpool = map.to;
int superclassIndex;
if(superClass == null)
superclassIndex = j.getSuperclassNameIndex();
else
superclassIndex = Util.findConstantClassIn(map, superClass);
List<String> interfaceNames = new ArrayList<String>(
new ArrayList<String>(Arrays.asList(
Util.getInterfaceNamesProper(j))));
for(String intRem : interfacesRemoved)
interfaceNames.remove(intRem);
for(String intAdd : interfacesAdded)
if(!interfaceNames.contains(intAdd))
interfaceNames.add(intAdd);
int[] interfaces = new int[interfaceNames.size()];
for(int i = 0; i < interfaces.length; i++)
interfaces[i] = Util.findConstantClassIn(map,
interfaceNames.get(i));
List<Field> fieldList = new ArrayList<Field>(
Arrays.asList(j.getFields()));
fieldList = fieldsPatch.patch(fieldList, map);
Field[] fields = fieldList.toArray(new Field[fieldList.size()]);
List<Method> methodList = new ArrayList<Method>(
Arrays.asList(j.getMethods()));
methodList = methodsPatch.patch(methodList, map);
Method[] methods = methodList.toArray(new Method[methodList.size()]);
List<Attribute> attributeList = new ArrayList<Attribute>(
Arrays.asList(j.getAttributes()));
attributeList = attributesPatch.patch(attributeList, map);
Attribute[] attributes = attributeList.toArray(
new Attribute[attributeList.size()]);
int classIndex = j.getClassNameIndex();
return new JavaClass(
classIndex,
superclassIndex,
"[-]",
majorVersion,
minorVersion,
flags,
cpool,
interfaces,
fields,
methods,
attributes);
}
/**
* Patches a class.
* @param j The class to patch.
* @return The patched class.
*/
public JavaClass patch(JavaClass j){
return patch(j, CPoolMap.generate(j.getConstantPool(), cpool));
}
@Override
public void patch(InputStream in, OutputStream out) throws IOException{
/*
* If a class format exception, or *ANY* other error during the patch
* (save IOException) occurs, the old file is copied over instead, and
* the program CONTINUES EXECUTING! A severe log message is entered.
*/
try{
patch(new ClassParser(in, this.name).parse()).dump(out);
in.close();
out.close();
}
catch(IOException e){
// IOExceptions simply get re-risen
throw e;
}
catch(ClassFormatException e){
logger.log(Level.SEVERE,
"Patch of '" + name + "' FAILED. The file " +
"will NOT BE PATCHED. The file could not be parsed.",
e);
Util.loop(in, out);
}
catch(Exception e){
logger.log(Level.SEVERE,
"Patch of '" + name + "' FAILED. The file will " +
"NOT BE PATCHED. A patch could not be created, please " +
"file a bug report with this log info.",
e);
Util.loop(in, out);
}
}
@Override
public void serialize(File root) throws IOException{
File outF = new File(root, name + POSTFIX);
outF.getParentFile().mkdirs();
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(outF));
out.writeObject(this);
out.close();
}
/**
* Checks if deserialization is possible from a file.
* @param f The file to check.
* @return Whether it can be deserialized.
*/
public static boolean canDeserializeAt(File f){
return f.getName().endsWith(POSTFIX);
}
/**
* Deserialized a file.
* @param root The root relate to.
* @param f The file to deserialize.
* @return The patch.
* @throws IOException If a read or parse error occurred.
*/
public static ClassPatch deserializeAt(File root, File f)
throws IOException{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(f));
ClassPatch cp;
try{
cp = (ClassPatch)in.readObject();
}
catch(ClassCastException e){
throw new IOException("Class patch not found." +
"Some other random stuff was there.", e);
}
catch(ClassNotFoundException e){
throw new IOException("Class patch not found." +
"Some other random stuff was there.", e);
}
in.close();
return cp;
}
}