/**
* 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 org.deephacks.confit.internal.cached.proxy;
import com.google.common.collect.Sets;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.FieldAccess;
import org.deephacks.confit.model.Bean;
import org.deephacks.confit.model.Schema;
import org.deephacks.confit.model.Schema.SchemaProperty;
import org.deephacks.confit.model.Schema.SchemaPropertyList;
import org.deephacks.confit.model.Schema.SchemaPropertyRef;
import org.deephacks.confit.model.Schema.SchemaPropertyRefList;
import org.deephacks.confit.model.Schema.SchemaPropertyRefMap;
import org.deephacks.confit.serialization.Conversion;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Responsible for generating proxy classes of real configurable classes and creating
* proxy objects that are handed off to clients.
*
*
* In order for proxies to work properly a configurable class must conform to the following
* requirements:
*
* - Declared as non-final (proxies extend configurable classes)
* - Fields that reference other configurable classes must be accessed using
* accessor/getter methods (i.e. public fields will not work).
*
*/
public class ConfigProxyGenerator {
/** classname suffix of each generated proxy class */
public static final String PROXY_CLASS_SUFFIX = "__javassist_config_proxy";
/** name of the field that store the ConfigReferenceHolder */
public static final String PROXY_FIELD_NAME = "__reference_holder";
/** schemaName -> Schema, schema awareness is needed to fetch references */
private static final HashMap<String, Schema> schemas = new HashMap<>();
/** a cache for already generated proxy classes */
private static final ConcurrentHashMap<String, Class<?>> proxyClassCache = new ConcurrentHashMap<>();
/** javassist class pool */
private static final ClassPool pool = ClassPool.getDefault();
/** convert bean properties in string form to real objects */
private static final Conversion converter = Conversion.get();
/**
* The proxy generator must be aware the schema of configurable classes,
* including their references, to ba able to create proxies from them.
*/
public void put(Schema schema) {
schemas.put(schema.getName(), schema);
}
public Object generateConfigProxy(Bean bean) {
try {
Schema schema = bean.getSchema();
Class<?> proxyClass = getProxyClass(schema);
Object proxyObject = newInstance(proxyClass);
// set @Id property on proxy object
setId(proxyObject, bean);
// set regular @Config properties on the proxy object
for(SchemaProperty property : schema.get(SchemaProperty.class)) {
setProperty(proxyObject, bean, property);
}
// set collection-like @Config properties on the proxy object
for(SchemaPropertyList property : schema.get(SchemaPropertyList.class)) {
setProperty(proxyObject, bean, property);
}
// attach the reference holder on the proxy object
// every reference is a String, the instance id.
setReferenceHolder(proxyObject, bean);
return proxyObject;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void setId(Object proxyObject, Bean bean) throws Exception {
String fieldName = bean.getSchema().getId().getName();
Field f = findField(proxyObject.getClass(), fieldName);
f.set(proxyObject, bean.getId().getInstanceId());
}
private void setReferenceHolder(Object proxyObject, Bean bean) throws Exception {
ConfigReferenceHolder proxy = new ConfigReferenceHolder(bean);
Field f = findField(proxyObject.getClass(), PROXY_FIELD_NAME);
f.set(proxyObject, proxy);
}
private void setProperty(Object proxyObject, Bean bean, SchemaPropertyList property) throws Exception {
String fieldName = property.getFieldName();
List<String> values = bean.getValues(fieldName);
if(values == null || values.size() == 0) {
return;
}
setValues(proxyObject, property, values);
}
private void setValues(Object proxyObject, SchemaPropertyList schema, List<String> stringValues) throws Exception {
String fieldName = schema.getFieldName();
Field f = findField(proxyObject.getClass(), fieldName);
f.setAccessible(true);
Class<?> collectionType = schema.getClassCollectionType();
if(Set.class.isAssignableFrom(collectionType)) {
Set values = (Set) converter.convert(Sets.newHashSet(stringValues), schema.getClassType());
f.set(proxyObject, values);
} else if (List.class.isAssignableFrom(collectionType)) {
List values = (List) converter.convert(stringValues, schema.getClassType());
f.set(proxyObject, values);
} else {
throw new UnsupportedOperationException("Collection type is not supported " + collectionType);
}
}
private void setProperty(Object proxyObject, Bean bean, SchemaProperty property) throws Exception {
String fieldName = property.getFieldName();
List<String> values = bean.getValues(fieldName);
if(values == null || values.size() == 0) {
return;
}
setValue(proxyObject, fieldName, property.getClassType(), values.get(0));
}
private void setValue(Object proxyObject, String fieldName, Class<?> type, String stringValue) throws Exception {
Field f = findField(proxyObject.getClass(), fieldName);
f.setAccessible(true);
Object value = converter.convert(stringValue, type);
f.set(proxyObject, value);
}
private synchronized Class<?> getProxyClass(Schema schema) throws Exception {
Class<?> proxyClass = proxyClassCache.get(schema.getName());
if(proxyClass != null) {
return proxyClass;
}
CtClass proxy = createCtClassProxy(schema);
CtClass referenceHolder = pool.get(ConfigReferenceHolder.class.getName());
CtField f = new CtField(referenceHolder, PROXY_FIELD_NAME, proxy);
f.setModifiers(Modifier.PUBLIC);
proxy.addField(f);
for (SchemaPropertyRef ref : schema.get(SchemaPropertyRef.class)) {
instrument(proxy, ref);
}
for (SchemaPropertyRefList ref : schema.get(SchemaPropertyRefList.class)) {
instrument(proxy, ref);
}
for (SchemaPropertyRefMap ref : schema.get(SchemaPropertyRefMap.class)) {
instrument(proxy, ref);
}
return createProxyClass(schema, proxy);
}
/**
* Instrument a single reference field, using the ConfigReferenceHolder to fetch the real
* reference from the cache and replace it with a real object.
*/
private void instrument(CtClass proxy, final SchemaPropertyRef ref) throws Exception {
final Schema schema = schemas.get(ref.getSchemaName());
checkNotNull(schema, "Schema not found for SchemaPropertyRef ["+ref+"]");
final String fieldName = ref.getFieldName();
// for help on javassist syntax, see chapter around javassist.expr.FieldAccess at
// http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/tutorial/tutorial2.html#before
proxy.instrument(new ExprEditor() {
public void edit(FieldAccess f) throws CannotCompileException {
if (f.getFieldName().equals(ref.getFieldName())) {
StringBuilder code = new StringBuilder();
code.append("{");
code.append("$_=("+schema.getType()+") this."+PROXY_FIELD_NAME+".getObjectReference(\""+fieldName+"\", \""+schema.getName()+"\");");
code.append("}");
f.replace(code.toString());
}
}
});
}
/**
* Instrument a Collection field holding references, using the ConfigReferenceHolder
* to fetch the real reference from the cache and replace it with a real object.
*/
private void instrument(CtClass proxy, final SchemaPropertyRefList ref) throws Exception {
final Schema schema = schemas.get(ref.getSchemaName());
checkNotNull(schema, "Schema not found for SchemaPropertyRefList ["+ref+"]");
final String fieldName = ref.getFieldName();
// for help on javassist syntax, see chapter around javassist.expr.FieldAccess at
// http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/tutorial/tutorial2.html#before
proxy.instrument(new ExprEditor() {
public void edit(FieldAccess f) throws CannotCompileException {
if (f.getFieldName().equals(ref.getFieldName())) {
StringBuilder code = new StringBuilder();
code.append("{");
code.append("$_=(java.util.List) this."+PROXY_FIELD_NAME+".getObjectReferenceList(\""+fieldName+"\", \""+schema.getName()+"\");");
code.append("}");
f.replace(code.toString());
}
}
});
}
/**
* Instrument a Map field holding references, using the ConfigReferenceHolder
* to fetch the real reference from the cache and replace it with a real object.
*/
private void instrument(CtClass proxy, final SchemaPropertyRefMap ref) throws Exception {
final Schema schema = schemas.get(ref.getSchemaName());
checkNotNull(schema, "Schema not found for SchemaPropertyRefMap ["+ref+"]");
final String fieldName = ref.getFieldName();
// for help on javassist syntax, see chapter around javassist.expr.FieldAccess at
// http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/tutorial/tutorial2.html#before
proxy.instrument(new ExprEditor() {
public void edit(FieldAccess f) throws CannotCompileException {
if (f.getFieldName().equals(ref.getFieldName())) {
StringBuilder code = new StringBuilder();
code.append("{");
code.append("$_=(java.util.Map) this."+PROXY_FIELD_NAME+".getObjectReferenceMap(\""+fieldName+"\", \""+schema.getName()+"\");");
code.append("}");
f.replace(code.toString());
}
}
});
}
/**
* Create a copy of the configurable class, a proxy class, and extend the
* configurable class with this proxy class. The idea is to let the proxy class
* override list methods of the configurable superclass [JLS 8.4.8.1].
*
* So when the real configurable class access a @Config field through a method
* it will access the field of the proxy instead of the real object.
*/
private CtClass createCtClassProxy(Schema schema) throws Exception {
CtClass proxyClass = pool.getAndRename(schema.getType(), schema.getType() + PROXY_CLASS_SUFFIX);
CtClass org = getCtClass(schema);
proxyClass.setSuperclass(org);
return proxyClass;
}
private CtClass getCtClass(Schema schema) {
String type = schema.getType();
try {
return pool.get(type);
} catch (NotFoundException e) {
throw new IllegalArgumentException(e);
}
}
private Class<?> createProxyClass(Schema schema, CtClass proxy) throws IOException, CannotCompileException {
Class<?> proxyClass;
byte[] enhanced = proxy.toBytecode();
ClassLoader cl = new ClassLoader() { };
ClassPool cp = new ClassPool( false );
cp.appendClassPath( new LoaderClassPath( cl ) );
CtClass enhancedCtClass = cp.makeClass( new ByteArrayInputStream( enhanced ) );
proxyClass = enhancedCtClass.toClass( cl, ConfigProxyGenerator.class.getProtectionDomain() );
proxyClassCache.put(schema.getName(), proxyClass);
return proxyClass;
}
private static Field findField(final Class<?> cls, String fieldName) {
Class<?> searchType = cls;
while (!Object.class.equals(searchType) && (searchType != null)) {
Field field = null;
try {
field = searchType.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
// ignore
}
if(field != null) {
field.setAccessible(true);
return field;
}
searchType = searchType.getSuperclass();
}
throw new RuntimeException("Could not find field " + fieldName + " on " + cls);
}
static <T> T newInstance(Class<T> type) throws InstantiationException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Constructor<?> c;
if(Modifier.isStatic(type.getModifiers())) {
c = type.getDeclaredConstructor(new Class[] {});
} else {
try {
Class<?> enclosing = type.getEnclosingClass();
if(type.getName().contains("$") && enclosing != null) {
throw new IllegalArgumentException("Non-static inner classes are not supported: " + type);
}
} catch (Exception e) {
// this may occur for byte code generated proxies
throw new IllegalArgumentException("Non-static inner classes are not supported: " + type);
}
c = type.getDeclaredConstructor();
}
c.setAccessible(true);
return type.cast(c.newInstance());
}
}