diff --git a/.github/workflows/analysis-of-endpoint-connections.yml b/.github/workflows/analysis-of-endpoint-connections.yml index 25849bddd012..2116605bea0a 100644 --- a/.github/workflows/analysis-of-endpoint-connections.yml +++ b/.github/workflows/analysis-of-endpoint-connections.yml @@ -1,12 +1,8 @@ +name: Analysis of Endpoint Connections + on: workflow_dispatch: - pull_request: - types: - - opened - - synchronize - paths: - - 'src/main/java/**' - - 'src/main/webapp/**' + push: # Keep in sync with build.yml and test.yml and codeql-analysis.yml env: @@ -15,7 +11,7 @@ env: java: 21 jobs: - analysis-of-endpoint-connections: + Parse-rest-calls-and-endpoints: timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -24,39 +20,70 @@ jobs: with: fetch-depth: 0 - - name: Get list of modified files - run: | - git diff --name-only origin/${{ github.event.pull_request.base.ref }} HEAD > modified_files.txt + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '${{ env.java }}' + distribution: 'temurin' + cache: 'gradle' - # Analyze the client sided REST-API calls - - name: Set up Node.js + - name: Set up node.js uses: actions/setup-node@v4 with: node-version: '${{ env.node }}' - - name: Install and compile TypeScript + - name: Parse client sided REST-API calls run: | - cd supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/ npm install - tsc -p tsconfig.analysisOfEndpointConnections.json - - - name: Run analysis-of-endpoint-connections-client - run: | + tsc -p supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/tsconfig.analysisOfEndpointConnections.json node supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/AnalysisOfEndpointConnectionsClient.js - - name: Upload JSON file + - name: Parse server sided Endpoints + run: ./gradlew :supporting_scripts:analysis-of-endpoint-connections:runEndpointParser + + - name: Upload parsing results uses: actions/upload-artifact@v4 with: - name: rest-calls-json - path: supporting_scripts/analysis-of-endpoint-connections/restCalls.json + name: REST API Parsing Results + path: | + supporting_scripts/analysis-of-endpoint-connections/endpoints.json + supporting_scripts/analysis-of-endpoint-connections/restCalls.json + + Analysis-of-endpoint-connections: + needs: Parse-rest-calls-and-endpoints + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - # Analyze the server sided endpoints - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '${{ env.java }}' + cache: 'gradle' - - name: Run analysis-of-endpoint-connections - run: | - ./gradlew :supporting_scripts:analysis-of-endpoint-connections:run --args="$(cat modified_files.txt)" + - name: Download JSON files + uses: actions/download-artifact@v4 + with: + name: REST API Parsing Results + path: supporting_scripts/analysis-of-endpoint-connections/ + + - name: Analyze endpoints + run: + ./gradlew :supporting_scripts:analysis-of-endpoint-connections:runEndpointAnalysis + + - name: Analyze rest calls + run: + ./gradlew :supporting_scripts:analysis-of-endpoint-connections:runRestCallAnalysis + + - name: Upload analysis results + uses: actions/upload-artifact@v4 + with: + name: Endpoint and REST Call Analysis Results + path: | + supporting_scripts/analysis-of-endpoint-connections/endpointAnalysisResult.json + supporting_scripts/analysis-of-endpoint-connections/restCallAnalysisResult.json diff --git a/settings.gradle b/settings.gradle index b9f9d365979c..c99752089fcd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,7 @@ pluginManagement { rootProject.name = 'Artemis' +// needed for rest call and endpoint analysis include 'supporting_scripts:analysis-of-endpoint-connections' // needed for programming exercise templates diff --git a/supporting_scripts/analysis-of-endpoint-connections/build.gradle b/supporting_scripts/analysis-of-endpoint-connections/build.gradle index ef2fa37c2f5a..0e41c785d65c 100644 --- a/supporting_scripts/analysis-of-endpoint-connections/build.gradle +++ b/supporting_scripts/analysis-of-endpoint-connections/build.gradle @@ -13,20 +13,26 @@ repositories { evaluationDependsOn(':') dependencies { - implementation rootProject.ext.qDoxVersionReusable + implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.0' + implementation 'com.github.javaparser:javaparser-core:3.26.0' + implementation 'com.github.javaparser:javaparser-core-serialization:3.26.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' implementation rootProject.ext.springBootStarterWeb + implementation 'org.slf4j:slf4j-api:1.7.32' + implementation 'ch.qos.logback:logback-classic:1.2.6' } -test { - useJUnitPlatform() +task runEndpointParser(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'de.tum.cit.endpointanalysis.EndpointParser' } -application { - mainClassName = 'de.tum.cit.endpointanalysis.AnalysisOfEndpointConnections' +task runEndpointAnalysis(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'de.tum.cit.endpointanalysis.EndpointAnalyzer' } -run { - if (project.hasProperty('appArgs')) { - args = project.appArgs.split(' ') - } +task runRestCallAnalysis(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'de.tum.cit.endpointanalysis.RestCallAnalyzer' } diff --git a/supporting_scripts/analysis-of-endpoint-connections/eslint.conjig.json b/supporting_scripts/analysis-of-endpoint-connections/eslint.config.json similarity index 72% rename from supporting_scripts/analysis-of-endpoint-connections/eslint.conjig.json rename to supporting_scripts/analysis-of-endpoint-connections/eslint.config.json index 0b47d7836035..393e1c0972cf 100644 --- a/supporting_scripts/analysis-of-endpoint-connections/eslint.conjig.json +++ b/supporting_scripts/analysis-of-endpoint-connections/eslint.config.json @@ -10,10 +10,5 @@ "jsx": true } }, - "rules": {}, - "settings": { - "react": { - "version": "detect" - } - } + "rules": {} } diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/AnalysisOfEndpointConnections.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/AnalysisOfEndpointConnections.java deleted file mode 100644 index b47b91d680d8..000000000000 --- a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/AnalysisOfEndpointConnections.java +++ /dev/null @@ -1,83 +0,0 @@ -package de.tum.cit.endpointanalysis; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -import com.thoughtworks.qdox.JavaProjectBuilder; -import com.thoughtworks.qdox.model.JavaAnnotation; -import com.thoughtworks.qdox.model.JavaClass; -import com.thoughtworks.qdox.model.JavaMethod; - -public class AnalysisOfEndpointConnections { - - /** - * This is the entry point of the analysis of server sided endpoints. - * - * @param args List of files that should be analyzed regarding endpoints. - */ - public static void main(String[] args) { - if (args.length == 0) { - System.out.println("No files to analyze."); - return; - } - String[] filePaths = args[0].split("\n"); - String[] serverFiles = Arrays.stream(filePaths).map(filePath -> Paths.get("..", "..", filePath).toString()) - .filter(filePath -> Files.exists(Paths.get(filePath)) && filePath.endsWith(".java")).toArray(String[]::new); - analyzeServerEndpoints(serverFiles); - } - - private static void analyzeServerEndpoints(String[] filePaths) { - final Set httpMethodClasses = Set.of(GetMapping.class.getName(), PostMapping.class.getName(), PutMapping.class.getName(), DeleteMapping.class.getName(), - PatchMapping.class.getName(), RequestMapping.class.getName()); - - JavaProjectBuilder builder = new JavaProjectBuilder(); - for (String filePath : filePaths) { - builder.addSourceTree(new File(filePath)); - } - - Collection classes = builder.getClasses(); - for (JavaClass javaClass : classes) { - Optional requestMappingOptional = javaClass.getAnnotations().stream() - .filter(annotation -> annotation.getType().getFullyQualifiedName().equals(RequestMapping.class.getName())).findFirst(); - - boolean hasEndpoint = javaClass.getMethods().stream().flatMap(method -> method.getAnnotations().stream()) - .anyMatch(annotation -> httpMethodClasses.contains(annotation.getType().getFullyQualifiedName())); - - if (hasEndpoint) { - System.out.println("=================================================="); - System.out.println("Class: " + javaClass.getFullyQualifiedName()); - requestMappingOptional.ifPresent(annotation -> System.out.println("Class Request Mapping: " + annotation.getProperty("value"))); - System.out.println("=================================================="); - } - - for (JavaMethod method : javaClass.getMethods()) { - for (JavaAnnotation annotation : method.getAnnotations()) { - if (httpMethodClasses.contains(annotation.getType().getFullyQualifiedName())) { - System.out.println("Endpoint: " + method.getName()); - System.out.println( - annotation.getType().getFullyQualifiedName().equals(RequestMapping.class.getName()) ? "RequestMapping·method: " + annotation.getProperty("method") - : "HTTP method annotation: " + annotation.getType().getName()); - System.out.println("Path: " + annotation.getProperty("value")); - System.out.println("Line: " + method.getLineNumber()); - List annotations = method.getAnnotations().stream().filter(a -> !a.equals(annotation)).map(a -> a.getType().getName()).toList(); - System.out.println("Other annotations: " + annotations); - System.out.println("---------------------------------------------------"); - } - } - } - } - } -} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointAnalysis.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointAnalysis.java new file mode 100644 index 000000000000..4c38f7e826e8 --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointAnalysis.java @@ -0,0 +1,6 @@ +package de.tum.cit.endpointanalysis; + +import java.util.List; + +public record EndpointAnalysis(List usedEndpoints, List unusedEndpoints) { +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointAnalyzer.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointAnalyzer.java new file mode 100644 index 000000000000..250966bfd432 --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointAnalyzer.java @@ -0,0 +1,144 @@ +package de.tum.cit.endpointanalysis; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class EndpointAnalyzer { + + private static String EndpointAnalysisResultPath = "endpointAnalysisResult.json"; + + private static final Logger logger = LoggerFactory.getLogger(EndpointAnalyzer.class); + + public static void main(String[] args) { + analyzeEndpoints(); + printEndpointAnalysisResult(); + } + + /** + * Analyzes server side endpoints and matches them with client side REST calls. + * + * This method reads endpoint and REST call information from JSON files, + * compares them to find matching REST calls for each endpoint, and writes + * the analysis result to a JSON file. Endpoints without matching REST calls + * are also recorded. + */ + private static void analyzeEndpoints() { + ObjectMapper mapper = new ObjectMapper(); + + try { + List endpointClasses = mapper.readValue(new File(EndpointParser.ENDPOINT_PARSING_RESULT_PATH), + new TypeReference>() { + }); + List restCallFiles = mapper.readValue(new File(EndpointParser.REST_CALL_PARSING_RESULT_PATH), + new TypeReference>() { + }); + + List endpointsAndMatchingRestCalls = new ArrayList<>(); + List unusedEndpoints = new ArrayList<>(); + + Map> restCallMap = new HashMap<>(); + + // Populate the map with rest calls + for (RestCallFileInformation restCallFile : restCallFiles) { + for (RestCallInformation restCall : restCallFile.restCalls()) { + String restCallURI = restCall.buildComparableRestCallUri(); + restCallMap.computeIfAbsent(restCallURI, uri -> new ArrayList<>()).add(restCall); + } + } + + for (EndpointClassInformation endpointClass : endpointClasses) { + for (EndpointInformation endpoint : endpointClass.endpoints()) { + + String endpointURI = endpoint.buildComparableEndpointUri(); + List matchingRestCalls = restCallMap.getOrDefault(endpointURI, new ArrayList<>()); + + // Check for wildcard endpoints if no exact match is found + checkForWildcardEndpoints(endpoint, matchingRestCalls, endpointURI, restCallMap); + + if (matchingRestCalls.isEmpty()) { + unusedEndpoints.add(endpoint); + } + else { + endpointsAndMatchingRestCalls.add(new UsedEndpoints(endpoint, matchingRestCalls, endpointClass.filePath())); + } + } + } + + EndpointAnalysis endpointAnalysis = new EndpointAnalysis(endpointsAndMatchingRestCalls, unusedEndpoints); + mapper.writeValue(new File(EndpointAnalysisResultPath), endpointAnalysis); + } + catch (IOException e) { + logger.error("Failed to analyze endpoints", e); + } + } + + /** + * Checks for wildcard endpoints and adds matching REST calls to the list. + * + * This method is used to find matching REST calls for endpoints that use wildcard URIs. + * If no exact match is found for an endpoint, it checks if the endpoint URI ends with a wildcard ('*'). + * It then iterates through the rest call map to find URIs that start with the same prefix as the endpoint URI + * (excluding the wildcard) and have the same HTTP method. If such URIs are found, they are added to the list of matching REST calls. + * + * @param endpoint The endpoint information to check for wildcard matches. + * @param matchingRestCalls The list of matching REST calls to be populated. + * @param endpointURI The URI of the endpoint being checked. + * @param restCallMap The map of rest call URIs to their corresponding information. + */ + private static void checkForWildcardEndpoints(EndpointInformation endpoint, List matchingRestCalls, String endpointURI, + Map> restCallMap) { + if (matchingRestCalls.isEmpty() && endpointURI.endsWith("*")) { + for (String uri : restCallMap.keySet()) { + if (uri.startsWith(endpoint.buildComparableEndpointUri().substring(0, endpoint.buildComparableEndpointUri().length() - 1)) + && endpoint.getHttpMethod().toLowerCase().equals(restCallMap.get(uri).get(0).method().toLowerCase())) { + matchingRestCalls.addAll(restCallMap.get(uri)); + } + } + } + } + + /** + * Prints the endpoint analysis result. + * + * This method reads the endpoint analysis result from a JSON file and prints + * the details of unused endpoints to the console. The details include the + * endpoint URI, HTTP method, file path, and line number. If no matching REST + * call is found for an endpoint, it prints a message indicating this. + */ + private static void printEndpointAnalysisResult() { + ObjectMapper mapper = new ObjectMapper(); + EndpointAnalysis endpointsAndMatchingRestCalls = null; + try { + endpointsAndMatchingRestCalls = mapper.readValue(new File(EndpointAnalysisResultPath), new TypeReference() { + }); + } + catch (IOException e) { + logger.error("Failed to deserialize endpoint analysis result", e); + return; + } + + endpointsAndMatchingRestCalls.unusedEndpoints().stream().forEach(endpoint -> { + logger.info("============================================="); + logger.info("Endpoint URI: {}", endpoint.buildCompleteEndpointURI()); + logger.info("HTTP method: {}", endpoint.httpMethodAnnotation()); + logger.info("File path: {}", endpoint.className()); + logger.info("Line: {}", endpoint.line()); + logger.info("============================================="); + logger.info("No matching REST call found for endpoint: {}", endpoint.buildCompleteEndpointURI()); + logger.info("---------------------------------------------"); + logger.info(""); + }); + + logger.info("Number of endpoints without matching REST calls: {}", endpointsAndMatchingRestCalls.unusedEndpoints().size()); + } +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointClassInformation.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointClassInformation.java new file mode 100644 index 000000000000..062a3671b1bf --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointClassInformation.java @@ -0,0 +1,6 @@ +package de.tum.cit.endpointanalysis; + +import java.util.List; + +public record EndpointClassInformation(String filePath, String classRequestMapping, List endpoints) { +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointInformation.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointInformation.java new file mode 100644 index 000000000000..19b32dc8881b --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointInformation.java @@ -0,0 +1,36 @@ +package de.tum.cit.endpointanalysis; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public record EndpointInformation(String requestMapping, String endpoint, String httpMethodAnnotation, String URI, String className, int line, List otherAnnotations) { + + public String buildCompleteEndpointURI() { + StringBuilder result = new StringBuilder(); + if (this.requestMapping != null && !this.requestMapping.isEmpty()) { + // Remove quotes from the requestMapping as they are used to define the String in the source code but are not part of the URI + result.append(this.requestMapping.replace("\"", "")); + } + // Remove quotes from the URI as they are used to define the String in the source code but are not part of the URI + result.append(this.URI.replace("\"", "")); + return result.toString(); + } + + String buildComparableEndpointUri() { + // Replace arguments with placeholder + return this.buildCompleteEndpointURI().replaceAll("\\{.*?\\}", ":param:"); + } + + @JsonIgnore + public String getHttpMethod() { + return switch (this.httpMethodAnnotation) { + case "GetMapping" -> "get"; + case "PostMapping" -> "post"; + case "PutMapping" -> "put"; + case "DeleteMapping" -> "delete"; + case "PatchMapping" -> "patch"; + default -> "No HTTP method annotation found"; + }; + } +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointParser.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointParser.java new file mode 100644 index 000000000000..b0ab2cdb1f0b --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/EndpointParser.java @@ -0,0 +1,217 @@ +package de.tum.cit.endpointanalysis; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.ArrayInitializerExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; + +public class EndpointParser { + + static final String ENDPOINT_PARSING_RESULT_PATH = "endpoints.json"; + + static final String REST_CALL_PARSING_RESULT_PATH = "restCalls.json"; + + private static final Logger logger = LoggerFactory.getLogger(EndpointParser.class); + + public static void main(String[] args) { + final Path absoluteDirectoryPath = Path.of("../../src/main/java").toAbsolutePath().normalize(); + + StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); + + String[] filesToParse = {}; + try (Stream paths = Files.walk(absoluteDirectoryPath)) { + filesToParse = paths.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".java")).map(Path::toString).toArray(String[]::new); + } + catch (IOException e) { + logger.error("Error reading files from directory: {}", absoluteDirectoryPath, e); + } + + parseServerEndpoints(filesToParse); + } + + /** + * Parses server endpoints from the given file paths. + * + * This method reads Java files from the specified file paths, extracts endpoint + * information annotated with HTTP method annotations, and writes the parsed + * endpoint information to a JSON file. It also logs any files that failed to parse. + * + * @param filePaths an array of file paths to parse for endpoint information + */ + private static void parseServerEndpoints(String[] filePaths) { + List endpointClasses = new ArrayList<>(); + final Set httpMethodClasses = Set.of(GetMapping.class.getSimpleName(), PostMapping.class.getSimpleName(), PutMapping.class.getSimpleName(), + DeleteMapping.class.getSimpleName(), PatchMapping.class.getSimpleName(), RequestMapping.class.getSimpleName()); + List filesFailedToParse = new ArrayList<>(); + + for (String filePath : filePaths) { + CompilationUnit compilationUnit; + try { + compilationUnit = StaticJavaParser.parse(new File(filePath)); + } + catch (Exception e) { + filesFailedToParse.add(filePath); + continue; + } + + List classes = compilationUnit.findAll(ClassOrInterfaceDeclaration.class); + for (ClassOrInterfaceDeclaration javaClass : classes) { + List endpoints = new ArrayList<>(); + final String classRequestMappingString = extractClassRequestMapping(javaClass, httpMethodClasses); + + endpoints.addAll(extractAnnotationPathValues(javaClass, httpMethodClasses, classRequestMappingString)); + + if (!endpoints.isEmpty()) { + endpointClasses.add(new EndpointClassInformation(javaClass.getNameAsString(), classRequestMappingString, endpoints)); + } + } + } + + printFilesFailedToParse(filesFailedToParse); + + writeEndpointsToFile(endpointClasses); + } + + /** + * Extracts endpoint information from the methods of a given class declaration. + * + * This method iterates over the methods of the provided class and their annotations. + * If an annotation matches one of the specified HTTP method annotations, it extracts + * the path values from the annotation and creates EndpointInformation objects for each path. + * + * @param javaClass the class declaration to extract endpoint information from + * @param httpMethodClasses a set of HTTP method annotation class names + * @param classRequestMappingString the class-level request mapping string + * @return a list of EndpointInformation objects representing the extracted endpoint information + */ + private static List extractAnnotationPathValues(ClassOrInterfaceDeclaration javaClass, Set httpMethodClasses, String classRequestMappingString) { + return javaClass.getMethods().stream() + .flatMap(method -> method.getAnnotations().stream().filter(annotation -> httpMethodClasses.contains(annotation.getNameAsString())) + .flatMap(annotation -> extractPathsFromAnnotation(annotation).stream() + .map(path -> new EndpointInformation(classRequestMappingString, method.getNameAsString(), annotation.getNameAsString(), path, + javaClass.getNameAsString(), method.getBegin().get().line, method.getAnnotations().stream().map(AnnotationExpr::toString).toList())))) + .toList(); + } + + /** + * Extracts the paths from the given annotation. + * + * This method processes the provided annotation to extract path values. + * It handles both single-member and normal annotations, extracting the + * path values from the annotation's member values or pairs. + * + * @param annotation the annotation to extract paths from + * @return a list of extracted path values + */ + private static List extractPathsFromAnnotation(AnnotationExpr annotation) { + List paths = new ArrayList<>(); + if (annotation instanceof SingleMemberAnnotationExpr singleMemberAnnotationExpr) { + Expression memberValue = singleMemberAnnotationExpr.getMemberValue(); + if (memberValue instanceof ArrayInitializerExpr arrayInitializerExpr) { + paths.addAll(arrayInitializerExpr.getValues().stream().map(Expression::toString).collect(Collectors.toList())); + } + else { + paths.add(memberValue.toString()); + } + } + else if (annotation instanceof NormalAnnotationExpr normalAnnotationExpr) { + normalAnnotationExpr.getPairs().stream().filter(pair -> "value".equals(pair.getNameAsString())).forEach(pair -> paths.add(pair.getValue().toString())); + } + return paths; + } + + /** + * Extracts the class-level request mapping from a given class declaration. + * + * This method scans the annotations of the provided class to find a `RequestMapping` annotation. + * It then checks if the class contains any methods annotated with HTTP method annotations. + * If such methods are found, it extracts the value of the `RequestMapping` annotation. + * + * @param javaClass the class declaration to extract the request mapping from + * @param httpMethodClasses a set of HTTP method annotation class names + * @return the extracted request mapping value, or an empty string if no request mapping is found or the class has no HTTP method annotations + */ + private static String extractClassRequestMapping(ClassOrInterfaceDeclaration javaClass, Set httpMethodClasses) { + boolean hasEndpoint = javaClass.getMethods().stream().flatMap(method -> method.getAnnotations().stream()) + .anyMatch(annotation -> httpMethodClasses.contains(annotation.getNameAsString())); + + if (!hasEndpoint) { + return ""; + } + + String classRequestMapping = javaClass.getAnnotations().stream().filter(annotation -> annotation.getNameAsString().equals(RequestMapping.class.getSimpleName())).findFirst() + .map(annotation -> { + if (annotation instanceof SingleMemberAnnotationExpr singleMemberAnnotationExpr) { + return singleMemberAnnotationExpr.getMemberValue().toString(); + } + else if (annotation instanceof NormalAnnotationExpr normalAnnotationExpr) { + return normalAnnotationExpr.getPairs().stream().filter(pair -> "path".equals(pair.getNameAsString())).map(pair -> pair.getValue().toString()).findFirst() + .orElse(""); + } + return ""; + }).orElse(""); + + return classRequestMapping; + } + + /** + * Prints the list of files that failed to parse. + * + * This method checks if the provided list of file paths is not empty. + * If it is not empty, it prints a message indicating that some files failed to parse, + * followed by the paths of the files that failed. + * + * @param filesFailedToParse the list of file paths that failed to parse + */ + private static void printFilesFailedToParse(List filesFailedToParse) { + if (!filesFailedToParse.isEmpty()) { + logger.warn("Files failed to parse:", filesFailedToParse); + for (String file : filesFailedToParse) { + logger.warn(file); + } + } + } + + /** + * Writes the list of endpoint class information to a JSON file. + * + * This method uses the Jackson ObjectMapper to serialize the list of + * EndpointClassInformation objects and write them to a file specified + * by the EndpointParsingResultPath constant. + * + * @param endpointClasses the list of EndpointClassInformation objects to write to the file + */ + private static void writeEndpointsToFile(List endpointClasses) { + try { + new ObjectMapper().writeValue(new File(ENDPOINT_PARSING_RESULT_PATH), endpointClasses); + } + catch (IOException e) { + logger.error("Failed to write endpoint information to file", e); + } + } +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallAnalysis.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallAnalysis.java new file mode 100644 index 000000000000..57b1d2dfc289 --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallAnalysis.java @@ -0,0 +1,6 @@ +package de.tum.cit.endpointanalysis; + +import java.util.List; + +public record RestCallAnalysis(List restCallsWithMatchingEndpoints, List restCallsWithoutMatchingEndpoints) { +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallAnalyzer.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallAnalyzer.java new file mode 100644 index 000000000000..aac71d6573bf --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallAnalyzer.java @@ -0,0 +1,144 @@ +package de.tum.cit.endpointanalysis; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class RestCallAnalyzer { + + private static final String REST_CALL_ANALYSIS_RESULT_PATH = "restCallAnalysisResult.json"; + + private static final Logger logger = LoggerFactory.getLogger(RestCallAnalyzer.class); + + public static void main(String[] args) { + analyzeRestCalls(); + printRestCallAnalysisResult(); + } + + /** + * The RestCallAnalyzer analyzes the client REST Calls and focuses on them having a matching Endpoint on the server + * + * This method reads endpoint and REST call information from JSON files. + * It then matches the REST calls with the endpoints they are calling and + * writes the analysis result to a JSON file. + * REST calls without matching endpoints are also recorded. + */ + private static void analyzeRestCalls() { + ObjectMapper mapper = new ObjectMapper(); + + try { + List endpointClasses = mapper.readValue(new File(EndpointParser.ENDPOINT_PARSING_RESULT_PATH), + new TypeReference>() { + }); + List restCalls = mapper.readValue(new File(EndpointParser.REST_CALL_PARSING_RESULT_PATH), new TypeReference>() { + }); + + List restCallsWithMatchingEndpoint = new ArrayList<>(); + List restCallsWithoutMatchingEndpoint = new ArrayList<>(); + + Map> endpointMap = new HashMap<>(); + + // Populate the map with endpoints + for (EndpointClassInformation endpointClass : endpointClasses) { + for (EndpointInformation endpoint : endpointClass.endpoints()) { + String endpointURI = endpoint.buildComparableEndpointUri(); + endpointMap.computeIfAbsent(endpointURI, uri -> new ArrayList<>()).add(endpoint); + } + } + + for (RestCallFileInformation restCallFile : restCalls) { + for (RestCallInformation restCall : restCallFile.restCalls()) { + String restCallURI = restCall.buildComparableRestCallUri(); + List matchingEndpoints = endpointMap.getOrDefault(restCallURI, new ArrayList<>()); + + checkForWildcardMatches(restCall, matchingEndpoints, restCallURI, endpointMap); + + if (matchingEndpoints.isEmpty()) { + restCallsWithoutMatchingEndpoint.add(restCall); + } + else { + for (EndpointInformation endpoint : matchingEndpoints) { + restCallsWithMatchingEndpoint.add(new RestCallWithMatchingEndpoint(endpoint, restCall, restCall.fileName())); + } + } + } + } + + RestCallAnalysis restCallAnalysis = new RestCallAnalysis(restCallsWithMatchingEndpoint, restCallsWithoutMatchingEndpoint); + mapper.writeValue(new File(REST_CALL_ANALYSIS_RESULT_PATH), restCallAnalysis); + } + catch (IOException e) { + logger.error("Failed to analyze REST calls", e); + } + } + + /** + * Checks for wildcard matches and adds matching endpoints to the list. + * + * This method is used to find matching endpoints for REST calls that use wildcard URIs. + * If no exact match is found for a REST call, it checks if the REST call URI ends with a wildcard ('*'). + * It then iterates through the endpoint map to find URIs that start with the same prefix as the REST call URI + * (excluding the wildcard) and have the same HTTP method. If such URIs are found, they are added to the list of matching endpoints. + * + * @param restCall The REST call information to check for wildcard matches. + * @param matchingEndpoints The list of matching endpoints to be populated. + * @param restCallURI The URI of the REST call being checked. + * @param endpointMap The map of endpoint URIs to their corresponding information. + */ + private static void checkForWildcardMatches(RestCallInformation restCall, List matchingEndpoints, String restCallURI, + Map> endpointMap) { + if (matchingEndpoints.isEmpty() && restCallURI.endsWith("*")) { + for (String uri : endpointMap.keySet()) { + if (uri.startsWith(restCallURI.substring(0, restCallURI.length() - 1)) + && endpointMap.get(uri).get(0).getHttpMethod().toLowerCase().equals(restCall.method().toLowerCase())) { + matchingEndpoints.addAll(endpointMap.get(uri)); + } + } + } + } + + /** + * Prints the endpoint analysis result. + * + * This method reads the endpoint analysis result from a JSON file and prints + * the details of unused endpoints to the console. The details include the + * endpoint URI, HTTP method, file path, and line number. If no matching REST + * call is found for an endpoint, it prints a message indicating this. + */ + private static void printRestCallAnalysisResult() { + ObjectMapper mapper = new ObjectMapper(); + + RestCallAnalysis restCallsAndMatchingEndpoints = null; + + try { + restCallsAndMatchingEndpoints = mapper.readValue(new File(REST_CALL_ANALYSIS_RESULT_PATH), new TypeReference() { + }); + } + catch (IOException e) { + logger.error("Failed to deserialize rest call analysis results", e); + } + + restCallsAndMatchingEndpoints.restCallsWithoutMatchingEndpoints().stream().forEach(endpoint -> { + logger.info("============================================="); + logger.info("REST call URI: {}", endpoint.buildCompleteRestCallURI()); + logger.info("HTTP method: {}", endpoint.method()); + logger.info("File path: {}", endpoint.fileName()); + logger.info("Line: {}", endpoint.line()); + logger.info("============================================="); + logger.info("No matching endpoint found for REST call: {}", endpoint.buildCompleteRestCallURI()); + logger.info("---------------------------------------------"); + logger.info(""); + }); + + logger.info("Number of REST calls without matching endpoints: {}", restCallsAndMatchingEndpoints.restCallsWithoutMatchingEndpoints().size()); + } +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallFileInformation.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallFileInformation.java new file mode 100644 index 000000000000..847ec03b1561 --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallFileInformation.java @@ -0,0 +1,4 @@ +package de.tum.cit.endpointanalysis; + +public record RestCallFileInformation(String fileName, RestCallInformation[] restCalls) { +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallInformation.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallInformation.java new file mode 100644 index 000000000000..fb1e44f92f2a --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallInformation.java @@ -0,0 +1,18 @@ +package de.tum.cit.endpointanalysis; + +public record RestCallInformation(String method, String url, int line, String fileName) { + + public String buildCompleteRestCallURI() { + return this.url.replace("`", ""); + } + + public String buildComparableRestCallUri() { + // Replace arguments with placeholder + String result = this.buildCompleteRestCallURI().replaceAll("\\$\\{.*?\\}", ":param:"); + + // Remove query parameters + result = result.split("\\?")[0]; + + return result; + } +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallWithMatchingEndpoint.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallWithMatchingEndpoint.java new file mode 100644 index 000000000000..0b3ebed9d527 --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/RestCallWithMatchingEndpoint.java @@ -0,0 +1,4 @@ +package de.tum.cit.endpointanalysis; + +public record RestCallWithMatchingEndpoint(EndpointInformation matchingEndpoint, RestCallInformation restCallInformation, String filePath) { +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/UsedEndpoints.java b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/UsedEndpoints.java new file mode 100644 index 000000000000..afdb8fa06846 --- /dev/null +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/java/de/tum/cit/endpointanalysis/UsedEndpoints.java @@ -0,0 +1,6 @@ +package de.tum.cit.endpointanalysis; + +import java.util.List; + +public record UsedEndpoints(EndpointInformation endpointInformation, List matchingRestCalls, String filePath) { +} diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/AnalysisOfEndpointConnectionsClient.ts b/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/AnalysisOfEndpointConnectionsClient.ts index f774acfdc92b..ad4f19a9322e 100644 --- a/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/AnalysisOfEndpointConnectionsClient.ts +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/AnalysisOfEndpointConnectionsClient.ts @@ -1,8 +1,9 @@ -import { readdirSync } from 'node:fs'; +import { readdirSync, readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { Preprocessor } from './Preprocessor'; import { Postprocessor } from './Postprocessor'; import { writeFileSync } from 'node:fs'; +import { parse, TSESTree } from '@typescript-eslint/typescript-estree'; /** * Recursively collects all TypeScript files in a directory. @@ -26,20 +27,58 @@ function collectTypeScriptFiles(dir: string, files: string[] = []) : string[] { return files; } +/** + * Parses a TypeScript file and returns its Abstract Syntax Tree (AST). + * + * @param filePath - The path to the TypeScript file to be parsed. + * @returns The TSESTree of the parsed TypeScript file. + */ +function parseTypeScriptFile(filePath: string): TSESTree.Program | null { + const code = readFileSync(filePath, 'utf8'); + try { + return parse(code, { + loc: true, + comment: true, + tokens: true, + ecmaVersion: 2020, + sourceType: 'module', + }); + } catch (error) { + console.error(`Failed to parse TypeScript file at ${filePath}:`, error); + console.error('Please make sure the file is valid TypeScript code.'); + return null; + } +} + const clientDirPath = resolve('src/main/webapp/app'); const tsFiles = collectTypeScriptFiles(clientDirPath); -// preprocess each file +// create and store Syntax Tree for each file +const astMap = new Map; tsFiles.forEach((filePath) => { - const preProcessor = new Preprocessor(filePath); - preProcessor.preprocessFile(); + const ast = parseTypeScriptFile(filePath); + if (ast) { + astMap.set(filePath, ast); + } +}); + +// preprocess each file +Array.from(astMap.keys()).forEach((filePath: string) => { + const ast = astMap.get(filePath); + if (ast) { + const preProcessor = new Preprocessor(ast); + preProcessor.preprocessFile(); + } }); // postprocess each file -tsFiles.forEach((filePath) => { - const postProcessor = new Postprocessor(filePath); - postProcessor.extractRestCalls(); +Array.from(astMap.keys()).forEach((filePath) => { + const ast = astMap.get(filePath); + if (ast) { + const postProcessor = new Postprocessor(filePath, ast); + postProcessor.extractRestCallsFromProgram(); + } }); try { diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Postprocessor.ts b/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Postprocessor.ts index b90d216a9796..18b54a5f0ac4 100644 --- a/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Postprocessor.ts +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Postprocessor.ts @@ -38,18 +38,18 @@ class ParsingResult { } export class Postprocessor { - static filesWithRestCalls: { filePath: string, restCalls: RestCall[] }[] = []; + static filesWithRestCalls: { fileName: string, restCalls: RestCall[] }[] = []; private readonly restCalls: RestCall[] = []; - private readonly filePath: string; + private readonly fileName: string; private readonly ast: TSESTree.Program; - constructor(filePath: string) { - this.filePath = filePath; - this.ast = Preprocessor.parseTypeScriptFile(Preprocessor.pathPrefix + filePath) - } - - extractRestCalls() { - this.extractRestCallsFromProgram(); + /** + * @param fileName - The name of the file being processed. + * @param ast - The abstract syntax tree (AST) of the processed file. + */ + constructor(fileName: string, ast: TSESTree.Program) { + this.fileName = fileName; + this.ast = ast; } extractRestCallsFromProgram() { @@ -61,7 +61,7 @@ export class Postprocessor { } }); if (this.restCalls.length > 0) { - Postprocessor.filesWithRestCalls.push( {filePath: this.filePath, restCalls: this.restCalls} ); + Postprocessor.filesWithRestCalls.push( {fileName: this.fileName, restCalls: this.restCalls} ); } } @@ -108,7 +108,7 @@ export class Postprocessor { urlEvaluationResult = this.evaluateUrl(node.arguments[0], methodDefinition, node, classBody); } - const fileName = this.filePath; + const fileName = this.fileName; if (urlEvaluationResult.resultType === ParsingResultType.EVALUATE_URL_SUCCESS) { for (let url of urlEvaluationResult.result) { this.restCalls.push({ method, url, line, fileName }); diff --git a/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Preprocessor.ts b/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Preprocessor.ts index a90f30fb7a5e..fcdebd828486 100644 --- a/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Preprocessor.ts +++ b/supporting_scripts/analysis-of-endpoint-connections/src/main/typeScript/Preprocessor.ts @@ -11,16 +11,16 @@ interface SuperClass { interface ChildClass { superClass: string; name: string; - memberVariables: Map; + memberVariables: Map; parentMethodCalls: ParentMethodCalls[]; } interface ParentMethodCalls { name: string; - parameters: MemberVariable[]; + parameters: Attribute[]; } -interface MemberVariable { +interface Attribute { name: string; type: string; value?: string; @@ -28,16 +28,16 @@ interface MemberVariable { export class Preprocessor { public static PREPROCESSING_RESULTS = new Map(); - public static readonly pathPrefix = '' private readonly directoryPrefix = 'src/main/webapp/'; - private readonly fileToPreprocess: string; private ast: TSESTree.Program; - private memberVariables: Map = new Map(); + private memberVariables: Map = new Map(); - constructor(fileToPreprocess: string) { - this.fileToPreprocess = fileToPreprocess; - this.ast = Preprocessor.parseTypeScriptFile(Preprocessor.pathPrefix + this.fileToPreprocess); + /** + * @param ast - The abstract syntax tree (AST) of the processed file. + */ + constructor(ast: TSESTree.Program) { + this.ast = ast; } /** @@ -48,10 +48,6 @@ export class Preprocessor { * It also handles named exports that are class declarations. */ preprocessFile() { - if (this.ast.type !== 'Program') { - return; - } - this.ast.body.forEach((node) => { if (node.type === 'ClassDeclaration') { this.preprocessClass(node); @@ -254,11 +250,11 @@ export class Preprocessor { * which scans the class body for a property matching the parameter name and returns its value. * * @param parameterName - The name of the parameter whose value is to be found. - * @param filePath - The path to the TypeScript file (relative to the base directory set in `pathPrefix` and `directoryPrefix`) where the parameter value is to be searched. + * @param filePath - The path to the TypeScript file (relative to the base directory set in `directoryPrefix`) where the parameter value is to be searched. * @returns The value of the parameter if found; otherwise, an empty string. */ findParameterValueByParameterNameAndFilePath (parameterName: string, filePath: string): string { - const targetAST = Preprocessor.parseTypeScriptFile(`${Preprocessor.pathPrefix}${this.directoryPrefix}${filePath}.ts`); + const targetAST = Preprocessor.parseTypeScriptFile(`${this.directoryPrefix}${filePath}.ts`); for (const node of targetAST.body) { if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'ClassDeclaration') {