Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
310 changes: 232 additions & 78 deletions src/main/java/unravel/java/Analyzer.java
Original file line number Diff line number Diff line change
@@ -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<Location, List<ClassNode>> classesByLocation = new LinkedHashMap<>();
private Map<ClassNode, String> classIds = new LinkedHashMap<>();
private Location location;
private ZipFile zipFile;
private LinkedList<Unit> 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<? extends ZipEntry> 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<? extends ZipEntry> 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<? extends ZipEntry> 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++));
}

/**
* <pre>
* 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)
* </pre>
*
* @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));
Expand All @@ -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<Location, List<ClassNode>> 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<Location, List<ClassNode>> 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<Object> 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;
}
}
Loading