diff --git a/.gitignore b/.gitignore
index 74d159f..af9d3d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,4 @@
.idea/
*.iml
target/
-ui/node_modules
-*.db
\ No newline at end of file
+ui/node_modules
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 85c5e77..689ada8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,9 +21,9 @@
- org.xerial
- sqlite-jdbc
- 3.28.0
+ org.nanohttpd
+ nanohttpd
+ 2.3.1
diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java
index ba8bafd..55a20ca 100644
--- a/src/main/java/perfix/Agent.java
+++ b/src/main/java/perfix/Agent.java
@@ -1,6 +1,7 @@
package perfix;
import perfix.instrument.Instrumentor;
+import perfix.server.HTTPServer;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
@@ -8,33 +9,33 @@ import java.util.Collections;
import java.util.List;
import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
public class Agent {
- public static final String DBFILE_PROPERTY = "perfix.db";
- public static final String DEFAULT_DBFILE = "perfix.db";
private static final String PORT_PROPERTY = "perfix.port";
private static final String INCLUDES_PROPERTY = "perfix.includes";
+
+ private static final String DEFAULT_PORT = "2048";
private static final String MESSAGE = " --- Perfix agent active";
+
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println(MESSAGE);
- String dbFile = System.getProperty(DBFILE_PROPERTY);
- if (dbFile == null) {
- dbFile = DEFAULT_DBFILE;
- }
- System.out.println(" --- SQLite file written to " + dbFile);
+
+ int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
Instrumentor.create(determineIncludes()).instrumentCode(instrumentation);
+
+ new HTTPServer(port).start();
}
private static List determineIncludes() {
String includesPropertyValue = System.getProperty(INCLUDES_PROPERTY);
- if (includesPropertyValue == null) {
+ if (includesPropertyValue==null){
System.out.println("WARNING: perfix.includes not set ");
return Collections.emptyList();
- }
- System.out.println(" --- Instrumenting packages: " + includesPropertyValue + ".*");
+ }
return new ArrayList<>(asList(includesPropertyValue.split(",")));
}
}
diff --git a/src/main/java/perfix/MethodInvocation.java b/src/main/java/perfix/MethodInvocation.java
new file mode 100644
index 0000000..92e4588
--- /dev/null
+++ b/src/main/java/perfix/MethodInvocation.java
@@ -0,0 +1,22 @@
+package perfix;
+
+/**
+ * contains start and stop time for method/query/servlet
+ */
+public class MethodInvocation {
+ private final long timestamp;
+ long duration;
+
+ MethodInvocation(String name) {
+ timestamp = System.nanoTime();
+
+ }
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public void registerEndingTime(long t1) {
+ duration = t1 - timestamp;
+ }
+}
diff --git a/src/main/java/perfix/MethodNode.java b/src/main/java/perfix/MethodNode.java
index 6ed82e1..5be85f6 100644
--- a/src/main/java/perfix/MethodNode.java
+++ b/src/main/java/perfix/MethodNode.java
@@ -1,29 +1,30 @@
package perfix;
-
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
public class MethodNode {
- private final String name;
- private final long timestamp;
- private final String threadName;
- private MethodNode parent;
- private long duration;
- private long invocationid;
-
+ public final String name;
+ public final List children;
+ public MethodNode parent;
+ private MethodInvocation invocation;
public MethodNode(String name) {
this.name = name;
- this.timestamp = System.nanoTime();
- this.threadName = Thread.currentThread().getName();
+ this.children = new ArrayList<>();
+ }
+
+ public void addChild(MethodNode child) {
+ children.add(child);
}
public String getName() {
return name;
}
- public long getId() {
- return Objects.hash(Thread.currentThread().getId(), timestamp);
+ public List getChildren() {
+ return children;
}
@Override
@@ -46,47 +47,11 @@ public class MethodNode {
return Objects.hash(name);
}
- public long getParentId() {
- if (parent == null) {
- return 0;
- } else {
- return parent.getId();
- }
+ public MethodInvocation getInvocation() {
+ return invocation;
}
- public long getDuration() {
- return duration;
- }
-
- public void registerEndingTime(long t1) {
- duration = t1 - timestamp;
- }
-
- public long getTimestamp() {
- return timestamp;
- }
-
- public MethodNode getParent() {
- return parent;
- }
-
- public void setParent(MethodNode parent) {
- this.parent = parent;
- }
-
- public String getThreadName() {
- return threadName;
- }
-
- public void setInvocationId(long invocationid) {
- this.invocationid = invocationid;
- }
-
- public long getInvocationId() {
- if (parent != null) {
- return parent.getInvocationId();
- } else {
- return getId();
- }
+ public void setInvocation(MethodInvocation invocation) {
+ this.invocation = invocation;
}
}
diff --git a/src/main/java/perfix/Registry.java b/src/main/java/perfix/Registry.java
index 73ddd83..b074ea0 100644
--- a/src/main/java/perfix/Registry.java
+++ b/src/main/java/perfix/Registry.java
@@ -2,65 +2,17 @@ package perfix;
import perfix.instrument.Util;
-import java.sql.*;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListMap;
-/**
- * Handles start and stop of method invocations. Measures method/sql/url invocations. Stores individual measurements in sqlite.
- */
-@SuppressWarnings("unused") //used from instrumented bytecode
public class Registry {
+ private static final List callstack = new ArrayList<>();
private static final ThreadLocal currentMethod = new ThreadLocal<>();
- private static final String INSERT_REPORT = "insert into report (thread, id, parent_id, invocation_id, timestamp, name, duration) values (?,?,?,?,?,?,?)";
- private static final String CREATE_TABLE = "create table report(thread varchar(255), id int, parent_id int, invocation_id int, timestamp int, name varchar(255), duration integer)";
- private static final String SElECT_TABLE = "SELECT name FROM sqlite_master WHERE type='table' AND name='report';";
- private static BlockingQueue queue = new LinkedBlockingQueue<>();
- private static Connection connection;
-
- static {
- initWorker();
- initDatabase();
- }
-
- private static void initWorker() {
- ExecutorService executorService = Executors.newFixedThreadPool(1);
- executorService.submit(() -> {
- while (true) {
- MethodNode methodNode = queue.take();
-
- store(methodNode);
- }
- });
- }
-
- private static void initDatabase() {
- try {
- connection = DriverManager.getConnection("jdbc:sqlite:" + getSqliteFile());
- Statement statement = connection.createStatement();
- ResultSet resultSet = statement.executeQuery(SElECT_TABLE);
- if (!resultSet.next()) {
- statement.execute(CREATE_TABLE);
- }
- } catch (SQLException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static String getSqliteFile() {
- String dbFileName = System.getProperty(Agent.DBFILE_PROPERTY);
- if (dbFileName == null) {
- return Agent.DEFAULT_DBFILE;
- } else {
- return dbFileName;
- }
- }
-
- public static MethodNode startJdbc(String name) {
+ @SuppressWarnings("unused") //used in generated code
+ public static MethodInvocation startJdbc(String name) {
if (!Util.isFirstExecutionStarted()) {
Util.startExecution();
return start(name);
@@ -69,54 +21,72 @@ public class Registry {
}
}
- public static MethodNode start(String name) {
+ @SuppressWarnings("unused")
+ public static MethodInvocation start(String name) {
+ MethodInvocation methodInvocation = new MethodInvocation(name);
MethodNode newNode = new MethodNode(name);
MethodNode parent = currentMethod.get();
if (parent != null) {
- newNode.setParent(parent);
- newNode.setInvocationId(parent.getInvocationId());
+ parent.addChild(newNode);
+ newNode.parent = parent;
+ } else {
+ callstack.add(newNode);
}
currentMethod.set(newNode);
- return newNode;
+ return methodInvocation;
}
@SuppressWarnings("unused")
- public static void stopJdbc() {
- MethodNode current = currentMethod.get();
- if (Util.isFirstExecutionStarted() && current != null) {
- stop();
+ public static void stopJdbc(MethodInvocation queryInvocation) {
+ if (Util.isFirstExecutionStarted() && queryInvocation != null) {
+ stop(queryInvocation);
Util.endExecution();
}
}
@SuppressWarnings("unused")
- public static void stop() {
- MethodNode current = currentMethod.get();
- if (current != null) {
- current.registerEndingTime(System.nanoTime());
- queue.add(current);
- currentMethod.set(current.getParent());
+ public static void stop(MethodInvocation methodInvocation) {
+ if (methodInvocation != null) {
+ methodInvocation.registerEndingTime(System.nanoTime());
}
+ MethodNode methodNode = currentMethod.get();
+ methodNode.setInvocation(methodInvocation);
+
+ currentMethod.set(methodNode.parent);
}
- private static void store(MethodNode methodNode) {
- try {
- PreparedStatement statement = connection.prepareStatement(INSERT_REPORT);
- statement.setString(1, methodNode.getThreadName());
- statement.setLong(2, methodNode.getId());
- statement.setLong(3, methodNode.getParentId());
- statement.setLong(4, methodNode.getInvocationId());
- statement.setLong(5, methodNode.getTimestamp());
- statement.setString(6, methodNode.getName());
- statement.setLong(7, methodNode.getDuration());
- statement.executeUpdate();
- statement.close();
- } catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException(e);
- }
+ public static SortedMap sortedMethodsByDuration() {
+ //walk the stack to group methods by their name
+ Map> methods = new ConcurrentHashMap<>();
+ collectInvocationsPerMethodName(methods, callstack);
+
+ //gather invocations by method name and calculate statistics
+ SortedMap sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder());
+ methods.forEach((name, measurements) -> {
+ long totalDuration = measurements.stream()
+ .filter(Objects::nonNull)
+ .mapToLong(MethodInvocation::getDuration).sum();
+ sortedByTotal.put(totalDuration, new Report(name, measurements.size(), totalDuration));
+ });
+ return sortedByTotal;
+ }
+
+ private static void collectInvocationsPerMethodName(Map> invocations, List nodes) {
+ nodes.forEach(methodNode -> {
+ invocations.computeIfAbsent(methodNode.getName(), key -> new ArrayList<>()).add(methodNode.getInvocation());
+ collectInvocationsPerMethodName(invocations, methodNode.children);
+ });
+
+ }
+
+ public static List getCallStack() {
+ return callstack;
+ }
+
+ public static void clear() {
+ callstack.clear();
}
}
diff --git a/src/main/java/perfix/instrument/ClassInstrumentor.java b/src/main/java/perfix/instrument/ClassInstrumentor.java
index 7d2da6f..0aa361d 100644
--- a/src/main/java/perfix/instrument/ClassInstrumentor.java
+++ b/src/main/java/perfix/instrument/ClassInstrumentor.java
@@ -24,6 +24,7 @@ public class ClassInstrumentor extends Instrumentor {
ClassInstrumentor(List includes, ClassPool classPool) {
super(includes, classPool);
try {
+ perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
stringClass = classpool.get(JAVA_STRING);
} catch (NotFoundException e) {
diff --git a/src/main/java/perfix/instrument/Instrumentor.java b/src/main/java/perfix/instrument/Instrumentor.java
index 3836187..0b03be7 100644
--- a/src/main/java/perfix/instrument/Instrumentor.java
+++ b/src/main/java/perfix/instrument/Instrumentor.java
@@ -9,6 +9,7 @@ import java.util.List;
public abstract class Instrumentor {
static final String JAVA_STRING = "java.lang.String";
static final String JAVA_HASHMAP = "java.util.HashMap";
+ static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation";
static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1";
static final String JAVASSIST_RETURNVALUE = "$_";
@@ -16,12 +17,14 @@ public abstract class Instrumentor {
final List includes;
protected CtClass stringClass;
protected CtClass hashMapClass;
+ protected CtClass perfixMethodInvocationClass;
Instrumentor(List includes, ClassPool classPool) {
this.includes = includes;
this.classpool = classPool;
try {
+ perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
stringClass = classpool.get(JAVA_STRING);
hashMapClass = classPool.get(JAVA_HASHMAP);
@@ -49,8 +52,9 @@ public abstract class Instrumentor {
/* record times at beginning and end of method body*/
void instrumentMethod(CtMethod methodToinstrument, String metricName) {
try {
- methodToinstrument.insertBefore("perfix.Registry.start(" + metricName + ");");
- methodToinstrument.insertAfter("perfix.Registry.stop();");
+ methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
+ methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");");
+ methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
@@ -62,8 +66,9 @@ public abstract class Instrumentor {
* (measured) calls if not handled in a way to prevent this */
void instrumentJdbcCall(CtMethod methodToinstrument, String metricName) {
try {
- methodToinstrument.insertBefore("perfix.Registry.startJdbc(" + metricName + ");");
- methodToinstrument.insertAfter("perfix.Registry.stopJdbc();");
+ methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
+ methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(" + metricName + ");");
+ methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
@@ -72,8 +77,9 @@ public abstract class Instrumentor {
/* record times at beginning and end of method body*/
void instrumentJdbcCall(CtMethod methodToinstrument) {
try {
- methodToinstrument.insertBefore("perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));");
- methodToinstrument.insertAfter("perfix.Registry.stopJdbc();");
+ methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
+ methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));");
+ methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
diff --git a/src/main/java/perfix/instrument/JdbcInstrumentor.java b/src/main/java/perfix/instrument/JdbcInstrumentor.java
index fd3d6f3..0ed5771 100644
--- a/src/main/java/perfix/instrument/JdbcInstrumentor.java
+++ b/src/main/java/perfix/instrument/JdbcInstrumentor.java
@@ -28,7 +28,7 @@ public class JdbcInstrumentor extends Instrumentor {
try {
preparedStatementInterface.getDeclaredMethod("setSqlForPerfix");
} catch (NotFoundException e1) {
-// e1.printStackTrace();
+ e1.printStackTrace();
try {
CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, "setSqlForPerfix", new CtClass[]{stringClass}, preparedStatementInterface);
preparedStatementInterface.addMethod(setSqlForPerfix);
@@ -94,7 +94,7 @@ public class JdbcInstrumentor extends Instrumentor {
getDeclaredMethods(preparedStatementClass, "setString", "setObject", "setDate", "setTime", "setTimestamp")
.forEach(method -> {
try {
- method.insertBefore("perfix.instrument.StatementText.set(_perfixSqlStatement,$1, \"'\"+$2+\"'\");");
+ method.insertBefore("perfix.instrument.StatementText.set(_perfixSqlStatement,$1, \"\'\"+$2+\"\'\");");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
@@ -161,7 +161,7 @@ public class JdbcInstrumentor extends Instrumentor {
}
- private void addPerfixFields(CtClass preparedStatementClass) throws CannotCompileException {
+ private void addPerfixFields(CtClass preparedStatementClass) throws CannotCompileException, NotFoundException {
// add a String field that will contain the statement
CtField perfixSqlField = new CtField(statementTextClass, "_perfixSqlStatement", preparedStatementClass);
perfixSqlField.setModifiers(Modifier.PRIVATE);
diff --git a/src/main/java/perfix/instrument/ServletInstrumentor.java b/src/main/java/perfix/instrument/ServletInstrumentor.java
index 969c203..35a8f98 100644
--- a/src/main/java/perfix/instrument/ServletInstrumentor.java
+++ b/src/main/java/perfix/instrument/ServletInstrumentor.java
@@ -26,10 +26,11 @@ public class ServletInstrumentor extends Instrumentor {
try {
stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> {
try {
- methodToInstrument.insertBefore("perfix.Registry.start($1.getRequestURI());");
- methodToInstrument.insertAfter("perfix.Registry.stop();");
+ methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
+ methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());");
+ methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
} catch (CannotCompileException e) {
- // ignore and return uninstrumented bytecode
+
}
});
return bytecode(classToInstrument);
diff --git a/src/main/java/perfix/server/HTTPServer.java b/src/main/java/perfix/server/HTTPServer.java
new file mode 100644
index 0000000..fa21ddf
--- /dev/null
+++ b/src/main/java/perfix/server/HTTPServer.java
@@ -0,0 +1,76 @@
+package perfix.server;
+
+import fi.iki.elonen.NanoHTTPD;
+import perfix.Registry;
+import perfix.server.json.Serializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class HTTPServer extends NanoHTTPD {
+
+
+ public HTTPServer(int port) {
+ super(port);
+ }
+
+ public void start() {
+
+ try {
+ start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
+ System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + getListeningPort() + "/");
+ } catch (IOException ioe) {
+ System.err.println(" --- Couldn't start Perfix http server:\n" + ioe);
+ }
+ }
+
+ @Override
+ public Response serve(IHTTPSession session) {
+ String uri = session.getUri();
+ switch (uri) {
+ case "/report":
+ return perfixMetrics();
+ case "/callstack":
+ return perfixCallstack();
+ case "/clear":
+ return clear();
+ default:
+ return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "NOT FOUND");
+ }
+ }
+
+ private Response perfixMetrics() {
+ try {
+ return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(new ArrayList<>(Registry.sortedMethodsByDuration().values()))));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(e.toString());
+ }
+ }
+
+ private Response addCors(Response response) {
+ response.addHeader("Access-Control-Allow-Origin", "*");
+ response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+ return response;
+ }
+
+ private Response perfixCallstack() {
+ try {
+ return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(e.toString());
+ }
+ }
+
+ private Response clear() {
+ Registry.clear();
+ try {
+ return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(e.toString());
+ }
+ }
+
+}
diff --git a/src/main/java/perfix/server/json/JSONSerializer.java b/src/main/java/perfix/server/json/JSONSerializer.java
new file mode 100644
index 0000000..7d11d1a
--- /dev/null
+++ b/src/main/java/perfix/server/json/JSONSerializer.java
@@ -0,0 +1,21 @@
+package perfix.server.json;
+
+import java.util.Formatter;
+
+public abstract class JSONSerializer {
+ protected abstract String handle(T object);
+
+ protected Formatter formatter = new Formatter();
+
+ public String toJSONString(T object) {
+ if (object == null) {
+ return "";
+ } else if (object instanceof Number || object instanceof Boolean) {
+ return "" + object.toString();
+ } else if (object instanceof CharSequence || object instanceof Character) {
+ return "\"" + object.toString() + "\"";
+ } else {
+ return handle(object);
+ }
+ }
+}
diff --git a/src/main/java/perfix/server/json/Serializer.java b/src/main/java/perfix/server/json/Serializer.java
new file mode 100644
index 0000000..fa4f42a
--- /dev/null
+++ b/src/main/java/perfix/server/json/Serializer.java
@@ -0,0 +1,45 @@
+package perfix.server.json;
+
+public class Serializer {
+ private static SerializerFactory instance = new SynthSerializerFactory();
+
+ public static String toJSONString(boolean b) {
+ return Boolean.toString(b);
+ }
+
+ public static String toJSONString(short s) {
+ return Short.toString(s);
+ }
+
+ public static String toJSONString(int i) {
+ return Integer.toString(i);
+ }
+
+ public static String toJSONString(float f) {
+ return Float.toString(f);
+ }
+
+ public static String toJSONString(double d) {
+ return Double.toString(d);
+ }
+
+ public static String toJSONString(long l) {
+ return Long.toString(l);
+ }
+
+ public static String toJSONString(char c) {
+ return "\"" + Character.toString(c) + "\"";
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String toJSONString(T o) {
+ if (o == null) {
+ return "null";
+ }
+ return instance.createSerializer((Class) o.getClass()).toJSONString(o);
+ }
+
+ public static void setInstance(SerializerFactory instance) {
+ Serializer.instance = instance;
+ }
+}
diff --git a/src/main/java/perfix/server/json/SerializerCreationException.java b/src/main/java/perfix/server/json/SerializerCreationException.java
new file mode 100644
index 0000000..1743429
--- /dev/null
+++ b/src/main/java/perfix/server/json/SerializerCreationException.java
@@ -0,0 +1,10 @@
+package perfix.server.json;
+
+@SuppressWarnings("serial")
+public class SerializerCreationException extends RuntimeException {
+
+ public SerializerCreationException(Throwable t) {
+ super(t);
+ }
+
+}
diff --git a/src/main/java/perfix/server/json/SerializerFactory.java b/src/main/java/perfix/server/json/SerializerFactory.java
new file mode 100644
index 0000000..9e2346c
--- /dev/null
+++ b/src/main/java/perfix/server/json/SerializerFactory.java
@@ -0,0 +1,5 @@
+package perfix.server.json;
+
+public interface SerializerFactory {
+ public JSONSerializer createSerializer(Class beanjavaClass);
+}
diff --git a/src/main/java/perfix/server/json/SynthSerializerFactory.java b/src/main/java/perfix/server/json/SynthSerializerFactory.java
new file mode 100644
index 0000000..880acbf
--- /dev/null
+++ b/src/main/java/perfix/server/json/SynthSerializerFactory.java
@@ -0,0 +1,350 @@
+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 wrappersAndString = new HashSet(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 mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap");
+
+ private static final Map> serializers = new HashMap<>();
+ private static final String ROOT_PACKAGE = "serializer.";
+
+ private final ClassPool pool = ClassPool.getDefault();
+ private final Map primitiveWrappers = new HashMap();
+ 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 JSONSerializer createSerializer(Class beanjavaClass) {
+ try {
+ CtClass beanClass = pool.get(beanjavaClass.getName());
+
+ return createSerializer(beanClass);
+ } catch (NotFoundException e) {
+ throw new SerializerCreationException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private JSONSerializer createSerializer(CtClass beanClass) {
+ if (serializers.containsKey(createSerializerName(beanClass))) {
+ return (JSONSerializer) serializers.get(createSerializerName(beanClass));
+ }
+ try {
+ return tryCreateSerializer(beanClass);
+ } catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException e) {
+ throw new SerializerCreationException(e);
+ }
+ }
+
+ private JSONSerializer tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
+ IllegalAccessException {
+ CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
+
+ addToJsonStringMethod(beanClass, serializerClass);
+
+ JSONSerializer 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 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 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 JSONSerializer createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException,
+ CannotCompileException {
+ return (JSONSerializer) 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 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 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 getGetters(CtClass beanClass) {
+ List methods = new ArrayList();
+ List 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 getAllFields(CtClass beanClass) {
+ try {
+ List 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 getAllFields(CtClass beanClass, List 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 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("\\[\\]", "");
+ }
+}