From 08ca64743439690c3f553d004656a658c1596e07 Mon Sep 17 00:00:00 2001 From: Semyen Soldatenko Date: Fri, 12 Feb 2021 22:02:07 +0100 Subject: [PATCH] Add WAR analyzer In this commit I introduced structure of json with units/classes/calls and added WAR file processor. --- build.gradle.kts | 5 + src/main/java/unravel/java/Analyzer.java | 310 +++++++++++++----- .../unravel/java/{Location.java => Unit.java} | 39 +-- src/main/java/unravel/java/UnitType.java | 6 + src/test/java/unravel/java/AppTest.java | 4 +- 5 files changed, 264 insertions(+), 100 deletions(-) rename src/main/java/unravel/java/{Location.java => Unit.java} (52%) create mode 100644 src/main/java/unravel/java/UnitType.java diff --git a/build.gradle.kts b/build.gradle.kts index b79807c..8fb966a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,11 @@ plugins { application } +java { + sourceCompatibility=JavaVersion.VERSION_11 + targetCompatibility=JavaVersion.VERSION_11 +} + repositories { // Use jcenter for resolving dependencies. // You can declare any Maven/Ivy/file repository here. diff --git a/src/main/java/unravel/java/Analyzer.java b/src/main/java/unravel/java/Analyzer.java index 20faccb..a88b8e5 100644 --- a/src/main/java/unravel/java/Analyzer.java +++ b/src/main/java/unravel/java/Analyzer.java @@ -1,77 +1,134 @@ package unravel.java; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; import javax.json.*; import javax.json.stream.JsonGenerator; import java.io.*; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.zip.ZipEntry; +import java.util.zip.ZipException; import java.util.zip.ZipFile; public class Analyzer { - private int num = 0; - private Map> classesByLocation = new LinkedHashMap<>(); - private Map classIds = new LinkedHashMap<>(); - private Location location; - private ZipFile zipFile; + private LinkedList units = new LinkedList<>(); + + private Unit currentUnit; public void analyzeFile(Path path) throws IOException { - zipFile = new ZipFile(path.toFile()); + if (isWarFile(path)) { + analyzeWarFile(path, path.getFileName().toString()); + } else if (isJarFile(path)) { + analyzeJarFile(path, path.getFileName().toString()); + } + } + + private boolean isWarFile(Path path) throws IOException { + try (ZipFile zipFile = new ZipFile(path.toFile())) { + ZipEntry webInf = zipFile.getEntry("WEB-INF"); + return webInf != null; + } catch (ZipException | FileNotFoundException e) { + return false; + } + } + + private boolean isJarFile(Path path) throws IOException { + try (ZipFile zipFile = new ZipFile(path.toFile())) { + ZipEntry webInf = zipFile.getEntry("WEB-INF"); + ZipEntry metaInf = zipFile.getEntry("META-INF"); + return webInf == null && metaInf != null; + } catch (ZipException | FileNotFoundException e) { + return false; + } + } + + private void analyzeWarFile(Path path, String unitName) throws IOException { + Unit prevUnit = currentUnit; + try (ZipFile zipFile = new ZipFile(path.toFile())) { + Unit war = new Unit(UnitType.WAR, unitName); + units.add(war); + currentUnit = war; + analyzeWebInfClasses(zipFile); + analyzeWebInfLibs(zipFile); + } finally { + currentUnit = prevUnit; + } + } + + private void analyzeWebInfClasses(ZipFile zipFile) throws IOException { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + if (zipEntry.getName().startsWith("WEB-INF/classes/")) { + parseZipEntry(zipEntry, zipFile); + } + } + } + + private void analyzeWebInfLibs(ZipFile zipFile) throws IOException { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + if (zipEntry.getName().matches("WEB-INF/lib/.*\\.jar")) { + Path jar = Files.createTempFile("unravel-java-tmp", ".jar"); + Unit unit = currentUnit; + try { + Files.copy(zipFile.getInputStream(zipEntry), jar, StandardCopyOption.REPLACE_EXISTING); + String jarName = Path.of(zipEntry.getName()).getFileName().toString(); + analyzeJarFile(jar, jarName); + } finally { + currentUnit = unit; + Files.deleteIfExists(jar); + } + } + } + } + + private void analyzeJarFile(Path path, String unitName) throws IOException { + Unit unit = currentUnit; try { - location = new Location("l" + num, "jar", zipFile.getName()); - num++; - classesByLocation.put(location, new ArrayList<>()); + Unit jar = new Unit(UnitType.JAR, unitName); + units.add(jar); + currentUnit = jar; + + ZipFile zipFile = new ZipFile(path.toFile()); Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry zipEntry = entries.nextElement(); - parseZipEntry(zipEntry); + parseZipEntry(zipEntry, zipFile); } } finally { - zipFile.close(); - zipFile = null; - location = null; + currentUnit = unit; } } - public void parseZipEntry(ZipEntry entry) throws IOException { + public void parseZipEntry(ZipEntry entry, ZipFile zipFile) throws IOException { if (entry.isDirectory()) { // Skipping. It is just a structure in jar file } else if (entry.getName().endsWith(".class")) { ClassReader classReader = new ClassReader(zipFile.getInputStream(entry)); ClassNode klazz = new ClassNode(); classReader.accept(klazz, 0); - classesByLocation.get(location).add(klazz); + currentUnit.addClass(klazz); } else { // skipping other files } } - private String getClassId(ClassNode node) { - return classIds.computeIfAbsent(node, n -> "c" + (num++)); - } - - /** - *
-     * Json should contain
-     * - classes
-     * - relations (extends, implements)
-     * - methods
-     * - references (types used as parameter types or method's return value type)
-     * - calls to another classes
-     * - stereotypes (like request scoped or REST resource)
-     * 
- * - * @param out output stream for json - */ public void writeJson(OutputStream out) { JsonObject report = Json.createObjectBuilder() - .add("locations", buildLocationsJson()) - .add("classes", buildClassesJson()) - .add("refs", buildReferences()) + .add("units", buildUnitsJson()) .build(); JsonWriterFactory factory = Json.createWriterFactory(Map.of(JsonGenerator.PRETTY_PRINTING, true)); @@ -80,64 +137,161 @@ public void writeJson(OutputStream out) { } } - private JsonArray buildLocationsJson() { + private JsonArray buildUnitsJson() { JsonArrayBuilder builder = Json.createArrayBuilder(); - for (Location location : classesByLocation.keySet()) { - JsonObjectBuilder locationBuilder = Json.createObjectBuilder() - .add("id", location.getId()) - .add("type", location.getType()) - .add("name", location.getName()); - builder.add(locationBuilder); + for (Unit unit : units) { + JsonObjectBuilder unitBuilder = Json.createObjectBuilder() + .add("type", unit.getType().toString()) + .add("name", unit.getName()) + .add("classes", buildClassesJson(unit)); + + builder.add(unitBuilder); } return builder.build(); } - private JsonArray buildClassesJson() { + private JsonArray buildClassesJson(Unit unit) { JsonArrayBuilder builder = Json.createArrayBuilder(); - for (Map.Entry> entry : classesByLocation.entrySet()) { - for (ClassNode classNode : entry.getValue()) { - JsonObjectBuilder classBuilder = Json.createObjectBuilder() - .add("id", getClassId(classNode)) - .add("name", classNode.name) - .add("location", entry.getKey().getId()); - builder.add(classBuilder); + for (ClassNode classNode : unit.getKlazzes()) { + JsonObjectBuilder classBuilder = Json.createObjectBuilder() + .add("name", classNode.name); + if (classNode.superName != null && !"java/lang/Object".equals(classNode.superName)) { + classBuilder.add("extends", classNode.superName); + } + if (classNode.outerClass != null) { + classBuilder.add("innerOf", classNode.outerClass); + } + if (classNode.interfaces.size() > 0) { + classBuilder.add("implements", Json.createArrayBuilder(classNode.interfaces)); } - } - return builder.build(); - } - private JsonArray buildReferences() { - JsonArrayBuilder builder = Json.createArrayBuilder(); - for (Map.Entry> entry : classesByLocation.entrySet()) { - for (ClassNode classNode : entry.getValue()) { - - if (classNode.superName != null && !"java/lang/Object".equals(classNode.superName)) { - JsonObjectBuilder extendsBuilder = Json.createObjectBuilder() - .add("from", getClassId(classNode)) - .add("type", "extends") - .add("to", ""); - builder.add(extendsBuilder); + if (classNode.visibleAnnotations != null || classNode.invisibleAnnotations != null) { + JsonArrayBuilder annotationsBuilder = Json.createArrayBuilder(); + if (classNode.visibleAnnotations != null) { + for (AnnotationNode annotation : classNode.visibleAnnotations) { + annotationsBuilder.add(buildAnnotationJson(annotation)); + } } - - if (classNode.interfaces != null && classNode.interfaces.size() > 0) { - for (String anInterface : classNode.interfaces) { - JsonObjectBuilder extendsBuilder = Json.createObjectBuilder() - .add("from", getClassId(classNode)) - .add("type", "implements") - .add("to", ""); - builder.add(extendsBuilder); + if (classNode.invisibleAnnotations != null) { + for (AnnotationNode annotation : classNode.invisibleAnnotations) { + annotationsBuilder.add(buildAnnotationJson(annotation)); } } + classBuilder.add("annotations", annotationsBuilder); + } + + if (classNode.fields.size() > 0) { + JsonObjectBuilder fieldsBuilder = Json.createObjectBuilder(); + for (FieldNode field : classNode.fields) { + fieldsBuilder.add(field.name, Type.getType(field.desc).getInternalName()); + } + classBuilder.add("fields", fieldsBuilder); + } + + if (classNode.methods.size() > 0) { + JsonArrayBuilder methodsBuilder = Json.createArrayBuilder(); + + for (MethodNode method : classNode.methods) { + JsonObjectBuilder methodBuilder = Json.createObjectBuilder(); + methodBuilder.add("name", method.name); + + Type returnType = Type.getReturnType(method.desc); + if (returnType.getSort() == Type.OBJECT) { + methodBuilder.add("return", returnType.getInternalName()); + } else if (returnType.getSort() == Type.ARRAY) { + methodBuilder.add("return", returnType.getElementType().getInternalName()); + } - if (classNode.nestHostClass != null) { - JsonObjectBuilder extendsBuilder = Json.createObjectBuilder() - .add("from", getClassId(classNode)) - .add("type", "innerOf") - .add("to", ""); - builder.add(extendsBuilder); + Type[] argumentTypes = Type.getArgumentTypes(method.desc); + if (argumentTypes.length > 0) { + JsonArrayBuilder argumentsBuilder = Json.createArrayBuilder(); + for (Type argumentType : argumentTypes) { + argumentsBuilder.add(argumentType.getInternalName()); + } + methodBuilder.add("arguments", argumentsBuilder); + } + + JsonArrayBuilder callsBuilder = Json.createArrayBuilder(); + for (AbstractInsnNode i : method.instructions) { + if (i instanceof MethodInsnNode) { + MethodInsnNode m = (MethodInsnNode) i; + if (!m.owner.equals(classNode.name)) { + callsBuilder.add(Json.createObjectBuilder() + .add("class", m.owner) + .add("method", m.name)); + } + } + } + JsonArray callsJson = callsBuilder.build(); + if (callsJson.size() > 0) { + methodBuilder.add("calls", callsJson); + } + methodsBuilder.add(methodBuilder); } + classBuilder.add("methods", methodsBuilder); } + builder.add(classBuilder); } return builder.build(); } + + private JsonObjectBuilder buildAnnotationJson(AnnotationNode annotation) { + Type a = Type.getType(annotation.desc); + JsonObjectBuilder annotationBuilder = Json.createObjectBuilder(); + annotationBuilder.add("type", a.getInternalName()); + if (annotation.values != null && annotation.values.size() > 0) { + annotationBuilder.add("values", buildAnnotationValuesJson(annotation.values)); + } + return annotationBuilder; + } + + private JsonObjectBuilder buildAnnotationValuesJson(List values) { + JsonObjectBuilder valuesBuilder = Json.createObjectBuilder(); + + for (int i = 0; i < values.size(); i += 2) { + String name = (String) values.get(i); + Object value = values.get(i + 1); + + if (value instanceof Byte) { + valuesBuilder.add(name, (Byte) value); + } else if (value instanceof Boolean) { + valuesBuilder.add(name, (Boolean) value); + } else if (value instanceof Character) { + valuesBuilder.add(name, (Character) value); + } else if (value instanceof Short) { + valuesBuilder.add(name, (Short) value); + } else if (value instanceof Integer) { + valuesBuilder.add(name, (Integer) value); + } else if (value instanceof Long) { + valuesBuilder.add(name, (Long) value); + } else if (value instanceof Float) { + valuesBuilder.add(name, (Float) value); + } else if (value instanceof Double) { + valuesBuilder.add(name, (Double) value); + } else if (value instanceof String) { + valuesBuilder.add(name, (String) value); + } else + if (value instanceof Type) { + valuesBuilder.add(name, ((Type) value).getInternalName()); // class + } else if (value instanceof String[] && ((String[]) value).length == 2) { + String enumType = ((String[]) value)[0]; + String enumValue = ((String[]) value)[1]; + valuesBuilder.add(name, enumValue); + } else if (value instanceof AnnotationNode) { + valuesBuilder.add(name, buildAnnotationJson((AnnotationNode) value)); + } else if (value instanceof List) { + JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + for (Object v : (List) value) { + JsonValue valueElement = buildAnnotationValuesJson(Arrays.asList("fake-name", v)) + .build() + .get("fake-name"); + arrayBuilder.add(valueElement); + } + valuesBuilder.add(name, arrayBuilder); + } else { + throw new IllegalStateException("Unknown Annotation parameter"); + } + } + return valuesBuilder; + } } diff --git a/src/main/java/unravel/java/Location.java b/src/main/java/unravel/java/Unit.java similarity index 52% rename from src/main/java/unravel/java/Location.java rename to src/main/java/unravel/java/Unit.java index b1c661d..76e48c2 100644 --- a/src/main/java/unravel/java/Location.java +++ b/src/main/java/unravel/java/Unit.java @@ -1,9 +1,14 @@ package unravel.java; +import org.objectweb.asm.tree.ClassNode; + +import java.util.LinkedList; +import java.util.List; + /** *
- * Location of class. Idea (not everything described here is implemented or designed).
+ * Part of analyzed application
  * Example:
  * 1. If jar file is analyzed
  *      - one locations for the jar itself
@@ -11,35 +16,23 @@
  * 2. If war is analyzed
  *      - one location for classes from META-INF/classes
  *      - one location per each jar from META-INF/libs
- * 3. If project is analyzed (we can know dependencies)
- *      - one location per each module in project
- *      - one location per each dependency
  * 
*/ -public class Location { - private String id; - private String type; +public class Unit { + private UnitType type; private String name; + private List klazzes = new LinkedList<>(); - public Location(String id, String type, String name) { - this.id = id; + public Unit(UnitType type, String name) { this.type = type; this.name = name; } - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getType() { + public UnitType getType() { return type; } - public void setType(String type) { + public void setType(UnitType type) { this.type = type; } @@ -50,4 +43,12 @@ public String getName() { public void setName(String name) { this.name = name; } + + public void addClass(ClassNode klazz) { + klazzes.add(klazz); + } + + public List getKlazzes() { + return klazzes; + } } diff --git a/src/main/java/unravel/java/UnitType.java b/src/main/java/unravel/java/UnitType.java new file mode 100644 index 0000000..3b1820e --- /dev/null +++ b/src/main/java/unravel/java/UnitType.java @@ -0,0 +1,6 @@ +package unravel.java; + +public enum UnitType { + WAR, + JAR +} diff --git a/src/test/java/unravel/java/AppTest.java b/src/test/java/unravel/java/AppTest.java index 7d8ca0b..dbd1805 100644 --- a/src/test/java/unravel/java/AppTest.java +++ b/src/test/java/unravel/java/AppTest.java @@ -46,8 +46,6 @@ void mainShouldAnalizeFile() throws Exception { JsonObject json = Json.createReader(new ByteArrayInputStream(testOutput.toByteArray())) .readObject(); - assertEquals(1, json.get("locations").asJsonArray().size()); - assertEquals(0, json.get("classes").asJsonArray().size()); - assertEquals(0, json.get("refs").asJsonArray().size()); + assertEquals(0, json.get("units").asJsonArray().size()); } }