/*
* Copyright (c) 2008-2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.db.server.upgrade.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.nio.channels.FileChannel;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.StringMemberValue;
import javassist.util.HotSwapper;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.services.util.LoggingUtils;
/**
* Change the class schema at runtime
*/
public class DbSchemaChanger {
static {
LoggingUtils.configureIfNecessary("dbtest-log4j.properties");
}
private static Logger log = LoggerFactory.getLogger(DbSchemaChanger.class);
private String className;
private ClassPool pool;
private CtClass cc;
private File classFile; // full path name
private File backupFile;
/**
* Create a DB schema changer
*
* @param className the name of the class whose schema is to be changed
* it should be the full class name
*/
public DbSchemaChanger(String className) throws Exception {
this.className = className;
// pool creation
pool = ClassPool.getDefault();
// extracting the class
cc = pool.getCtClass(className);
String classFileName = cc.getURL().getFile();
classFile = new File(classFileName);
log.info("The class file:{} package:{}", classFileName, cc.getPackageName());
}
/**
* Begin change the schema
* This method should be called before making any changes to the class
*/
public DbSchemaChanger beginChange() throws Exception {
// backup the original class file
backupFile = File.createTempFile("dataobj", ".class");
log.info("copy from {} to {}", classFile.getAbsolutePath(), backupFile.getAbsolutePath());
copyFile(classFile, backupFile);
cc.defrost();
return this;
}
/**
* After make the changes to the class, this method should be called to make the changes
* taking effect
*/
public void endChange() throws Exception {
String dir = getClassRootDir();
log.info("write changed class back to {}", dir);
cc.writeFile(dir);
cc.detach();
log.info("wait 5 seconds for changes to take effect");
// sleep 5 seconds to wait for the schema changes taking effect.
Thread.currentThread().sleep(5000);
log.info("done");
}
/*
* return the root dir of a class file
* i.e. return the ${root} of {root}/pkg1/pkg2/foo.class
*/
private String getClassRootDir() {
// translate package into sub dirs
String subDirs = cc.getPackageName().replace('.', '/');
String classFileName = classFile.getAbsolutePath();
int lastIndex = classFileName.lastIndexOf(subDirs);
return classFileName.substring(0, lastIndex);
}
/**
* restore the class to the version before the change
*/
public void restoreClass() throws Exception {
if (backupFile == null) {
return;
}
copyFile(backupFile, classFile);
log.info("wait 5 seconds for restored class to take effect");
// wait 5 seconds for the changes to take effect.
Thread.currentThread().sleep(5000);
log.info("restore done");
// delete backup file
boolean deleted = backupFile.delete();
log.info("delete backup file {} sucess={}", backupFile.getAbsolutePath(), deleted);
backupFile = null;
}
/**
* add an annotation to a method
*
* @param methodName the method to which the annotation to be added
* @param annotationName the annotation name, it should be a full name
* @param values the attributes of the annotation
*/
public DbSchemaChanger addAnnotation(String methodName, String annotationName, Map<String, Object> values)
throws Exception {
// looking for the method to apply the annotation on
CtMethod methodDescriptor = cc.getDeclaredMethod(methodName);
// create the annotation
ClassFile ccFile = cc.getClassFile();
ccFile.setVersionToJava5();
ConstPool constpool = ccFile.getConstPool();
MethodInfo minfo = methodDescriptor.getMethodInfo();
AnnotationsAttribute attr = (AnnotationsAttribute) minfo.getAttribute(AnnotationsAttribute.visibleTag);
if (attr == null) {
attr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
}
Annotation annot = new Annotation(annotationName, constpool);
Set<Map.Entry<String, Object>> entries = values.entrySet();
for (Map.Entry<String, Object> entry : entries) {
String attrName = entry.getKey();
Object attrValue = entry.getValue();
if (attrValue instanceof String) {
annot.addMemberValue(attrName, new StringMemberValue((String) attrValue, ccFile.getConstPool()));
} else {
throw new RuntimeException(String.format("Unsupported attribute type %s of %s", attrName, attrValue));
}
}
attr.addAnnotation(annot);
// add the annotation to the method descriptor
minfo.addAttribute(attr);
log.info("add {} to method {}", attr, methodDescriptor);
return this;
}
/**
* remove an annotation from a method
*
* @param methodName the method to which the annotation to be removed
* @param annotationName the annotation name, it should be a full name
*/
public DbSchemaChanger removeAnnotation(String methodName, String annotationName) throws Exception {
// looking for the method to apply the annotation on
CtMethod methodDescriptor = cc.getDeclaredMethod(methodName);
// create the annotation
ClassFile ccFile = cc.getClassFile();
ccFile.setVersionToJava5();
ConstPool constpool = ccFile.getConstPool();
MethodInfo minfo = methodDescriptor.getMethodInfo();
AnnotationsAttribute attr = (AnnotationsAttribute) minfo.getAttribute(AnnotationsAttribute.visibleTag);
Annotation[] annotations = attr.getAnnotations();
List<Annotation> list = new ArrayList();
for (Annotation annotation : annotations) {
if (!annotation.getTypeName().equals(annotationName)) {
list.add(annotation);
}
}
Annotation[] newAnnotations = list.toArray(new Annotation[0]);
attr.setAnnotations(newAnnotations);
minfo.addAttribute(attr);
return this;
}
/**
* Add a bean property to the class i.e. add followings to a class:
* 1. a class field
* 2. a getter method
* 3. a setter method
*
* @param propertyName the bean property name
* @param propertyClazz the bean property type
* @param columnName the corresponding column name of the bean property
*/
public <T> DbSchemaChanger addBeanProperty(String propertyName, Class<T> propertyClazz, String columnName)
throws Exception {
CtField f = new CtField(pool.get(propertyClazz.getName()), propertyName, cc);
cc.addField(f);
String getterMethodName = generateGetterMethodName(propertyName);
StringBuilder method = new StringBuilder("public ");
method.append(propertyClazz.getName()); // return type
method.append(" ");
method.append(getterMethodName);
method.append("() {\n return ");
method.append(propertyName);
method.append(";\n }");
log.info("Generate getter method = {}", method.toString());
CtMethod getter = CtNewMethod.make(method.toString(), cc);
cc.addMethod(getter);
String setterMethodName = generateSetterMethodName(propertyName);
method = new StringBuilder("public void ");
method.append(setterMethodName);
method.append("(");
method.append(propertyClazz.getName());
method.append(" ");
method.append(propertyName);
method.append(") {\n this.");
method.append(propertyName);
method.append(" = ");
method.append(propertyName);
method.append(";\n setChanged(\"");
method.append(columnName);
method.append("\");\n }");
log.info("Generate setter method = {}", method.toString());
CtMethod setter = CtNewMethod.make(method.toString(), cc);
cc.addMethod(setter);
dumpClassInfo();
return this;
}
private static String generateGetterMethodName(String propertyName) {
StringBuilder builder = new StringBuilder("get");
return buildMethodName(builder, propertyName);
}
private static String buildMethodName(StringBuilder builder, String propertyName) {
char firstChar = propertyName.charAt(0);
int index = 1;
if (firstChar == '_') {
firstChar = propertyName.charAt(1);
index = 2;
}
builder.append(String.valueOf(firstChar).toUpperCase());
builder.append(propertyName.substring(index));
return builder.toString();
}
private static String generateSetterMethodName(String propertyName) {
StringBuilder builder = new StringBuilder("set");
return buildMethodName(builder, propertyName);
}
private void dumpClassInfo() {
CtField[] fields = cc.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
log.info(fields[i].getName());
}
CtMethod[] methods = cc.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
log.info(methods[i].getName());
}
}
/**
* Remove a bean property from the class
*
* @param propertyName the name of the property to be remooved
*/
public DbSchemaChanger removeBeanProperty(String propertyName) throws Exception {
String getterMethodName = generateGetterMethodName(propertyName);
removeMethod(getterMethodName);
String setterMethodName = generateSetterMethodName(propertyName);
removeMethod(setterMethodName);
dumpClassInfo();
return this;
}
/**
* remove the method from the class
*
* @param methodName the name of the method to be removed
*/
private void removeMethod(String methodName) throws Exception {
CtMethod[] methods = cc.getDeclaredMethods();
CtMethod method = null;
for (CtMethod m : methods) {
if (m.getName().equals(methodName)) {
method = m;
break;
}
}
if (method != null) {
cc.removeMethod(method);
log.info("The method {} is removed", methodName);
} else {
log.warn("The class {} has not method {} ", cc.getName(), methodName);
}
}
/**
* verify the method does have the annotation
*
* @param methodName the name of the method to be checked
* @param annotationName the name of the annotation to be checked
*/
public void verifyAnnotation(String methodName, String annotationName) throws Exception {
Class clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName);
// getting the annotation
Class annotationClazz = Class.forName(annotationName);
java.lang.annotation.Annotation annotation = method.getAnnotation(annotationClazz);
Assert.assertNotNull(annotation);
}
/**
* verify the class doesn't have the bean property
*
* @param propertyName the name of the property
*/
public void verifyBeanPropertyNotExist(String propertyName) throws Exception {
Class clazz = Class.forName(className);
// make sure that the 'getter' method doesn't exist
String getterMethodName = generateGetterMethodName(propertyName);
verifyMethodNotExist(getterMethodName);
String setterMethodName = generateSetterMethodName(propertyName);
verifyMethodNotExist(setterMethodName);
}
private void verifyMethodNotExist(String methodName) throws Exception {
Method method = getMethod(methodName);
Assert.assertNull(method);
}
private Method getMethod(String methodName) throws Exception {
Class clazz = Class.forName(className);
Method method = null;
Method[] methods = clazz.getMethods();
for (Method m : methods) {
if (m.getName().equals(methodName)) {
method = m;
break;
}
}
return method;
}
/**
* verify the class has the bean property
*
* @param propertyName the name of the property
*/
public void verifyBeanPropertyExist(String propertyName) throws Exception {
Class clazz = Class.forName(className);
// make sure that the 'getter' method doesn't exist
String getterMethodName = generateGetterMethodName(propertyName);
verifyMethodExist(getterMethodName);
String setterMethodName = generateSetterMethodName(propertyName);
verifyMethodExist(setterMethodName);
}
private void verifyMethodExist(String methodName) throws Exception {
Method method = getMethod(methodName);
Assert.assertNotNull(method);
}
private void copyFile(File sourceFile, File targetFile) throws Exception {
log.info("copy from {} to {}", sourceFile, targetFile);
FileChannel source = new FileInputStream(sourceFile).getChannel();
FileChannel target = new FileOutputStream(targetFile).getChannel();
target.transferFrom(source, 0, source.size());
}
public enum InjectModeEnum {
BEFORE,
AFTER;
}
public void insertCodes(String methodName, String codes, InjectModeEnum mode) throws Exception {
CtMethod method = cc.getDeclaredMethod(methodName);
doInsertCodes(method, codes, mode);
byte[] classFile = cc.toBytecode();
HotSwapper hs = new HotSwapper(8000); // 8000 is a port number.
hs.reload(className, classFile);
}
private static void doInsertCodes(CtMethod method, String codes, InjectModeEnum mode) throws Exception {
switch (mode) {
case BEFORE:
method.insertBefore(codes);
break;
case AFTER:
method.insertAfter(codes);
break;
default:
log.error("The inject mode({}) is not supported.", mode.toString());
}
}
/**
* Check if the class has been loaded by the system class loader or not
*/
public boolean isLoaded() {
try {
// ClassLoader.findLoadedClass() is 'protected'
// make it accessible outside
Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] { String.class });
m.setAccessible(true);
// call cl.findLoadedClass(className)
ClassLoader cl = ClassLoader.getSystemClassLoader();
Object obj = m.invoke(cl, className);
return obj != null;
} catch (Exception e) {
log.error("Failed to check if the class {} is loaded e=", className, e);
}
return false;
}
}