diff --git a/.gitignore b/.gitignore
index af9d3d9..74d159f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
.idea/
*.iml
target/
-ui/node_modules
\ No newline at end of file
+ui/node_modules
+*.db
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 689ada8..85c5e77 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,9 +21,9 @@
- org.nanohttpd
- nanohttpd
- 2.3.1
+ org.xerial
+ sqlite-jdbc
+ 3.28.0
diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java
index 55a20ca..ba8bafd 100644
--- a/src/main/java/perfix/Agent.java
+++ b/src/main/java/perfix/Agent.java
@@ -1,7 +1,6 @@
package perfix;
import perfix.instrument.Instrumentor;
-import perfix.server.HTTPServer;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
@@ -9,33 +8,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);
-
- int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
+ String dbFile = System.getProperty(DBFILE_PROPERTY);
+ if (dbFile == null) {
+ dbFile = DEFAULT_DBFILE;
+ }
+ System.out.println(" --- SQLite file written to " + dbFile);
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
deleted file mode 100644
index 92e4588..0000000
--- a/src/main/java/perfix/MethodInvocation.java
+++ /dev/null
@@ -1,22 +0,0 @@
-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 5be85f6..6ed82e1 100644
--- a/src/main/java/perfix/MethodNode.java
+++ b/src/main/java/perfix/MethodNode.java
@@ -1,30 +1,29 @@
package perfix;
-import java.util.ArrayList;
-import java.util.List;
+
import java.util.Objects;
public class MethodNode {
- public final String name;
- public final List children;
- public MethodNode parent;
- private MethodInvocation invocation;
+ private final String name;
+ private final long timestamp;
+ private final String threadName;
+ private MethodNode parent;
+ private long duration;
+ private long invocationid;
+
public MethodNode(String name) {
this.name = name;
- this.children = new ArrayList<>();
- }
-
- public void addChild(MethodNode child) {
- children.add(child);
+ this.timestamp = System.nanoTime();
+ this.threadName = Thread.currentThread().getName();
}
public String getName() {
return name;
}
- public List getChildren() {
- return children;
+ public long getId() {
+ return Objects.hash(Thread.currentThread().getId(), timestamp);
}
@Override
@@ -47,11 +46,47 @@ public class MethodNode {
return Objects.hash(name);
}
- public MethodInvocation getInvocation() {
- return invocation;
+ public long getParentId() {
+ if (parent == null) {
+ return 0;
+ } else {
+ return parent.getId();
+ }
}
- public void setInvocation(MethodInvocation invocation) {
- this.invocation = 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();
+ }
}
}
diff --git a/src/main/java/perfix/Registry.java b/src/main/java/perfix/Registry.java
index b074ea0..73ddd83 100644
--- a/src/main/java/perfix/Registry.java
+++ b/src/main/java/perfix/Registry.java
@@ -2,17 +2,65 @@ package perfix;
import perfix.instrument.Util;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentSkipListMap;
+import java.sql.*;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+/**
+ * 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';";
- @SuppressWarnings("unused") //used in generated code
- public static MethodInvocation startJdbc(String name) {
+ 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) {
if (!Util.isFirstExecutionStarted()) {
Util.startExecution();
return start(name);
@@ -21,72 +69,54 @@ public class Registry {
}
}
- @SuppressWarnings("unused")
- public static MethodInvocation start(String name) {
- MethodInvocation methodInvocation = new MethodInvocation(name);
+ public static MethodNode start(String name) {
MethodNode newNode = new MethodNode(name);
MethodNode parent = currentMethod.get();
if (parent != null) {
- parent.addChild(newNode);
- newNode.parent = parent;
- } else {
- callstack.add(newNode);
+ newNode.setParent(parent);
+ newNode.setInvocationId(parent.getInvocationId());
}
currentMethod.set(newNode);
- return methodInvocation;
+ return newNode;
}
@SuppressWarnings("unused")
- public static void stopJdbc(MethodInvocation queryInvocation) {
- if (Util.isFirstExecutionStarted() && queryInvocation != null) {
- stop(queryInvocation);
+ public static void stopJdbc() {
+ MethodNode current = currentMethod.get();
+ if (Util.isFirstExecutionStarted() && current != null) {
+ stop();
Util.endExecution();
}
}
@SuppressWarnings("unused")
- public static void stop(MethodInvocation methodInvocation) {
- if (methodInvocation != null) {
- methodInvocation.registerEndingTime(System.nanoTime());
+ public static void stop() {
+ MethodNode current = currentMethod.get();
+ if (current != null) {
+ current.registerEndingTime(System.nanoTime());
+ queue.add(current);
+ currentMethod.set(current.getParent());
}
- MethodNode methodNode = currentMethod.get();
- methodNode.setInvocation(methodInvocation);
-
- currentMethod.set(methodNode.parent);
}
- 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();
+ 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);
+ }
}
}
diff --git a/src/main/java/perfix/instrument/ClassInstrumentor.java b/src/main/java/perfix/instrument/ClassInstrumentor.java
index 0aa361d..7d2da6f 100644
--- a/src/main/java/perfix/instrument/ClassInstrumentor.java
+++ b/src/main/java/perfix/instrument/ClassInstrumentor.java
@@ -24,7 +24,6 @@ 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 0b03be7..3836187 100644
--- a/src/main/java/perfix/instrument/Instrumentor.java
+++ b/src/main/java/perfix/instrument/Instrumentor.java
@@ -9,7 +9,6 @@ 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 = "$_";
@@ -17,14 +16,12 @@ 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);
@@ -52,9 +49,8 @@ public abstract class Instrumentor {
/* record times at beginning and end of method body*/
void instrumentMethod(CtMethod methodToinstrument, String metricName) {
try {
- methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
- methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");");
- methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
+ methodToinstrument.insertBefore("perfix.Registry.start(" + metricName + ");");
+ methodToinstrument.insertAfter("perfix.Registry.stop();");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
@@ -66,9 +62,8 @@ public abstract class Instrumentor {
* (measured) calls if not handled in a way to prevent this */
void instrumentJdbcCall(CtMethod methodToinstrument, String metricName) {
try {
- methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
- methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(" + metricName + ");");
- methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);");
+ methodToinstrument.insertBefore("perfix.Registry.startJdbc(" + metricName + ");");
+ methodToinstrument.insertAfter("perfix.Registry.stopJdbc();");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
@@ -77,9 +72,8 @@ public abstract class Instrumentor {
/* record times at beginning and end of method body*/
void instrumentJdbcCall(CtMethod methodToinstrument) {
try {
- methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
- methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));");
- methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);");
+ methodToinstrument.insertBefore("perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));");
+ methodToinstrument.insertAfter("perfix.Registry.stopJdbc();");
} 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 77d385a..f7eae88 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, NotFoundException {
+ private void addPerfixFields(CtClass preparedStatementClass) throws CannotCompileException {
// 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 35a8f98..969c203 100644
--- a/src/main/java/perfix/instrument/ServletInstrumentor.java
+++ b/src/main/java/perfix/instrument/ServletInstrumentor.java
@@ -26,11 +26,10 @@ public class ServletInstrumentor extends Instrumentor {
try {
stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> {
try {
- methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
- methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());");
- methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
+ methodToInstrument.insertBefore("perfix.Registry.start($1.getRequestURI());");
+ methodToInstrument.insertAfter("perfix.Registry.stop();");
} 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
deleted file mode 100644
index fa21ddf..0000000
--- a/src/main/java/perfix/server/HTTPServer.java
+++ /dev/null
@@ -1,76 +0,0 @@
-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
deleted file mode 100644
index 7d11d1a..0000000
--- a/src/main/java/perfix/server/json/JSONSerializer.java
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index fa4f42a..0000000
--- a/src/main/java/perfix/server/json/Serializer.java
+++ /dev/null
@@ -1,45 +0,0 @@
-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
deleted file mode 100644
index 1743429..0000000
--- a/src/main/java/perfix/server/json/SerializerCreationException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 9e2346c..0000000
--- a/src/main/java/perfix/server/json/SerializerFactory.java
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index 880acbf..0000000
--- a/src/main/java/perfix/server/json/SynthSerializerFactory.java
+++ /dev/null
@@ -1,350 +0,0 @@
-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("\\[\\]", "");
- }
-}