perfix/src/main/java/perfix/server/json/SynthSerializerFactory.java

350 lines
13 KiB
Java

package perfix.server.json;
import javassist.*;
import java.util.*;
import static java.util.Arrays.asList;
public class SynthSerializerFactory implements SerializerFactory {
private static final String STRING = "java.lang.String";
private static final String BOOLEAN = "java.lang.Boolean";
private static final String CHARACTER = "java.lang.Character";
private static final String BYTE = "java.lang.Byte";
private static final String DOUBLE = "java.lang.Double";
private static final String FLOAT = "java.lang.Float";
private static final String LONG = "java.lang.Long";
private static final String SHORT = "java.lang.Short";
private static final String INTEGER = "java.lang.Integer";
private final static Set<String> wrappersAndString = new HashSet<String>(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
STRING));
private static final String COLLECTION = "java.util.Collection";
private static final String LIST = "java.util.List";
private static final String SET = "java.util.Set";
private static final List<String> mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap");
private static final Map<String, JSONSerializer<?>> serializers = new HashMap<>();
private static final String ROOT_PACKAGE = "serializer.";
private final ClassPool pool = ClassPool.getDefault();
private final Map<String, CtClass> primitiveWrappers = new HashMap<String, CtClass>();
private CtClass serializerBase;
SynthSerializerFactory() {
init();
}
private static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) {
return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName());
}
void init() {
try {
serializerBase = pool.get(JSONSerializer.class.getName());
primitiveWrappers.put("int", pool.get(INTEGER));
primitiveWrappers.put("short", pool.get(SHORT));
primitiveWrappers.put("byte", pool.get(BYTE));
primitiveWrappers.put("long", pool.get(LONG));
primitiveWrappers.put("float", pool.get(FLOAT));
primitiveWrappers.put("double", pool.get(DOUBLE));
primitiveWrappers.put("boolean", pool.get(BOOLEAN));
primitiveWrappers.put("char", pool.get(CHARACTER));
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
try {
CtClass beanClass = pool.get(beanjavaClass.getName());
return createSerializer(beanClass);
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializer(CtClass beanClass) {
if (serializers.containsKey(createSerializerName(beanClass))) {
return (JSONSerializer<T>) serializers.get(createSerializerName(beanClass));
}
try {
return tryCreateSerializer(beanClass);
} catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException e) {
throw new SerializerCreationException(e);
}
}
private <T> JSONSerializer<T> tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
IllegalAccessException {
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
addToJsonStringMethod(beanClass, serializerClass);
JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass);
serializers.put(createSerializerName(beanClass), jsonSerializer);
return jsonSerializer;
}
/*
* create method source, compile it and add it to the class under construction
*/
private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws NotFoundException, CannotCompileException {
String body = createToJSONStringMethodSource(beanClass);
serializerClass.addMethod(CtNewMethod.make(body, serializerClass));
}
/*
* Creates the source, handling the for JSON different types of classes
*/
private String createToJSONStringMethodSource(CtClass beanClass) throws NotFoundException {
String source = "public String handle(Object object){\n";
if (beanClass.isArray()) {
source += "\tObject[] array=(Object[])object;\n";
source += handleArray(beanClass);
} else if (isCollection(beanClass)) {
source += "\tObject[] array=((java.util.Collection)object).toArray();\n";
source += handleArray(beanClass);
} else if (isMap(beanClass)) {
source += handleMap(beanClass);
} else if (!isPrimitiveOrWrapperOrString(beanClass)) {
List<CtMethod> getters = getGetters(beanClass);
if (shouldAddGetterCallers(getters)) {
source = addGetterCallers(beanClass, source, getters);
}
} else {
source += "\treturn \"\";}";
}
return source;
}
/*
* Any Collection is converted to an array, after which code is generated to handle the single elements.
*
* A subserializer is created for every single element, but most of the time it will be the same cached instance.
*
* The generated code fills a StringBuilder. The values are generated by the subserializers
*/
private String handleArray(CtClass beanClass) {
String source = "\tjava.util.StringJoiner result=new java.util.StringJoiner(\",\",\"[\",\"]\");\n";
source += "\tfor (int i=0; i<array.length; i++){\n";
source += "\t\tresult.add(" + Serializer.class.getName() + ".toJSONString(array[i]));\n";
source += "\t};\n\treturn result.toString();\n}";
return source;
}
private String handleMap(CtClass beanClass) {
String source = "StringBuilder result=new StringBuilder(\"{\");\n";
source += "\tfor (java.util.Iterator entries=((java.util.Map)object).entrySet().iterator();entries.hasNext();){\n";
source += "\t\tjava.util.Map.Entry entry=(java.util.Map.Entry)entries.next();\n";
source += "\t\tresult.append(\"\\\"\"+entry.getKey().toString()+\"\\\"\");\n";
source += "\t\tresult.append(\": \");\n";
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(entry.getValue()));\n";
source += "\t\tresult.append(\", \");\n";
source += "\t};\n";
source += "\tresult.setLength(result.length()-2);\n";
source += "\tresult.append(\"}\");\n";
source += "\treturn result.toString();\n";
source += "}";
return source;
}
/*
* If the class contains fields for which public getters are available, then these will be called in the generated code.
*/
private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> getters) throws NotFoundException {
int index = 0;
source += "\treturn ";
source += "\"{";
for (CtMethod getter : getters) {
source = addPair(beanClass, source, getter);
if (index++ < getters.size() - 1) {
source += ",";
}
}
source += "}\";\n}";
return source;
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException,
CannotCompileException {
return (JSONSerializer<T>) serializerClass.toClass().newInstance();
}
/*
* custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes
*
* Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
*/
public String createSerializerName(CtClass beanClass) {
return createSerializerName(beanClass.getName());
}
public String createSerializerName(String name) {
return ROOT_PACKAGE + name.replaceAll("\\[\\]", "Array") + "Serializer";
}
private boolean isCollection(CtClass beanClass) throws NotFoundException {
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
boolean is = interfaces.stream().anyMatch(interfaze -> interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET));
return is;
}
private boolean isMap(CtClass beanClass) throws NotFoundException {
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
return interfaces.stream().anyMatch(i -> mapInterfaces.contains(i.getName()));
}
/*
* The JSON vernacular for key:value is pair...
*/
private String addPair(CtClass classToSerialize, String source, CtMethod getter) throws NotFoundException {
source += jsonKey(getter);
source += ":";
source += jsonValue(classToSerialize, getter);
return source;
}
/*
* derive property key from getter
*/
private String jsonKey(CtMethod getter) {
return "\\\"" + toFieldName(getter.getName()) + "\\\"";
}
private String jsonValue(CtClass classToSerialize, CtMethod getter) throws NotFoundException {
String source = "";
CtClass returnType = getter.getReturnType();
/* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */
source = createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter, source, returnType);
return source;
}
private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod getter, String source, CtClass returnType) {
/* NB there does not seem to be auto(un))boxing nor generic types (or other jdk1.5 stuff) in javassist compileable code */
source += "\"+" + Serializer.class.getName() + ".toJSONString(";
// cast because of lack of generics
source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()";
source += ")+\"";
return source;
}
/*
* turns for example 'getValue' into 'value'
*/
private String toFieldName(String name) {
return name.substring(3, 4).toLowerCase() + (name.length() > 4 ? name.substring(4) : "");
}
public String regularClassname(String name) {
return name.replaceAll("\\$", ".");
}
private String cast(String classToSerialize) {
return "(" + classToSerialize + ")";
}
/*
* Retrieves getter methods from a class
*/
private List<CtMethod> getGetters(CtClass beanClass) {
List<CtMethod> methods = new ArrayList<CtMethod>();
List<CtField> fields = getAllFields(beanClass);
for (CtField field : fields) {
try {
CtMethod method = beanClass.getMethod(getGetterMethod(field), getDescription(field));
if (Modifier.isPublic(method.getModifiers())) {
methods.add(method);
}
} catch (NotFoundException n) {
// ignore
}
}
return methods;
}
private String getGetterMethod(CtField field) {
return "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
}
private List<CtField> getAllFields(CtClass beanClass) {
try {
List<CtField> allfields = new ArrayList<>();
for (CtField field : beanClass.getDeclaredFields()) {
allfields.add(field);
}
if (beanClass.getSuperclass() != null) {
return getAllFields(beanClass.getSuperclass(), allfields);
}
return allfields;
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
private List<CtField> getAllFields(CtClass beanClass, List<CtField> allfields) {
for (CtField field : beanClass.getDeclaredFields()) {
allfields.add(field);
}
return allfields;
}
/*
* is getter list is not empty then callers should be added
*/
boolean shouldAddGetterCallers(List<CtMethod> getters) {
return !getters.isEmpty();
}
String getDescription(CtField field) throws NotFoundException {
if (field.getType().isArray()) {
return "()[" + innerClassName(field.getType().getName()) + ";";
} else if (!field.getType().isPrimitive()) {
return "()" + innerClassName(field.getType().getName()) + ";";
} else {
return "()" + asPrimitive(field.getType().getName());
}
}
String asPrimitive(String name) {
switch (name) {
case "int":
return "I";
case "byte":
return "B";
case "float":
return "F";
case "long":
return "J";
case "boolean":
return "Z";
case "char":
return "C";
case "double":
return "D";
case "short":
return "S";
}
return "";
}
String innerClassName(String name) {
return "L" + name.replaceAll("\\.", "/").replaceAll("\\[\\]", "");
}
}