diff --git a/README.md b/README.md index e3d9c8a..2486cde 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/9A22t-SS) Разработать standalone приложение, которое имеет следующие возможности: Принимает на вход проект в виде .jar файла diff --git a/build.gradle.kts b/build.gradle.kts index 2b92afd..466180c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,4 +22,13 @@ dependencies { tasks.test { useJUnitPlatform() +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "org.example.Main" + } + + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } \ No newline at end of file diff --git a/src/main/java/org/example/Main.java b/src/main/java/org/example/Main.java new file mode 100644 index 0000000..c371fc6 --- /dev/null +++ b/src/main/java/org/example/Main.java @@ -0,0 +1,64 @@ +package org.example; + +import org.example.analyzer.JarMetricsAnalyzer; +import org.example.model.Metrics; +import org.example.output.ConsoleOutputFormatter; +import org.example.output.JsonOutputFormatter; + +import java.io.File; +import java.io.IOException; + +public class Main { + + public static void main(String[] args) { + if (args.length == 0) { + System.exit(1); + } + + String jarFilePath = args[0]; + String outputFilePath = null; + + for (int i = 1; i < args.length; i++) { + if (args[i].equals("--output") && i + 1 < args.length) { + outputFilePath = args[i + 1]; + i++; + } + } + + File jarFile = new File(jarFilePath); + if (!jarFile.exists()) { + System.err.println("Ошибка: JAR файл не найден: " + jarFilePath); + System.exit(1); + } + + if (!jarFile.isFile()) { + System.err.println("Ошибка: Указанный путь не является файлом: " + jarFilePath); + System.exit(1); + } + + try { + System.out.println("Анализ JAR файла: " + jarFilePath); + + JarMetricsAnalyzer analyzer = new JarMetricsAnalyzer(); + Metrics metrics = analyzer.analyze(jarFilePath); + + ConsoleOutputFormatter consoleFormatter = new ConsoleOutputFormatter(); + consoleFormatter.print(metrics); + + if (outputFilePath != null) { + JsonOutputFormatter jsonFormatter = new JsonOutputFormatter(); + jsonFormatter.writeToFile(metrics, outputFilePath); + System.out.println("Метрики сохранены в файл: " + outputFilePath); + } + + } catch (IOException e) { + System.err.println("Ошибка при анализе JAR файла: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } catch (Exception e) { + System.err.println("Неожиданная ошибка: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/src/main/java/org/example/analyzer/JarMetricsAnalyzer.java b/src/main/java/org/example/analyzer/JarMetricsAnalyzer.java new file mode 100644 index 0000000..f146939 --- /dev/null +++ b/src/main/java/org/example/analyzer/JarMetricsAnalyzer.java @@ -0,0 +1,29 @@ +package org.example.analyzer; + +import org.example.model.Metrics; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarMetricsAnalyzer { + + public Metrics analyze(String jarFilePath) throws IOException { + MetricsCollector collector = new MetricsCollector(); + + try (JarFile jarFile = new JarFile(jarFilePath)) { + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + + if (entry.getName().endsWith(".class")) { + collector.processClass(jarFile.getInputStream(entry)); + } + } + } + + return collector.calculateMetrics(); + } +} diff --git a/src/main/java/org/example/analyzer/MetricsCollector.java b/src/main/java/org/example/analyzer/MetricsCollector.java new file mode 100644 index 0000000..d28da64 --- /dev/null +++ b/src/main/java/org/example/analyzer/MetricsCollector.java @@ -0,0 +1,77 @@ +package org.example.analyzer; + +import org.example.model.ClassInfo; +import org.example.model.Metrics; +import org.example.model.MethodInfo; +import org.example.visitor.*; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class MetricsCollector { + private final Map classInfoMap; + private final InheritanceVisitor inheritanceVisitor; + + public MetricsCollector() { + this.classInfoMap = new HashMap<>(); + this.inheritanceVisitor = new InheritanceVisitor(classInfoMap); + } + + public void processClass(InputStream classStream) throws IOException { + ClassReader classReader = new ClassReader(classStream); + String className = classReader.getClassName(); + + ClassInfo classInfo = classInfoMap.computeIfAbsent(className, + k -> new ClassInfo(className, classReader.getSuperName())); + + ClassMetricsVisitor metricsVisitor = new ClassMetricsVisitor(classInfo, classInfoMap); + classReader.accept(metricsVisitor, 0); + + classInfo.setOverriddenMethodsCount(metricsVisitor.getOverriddenMethodsCount()); + } + + public Metrics calculateMetrics() { + Metrics metrics = new Metrics(); + + if (classInfoMap.isEmpty()) { + return metrics; + } + + int totalInheritanceDepth = 0; + int maxInheritanceDepth = 0; + int totalAbcMetric = 0; + int totalFields = 0; + int totalOverriddenMethods = 0; + int classCount = classInfoMap.size(); + + for (ClassInfo classInfo : classInfoMap.values()) { + int depth = inheritanceVisitor.calculateInheritanceDepth(classInfo.getName()); + classInfo.setInheritanceDepth(depth); + totalInheritanceDepth += depth; + maxInheritanceDepth = Math.max(maxInheritanceDepth, depth); + + for (MethodInfo method : classInfo.getMethods()) { + totalAbcMetric += method.getAssignmentCount(); + } + + totalFields += classInfo.getFieldCount(); + + totalOverriddenMethods += classInfo.getOverriddenMethodsCount(); + } + + metrics.setMaxInheritanceDepth(maxInheritanceDepth); + metrics.setAvgInheritanceDepth((double) totalInheritanceDepth / classCount); + metrics.setTotalAbcMetric(totalAbcMetric); + metrics.setAvgOverriddenMethods((double) totalOverriddenMethods / classCount); + metrics.setAvgFieldsPerClass((double) totalFields / classCount); + + return metrics; + } + + public Map getClassInfoMap() { + return classInfoMap; + } +} diff --git a/src/main/java/org/example/model/ClassInfo.java b/src/main/java/org/example/model/ClassInfo.java new file mode 100644 index 0000000..c7e05ac --- /dev/null +++ b/src/main/java/org/example/model/ClassInfo.java @@ -0,0 +1,73 @@ +package org.example.model; + +import java.util.ArrayList; +import java.util.List; + +public class ClassInfo { + private final String name; + private final String superName; + private final List methods; + private int fieldCount; + private int inheritanceDepth; + private int overriddenMethodsCount; + + public ClassInfo(String name, String superName) { + this.name = name; + this.superName = superName; + this.methods = new ArrayList<>(); + this.fieldCount = 0; + this.inheritanceDepth = -1; + this.overriddenMethodsCount = 0; + } + + public String getName() { + return name; + } + + public String getSuperName() { + return superName; + } + + public List getMethods() { + return methods; + } + + public void addMethod(MethodInfo method) { + this.methods.add(method); + } + + public int getFieldCount() { + return fieldCount; + } + + public void setFieldCount(int fieldCount) { + this.fieldCount = fieldCount; + } + + public int getInheritanceDepth() { + return inheritanceDepth; + } + + public void setInheritanceDepth(int inheritanceDepth) { + this.inheritanceDepth = inheritanceDepth; + } + + public int getOverriddenMethodsCount() { + return overriddenMethodsCount; + } + + public void setOverriddenMethodsCount(int overriddenMethodsCount) { + this.overriddenMethodsCount = overriddenMethodsCount; + } + + @Override + public String toString() { + return "ClassInfo{" + + "name='" + name + '\'' + + ", superName='" + superName + '\'' + + ", methods=" + methods.size() + + ", fieldCount=" + fieldCount + + ", inheritanceDepth=" + inheritanceDepth + + '}'; + } +} diff --git a/src/main/java/org/example/model/MethodInfo.java b/src/main/java/org/example/model/MethodInfo.java new file mode 100644 index 0000000..5a13f33 --- /dev/null +++ b/src/main/java/org/example/model/MethodInfo.java @@ -0,0 +1,46 @@ +package org.example.model; + +public class MethodInfo { + private final String name; + private final String descriptor; + private int assignmentCount; + + public MethodInfo(String name, String descriptor) { + this.name = name; + this.descriptor = descriptor; + this.assignmentCount = 0; + } + + public String getName() { + return name; + } + + public String getDescriptor() { + return descriptor; + } + + public int getAssignmentCount() { + return assignmentCount; + } + + public void setAssignmentCount(int assignmentCount) { + this.assignmentCount = assignmentCount; + } + + public void incrementAssignmentCount() { + this.assignmentCount++; + } + + public String getSignature() { + return name + descriptor; + } + + @Override + public String toString() { + return "MethodInfo{" + + "name='" + name + '\'' + + ", descriptor='" + descriptor + '\'' + + ", assignmentCount=" + assignmentCount + + '}'; + } +} diff --git a/src/main/java/org/example/model/Metrics.java b/src/main/java/org/example/model/Metrics.java new file mode 100644 index 0000000..9d58a87 --- /dev/null +++ b/src/main/java/org/example/model/Metrics.java @@ -0,0 +1,68 @@ +package org.example.model; + +public class Metrics { + private int maxInheritanceDepth; + private double avgInheritanceDepth; + private int totalAbcMetric; + private double avgOverriddenMethods; + private double avgFieldsPerClass; + + public Metrics() { + this.maxInheritanceDepth = 0; + this.avgInheritanceDepth = 0.0; + this.totalAbcMetric = 0; + this.avgOverriddenMethods = 0.0; + this.avgFieldsPerClass = 0.0; + } + + public int getMaxInheritanceDepth() { + return maxInheritanceDepth; + } + + public void setMaxInheritanceDepth(int maxInheritanceDepth) { + this.maxInheritanceDepth = maxInheritanceDepth; + } + + public double getAvgInheritanceDepth() { + return avgInheritanceDepth; + } + + public void setAvgInheritanceDepth(double avgInheritanceDepth) { + this.avgInheritanceDepth = avgInheritanceDepth; + } + + public int getTotalAbcMetric() { + return totalAbcMetric; + } + + public void setTotalAbcMetric(int totalAbcMetric) { + this.totalAbcMetric = totalAbcMetric; + } + + public double getAvgOverriddenMethods() { + return avgOverriddenMethods; + } + + public void setAvgOverriddenMethods(double avgOverriddenMethods) { + this.avgOverriddenMethods = avgOverriddenMethods; + } + + public double getAvgFieldsPerClass() { + return avgFieldsPerClass; + } + + public void setAvgFieldsPerClass(double avgFieldsPerClass) { + this.avgFieldsPerClass = avgFieldsPerClass; + } + + @Override + public String toString() { + return "Metrics{" + + "maxInheritanceDepth=" + maxInheritanceDepth + + ", avgInheritanceDepth=" + avgInheritanceDepth + + ", totalAbcMetric=" + totalAbcMetric + + ", avgOverriddenMethods=" + avgOverriddenMethods + + ", avgFieldsPerClass=" + avgFieldsPerClass + + '}'; + } +} diff --git a/src/main/java/org/example/output/ConsoleOutputFormatter.java b/src/main/java/org/example/output/ConsoleOutputFormatter.java new file mode 100644 index 0000000..9cfdc28 --- /dev/null +++ b/src/main/java/org/example/output/ConsoleOutputFormatter.java @@ -0,0 +1,19 @@ +package org.example.output; + +import org.example.model.Metrics; + +public class ConsoleOutputFormatter { + + public void print(Metrics metrics) { + String separator = "============================================================"; + System.out.println(separator); + System.out.println("МЕТРИКИ АНАЛИЗА БАЙТКОДА"); + System.out.println(separator); + System.out.println("Максимальная глубина наследования: " + metrics.getMaxInheritanceDepth()); + System.out.println("Средняя глубина наследования: " + metrics.getAvgInheritanceDepth()); + System.out.println("Метрика ABC (присваивания): " + metrics.getTotalAbcMetric()); + System.out.println("Среднее количество переопределенных методов: " + metrics.getAvgOverriddenMethods()); + System.out.println("Среднее количество полей в классе: " + metrics.getAvgFieldsPerClass()); + System.out.println(separator); + } +} diff --git a/src/main/java/org/example/output/JsonOutputFormatter.java b/src/main/java/org/example/output/JsonOutputFormatter.java new file mode 100644 index 0000000..187f929 --- /dev/null +++ b/src/main/java/org/example/output/JsonOutputFormatter.java @@ -0,0 +1,36 @@ +package org.example.output; + +import org.example.model.Metrics; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Locale; + +public class JsonOutputFormatter { + + public void writeToFile(Metrics metrics, String outputPath) throws IOException { + String json = toJson(metrics); + + try (FileWriter writer = new FileWriter(outputPath)) { + writer.write(json); + } + } + + public String toJson(Metrics metrics) { + StringBuilder json = new StringBuilder(); + json.append("{\n"); + json.append(String.format(Locale.US, " \"maxInheritanceDepth\": %d,\n", + metrics.getMaxInheritanceDepth())); + json.append(String.format(Locale.US, " \"avgInheritanceDepth\": %.2f,\n", + metrics.getAvgInheritanceDepth())); + json.append(String.format(Locale.US, " \"totalAbcMetric\": %d,\n", + metrics.getTotalAbcMetric())); + json.append(String.format(Locale.US, " \"avgOverriddenMethods\": %.2f,\n", + metrics.getAvgOverriddenMethods())); + json.append(String.format(Locale.US, " \"avgFieldsPerClass\": %.2f\n", + metrics.getAvgFieldsPerClass())); + json.append("}"); + + return json.toString(); + } +} diff --git a/src/main/java/org/example/visitor/AbcMethodVisitor.java b/src/main/java/org/example/visitor/AbcMethodVisitor.java new file mode 100644 index 0000000..07a4343 --- /dev/null +++ b/src/main/java/org/example/visitor/AbcMethodVisitor.java @@ -0,0 +1,24 @@ +package org.example.visitor; + +import org.example.model.MethodInfo; +import org.objectweb.asm.MethodVisitor; + +import static org.objectweb.asm.Opcodes.*; + +public class AbcMethodVisitor extends MethodVisitor { + private final MethodInfo methodInfo; + + public AbcMethodVisitor(MethodVisitor methodVisitor, MethodInfo methodInfo) { + super(ASM9, methodVisitor); + this.methodInfo = methodInfo; + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (opcode == ISTORE || opcode == LSTORE || opcode == FSTORE || + opcode == DSTORE || opcode == ASTORE) { + methodInfo.incrementAssignmentCount(); + } + super.visitVarInsn(opcode, var); + } +} diff --git a/src/main/java/org/example/visitor/ClassMetricsVisitor.java b/src/main/java/org/example/visitor/ClassMetricsVisitor.java new file mode 100644 index 0000000..27d97be --- /dev/null +++ b/src/main/java/org/example/visitor/ClassMetricsVisitor.java @@ -0,0 +1,132 @@ +package org.example.visitor; + +import org.example.model.ClassInfo; +import org.example.model.MethodInfo; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.objectweb.asm.Opcodes.*; + +public class ClassMetricsVisitor extends ClassVisitor { + private final ClassInfo classInfo; + private final Map classInfoMap; + private int overriddenMethodsCount; + + public ClassMetricsVisitor(ClassInfo classInfo, Map classInfoMap) { + super(ASM9); + this.classInfo = classInfo; + this.classInfoMap = classInfoMap; + this.overriddenMethodsCount = 0; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + ClassInfo existingClassInfo = classInfoMap.computeIfAbsent(name, k -> new ClassInfo(name, superName)); + if (existingClassInfo.getSuperName() == null && superName != null) { + classInfoMap.put(name, new ClassInfo(name, superName)); + } + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + classInfo.setFieldCount(classInfo.getFieldCount() + 1); + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + MethodInfo methodInfo = new MethodInfo(name, descriptor); + classInfo.addMethod(methodInfo); + + if (!name.equals("") && !name.equals("")) { + String methodSignature = name + descriptor; + if (isMethodOverridden(methodSignature, classInfo.getSuperName())) { + overriddenMethodsCount++; + } + } + + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + return new AbcMethodVisitor(mv, methodInfo); + } + + private boolean isMethodOverridden(String methodSignature, String superClassName) { + if (superClassName == null || superClassName.equals("java/lang/Object")) { + return isObjectMethod(methodSignature); + } + + ClassInfo superClassInfo = classInfoMap.get(superClassName); + + if (superClassInfo != null) { + Set superMethodSignatures = getSuperClassMethodSignatures(superClassInfo); + if (superMethodSignatures.contains(methodSignature)) { + return true; + } + return isMethodOverridden(methodSignature, superClassInfo.getSuperName()); + } else { + try { + Set externalSuperMethods = getExternalClassMethods(superClassName); + return externalSuperMethods.contains(methodSignature); + } catch (Exception e) { + return false; + } + } + } + + private boolean isObjectMethod(String methodSignature) { + return methodSignature.equals("toString()Ljava/lang/String;") || + methodSignature.equals("equals(Ljava/lang/Object;)Z") || + methodSignature.equals("hashCode()I") || + methodSignature.equals("clone()Ljava/lang/Object;") || + methodSignature.equals("finalize()V"); + } + + private Set getSuperClassMethodSignatures(ClassInfo superClassInfo) { + Set signatures = new HashSet<>(); + for (MethodInfo method : superClassInfo.getMethods()) { + signatures.add(method.getSignature()); + } + return signatures; + } + + private Set getExternalClassMethods(String className) throws IOException { + Set methodSignatures = new HashSet<>(); + + String resourceName = className + ".class"; + InputStream classStream = ClassLoader.getSystemResourceAsStream(resourceName); + + if (classStream != null) { + try { + ClassReader cr = new ClassReader(classStream); + cr.accept(new ClassVisitor(ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + if (!name.equals("") && !name.equals("")) { + methodSignatures.add(name + descriptor); + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + }, ClassReader.SKIP_CODE); + } finally { + classStream.close(); + } + } + + return methodSignatures; + } + + public int getOverriddenMethodsCount() { + return overriddenMethodsCount; + } +} diff --git a/src/main/java/org/example/visitor/InheritanceVisitor.java b/src/main/java/org/example/visitor/InheritanceVisitor.java new file mode 100644 index 0000000..308a75b --- /dev/null +++ b/src/main/java/org/example/visitor/InheritanceVisitor.java @@ -0,0 +1,65 @@ +package org.example.visitor; + +import org.example.model.ClassInfo; +import org.objectweb.asm.ClassVisitor; + +import java.util.Map; + +import static org.objectweb.asm.Opcodes.ASM9; + +public class InheritanceVisitor extends ClassVisitor { + private final Map classInfoMap; + + public InheritanceVisitor(Map classInfoMap) { + super(ASM9); + this.classInfoMap = classInfoMap; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + ClassInfo classInfo = classInfoMap.computeIfAbsent(name, k -> new ClassInfo(name, superName)); + + if (classInfo.getSuperName() == null && superName != null) { + classInfoMap.put(name, new ClassInfo(name, superName)); + } + + super.visit(version, access, name, signature, superName, interfaces); + } + + public int calculateInheritanceDepth(String className) { + if (className == null || className.equals("java/lang/Object")) { + return 0; + } + + ClassInfo classInfo = classInfoMap.get(className); + + if (classInfo != null && classInfo.getInheritanceDepth() != -1) { + return classInfo.getInheritanceDepth(); + } + + if (classInfo == null) { + try { + String javaClassName = className.replace('/', '.'); + Class clazz = Class.forName(javaClassName); + Class superClass = clazz.getSuperclass(); + + if (superClass == null) { + return 0; + } + + String superClassName = superClass.getName().replace('.', '/'); + int depth = 1 + calculateInheritanceDepth(superClassName); + return depth; + } catch (ClassNotFoundException e) { + return 1; + } + } + + String superName = classInfo.getSuperName(); + int depth = 1 + calculateInheritanceDepth(superName); + + classInfo.setInheritanceDepth(depth); + + return depth; + } +}