From c05b809a4602c61dded7af0b999411f575cc04f5 Mon Sep 17 00:00:00 2001 From: Gabriel Goulis Date: Tue, 3 Dec 2024 20:00:11 -0600 Subject: [PATCH] Library init --- .github/workflows/main.yml | 19 + .gitignore | 5 + .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .mvn/wrapper/maven-wrapper.properties | 1 + README.md | 126 +++- pom.xml | 154 +++++ .../ecs/lab/common/config/Config.java | 88 +++ .../ecs/lab/common/config/ConfigUtil.java | 27 + .../ecs/lab/common/config/package-info.java | 11 + .../ecs/lab/common/error/Error.java | 67 ++ .../ecs/lab/common/error/package-info.java | 8 + .../lab/common/models/enums/ClassRole.java | 49 ++ .../common/models/enums/EndpointTemplate.java | 163 +++++ .../ecs/lab/common/models/enums/FileType.java | 10 + .../lab/common/models/enums/HttpMethod.java | 16 + .../common/models/enums/RestCallTemplate.java | 221 +++++++ .../ecs/lab/common/models/ir/Annotation.java | 89 +++ .../ecs/lab/common/models/ir/ConfigFile.java | 28 + .../ecs/lab/common/models/ir/Endpoint.java | 60 ++ .../ecs/lab/common/models/ir/Field.java | 39 ++ .../ecs/lab/common/models/ir/Flow.java | 68 +++ .../ecs/lab/common/models/ir/JClass.java | 137 +++++ .../ecs/lab/common/models/ir/Method.java | 100 +++ .../ecs/lab/common/models/ir/MethodCall.java | 92 +++ .../lab/common/models/ir/Microservice.java | 317 ++++++++++ .../common/models/ir/MicroserviceSystem.java | 168 +++++ .../ecs/lab/common/models/ir/Node.java | 31 + .../ecs/lab/common/models/ir/Parameter.java | 52 ++ .../ecs/lab/common/models/ir/ProjectFile.java | 28 + .../ecs/lab/common/models/ir/RestCall.java | 112 ++++ .../ecs/lab/common/models/package-info.java | 26 + .../serialization/JsonSerializable.java | 42 ++ .../serialization/MethodCallDeserializer.java | 51 ++ .../serialization/MethodDeserializer.java | 60 ++ .../ProjectFileDeserializer.java | 27 + .../models/serialization/package-info.java | 17 + .../ecs/lab/common/package-info.java | 14 + .../ecs/lab/common/services/GitService.java | 294 +++++++++ .../lab/common/services/LoggerManager.java | 66 ++ .../ecs/lab/common/utils/FileUtils.java | 154 +++++ .../ecs/lab/common/utils/FlowUtils.java | 255 ++++++++ .../lab/common/utils/JsonReadWriteUtils.java | 82 +++ .../common/utils/NonJsonReadWriteUtils.java | 160 +++++ .../lab/common/utils/SourceToObjectUtils.java | 575 ++++++++++++++++++ .../ecs/lab/common/utils/package-info.java | 12 + .../ecs/lab/delta/models/Delta.java | 112 ++++ .../ecs/lab/delta/models/SystemChange.java | 49 ++ .../lab/delta/models/enums/ChangeType.java | 25 + .../ecs/lab/delta/models/package-info.java | 11 + .../ecs/lab/delta/package-info.java | 16 + .../services/DeltaExtractionService.java | 230 +++++++ .../ecs/lab/delta/services/package-info.java | 8 + .../lab/intermediate/create/package-info.java | 8 + .../create/services/IRExtractionService.java | 274 +++++++++ .../create/services/package-info.java | 8 + .../lab/intermediate/merge/package-info.java | 8 + .../merge/services/MergeService.java | 336 ++++++++++ .../merge/services/package-info.java | 8 + .../ecs/lab/intermediate/package-info.java | 13 + .../intermediate/utils/StringParserUtils.java | 40 ++ src/main/resources/application.properties | 0 src/main/resources/log4j2.xml | 15 + src/test/java/IRExtractionTest.java | 14 + src/test/resources/test_config.json | 5 + 64 files changed, 5300 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 pom.xml create mode 100644 src/main/java/edu/university/ecs/lab/common/config/Config.java create mode 100644 src/main/java/edu/university/ecs/lab/common/config/ConfigUtil.java create mode 100644 src/main/java/edu/university/ecs/lab/common/config/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/common/error/Error.java create mode 100644 src/main/java/edu/university/ecs/lab/common/error/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/enums/ClassRole.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/enums/EndpointTemplate.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/enums/FileType.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/enums/HttpMethod.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/enums/RestCallTemplate.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Annotation.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/ConfigFile.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Endpoint.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Field.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Flow.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/JClass.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Method.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/MethodCall.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Microservice.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/MicroserviceSystem.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Node.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/Parameter.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/ProjectFile.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/ir/RestCall.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/serialization/JsonSerializable.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/serialization/MethodCallDeserializer.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/serialization/MethodDeserializer.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/serialization/ProjectFileDeserializer.java create mode 100644 src/main/java/edu/university/ecs/lab/common/models/serialization/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/common/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/common/services/GitService.java create mode 100644 src/main/java/edu/university/ecs/lab/common/services/LoggerManager.java create mode 100644 src/main/java/edu/university/ecs/lab/common/utils/FileUtils.java create mode 100644 src/main/java/edu/university/ecs/lab/common/utils/FlowUtils.java create mode 100644 src/main/java/edu/university/ecs/lab/common/utils/JsonReadWriteUtils.java create mode 100644 src/main/java/edu/university/ecs/lab/common/utils/NonJsonReadWriteUtils.java create mode 100644 src/main/java/edu/university/ecs/lab/common/utils/SourceToObjectUtils.java create mode 100644 src/main/java/edu/university/ecs/lab/common/utils/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/models/Delta.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/models/SystemChange.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/models/enums/ChangeType.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/models/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/services/DeltaExtractionService.java create mode 100644 src/main/java/edu/university/ecs/lab/delta/services/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/create/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/create/services/IRExtractionService.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/create/services/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/merge/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/merge/services/MergeService.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/merge/services/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/package-info.java create mode 100644 src/main/java/edu/university/ecs/lab/intermediate/utils/StringParserUtils.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/log4j2.xml create mode 100644 src/test/java/IRExtractionTest.java create mode 100644 src/test/resources/test_config.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..301a70ac --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: Publish package to GitHub Packages + +on: push +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: '16' + distribution: 'adopt' + - name: Publish package + run: mvn --batch-mode deploy + env: + GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7e384222 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Intellij +.idea + +# Maven +target/ \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..c2195a64 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip diff --git a/README.md b/README.md index dfb218b3..71afac2d 100644 --- a/README.md +++ b/README.md @@ -1 +1,125 @@ -# cimet-extract-lib \ No newline at end of file +# CIMET Extraction Library + +This Maven library props up the functionality of CIMET2. +It is intended to be used as a temporal parser of microservice systems. +It is capable of extracting intermediate representations (IR) of the system +and delta representations of the changes to the system. + +## Prerequisites + +* Maven 3.6+ +* Java 11+ (11 Recommended) + +## To Compile: + ``mvn clean install -DskipTests`` + +## Extracting an Intermediate Representation: +- Run or compile the main method of ``IRExtractionRunner.java`` in the IDE of your choice or via the command line. +- Command line args list containing ``/path/to/config/.json`` + +Sample input config file: + +```json +{ + "systemName": "Train-ticket", + "repositoryURL": "https://github.com/g-goulis/train-ticket-microservices-test.git", + "endCommit": "06f3e1efe2e2539d05d91b0699cc8d9fe7be29d7", + "baseBranch": "main" +} +``` + +Sample output produced: +```json +{ + "name": "Train-ticket", + "commitID": "1.0", + "microservices": [ + { + "name": "ts-rebook-service", + "path": ".\\clone\\train-ticket-microservices-test\\ts-rebook-service", + "controllers": [ + { + "packageName": "com.cloudhubs.trainticket.rebook.controller", + "name": "WaitListOrderController.java", + "path": ".\\clone\\train-ticket-microservices-test\\ts-rebook-service\\src\\main\\java\\com\\cloudhubs\\trainticket\\rebook\\controller\\WaitListOrderController.java", + "classRole": "CONTROLLER", + "annotations": [ + { + "name": "RequestMapping", + "contents": "\"/api/v1/waitorderservice\"" + }, + ... + ], + "fields": [ + { + "name": "waitListOrderService", + "type": "WaitListOrderService" + }, + ... + ], + "methods": [ + { + "name": "getAllOrders", + "annotations": [ + { + "name": "GetMapping", + "contents": "[path \u003d \"/orders\"]" + } + ], + "parameters": [ + { + "name": "HttpHeaders", + "type": "headers" + } + ], + "returnType": "HttpEntity", + "url": "/api/v1/waitorderservice/orders", + "httpMethod": "GET", + "microserviceName": "ts-rebook-service" + }, + ... + ], + "methodCalls": [ + { + "name": "info", + "objectName": "LOGGER", + "calledFrom": "getWaitListOrders", + "parameterContents": "\"[getWaitListOrders][Get All Wait List Orders]\"" + }, + ... + ] + }, + ... + ], + "Services": [...], + "Repositories": [...], + "Entities": [...], + ], + "orphans": [...] +} +``` + +## Extracting a Delta Change Impact: +- Run or compile the main method of ``DeltaExtractionRunner.java`` in the IDE of your choice or via the command line. +- Command line args list containing ``/path/to/config/.json `` + +Sample output produced: +```json +{ + "oldCommit": "06f3e1efe2e2539d05d91b0699cc8d9fe7be29d7", + "newCommit": "82949fa07dcf82f66641f5807d629d15bab663a6", + "changes": [ + { + "oldPath": ".\\clone\\train-ticket-microservices-test\\ts-price-service\\src\\main\\java\\com\\cloudhubs\\trainticket\\price\\controller\\PriceController.java", + "newPath": ".\\clone\\train-ticket-microservices-test\\ts-price-service\\src\\main\\java\\com\\cloudhubs\\trainticket\\price\\controller\\PriceController.java", + "changeType": "MODIFY", + "classChange": {} + }, + ... + ] +} +``` + +## Merging an IR & System Change: +- Run or compile the main method of ``IRMergeRunner.java`` in the IDE of your choice or via the command line. +- Provide command line args containing ``path/to/IR/.json path/to/Delta/.json /path/to/config/.json`` diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..2dc1055b --- /dev/null +++ b/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + edu.university.ecs.lab + cimet-extract-lib + 1.0-SNAPSHOT-2 + + 16 + 16 + true + 3.8.1 + + + + + + github + GitHub Packages + https://maven.pkg.github.com/cloudhubs/cimet-extract-lib + + + + jar + + + + + com.github.javaparser + javaparser-symbol-solver-core + 3.16.3 + + + org.projectlombok + lombok + 1.18.30 + provided + + + com.google.code.gson + gson + 2.8.9 + + + org.eclipse.jgit + org.eclipse.jgit + 6.8.0.202311291450-r + + + org.apache.logging.log4j + log4j-api + 2.23.1 + + + org.apache.logging.log4j + log4j-core + 2.23.1 + + + com.fasterxml.jackson.core + jackson-core + 2.17.1 + + + javax.json + javax.json-api + 1.1.4 + + + junit + junit + 4.13.2 + test + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.13.0 + + + org.json + json + 20210307 + + + org.apache.poi + poi-ooxml + 5.2.3 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + + org.apache.maven.plugins + maven-compiler-plugin + + 16 + 16 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + ${mainClass} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + jar-with-dependencies + + + + + assemble-all + package + + single + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.3 + + + + + diff --git a/src/main/java/edu/university/ecs/lab/common/config/Config.java b/src/main/java/edu/university/ecs/lab/common/config/Config.java new file mode 100644 index 00000000..96ba210c --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/config/Config.java @@ -0,0 +1,88 @@ +package edu.university.ecs.lab.common.config; + +import edu.university.ecs.lab.common.error.Error; +import lombok.Getter; +import lombok.Setter; + +import java.util.Objects; +import java.util.Optional; + +import static edu.university.ecs.lab.common.error.Error.NULL_ERROR; + +/** + * Model to represent the JSON configuration file + * Some additional notes, this object is p + */ +@Getter +@Setter +public class Config { + private static final String GIT_SCHEME_DOMAIN = "https://github.com/"; + private static final String GIT_PATH_EXTENSION = ".git"; + + /** + * The name of the system analyzed + */ + private final String systemName; + + /** + * The path to write cloned repository files to + */ + private final String repositoryURL; + + /** + * Initial starting commit for repository + */ + private final String branch; + + + public Config(String systemName, String repositoryURL, String branch) throws Exception { + validateConfig(systemName, repositoryURL, branch); + + this.systemName = systemName; + this.repositoryURL = repositoryURL; + this.branch = branch; + } + + /** + * Check that config file is valid and has all required fields + */ + + private void validateConfig(String systemName, String repositoryURL, String branch) { + try { + Objects.requireNonNull(systemName); + Objects.requireNonNull(repositoryURL); + Objects.requireNonNull(branch); + validateConfig(systemName, repositoryURL, branch); + + assert !systemName.isBlank() && !repositoryURL.isBlank() && !branch.isBlank(); + } catch (Exception e) { + Error.reportAndExit(Error.INVALID_CONFIG, Optional.of(e)); + } + Objects.requireNonNull(systemName, NULL_ERROR.getMessage()); + Objects.requireNonNull(repositoryURL, NULL_ERROR.getMessage()); + Objects.requireNonNull(branch, NULL_ERROR.getMessage()); + validateRepositoryURL(repositoryURL); + } + + /** + * The list of repository objects as indicated by config + */ + + private void validateRepositoryURL(String repositoryURL) { + if (!(repositoryURL.isBlank() || repositoryURL.startsWith(GIT_SCHEME_DOMAIN) || repositoryURL.endsWith(GIT_PATH_EXTENSION))) { + Error.reportAndExit(Error.INVALID_REPOSITORY_URL, Optional.empty()); + } + } + + /** + * This method gets the repository name parsed from the repositoryURL + * + * @return the plain string repository name with no path related characters + */ + public String getRepoName() { + int lastSlashIndex = repositoryURL.lastIndexOf("/"); + int lastDotIndex = repositoryURL.lastIndexOf('.'); + return repositoryURL.substring(lastSlashIndex + 1, lastDotIndex); + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/config/ConfigUtil.java b/src/main/java/edu/university/ecs/lab/common/config/ConfigUtil.java new file mode 100644 index 00000000..49da39ef --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/config/ConfigUtil.java @@ -0,0 +1,27 @@ +package edu.university.ecs.lab.common.config; + +import edu.university.ecs.lab.common.utils.JsonReadWriteUtils; + +/** + * Utility class for reading and validating the input config file + */ +public class ConfigUtil { + + /** + * Prevent instantiation + */ + private ConfigUtil() { + } + + /** + * This method read's the input config and return Config object + * + * @param configPath path to the input config file + * @return Config object + */ + public static Config readConfig(String configPath) { + return JsonReadWriteUtils.readFromJSON(configPath, Config.class); + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/config/package-info.java b/src/main/java/edu/university/ecs/lab/common/config/package-info.java new file mode 100644 index 00000000..f72cb654 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/config/package-info.java @@ -0,0 +1,11 @@ +/** + * Provides classes and utilities for handling configuration files related to microservice systems. + *

+ * This package includes: + * - {@link edu.university.ecs.lab.common.config.Config}: Represents a configuration model for managing + * JSON configuration files, including system name, repository URL, base commit and branch, and paths + * to microservices within the repository. + * - {@link edu.university.ecs.lab.common.config.ConfigUtil}: Utility class for reading and validating + * JSON configuration files, converting them into {@code Config} objects. + */ +package edu.university.ecs.lab.common.config; diff --git a/src/main/java/edu/university/ecs/lab/common/error/Error.java b/src/main/java/edu/university/ecs/lab/common/error/Error.java new file mode 100644 index 00000000..3daa7e9c --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/error/Error.java @@ -0,0 +1,67 @@ +package edu.university.ecs.lab.common.error; + +import edu.university.ecs.lab.common.services.LoggerManager; +import lombok.Getter; + +import java.util.Optional; + +/** + * Enum representing different error types with corresponding error codes and messages. + */ +@Getter +public enum Error { + UNKNOWN_ERROR(1, "Unknown error has occured!"), + NULL_ERROR(1, "Input cannot be null!"), + INVALID_REPOSITORY_URL(2, "Invalid repository URL!"), + INVALID_REPO_PATHS(3, "Invalid relative repository paths!"), + INVALID_REPO_PATH(4, "Invalid repository relative path after update! Skipping!"), + INVALID_CONFIG_PATH(5, "Invalid configuration file path!"), + REPO_DONT_EXIST(6, "The specified repository does not exist!"), + GIT_FAILED(7, "The requested git action failed for an unknown reason!"), + INVALID_ARGS(8, "Invalid arguments!"), + INVALID_JSON_READ(9, "Unable to read JSON from file!"), + INVALID_JSON_WRITE(10, "Unable to write JSON to file!"), + JPARSE_FAILED(10, "Failed to parse Java Code!"), + INVALID_CONFIG(10, "Invalid configuration file!"), + MISSING_CONFIG(10, "Missing configuration file!"); + + /** + * The unique error code identifying the error type. + */ + private final int code; + /** + * The detailed message describing the error. + */ + private final String message; + + /** + * Constructor for Error enum. + * + * @param code The error code. + * @param message The error message. + */ + Error(int code, String message) { + this.code = code; + this.message = message; + } + + /** + * Prints the error message to standard error and exits the program with the error code. + * + * @param error The error enum value to report and exit with. + */ + public static void reportAndExit(Error error, Optional exception) { + LoggerManager.error(error::getMessage, exception); + System.exit(error.code); + } + + /** + * Returns a string representation of the error. + * + * @return The formatted string representation of the error. + */ + @Override + public String toString() { + return "Error " + code + ": " + message; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/error/package-info.java b/src/main/java/edu/university/ecs/lab/common/error/package-info.java new file mode 100644 index 00000000..89f7d9e7 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/error/package-info.java @@ -0,0 +1,8 @@ +/** + * Provides an enumeration for handling errors within the tool for the creation of the end product. + *

+ * This package contains an enumeration representing various error types that can occur during + * the operation of the tool. Each error type includes an error code and a descriptive message + * to identify and communicate errors effectively. + */ +package edu.university.ecs.lab.common.error; diff --git a/src/main/java/edu/university/ecs/lab/common/models/enums/ClassRole.java b/src/main/java/edu/university/ecs/lab/common/models/enums/ClassRole.java new file mode 100644 index 00000000..1848f469 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/enums/ClassRole.java @@ -0,0 +1,49 @@ +package edu.university.ecs.lab.common.models.enums; + +import edu.university.ecs.lab.common.models.ir.JClass; +import lombok.Getter; + +/** + * Enum to represent the role of a class in a system + */ +public enum ClassRole { + CONTROLLER(JClass.class), + SERVICE(JClass.class), + REPOSITORY(JClass.class), + ENTITY(JClass.class), + REP_REST_RSC(JClass.class), + FEIGN_CLIENT(JClass.class), + UNKNOWN(null); + + /** + * Get the associated class type for a role + */ + @Getter + private final Class classType; + + /** + * Private constructor to link enum to class type + * + * @param classType the class type to associate with the role + */ + ClassRole(Class classType) { + this.classType = classType; + } + + /** + * Get the class role from the class type + * + * @param roleName the name of the class role + * @return associated class type if it exists, else null (unknown or not found) + */ + public static Class classFromRoleName(String roleName) { + // Iterate over type names + for (ClassRole role : ClassRole.values()) { + if (role.name().equalsIgnoreCase(roleName)) { + return role.classType; + } + } + return null; + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/enums/EndpointTemplate.java b/src/main/java/edu/university/ecs/lab/common/models/enums/EndpointTemplate.java new file mode 100644 index 00000000..fa90fcf4 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/enums/EndpointTemplate.java @@ -0,0 +1,163 @@ +package edu.university.ecs.lab.common.models.enums; + +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.expr.*; +import edu.university.ecs.lab.intermediate.utils.StringParserUtils; +import lombok.Getter; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Factory class for generating an endpoint template from annotations + */ +@Getter +public class EndpointTemplate { + public static final List ENDPOINT_ANNOTATIONS = Arrays.asList("RequestMapping", "GetMapping", "PutMapping", "PostMapping", "DeleteMapping", "PatchMapping"); + private final HttpMethod httpMethod; + private final String name; + private final String url; + + + + public EndpointTemplate(AnnotationExpr requestMapping, AnnotationExpr endpointMapping) { + HttpMethod finalHttpMethod = HttpMethod.ALL; + + String preUrl = ""; + if(requestMapping != null) { + if (requestMapping instanceof NormalAnnotationExpr) { + NormalAnnotationExpr nae = (NormalAnnotationExpr) requestMapping; + for (MemberValuePair pair : nae.getPairs()) { + if (pair.getNameAsString().equals("value")) { + preUrl = pair.getValue().toString().replaceAll("\"", ""); + } + } + } else if (requestMapping instanceof SingleMemberAnnotationExpr) { + preUrl = requestMapping.asSingleMemberAnnotationExpr().getMemberValue().toString().replaceAll("\"", ""); + } + } + + String url = ""; + if (endpointMapping instanceof NormalAnnotationExpr) { + NormalAnnotationExpr nae = (NormalAnnotationExpr) endpointMapping; + for (MemberValuePair pair : nae.getPairs()) { + if (pair.getNameAsString().equals("method")) { + String methodValue = pair.getValue().toString(); + finalHttpMethod = httpFromMapping(methodValue); + } else if(pair.getNameAsString().equals("path") || pair.getNameAsString().equals("value")) { + url = pair.getValue().toString().replaceAll("\"", ""); + } + } + } else if (endpointMapping instanceof SingleMemberAnnotationExpr) { + url = endpointMapping.asSingleMemberAnnotationExpr().getMemberValue().toString().replaceAll("\"", ""); + } else if(endpointMapping instanceof MarkerAnnotationExpr) { + if(preUrl.isEmpty()) { + url = "/"; + } + } + + if(finalHttpMethod == HttpMethod.ALL) { + finalHttpMethod = httpFromMapping(endpointMapping.getNameAsString()); + } + + String finalURL = ""; + // Ensure preUrl starts with a slash if it exists + if((!preUrl.isEmpty() && !preUrl.startsWith("/"))) { + preUrl = "/" + preUrl; + // Ensure Url starts with a slash if it exists + } else if ((!url.isEmpty() && !url.startsWith("/"))) { + url = "/" + url; + } + + + if(preUrl.isEmpty() && url.isEmpty()) { + finalURL = "/"; + } else { + finalURL = preUrl + url; + } + + // Replace any double slashes + finalURL = finalURL.replaceAll("//", "/"); + // If it ends with a slash remove it + finalURL = finalURL.endsWith("/") && !finalURL.equals("/") ? finalURL.substring(0, finalURL.length() - 1) : finalURL; + + + // Get query Parameters + + this.httpMethod = finalHttpMethod; + this.name = endpointMapping.getNameAsString(); + this.url = simplifyEndpointURL(finalURL); + } + + + /** + * Method to get http method from mapping + * + * @param mapping mapping string for a given method + * @return HttpMethod object of same method type + */ + private static HttpMethod httpFromMapping(String mapping) { + switch (mapping) { + case "GetMapping": + case "RequestMethod.GET": + return HttpMethod.GET; + case "PostMapping": + case "RequestMethod.POST": + return HttpMethod.POST; + case "DeleteMapping": + case "RequestMethod.DELETE": + return HttpMethod.DELETE; + case "PutMapping": + case "RequestMethod.PUT": + return HttpMethod.PUT; + case "PatchMapping": + case "RequestMethod.PATCH": + return HttpMethod.PATCH; + default: + return HttpMethod.ALL; + } + + } + + /** + * Method to get endpoint path from annotations + * + * @param ae annotation expression from method + * @param url string formatted as a url + * @return endpoint path/url from annotation expression + */ + public static String getPathFromAnnotation(AnnotationExpr ae, String url) { + // Annotations of type @Mapping("/endpoint") + if (ae.isSingleMemberAnnotationExpr()) { + url = url + StringParserUtils.simplifyEndpointURL( + StringParserUtils.removeOuterQuotations( + ae.asSingleMemberAnnotationExpr().getMemberValue().toString())); + } + + // Annotations of type @Mapping(path="/endpoint") + else if (ae.isNormalAnnotationExpr() && !ae.asNormalAnnotationExpr().getPairs().isEmpty()) { + for (MemberValuePair mvp : ae.asNormalAnnotationExpr().getPairs()) { + if (mvp.getName().toString().equals("path") || mvp.getName().toString().equals("value")) { + url = url + StringParserUtils.simplifyEndpointURL( + StringParserUtils.removeOuterQuotations(mvp.getValue().toString())); + break; + } + } + } + return url; + } + + /** + * Simplifies all path arguments to {?}. + * + * @param url the endpoint URL + * @return the simplified endpoint URL + */ + public static String simplifyEndpointURL(String url) { + return url.replaceAll("\\{[^{}]*\\}", "{?}"); + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/enums/FileType.java b/src/main/java/edu/university/ecs/lab/common/models/enums/FileType.java new file mode 100644 index 00000000..5b3d69e2 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/enums/FileType.java @@ -0,0 +1,10 @@ +package edu.university.ecs.lab.common.models.enums; + +/** + * File types enum + */ +public enum FileType { + JCLASS, + CONFIG, + POM +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/enums/HttpMethod.java b/src/main/java/edu/university/ecs/lab/common/models/enums/HttpMethod.java new file mode 100644 index 00000000..9f99a8a8 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/enums/HttpMethod.java @@ -0,0 +1,16 @@ +package edu.university.ecs.lab.common.models.enums; + +/** + * Enum to represent the HTTP methods + */ +public enum HttpMethod { + GET, + PUT, + POST, + DELETE, + OPTIONS, + HEAD, + PATCH, + ALL, + NONE +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/enums/RestCallTemplate.java b/src/main/java/edu/university/ecs/lab/common/models/enums/RestCallTemplate.java new file mode 100644 index 00000000..ecab9dc1 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/enums/RestCallTemplate.java @@ -0,0 +1,221 @@ +package edu.university.ecs.lab.common.models.enums; + +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.*; +import edu.university.ecs.lab.common.models.ir.MethodCall; +import edu.university.ecs.lab.intermediate.utils.StringParserUtils; +import javassist.expr.Expr; +import lombok.Getter; + +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Enum to represent Spring methodName and HttpMethod combinations and determine HttpMethod from + * methodName. + */ +@Getter +public class RestCallTemplate { + public static final Set REST_OBJECTS = Set.of("RestTemplate", "OAuth2RestOperations", "OAuth2RestTemplate", "WebClient"); + public static final Set REST_METHODS = Set.of("getForObject", "postForObject", "patchForObject", "put", "delete", "exchange", "get", "post", "options", "patch"); + private static final String UNKNOWN_VALUE = "{?}"; + + private final String url; + private final HttpMethod httpMethod; + private final CompilationUnit cu; + private final MethodCallExpr mce; + + public RestCallTemplate(MethodCallExpr mce, MethodCall mc, CompilationUnit cu) { + this.cu = cu; + this.mce = mce; + this.url = simplifyEndpointURL(preParseURL(mce, mc)); + this.httpMethod = getHttpFromName(mce); + } + + /** + * Find the RestTemplate by the method name. + * + * @param mce the method call + * @return the RestTemplate found (null if not found) + */ + public HttpMethod getHttpFromName(MethodCallExpr mce) { + String methodName = mce.getNameAsString(); + switch (methodName) { + case "getForObject": + case "get": + return HttpMethod.GET; + case "postForObject": + case "post": + return HttpMethod.POST; + case "patchForObject": + case "patch": + return HttpMethod.PATCH; + case "put": + return HttpMethod.PUT; + case "delete": + return HttpMethod.DELETE; + case "exchange": + return getHttpMethodForExchange(mce.getArguments().stream().map(Node::toString).collect(Collectors.joining())); + } + + return HttpMethod.NONE; + } + + /** + * Get the HTTP method for the JSF exchange() method call. + * + * @param arguments the arguments of the exchange() method + * @return the HTTP method extracted + */ + public HttpMethod getHttpMethodForExchange(String arguments) { + if (arguments.contains("HttpMethod.POST")) { + return HttpMethod.POST; + } else if (arguments.contains("HttpMethod.PUT")) { + return HttpMethod.PUT; + } else if (arguments.contains("HttpMethod.DELETE")) { + return HttpMethod.DELETE; + } else if (arguments.contains("HttpMethod.PATCH")) { + return HttpMethod.PATCH; + } else { + return HttpMethod.GET; // default + } + } + + /** + * Find the URL from the given expression. + * + * @param exp the expression to extract url from + * @return the URL found + */ + private String parseURL(Expression exp) { + if (exp.isStringLiteralExpr()) { + return exp.asStringLiteralExpr().asString(); + } else if (exp.isFieldAccessExpr()) { + return parseFieldValue(exp.asFieldAccessExpr().getNameAsString()); + } else if (exp.isBinaryExpr()) { + String left = parseURL(exp.asBinaryExpr().getLeft()); + String right = parseURL(exp.asBinaryExpr().getRight()); + return left + right; + } else if(exp.isEnclosedExpr()) { + return parseURL(exp.asEnclosedExpr()); + // Base case, if we are a method call or a u + } else if(exp.isMethodCallExpr()) { + // Here we may try to find a modified url in a method call expr + return backupParseURL(exp).isEmpty() ? UNKNOWN_VALUE : backupParseURL(exp); + } else if(exp.isNameExpr()) { + // Special case + if(exp.asNameExpr().getNameAsString().contains("uri") || exp.asNameExpr().getNameAsString().contains("url")) { + return ""; + } + return UNKNOWN_VALUE; + } + + // If all fails, try to find some viable url + return backupParseURL(exp); + } + + /** + * Find the URL from the given expression. + * + * @param exp the expression to extract url from + * @return the URL found + */ + private String backupParseURL(Expression exp) { + // Regular expression to match the first instance of "/.*" + String regex = "\".*(/.+?)\""; + + // Compile the pattern + Pattern pattern = Pattern.compile(regex); + + // Create a matcher for the input string + Matcher matcher = pattern.matcher(exp.toString()); + + // Find the first match + if (matcher.find()) { + // Extract the first instance of "/.*" + String extracted = matcher.group(0).replace("\"", ""); // Group 1 corresponds to the part in parentheses (captured group) + + // Replace string formatters if they are present + extracted = extracted.replaceAll("%[sdif]", UNKNOWN_VALUE); + + return cleanURL(extracted); + } + + return ""; + + } + + /** + * Shorten URLs to only endpoint query + * + * @param str full url + * @return url query + */ + private static String cleanURL(String str) { + str = str.replace("http://", ""); + str = str.replace("https://", ""); + + // Remove everything before the first / + int backslashNdx = str.indexOf("/"); + if (backslashNdx > 0) { + str = str.substring(backslashNdx); + } + + // Remove any trailing quotes + if (str.endsWith("\"")) { + str = str.substring(0, str.length() - 1); + } + + // Remove trailing / (does not affect functionality, trailing /'s are ignored in Spring) + if (str.endsWith("/")) { + str = str.substring(0, str.length() - 1); + } + + return str; + } + + private String parseFieldValue(String fieldName) { + for (FieldDeclaration fd : cu.findAll(FieldDeclaration.class)) { + if (fd.getVariables().toString().contains(fieldName)) { + Expression init = fd.getVariable(0).getInitializer().orElse(null); + if (init != null) { + return StringParserUtils.removeOuterQuotations(init.toString()); + } + } + } + + return ""; + } + + + private String preParseURL(MethodCallExpr mce, MethodCall mc) { + + // Nuance for webclient with method appender pattern + if(mc.getObjectType().equals("WebClient")) { + if(mce.getParentNode().isPresent()) { + if(mce.getParentNode().get() instanceof MethodCallExpr pmce) { + return pmce.getArguments().get(0).toString().isEmpty() ? "" : cleanURL(parseURL(pmce.getArguments().get(0))); + } + } + } else { + return mce.getArguments().get(0).toString().isEmpty() ? "" : cleanURL(parseURL(mce.getArguments().get(0))); + } + + return ""; + } + + /** + * Simplifies all path arguments to {?}. + * + * @param url the endpoint URL + * @return the simplified endpoint URL + */ + public static String simplifyEndpointURL(String url) { + return url.replaceAll("\\{[^{}]*\\}", "{?}"); + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Annotation.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Annotation.java new file mode 100644 index 00000000..171b9f27 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Annotation.java @@ -0,0 +1,89 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.github.javaparser.ast.expr.*; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Represents an annotation in Java + */ +@Data +@EqualsAndHashCode +public class Annotation extends Node { + + private Map attributes; + + public Annotation(AnnotationExpr annotationExpr, String packageAndClassName) { + this.name = annotationExpr.getNameAsString(); + this.packageAndClassName = packageAndClassName; + this.attributes = parseAttributes(annotationExpr); + } + + public Annotation(String name, String packageAndClassName, HashMap attributes) { + this.name = name; + this.packageAndClassName = packageAndClassName; + this.attributes = attributes; + } + + /** + * Get contents of annotation object + * + * @return comma-delimmited list of annotation content key-value pairs + */ + public String getContents() { + return getAttributes().entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).collect(Collectors.joining(",")); + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + Gson gson = new Gson(); + + jsonObject.addProperty("name", getName()); + jsonObject.addProperty("packageAndClassName", getPackageAndClassName()); + JsonElement attributesJson = gson.toJsonTree(attributes, new TypeToken>(){}.getType()); + jsonObject.add("attributes", attributesJson); + + return jsonObject; + } + + /** + * Map attributes from annotation expression + * + * @param annotationExpr annotation expression object to parse + * @return map of annotation attributes and their values + */ + private static HashMap parseAttributes(AnnotationExpr annotationExpr) { + HashMap attributes = new HashMap<>(); + + if(annotationExpr instanceof MarkerAnnotationExpr) { + return attributes; + } else if (annotationExpr instanceof SingleMemberAnnotationExpr smAnnotationExpr) { + if(smAnnotationExpr.getMemberValue() instanceof StringLiteralExpr sle) { + attributes.put("default", sle.asString()); + } else { + return attributes; + } + } else if (annotationExpr instanceof NormalAnnotationExpr nAnnotationExpr) { + for(MemberValuePair mvp : nAnnotationExpr.getPairs()) { + if(mvp.getValue() instanceof StringLiteralExpr sle) { + attributes.put(mvp.getNameAsString(), sle.asString()); + } + } + } + + return attributes; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/ConfigFile.java b/src/main/java/edu/university/ecs/lab/common/models/ir/ConfigFile.java new file mode 100644 index 00000000..971bb86a --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/ConfigFile.java @@ -0,0 +1,28 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.enums.FileType; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Getter; + +/** + * Represents a project configuration file + */ +@Getter +public class ConfigFile extends ProjectFile implements JsonSerializable { + private final JsonObject data; + + public ConfigFile(String path, String name, JsonObject data, FileType type) { + this.path = path; + this.name = name; + this.data = data; + this.fileType = FileType.CONFIG; + } + + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = super.toJsonObject(); + jsonObject.add("data", data); + return jsonObject; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Endpoint.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Endpoint.java new file mode 100644 index 00000000..664fdc4a --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Endpoint.java @@ -0,0 +1,60 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.enums.HttpMethod; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.HashSet; +import java.util.Set; +import java.util.List; + +/** + * Represents an extension of a method declaration. An endpoint exists at the controller level and + * signifies an open mapping that can be the target of a rest call. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Endpoint extends Method { + + /** + * The URL of the endpoint e.g. /api/v1/users/login, May have parameters like {param} + * which are converted to {?} + */ + private String url; + + /** + * The HTTP method of the endpoint, e.g. GET, POST, etc. + */ + private HttpMethod httpMethod; + + + + public Endpoint(String methodName, String packageName, Set parameters, String returnType, Set annotations, String microserviceName, + String className) { + super(methodName, packageName, parameters, returnType, annotations, microserviceName, className); + } + + public Endpoint(Method method, String url, HttpMethod httpMethod) { + super(method.name, method.packageAndClassName, method.parameters, method.returnType, method.annotations, method.microserviceName, method.className); + this.url = url; + this.httpMethod = httpMethod; + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = super.toJsonObject(); + + jsonObject.addProperty("url", url); + jsonObject.addProperty("httpMethod", httpMethod.name()); + + return jsonObject; + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Field.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Field.java new file mode 100644 index 00000000..7e21f92f --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Field.java @@ -0,0 +1,39 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Represents a field attribute in a Java class or in our case a JClass. + */ +@Data +@EqualsAndHashCode +public class Field extends Node { + /** + * Java class type of the class variable e.g. String + */ + private String type; + + public Field(String name, String packageAndClassName, String type) { + this.name = name; + this.packageAndClassName = packageAndClassName; + this.type = type; + } + + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("name", getName()); + jsonObject.addProperty("packageAndClassName", getPackageAndClassName()); + jsonObject.addProperty("type", getType()); + + return jsonObject; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Flow.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Flow.java new file mode 100644 index 00000000..afa51337 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Flow.java @@ -0,0 +1,68 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.*; + +/** Represents a flow from controller level down to DAO. */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +public class Flow implements JsonSerializable { + private Microservice model; + private JClass controller; + private Endpoint controllerMethod; + private MethodCall serviceMethodCall; + private Field controllerServiceField; + private JClass service; + private Method serviceMethod; + private MethodCall repositoryMethodCall; + private Field serviceRepositoryField; + private JClass repository; + private Method repositoryMethod; + + /** + * Create JSON object from flow object + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.add("model", model == null ? JsonNull.INSTANCE : model.toJsonObject()); + jsonObject.add("controller", controller == null ? JsonNull.INSTANCE : controller.toJsonObject()); + jsonObject.add("controllerMethod", controllerMethod == null ? JsonNull.INSTANCE : controllerMethod.toJsonObject()); + jsonObject.add("serviceMethodCall", serviceMethodCall == null ? JsonNull.INSTANCE : serviceMethodCall.toJsonObject()); + jsonObject.add("service", service == null ? JsonNull.INSTANCE : service.toJsonObject()); + jsonObject.add("serviceMethod", serviceMethod == null ? JsonNull.INSTANCE : serviceMethod.toJsonObject()); + jsonObject.add("repositoryMethodCall", repositoryMethodCall == null ? JsonNull.INSTANCE : repositoryMethodCall.toJsonObject()); + jsonObject.add("serviceRepositoryField", serviceRepositoryField == null ? JsonNull.INSTANCE : serviceRepositoryField.toJsonObject()); + jsonObject.add("repository", repository == null ? JsonNull.INSTANCE : repository.toJsonObject()); + jsonObject.add("repositoryMethod", repositoryMethod == null ? JsonNull.INSTANCE : repositoryMethod.toJsonObject()); + + return jsonObject; + } + + /** + * Create JSON object from flow object with only names + * + * @return flow JSON object + */ + public JsonObject toSmallJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("model", model == null ? "" : model.getName()); + jsonObject.addProperty("controller", controller == null ? "" : controller.getName()); + jsonObject.addProperty("controllerMethod", controllerMethod == null ? "" : controllerMethod.getName()); + jsonObject.addProperty("serviceMethodCall", serviceMethodCall == null ? "" : serviceMethodCall.getName()); + jsonObject.addProperty("service", service == null ? "" : service.getName()); + jsonObject.addProperty("serviceMethod", serviceMethod == null ? "" : serviceMethod.getName()); + jsonObject.addProperty("repositoryMethodCall", repositoryMethodCall == null ? "" : repositoryMethodCall.getName()); + jsonObject.addProperty("serviceRepositoryField", serviceRepositoryField == null ? "" : serviceRepositoryField.getName()); + jsonObject.addProperty("repository", repository == null ? "" : repository.getName()); + jsonObject.addProperty("repositoryMethod", repositoryMethod == null ? "" : repositoryMethod.getName()); + + return jsonObject; + } +} \ No newline at end of file diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/JClass.java b/src/main/java/edu/university/ecs/lab/common/models/ir/JClass.java new file mode 100644 index 00000000..c1ebc9bf --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/JClass.java @@ -0,0 +1,137 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.enums.ClassRole; +import edu.university.ecs.lab.common.models.enums.FileType; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents a class in Java. It holds all information regarding that class including all method + * declarations, method calls, fields, etc. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class JClass extends ProjectFile implements JsonSerializable { + private String packageName; + + /** + * Class implementations + */ + private Set implementedTypes; + + /** + * Role of the class in the microservice system. See {@link ClassRole} + */ + private ClassRole classRole; + + /** + * Set of methods in the class + */ + private Set methods; + + /** + * Set of class fields + */ + private Set fields; + + /** + * Set of class level annotations + */ + private Set annotations; + + /** + * List of method invocations made from within this class + */ + private List methodCalls; + + + public JClass(String name, String path, String packageName, ClassRole classRole) { + this.name = name; + this.packageName = packageName; + this.path = path; + this.classRole = classRole; + this.methods = new HashSet<>(); + this.fields = new HashSet<>(); + this.annotations = new HashSet<>(); + this.methodCalls = new ArrayList<>(); + this.implementedTypes = new HashSet<>(); + this.fileType = FileType.JCLASS; + } + + public JClass(String name, String path, String packageName, ClassRole classRole, Set methods, Set fields, Set classAnnotations, List methodCalls, Set implementedTypes) { + this.name = name; + this.packageName = packageName; + this.path = path; + this.classRole = classRole; + this.methods = methods; + this.fields = fields; + this.annotations = classAnnotations; + this.methodCalls = methodCalls; + this.implementedTypes = implementedTypes; + this.fileType = FileType.JCLASS; + } + + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = super.toJsonObject(); + Gson gson = new Gson(); + + jsonObject.addProperty("packageName", getPackageName()); + jsonObject.addProperty("classRole", getClassRole().name()); + jsonObject.add("annotations", JsonSerializable.toJsonArray(getAnnotations())); + jsonObject.add("fields", JsonSerializable.toJsonArray(getFields())); + jsonObject.add("methods", JsonSerializable.toJsonArray(getMethods())); + jsonObject.add("methodCalls", JsonSerializable.toJsonArray(getMethodCalls())); + jsonObject.add("implementedTypes", gson.toJsonTree(getImplementedTypes()).getAsJsonArray()); + + return jsonObject; + } + + /** + * This method returns all endpoints found in the methods of this class, + * grouped under the same list as an Endpoint is an extension of a Method + * see {@link Endpoint} + * @return set of all endpoints + */ + public Set getEndpoints() { + if((!getClassRole().equals(ClassRole.CONTROLLER) && !getClassRole().equals(ClassRole.REP_REST_RSC)) || getMethods().isEmpty()) { + return new HashSet<>(); + } + return methods.stream().filter(method -> method instanceof Endpoint).map(method -> (Endpoint) method).collect(Collectors.toUnmodifiableSet()); + } + + /** + * This method returns all restCalls found in the methodCalls of this class, + * grouped under the same list as an RestCall is an extension of a MethodCall + * see {@link RestCall} + * @return set of all restCalls + */ + public List getRestCalls() { + + return methodCalls.stream().filter(methodCall -> methodCall instanceof RestCall).map(methodCall -> (RestCall) methodCall).collect(Collectors.toUnmodifiableList()); + } + + /** + * If we are adding a class or a class is being adopted/orphanized lets update ms name + * + * @param name + */ + public void updateMicroserviceName(String name) { + methodCalls.forEach(methodCall -> methodCall.setMicroserviceName(name)); + methods.forEach(methodCall -> methodCall.setMicroserviceName(name)); + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Method.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Method.java new file mode 100644 index 00000000..60852ea3 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Method.java @@ -0,0 +1,100 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.*; + +/** + * Represents a method declaration in Java. + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class Method extends Node { + // Protection Not Yet Implemented + // protected String protection; + + /** + * Set of fields representing parameters + */ + protected Set parameters; + + /** + * Java return type of the method + */ + protected String returnType; + + /** + * The microservice id that this method belongs to + */ + protected String microserviceName; + + /** + * Method definition level annotations + */ + protected Set annotations; + + /** + * The class id that this method belongs to + */ + protected String className; + + public Method(String name, String packageAndClassName, Set parameters, String typeAsString, Set annotations, String microserviceName, + String className) { + this.name = name; + this.packageAndClassName = packageAndClassName; + this.parameters = parameters; + this.returnType = typeAsString; + this.annotations = annotations; + this.microserviceName = microserviceName; + this.className = className; + } + + public Method(MethodDeclaration methodDeclaration) { + this.name = methodDeclaration.getNameAsString(); + this.packageAndClassName = methodDeclaration.getClass().getPackageName() + "." + methodDeclaration.getClass().getName(); + this.parameters = parseParameters(methodDeclaration.getParameters()); + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("name", getName()); + jsonObject.addProperty("packageAndClassName", getPackageAndClassName()); + jsonObject.add("annotations", JsonSerializable.toJsonArray(getAnnotations())); + jsonObject.add("parameters", JsonSerializable.toJsonArray(getParameters())); + jsonObject.addProperty("returnType", getReturnType()); + jsonObject.addProperty("microserviceName", microserviceName); + jsonObject.addProperty("className", className); + + return jsonObject; + } + + /** + * Get set of parameters from node list + * + * @param parameters Node list of javaparser parameter objects + * @return set of parameter objects + */ + private Set parseParameters(NodeList parameters) { + HashSet parameterSet = new HashSet<>(); + + for(com.github.javaparser.ast.body.Parameter parameter : parameters) { + parameterSet.add(new Parameter(parameter, getPackageAndClassName())); + } + + + return parameterSet; + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/MethodCall.java b/src/main/java/edu/university/ecs/lab/common/models/ir/MethodCall.java new file mode 100644 index 00000000..2c18fb26 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/MethodCall.java @@ -0,0 +1,92 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Represents a method call in Java. + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class MethodCall extends Node { + + /** + * Name of object that defines the called method (Maybe a static class instance, just whatever is before + * the ".") + */ + protected String objectName; + + /** + * Type of object that defines that method + */ + protected String objectType; + + /** + * Name of method that contains this call + */ + protected String calledFrom; + + /** + * Contents within the method call (params) but as a raw string + */ + protected String parameterContents; + + /** + * The name of the microservice this MethodCall is called from + */ + protected String microserviceName; + + /** + * The class id that this MethodCall is called from + */ + protected String className; + + public MethodCall(String name, String packageName,String objectType, String objectName, String calledFrom, String parameterContents, String microserviceName, + String className) { + this.name = name; + this.packageAndClassName = packageName; + this.objectName = objectName; + this.objectType = objectType; + this.calledFrom = calledFrom; + this.parameterContents = parameterContents; + this.microserviceName = microserviceName; + this.className = className; + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("name", getName()); + jsonObject.addProperty("packageAndClassName", getPackageAndClassName()); + jsonObject.addProperty("objectName", getObjectName()); + jsonObject.addProperty("calledFrom", getCalledFrom()); + jsonObject.addProperty("objectType", getObjectType()); + jsonObject.addProperty("parameterContents", getParameterContents()); + jsonObject.addProperty("microserviceName", microserviceName); + jsonObject.addProperty("className", className); + + return jsonObject; + } + + /** + * Checks if a method call matches a given method + * + * @param methodCall method call object to match + * @param method method object to match + * @return true if method call and method match, false otherwise + */ + public static boolean matchMethod(MethodCall methodCall, Method method) { + return methodCall.microserviceName.equals(method.microserviceName) && methodCall.objectType.equals(method.className) + && methodCall.name.equals(method.name); + + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Microservice.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Microservice.java new file mode 100644 index 00000000..9b2aedf8 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Microservice.java @@ -0,0 +1,317 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import edu.university.ecs.lab.common.utils.FileUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Represents the overarching structure of a microservice system. It is composed of classes which + * hold all information in that class. + */ +@Data +@AllArgsConstructor +@EqualsAndHashCode +public class Microservice implements JsonSerializable { + /** + * The name of the service (ex: "ts-assurance-service") + */ + private String name; + + /** + * The path to the folder that represents the microservice + */ + private String path; + + /** + * Controller classes belonging to the microservice. + */ + private final Set controllers; + + /** + * Service classes to the microservice. + */ + private final Set services; + + /** + * Repository classes belonging to the microservice. + */ + private final Set repositories; + + /** + * Entity classes belonging to the microservice. + */ + private final Set entities; + + /** + * Embeddable classes belonging to the microservice. + */ +// private final Set embeddables; + + /** + * Feign client classes belonging to the microservice. + */ + private final Set feignClients; + + /** + * Static files belonging to the microservice. + */ + private final Set files; + + public Microservice(String name, String path) { + this.name = name; + this.path = path; + this.controllers = new HashSet<>(); + this.services = new HashSet<>(); + this.repositories = new HashSet<>(); + this.entities = new HashSet<>(); +// this.embeddables = new HashSet<>(); + this.feignClients = new HashSet<>(); + this.files = new HashSet<>(); + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("name", name); + jsonObject.addProperty("path", path); + jsonObject.add("controllers", JsonSerializable.toJsonArray(controllers)); + jsonObject.add("entities", JsonSerializable.toJsonArray(entities)); + jsonObject.add("feignClients", JsonSerializable.toJsonArray(feignClients)); + jsonObject.add("services", JsonSerializable.toJsonArray(services)); + jsonObject.add("repositories", JsonSerializable.toJsonArray(repositories)); + jsonObject.add("files", JsonSerializable.toJsonArray(files)); + + return jsonObject; + } + + + /** + * see {@link JsonSerializable#toJsonArray(Iterable)} + */ + private static JsonArray toJsonArray(Iterable list) { + JsonArray jsonArray = new JsonArray(); + for (JsonObject object : list) { + jsonArray.add(object); + } + return jsonArray; + } + + /** + * Update's the microservice name of the JClass and add's + * it to the appropriate Set + * + * @param jClass the JClass to add + */ + public void addJClass(JClass jClass) { + jClass.updateMicroserviceName(getName()); + + switch (jClass.getClassRole()) { + case CONTROLLER: + controllers.add(jClass); + break; + case SERVICE: + services.add(jClass); + break; + case REPOSITORY: + case REP_REST_RSC: + repositories.add(jClass); + break; + case ENTITY: + entities.add(jClass); + break; + case FEIGN_CLIENT: + feignClients.add(jClass); + break; + + + } + } + + /** + * This method removes a JClass from the microservice + * by looking up it's path + * + * @param path the path to search for removal + */ + public void removeJClass(String path) { + Set classes = getClasses(); + JClass removeClass = null; + + for (JClass jClass : classes) { + if (jClass.getPath().equals(path)) { + removeClass = jClass; + break; + } + } + + // If we cannot find the class no problem, we will skip it quietly + if (removeClass == null) { + return; + } + + switch (removeClass.getClassRole()) { + case CONTROLLER: + controllers.remove(removeClass); + break; + case SERVICE: + services.remove(removeClass); + break; + case REPOSITORY: + case REP_REST_RSC: + repositories.remove(removeClass); + break; + case ENTITY: + entities.remove(removeClass); + break; + case FEIGN_CLIENT: + feignClients.remove(removeClass); + break; + } + } + + /** + * This method removes a ProjectFile from the microservice + * by looking up it's path + * + * @param filePath the path to search for + */ + public void removeProjectFile(String filePath) { + + if(FileUtils.isConfigurationFile(filePath)) { + // First search configFile because there are less + ConfigFile removeFile = null; + + for (ConfigFile configFile : getFiles()) { + if (configFile.getPath().equals(filePath)) { + removeFile = configFile; + break; + } + } + + // If we cannot find the class no problem, we will skip it quietly + if (removeFile == null) { + return; + } + + getFiles().remove(removeFile); + + } else { + Set classes = getClasses(); + JClass removeClass = null; + + for (JClass jClass : classes) { + if (jClass.getPath().equals(filePath)) { + removeClass = jClass; + break; + } + } + + // If we cannot find the class no problem, we will skip it quietly + if (removeClass == null) { + return; + } + + switch (removeClass.getClassRole()) { + case CONTROLLER: + controllers.remove(removeClass); + break; + case SERVICE: + services.remove(removeClass); + break; + case REPOSITORY: + case REP_REST_RSC: + repositories.remove(removeClass); + break; + case ENTITY: + entities.remove(removeClass); + break; + case FEIGN_CLIENT: + feignClients.remove(removeClass); + break; + } + + } + } + + /** + * This method returns all classes of the microservice in a new set + * + * @return the set of all JClasses + */ + public Set getClasses() { + Set classes = new HashSet<>(); + classes.addAll(getControllers()); + classes.addAll(getServices()); + classes.addAll(getRepositories()); + classes.addAll(getEntities()); + classes.addAll(getFeignClients()); + + return classes; + } + + /** + * This method returns all files of a microservice, it is + * the aggregate of getClasses() and getFiles() + * + * @return the set of all classes and files + */ + public Set getAllFiles() { + Set set = new HashSet<>(getClasses()); + set.addAll(getFiles()); + set.addAll(getClasses()); + return set; + } + + /** + * This method returns all rest calls of a microservice + * + * @return the list of all rest calls + */ + public List getRestCalls () { + return getClasses().stream() + .flatMap(jClass -> jClass.getRestCalls().stream()).collect(Collectors.toList()); + } + + /** + * This method returns all endpoints of a microservice + * + * @return the set of all endpoints + */ + public Set getEndpoints () { + return getControllers().stream().flatMap(controller -> + controller.getEndpoints().stream()).collect(Collectors.toSet()); + } + + /** + * This method returns all method calls of a microservice + * + * @return the set of all method calls + */ + public Set getMethodCalls () { + return getClasses().stream().flatMap(jClass -> jClass.getMethodCalls().stream()).collect(Collectors.toSet()); + } + + /** + * This method returns all methods of a microservice + * + * @return the set of all methods + */ + public Set getMethods () { + return getClasses().stream().flatMap(jClass -> jClass.getMethods().stream()).collect(Collectors.toSet()); + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/MicroserviceSystem.java b/src/main/java/edu/university/ecs/lab/common/models/ir/MicroserviceSystem.java new file mode 100644 index 00000000..83a7c2d5 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/MicroserviceSystem.java @@ -0,0 +1,168 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.enums.FileType; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.HashSet; +import java.util.Set; + +/** + * Represents the intermediate structure of a microservice system. + */ +@Data +@AllArgsConstructor +@EqualsAndHashCode +public class MicroserviceSystem implements JsonSerializable { + /** + * The name of the system + */ + private String name; + + /** + * The commit ID of the system + */ + private String commitID; + + /** + * Set of microservices in the system + */ + private Set microservices; + + /** + * Set of present files (class or configurations) who have no microservice + */ + private Set orphans; + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("name", name); + jsonObject.addProperty("commitID", commitID); + jsonObject.add("microservices", JsonSerializable.toJsonArray(microservices)); + jsonObject.add("orphans", JsonSerializable.toJsonArray(orphans)); + + return jsonObject; + } + + /** + * Returns the microservice whose path is the start of the passed path + * + * @param path the path to search for + * @return microservice instance of matching path or null + */ + public Microservice findMicroserviceByPath(String path) { + return getMicroservices().stream().filter(microservice -> path.startsWith(microservice.getPath())).findFirst().orElse(null); + } + + + /** + * Given an existing microservice, if it must now be orphanized + * then all JClasses belonging to that service will be added to + * the system's pool of orphans for later use + * + * @param microservice the microservice to orphanize + */ + public void orphanize(Microservice microservice) { + Set classes = microservice.getClasses(); + classes.forEach(c -> c.updateMicroserviceName("")); + orphans.addAll(classes); + } + + /** + * Given a new or modified microservice, we must adopt awaiting + * orphans based on their file paths containing the microservices + * (folder) path + * + * @param microservice the microservice adopting orphans + */ + public void adopt(Microservice microservice) { + Set updatedOrphans = new HashSet<>(getOrphans()); + + for (ProjectFile file : getOrphans()) { + // If the microservice is in the same folder as the path to the microservice + if (file.getPath().contains(microservice.getPath())) { + if(file.getFileType().equals(FileType.JCLASS)) { + JClass jClass = (JClass) file; + jClass.updateMicroserviceName(microservice.getName()); + microservice.addJClass(jClass); + updatedOrphans.remove(file); + } else { + microservice.getFiles().add((ConfigFile) file); + } + } + + } + + setOrphans(updatedOrphans); + + } + + /** + * Get the class of a given endpoint + * + * @param path endpoint + * @return class that endpoint is in + */ + public JClass findClass(String path){ + JClass returnClass = null; + returnClass = getMicroservices().stream().flatMap(m -> m.getClasses().stream()).filter(c -> c.getPath().equals(path)).findFirst().orElse(null); + if(returnClass == null){ + returnClass = getOrphans().stream().filter(c -> c instanceof JClass).filter(c -> c.getPath().equals(path)).map(c -> (JClass) c).findFirst().orElse(null); + } + + return returnClass; + } + + /** + * Get the file of a given endpoint + * + * @param path endpoint + * @return file that endpoint is in + */ + public ProjectFile findFile(String path){ + ProjectFile returnFile = null; + returnFile = getMicroservices().stream().flatMap(m -> m.getAllFiles().stream()).filter(c -> c.getPath().equals(path)).findFirst().orElse(null); + if(returnFile == null){ + returnFile = getOrphans().stream().filter(c -> c.getPath().equals(path)).findFirst().orElse(null); + } + + return returnFile; + } + + /** + * This method returns the name of the microservice associated with + * a file that exists in the system. Note this method will not work + * if the file is not present somewhere in the system + * + * @param path the ProjectFile path + * @return string name of microservice or "" if it does not exist + */ + public String getMicroserviceFromFile(String path){ + for(Microservice microservice : getMicroservices()) { + for(ProjectFile file : microservice.getFiles()) { + if(file.getPath().equals(path)) { + return microservice.getName(); + } + } + } + + return ""; + } + + public void orphanizeAndAdopt(Microservice microservice) { + orphanize(microservice); + for(Microservice m : getMicroservices()){ + adopt(m); + } + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Node.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Node.java new file mode 100644 index 00000000..f87fbcb7 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Node.java @@ -0,0 +1,31 @@ +package edu.university.ecs.lab.common.models.ir; + +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; + +/** + * Abstract class for general datatypes that fall under + * JClass + */ +@Data +public abstract class Node implements JsonSerializable { + /** + * Name of the structure + */ + protected String name; + + /** + * Name of the package + class (package path e.g. edu.university.lab.AdminController) + */ + protected String packageAndClassName; + + /** + * This method generates a unique ID for datatypes that fall + * under a JClass + * + * @return the string unique ID + */ + public final String getID() { + return packageAndClassName + "." + name; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/Parameter.java b/src/main/java/edu/university/ecs/lab/common/models/ir/Parameter.java new file mode 100644 index 00000000..81a1b21b --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/Parameter.java @@ -0,0 +1,52 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Represents a method call parameter + */ +@Data +public class Parameter extends Node implements JsonSerializable { + + /** + * Java class type of the class variable e.g. String + */ + private String type; + + private Set annotations; + + public Parameter(String name, String packageAndClassName, String type, Set annotations) { + this.name = name; + this.packageAndClassName = packageAndClassName; + this.type = type; + this.annotations = annotations; + } + + public Parameter(com.github.javaparser.ast.body.Parameter parameter, String packageAndClassName) { + this.name = parameter.getNameAsString(); + this.type = parameter.getTypeAsString(); + this.packageAndClassName = packageAndClassName; + this.annotations = parameter.getAnnotations().stream().map(annotationExpr -> new Annotation(annotationExpr, packageAndClassName)).collect(Collectors.toSet()); + } + + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("name", getName()); + jsonObject.addProperty("packageAndClassName", getPackageAndClassName()); + jsonObject.addProperty("type", getType()); + jsonObject.add("annotations", JsonSerializable.toJsonArray(annotations)); + + return jsonObject; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/ProjectFile.java b/src/main/java/edu/university/ecs/lab/common/models/ir/ProjectFile.java new file mode 100644 index 00000000..01f6194b --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/ProjectFile.java @@ -0,0 +1,28 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.enums.FileType; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * This class represents any file in a project's directory + */ +@Data +@EqualsAndHashCode +public abstract class ProjectFile implements JsonSerializable { + protected String name; + protected String path; + protected FileType fileType; + + + @Override + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("name", name); + jsonObject.addProperty("path", path); + jsonObject.addProperty("fileType", fileType.name()); + return jsonObject; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/ir/RestCall.java b/src/main/java/edu/university/ecs/lab/common/models/ir/RestCall.java new file mode 100644 index 00000000..c8d28fdd --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/ir/RestCall.java @@ -0,0 +1,112 @@ +package edu.university.ecs.lab.common.models.ir; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.enums.HttpMethod; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import edu.university.ecs.lab.common.utils.JsonReadWriteUtils; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Set; + + +/** + * Represents an extension of a method call. A rest call exists at the service level and represents + * a call to an endpoint mapping. + */ +@Data +@EqualsAndHashCode(callSuper=true) +public class RestCall extends MethodCall { + + /** + * The URL of the rest call e.g. /api/v1/users/login, May have dynamic parameters + * which are converted to {?} + */ + private String url; + + /** + * The httpMethod of the api endpoint e.g. GET, POST, PUT see semantics.models.enums.httpMethod + */ + private HttpMethod httpMethod; + + + + + public RestCall(String methodName, String packageAndClassName, String objectType, String objectName, String calledFrom, String parameterContents, + String microserviceName, String className) { + super(methodName, packageAndClassName, objectType, objectName, calledFrom, parameterContents, + microserviceName, className); + } + + public RestCall(MethodCall methodCall, String url, HttpMethod httpMethod) { + super(methodCall.name, methodCall.packageAndClassName, methodCall.objectType, methodCall.objectName, methodCall.calledFrom, methodCall.parameterContents, + methodCall.microserviceName, methodCall.className); + this.url = url; + this.httpMethod = httpMethod; + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + public JsonObject toJsonObject() { + JsonObject jsonObject = super.toJsonObject(); + + jsonObject.addProperty("url", url); + jsonObject.addProperty("httpMethod", httpMethod.name()); + + return jsonObject; + } + + /** + * Checks if a rest call matches a given endpoint + * + * @param restcall rest call to match + * @param endpoint endpoint to match + * @return true if rest call and enpoint match, false otherwise + */ + public static boolean matchEndpoint(RestCall restcall, Endpoint endpoint){ + if(restcall.getMicroserviceName().equals(endpoint.getMicroserviceName())){ + return false; + } + + int queryParamIndex = restcall.getUrl().replace("{?}", "temp").indexOf("?"); + String baseURL = queryParamIndex == -1 ? restcall.getUrl() : restcall.getUrl().substring(0, queryParamIndex); + return baseURL.equals(endpoint.getUrl()) && (restcall.getHttpMethod().equals(endpoint.getHttpMethod()) || endpoint.getHttpMethod().equals(HttpMethod.ALL)) && matchQueryParams(restcall, endpoint, queryParamIndex); + } + + /** + * Checks if rest call parameters match parameters for the target endpoint + * + * @param restCall rest call to match + * @param endpoint endpoint to match + * @param queryParamIndex string index at which query parameters start + * @return true if parameters match, false otherwise + */ + private static boolean matchQueryParams(RestCall restCall, Endpoint endpoint, int queryParamIndex) { + for(Parameter parameter : endpoint.getParameters()) { + for(Annotation annotation : parameter.getAnnotations()) { + if(annotation.getName().equals("RequestParam")) { + String queryParameterName = ""; + if(annotation.getAttributes().containsKey("default")) { + queryParameterName = annotation.getAttributes().get("default"); + } else if(annotation.getAttributes().containsKey("name")) { + if(annotation.getAttributes().containsKey("required") + && annotation.getAttributes().get("required").equals("false")) { + continue; + } + queryParameterName = annotation.getAttributes().get("name"); + } else { + queryParameterName = parameter.getName(); + } + + if(!restCall.getUrl().substring(queryParamIndex + 1, restCall.getUrl().length()).contains(queryParameterName + "=")) { + return false; + } + } + } + } + return true; + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/package-info.java b/src/main/java/edu/university/ecs/lab/common/models/package-info.java new file mode 100644 index 00000000..26f71d29 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/package-info.java @@ -0,0 +1,26 @@ +/** + * Provides classes and sub-packages that represent various components of a microservice system + * and facilitate configuration of these representations in JSON format. + *

+ * This package includes: + * - + * - {@link edu.university.ecs.lab.common.models.enums}: Enumerations used for categorizing different components, such as Class Roles, HTTP Methods, etc. + * - {@link edu.university.ecs.lab.common.models.serialization}: Serialization and deserialization utilities for converting Java objects to JSON and vice versa + * - Other model classes representing key elements of the microservice system: + * - {@link edu.university.ecs.lab.common.models.ir.Annotation}: Represents annotations within classes. + * - {@link edu.university.ecs.lab.common.models.sdg.RestCallEdge}: Represents an edge in a network graph schema + * modeling microservice connections. + * - {@link edu.university.ecs.lab.common.models.ir.Endpoint}: Represents an endpoint exposed by a microservice. + * - {@link edu.university.ecs.lab.common.models.ir.Field}: Represents fields within classes. + * - {@link edu.university.ecs.lab.common.models.ir.JClass}: Represents a Java class within a microservice. + * - {@link edu.university.ecs.lab.common.models.ir.Method}: Represents a method within classes. + * - {@link edu.university.ecs.lab.common.models.ir.MethodCall}: Represents a method call within microservices. + * - {@link edu.university.ecs.lab.common.models.ir.Microservice}: Represents a microservice within the system, + * including its components like controllers, services, etc. + * - {@link edu.university.ecs.lab.common.models.ir.MicroserviceSystem}: Represents a microservice system and all its components, + * including the name of the system, the set of microservices, etc. + * - {@link edu.university.ecs.lab.common.models.sdg.ServiceDependencyGraph}: Represents the microservice system in a Static SDG schema (nodes and edges) + * - {@link edu.university.ecs.lab.common.models.ir.RestCall}: Represents an a call to an endpoing mapping and exists at the service level + * + */ +package edu.university.ecs.lab.common.models; \ No newline at end of file diff --git a/src/main/java/edu/university/ecs/lab/common/models/serialization/JsonSerializable.java b/src/main/java/edu/university/ecs/lab/common/models/serialization/JsonSerializable.java new file mode 100644 index 00000000..f822d4c5 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/serialization/JsonSerializable.java @@ -0,0 +1,42 @@ +package edu.university.ecs.lab.common.models.serialization; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.ir.Flow; + +/** + * Interface for classes that can be serialized to JSON object + */ +public interface JsonSerializable { + /** + * This method is a generalizable definition for converting an object of + * any type to a JsonObject + * + * @return a JsonObject representing this + */ + JsonObject toJsonObject(); + + /** + * This method is a generalizable implementation for converting an iterable of + * objects that extends this class using {@link JsonSerializable#toJsonObject()} to a JsonArray + * + * @param list + * @return + */ + static JsonArray toJsonArray(Iterable list) { + JsonArray jsonArray = new JsonArray(); + for (JsonSerializable item : list) { + jsonArray.add(item.toJsonObject()); + } + return jsonArray; + } + + static JsonArray toShortJsonArray(Iterable list) { + JsonArray jsonArray = new JsonArray(); + for (Flow item : list) { + jsonArray.add(item.toSmallJsonObject()); + } + return jsonArray; + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/serialization/MethodCallDeserializer.java b/src/main/java/edu/university/ecs/lab/common/models/serialization/MethodCallDeserializer.java new file mode 100644 index 00000000..289d1ffc --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/serialization/MethodCallDeserializer.java @@ -0,0 +1,51 @@ +package edu.university.ecs.lab.common.models.serialization; + +import com.google.gson.*; +import edu.university.ecs.lab.common.models.ir.MethodCall; +import edu.university.ecs.lab.common.models.ir.RestCall; +import edu.university.ecs.lab.common.models.enums.HttpMethod; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +/** + * Class for deserializing a MethodCall when using Gson + */ +public class MethodCallDeserializer implements JsonDeserializer { + + @Override + public MethodCall deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + if (jsonObject.has("url")) { + return jsonToRestCall(jsonObject); + } else { + return jsonToMethodCall(jsonObject); + } + } + + private MethodCall jsonToMethodCall(JsonObject json) throws JsonParseException { + MethodCall methodCall = new MethodCall(); + methodCall.setName(json.get("name").getAsString()); + methodCall.setCalledFrom(json.get("calledFrom").getAsString()); + methodCall.setObjectName(json.get("objectName").getAsString()); + methodCall.setParameterContents(json.get("parameterContents").getAsString()); + methodCall.setPackageAndClassName(json.get("packageAndClassName").getAsString()); + methodCall.setObjectType(json.get("objectType").getAsString()); + methodCall.setMicroserviceName(json.get("microserviceName").getAsString()); + methodCall.setClassName(json.get("className").getAsString()); + + + return methodCall; + } + + private RestCall jsonToRestCall(JsonObject json) throws JsonParseException { + MethodCall methodCall = jsonToMethodCall(json); + String url = json.get("url").getAsString(); + String httpMethod = json.get("httpMethod").getAsString(); + + + + + return new RestCall(methodCall, url, HttpMethod.valueOf(httpMethod)); + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/serialization/MethodDeserializer.java b/src/main/java/edu/university/ecs/lab/common/models/serialization/MethodDeserializer.java new file mode 100644 index 00000000..be163af8 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/serialization/MethodDeserializer.java @@ -0,0 +1,60 @@ +package edu.university.ecs.lab.common.models.serialization; + +import com.google.gson.*; +import edu.university.ecs.lab.common.models.ir.*; +import edu.university.ecs.lab.common.models.enums.HttpMethod; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.List; + +/** + * Class for deserializing a Method when using Gson + */ +public class MethodDeserializer implements JsonDeserializer { + + @Override + public Method deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + if (jsonObject.has("url")) { + return jsonToEndpoint(jsonObject, context); + } else { + return jsonToMethod(jsonObject, context); + } + } + + private Method jsonToMethod(JsonObject json, JsonDeserializationContext context) throws JsonParseException { + Method method = new Method(); + method.setName(json.get("name").getAsString()); + method.setReturnType(json.get("returnType").getAsString()); + + Set annotations = new HashSet<>(); + for (JsonElement annotationJson : json.get("annotations").getAsJsonArray()) { + annotations.add(context.deserialize(annotationJson, Annotation.class)); + } + method.setAnnotations(annotations); + + Set parameters = new HashSet<>(); + for (JsonElement fieldJson : json.get("parameters").getAsJsonArray()) { + parameters.add(context.deserialize(fieldJson, Parameter.class)); + } + method.setParameters(parameters); + method.setPackageAndClassName(json.get("packageAndClassName").getAsString()); + method.setMicroserviceName(json.get("microserviceName").getAsString()); + method.setClassName(json.get("className").getAsString()); + + + return method; + } + + private Method jsonToEndpoint(JsonObject json, JsonDeserializationContext context) throws JsonParseException { + Method method = jsonToMethod(json, context); + String url = json.get("url").getAsString(); + String httpMethod = json.get("httpMethod").getAsString(); + + + return new Endpoint(method, url, HttpMethod.valueOf(httpMethod)); + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/serialization/ProjectFileDeserializer.java b/src/main/java/edu/university/ecs/lab/common/models/serialization/ProjectFileDeserializer.java new file mode 100644 index 00000000..49ec3069 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/serialization/ProjectFileDeserializer.java @@ -0,0 +1,27 @@ +package edu.university.ecs.lab.common.models.serialization; + +import com.google.gson.*; +import edu.university.ecs.lab.common.models.ir.*; + +import java.lang.reflect.Type; + +public class ProjectFileDeserializer implements JsonDeserializer { + + @Override + public ProjectFile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + String type = jsonObject.get("fileType").getAsString(); + switch (type) { + case "JCLASS": + return context.deserialize(json, JClass.class); + case "POM": + return context.deserialize(json, ConfigFile.class); + case "CONFIG": + return context.deserialize(json, ConfigFile.class); + default: + throw new JsonParseException("Unsupported type: " + type); + } + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/models/serialization/package-info.java b/src/main/java/edu/university/ecs/lab/common/models/serialization/package-info.java new file mode 100644 index 00000000..9fc3677d --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/models/serialization/package-info.java @@ -0,0 +1,17 @@ +/** + * Provides utilities and classes for serializing Java objects to JSON and deserializing JSON + * back to Java objects using Gson library. + *

+ * This package includes: + * - {@link edu.university.ecs.lab.common.models.serialization.JsonSerializable}: Interface for classes + * that can be serialized to JSON objects. + * - {@link edu.university.ecs.lab.common.models.serialization.MethodCallDeserializer}: Deserializer + * for converting JSON to {@link edu.university.ecs.lab.common.models.ir.MethodCall} and + * {@link edu.university.ecs.lab.common.models.ir.RestCall} objects. + * - {@link edu.university.ecs.lab.common.models.serialization.MethodDeserializer}: Deserializer for + * converting JSON to {@link edu.university.ecs.lab.common.models.ir.Method} and + * {@link edu.university.ecs.lab.common.models.ir.Endpoint} objects. + *

+ * These classes facilitate conversion between Java objects and JSON representations. + */ +package edu.university.ecs.lab.common.models.serialization; diff --git a/src/main/java/edu/university/ecs/lab/common/package-info.java b/src/main/java/edu/university/ecs/lab/common/package-info.java new file mode 100644 index 00000000..3be1b0ec --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/package-info.java @@ -0,0 +1,14 @@ +/** + * Provides various components for managing and configuring a microservice system. + *

+ * This package includes: + * - {@link edu.university.ecs.lab.common.config}: Classes for handling configuration files + * and Git repository path extensions. + * - {@link edu.university.ecs.lab.common.error}: Enumerations for handling errors within the tool. + * - {@link edu.university.ecs.lab.common.models}: Components representing various aspects of + * microservices, including annotations, edges, endpoints, fields, classes, methods, method calls, + * microservices, microservice systems, network graphs, and REST calls. + * - {@link edu.university.ecs.lab.common.utils}: Utility classes for file management, JSON handling, + * and source code parsing. + */ +package edu.university.ecs.lab.common; diff --git a/src/main/java/edu/university/ecs/lab/common/services/GitService.java b/src/main/java/edu/university/ecs/lab/common/services/GitService.java new file mode 100644 index 00000000..9e72a04c --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/services/GitService.java @@ -0,0 +1,294 @@ +package edu.university.ecs.lab.common.services; + +import edu.university.ecs.lab.common.config.Config; +import edu.university.ecs.lab.common.config.ConfigUtil; +import edu.university.ecs.lab.common.error.Error; +import edu.university.ecs.lab.common.utils.FileUtils; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.*; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Service to perform Git opperations + */ +public class GitService { + private static final int EXIT_SUCCESS = 0; + private static final String HEAD_COMMIT = "HEAD"; + + private final Config config; + private final Repository repository; + + /** + * Create a Git service object from a project configuration file + * + * @param configPath path to project configuration file + */ + public GitService(String configPath) { + this.config = ConfigUtil.readConfig(configPath); + FileUtils.makeDirs(); + cloneRemote(); + this.repository = initRepository(); + } + + /** + * Method to clone a repository + */ + public void cloneRemote() { + String repositoryPath = FileUtils.getRepositoryPath(config.getRepoName()); + + // Check if repository was already cloned + if (new File(repositoryPath).exists()) { + return; + } + + // Create and execute operating system process to clone repository + try { + ProcessBuilder processBuilder = + new ProcessBuilder("git", "clone", config.getRepositoryURL(), repositoryPath); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + + if (exitCode != EXIT_SUCCESS) { + throw new Exception(); + } + + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + LoggerManager.info(() -> "Cloned repository " + config.getRepoName()); + } + + /** + * Method to reset repository to a given commit + * + * @param commitID commit id to reset to + */ + public void resetLocal(String commitID) { + validateLocalExists(); + + if (Objects.isNull(commitID) || commitID.isEmpty()) { + return; + } + + // Reset branch to old commit + try (Git git = new Git(repository)) { + git.reset().setMode(ResetCommand.ResetType.HARD).setRef(commitID).call(); + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + LoggerManager.info(() -> "Set repository " + config.getRepoName() + " to " + commitID); + } + + /** + * Method to check that local directory exists + */ + private void validateLocalExists() { + File file = new File(FileUtils.getRepositoryPath(config.getRepoName())); + if (!(file.exists() && file.isDirectory())) { + Error.reportAndExit(Error.REPO_DONT_EXIST, Optional.empty()); + } + } + + /** + * Method to initialize repository from repository name + * + * @return file repository + */ + public Repository initRepository() { + validateLocalExists(); + + Repository repository = null; + + try { + File repositoryPath = new File(FileUtils.getRepositoryPath(config.getRepoName())); + repository = new FileRepositoryBuilder().setGitDir(new File(repositoryPath, ".git")).build(); + + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + return repository; + } + + /** + * Method to get differences between old and new commits + * + * @param commitOld old commit id + * @param commitNew new commit id + * @return list of changes from old commit to new commit + */ + public List getDifferences(String commitOld, String commitNew) { + List returnList = null; + RevCommit oldCommit = null, newCommit = null; + RevWalk revWalk = new RevWalk(repository); + + try { + // Parse the old and new commits + oldCommit = revWalk.parseCommit(repository.resolve(commitOld)); + newCommit = revWalk.parseCommit(repository.resolve(commitNew)); + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + // Prepare tree parsers for both commits + try (ObjectReader reader = repository.newObjectReader()) { + CanonicalTreeParser oldTreeParser = new CanonicalTreeParser(); + CanonicalTreeParser newTreeParser = new CanonicalTreeParser(); + + // Use tree objects from the commits + oldTreeParser.reset(reader, oldCommit.getTree().getId()); + newTreeParser.reset(reader, newCommit.getTree().getId()); + + // Compute differences between the trees of the two commits + try (Git git = new Git(repository)) { + List rawDiffs = git.diff() + .setOldTree(oldTreeParser) + .setNewTree(newTreeParser) + .call(); + + // Filter out diffs that only contain whitespace or comment changes + RevCommit finalOldCommit = oldCommit; + RevCommit finalNewCommit = newCommit; + returnList = rawDiffs.stream() + .filter(diff -> isCodeChange(diff, repository, finalOldCommit, finalNewCommit)) + .collect(Collectors.toList()); + } + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + LoggerManager.debug(() -> "Got differences of repository " + config.getRepoName() + " between " + commitOld + " -> " + commitNew); + + return returnList; + } + + /** + * Method to check if a commit difference was a change to the code + * + * @param diff DiffEntry object + * @param repository repository to check + * @param oldCommit old commit id + * @param newCommit new commit id + * + * @return true if difference was a change to the code, false otherwise + */ + private boolean isCodeChange(DiffEntry diff, Repository repository, RevCommit oldCommit, RevCommit newCommit) { + if((!diff.getOldPath().endsWith(".java") && !diff.getNewPath().endsWith(".java"))) { + return true; + } + + // Read the file contents before and after the changes + String oldContent = getContentFromTree(repository, oldCommit.getTree().getId(), diff.getOldPath()); + String newContent = getContentFromTree(repository, newCommit.getTree().getId(), diff.getNewPath()); + + // Remove comments and whitespace from both contents + String oldCode = stripCommentsAndWhitespace(oldContent); + String newCode = stripCommentsAndWhitespace(newContent); + + // If the meaningful code is different, return true + return !oldCode.equals(newCode); + } + + /** + * Get file data from a file tree + * + * @param repository repository to check + * @param treeId id of the tree to check + * @param filePath file to get data from + * @return data from the file, or an empty string if an error occurs or file is not found + */ + private String getContentFromTree(Repository repository, ObjectId treeId, String filePath) { + try (ObjectReader reader = repository.newObjectReader(); + TreeWalk treeWalk = new TreeWalk(repository)) { + + // Add the tree to the tree walker + treeWalk.addTree(treeId); + treeWalk.setRecursive(true); // We want to search recursively + + // Walk through the tree to find the file + while (treeWalk.next()) { + String currentPath = treeWalk.getPathString(); + if (currentPath.equals(filePath)) { + // Ensure we have a blob (file) and not a tree + if (treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { + // Read the file content and return it + byte[] data = reader.open(treeWalk.getObjectId(0)).getBytes(); + return new String(data, StandardCharsets.UTF_8); + } + } + } + + } catch (Exception e) { + // Return an empty string in case of an error + return ""; + } + + // If the file is not found, return an empty string + return ""; + } + + /** + * Remove comments and whitespace from file content + * + * @param content string of all file content + * @return string of file content with whitespace and comments removed + */ + private String stripCommentsAndWhitespace(String content) { + return content.replaceAll("(//.*|/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/|\\s+)", ""); + } + + /** + * Get Git log + * + * @return Git log as a list + */ + public Iterable getLog() { + Iterable returnList = null; + + try (Git git = new Git(repository)) { + returnList = git.log().call(); + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + return returnList; + } + + /** + * Get head commit for the repository + * + * @return commit id of head commit + */ + public String getHeadCommit() { + String commitID = ""; + + try { + Ref head = repository.findRef(HEAD_COMMIT); + RevWalk walk = new RevWalk(repository); + ObjectId commitId = head.getObjectId(); + RevCommit commit = walk.parseCommit(commitId); + commitID = commit.getName(); + walk.close(); + } catch (Exception e) { + Error.reportAndExit(Error.GIT_FAILED, Optional.of(e)); + } + + return commitID; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/services/LoggerManager.java b/src/main/java/edu/university/ecs/lab/common/services/LoggerManager.java new file mode 100644 index 00000000..7bf5e225 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/services/LoggerManager.java @@ -0,0 +1,66 @@ +package edu.university.ecs.lab.common.services; + + +import edu.university.ecs.lab.common.error.Error; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Static functions to manage logger object + */ +public class LoggerManager { + private static final Logger logger = LogManager.getLogger(LoggerManager.class); + + + /** + * Log an info message + * + * @param msgSupplier the message to log + */ + public static void info(Supplier msgSupplier) { + log(Level.INFO, msgSupplier); + } + + /** + * Log a warning message + * + * @param msgSupplier the message to log + */ + public static void warn(Supplier msgSupplier) { + log(Level.WARN, msgSupplier); + } + + /** + * Log a debug message + * + * @param msgSupplier the message to log + */ + public static void debug(Supplier msgSupplier) { + log(Level.DEBUG, msgSupplier); + } + + /** + * Log an error message + * + * @param msgSupplier the message to log + */ + public static void error(Supplier msgSupplier, Optional exception) { + log(Level.ERROR, msgSupplier); + exception.ifPresent(e -> logger.error(e.getMessage(), e)); + } + + /** + * Log message + * + * @param level the logging level + * @param msgSupplier the message to log + */ + private static void log(Level level, Supplier msgSupplier) { + logger.log(level, msgSupplier.get()); + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/common/utils/FileUtils.java b/src/main/java/edu/university/ecs/lab/common/utils/FileUtils.java new file mode 100644 index 00000000..26947cf9 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/utils/FileUtils.java @@ -0,0 +1,154 @@ +package edu.university.ecs.lab.common.utils; + +import edu.university.ecs.lab.common.error.Error; +import java.io.File; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + + +/** + * Manages all file paths and file path conversion functions. + */ +public class FileUtils { + public static final Set VALID_FILES = Set.of("pom.xml", ".java", ".yml", "build.gradle"); + public static final String SYS_SEPARATOR = System.getProperty("file.separator"); + public static final String SPECIAL_SEPARATOR = SYS_SEPARATOR.replace("\\", "\\\\"); + private static final String DEFAULT_OUTPUT_PATH = "output"; + private static final String DEFAULT_CLONE_PATH = "clone"; + private static final String DOT = "."; + public static final String GIT_SEPARATOR = "/"; + + /** + * Private constructor to prevent instantiation. + */ + private FileUtils() {} + + /** + * This method returns the relative path of the cloned repository directory as ./DEFAULT_CLONE_PATH/repoName. + * This will be a working relative path to the repository directory on the local file system. + * + * @param repoName the name of the repo + * @return the relative path string where that repository is cloned to + */ + public static String getRepositoryPath(String repoName) { + return getClonePath() + SYS_SEPARATOR + repoName; + } + + /** + * This method returns the relative local path of the output directory as ./DEFAULT_OUTPUT_PATH. + * This will be a working relative path to the output directory on the local file system. + * + * @return the relative path string where the output will exist + */ + public static String getOutputPath() { + return DOT + SYS_SEPARATOR + DEFAULT_OUTPUT_PATH; + } + + /** + * This method returns the relative local path of the output directory as ./DEFAULT_OUTPUT_PATH. + * This will be a working relative path to the output directory on the local file system. + * + * @return the relative path string where the output will exist + */ + public static String getClonePath() { + return DOT + SYS_SEPARATOR + DEFAULT_CLONE_PATH; + } + + /** + * This method converts a path of the form .\clone\repoName\pathToFile to the form + * /pathToFile + * + * @param localPath the local path to be converted + * @param repoName the name of the repo cloned locally + * @return the relative repo path + */ + public static String localPathToGitPath(String localPath, String repoName) { + return localPath.replace(FileUtils.getRepositoryPath(repoName), "").replaceAll(SPECIAL_SEPARATOR, GIT_SEPARATOR); + } + /** + * This method converts a path of the form .\clone\repoName\pathToFile to the form + * /pathToFile + * + * @param localPath the local path to be converted + * @param repoName the name of the repo cloned locally + * @return the relative repo path + */ + public static String gitPathToLocalPath(String localPath, String repoName) { + return getRepositoryPath(repoName) + localPath.replace(GIT_SEPARATOR, SYS_SEPARATOR); + } + + + + @Deprecated + public static String getMicroserviceNameFromPath(String path) { + if (!path.startsWith(DOT + SYS_SEPARATOR + DEFAULT_CLONE_PATH + SYS_SEPARATOR)) { + Error.reportAndExit(Error.INVALID_REPO_PATHS, Optional.empty()); + } + + String[] split = path.replace(DOT + SYS_SEPARATOR + DEFAULT_CLONE_PATH + SYS_SEPARATOR, "").split(SPECIAL_SEPARATOR); + return split[split.length-1]; + } + + /** + * This method returns a Git path without the filename at the end. + * + * @param path the path to remove filename from + * @return the path without the file name or if too short just GIT_SEPARATOR + */ + public static String getGitPathNoFileName(String path) { + String[] split = path.split(GIT_SEPARATOR); + + if(split.length > 1) { + return String.join(GIT_SEPARATOR, Arrays.copyOfRange(split, 0, split.length - 1)); + } else { + return GIT_SEPARATOR; + } + } + + /** + * This method creates the default output and clone directories + */ + public static void makeDirs() { + try { + new File(getOutputPath()).mkdirs(); + new File(getClonePath()).mkdirs(); + } catch (Exception e) { + Error.reportAndExit(Error.INVALID_REPO_PATHS, Optional.of(e)); + } + } + + /** + * This method filters the file's that should be present in the project + * + * @param path the file for checking + * @return boolean true if it belongs in the project + */ + public static boolean isValidFile(String path) { + // Special check for github metadata files + if(path.contains(".github")) { + return false; + } + + for(String f : VALID_FILES) { + if(path.endsWith(f)) { + return true; + } + } + + return false; + } + + /** + * This method filters the static files present in the project, + * not including Java source file but configuration files only + * + * @param path the file for checking + * @return boolean true if it is a configuration file + */ + public static boolean isConfigurationFile(String path) { + return isValidFile(path) && !path.endsWith(".java"); + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/utils/FlowUtils.java b/src/main/java/edu/university/ecs/lab/common/utils/FlowUtils.java new file mode 100644 index 00000000..578bb16b --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/utils/FlowUtils.java @@ -0,0 +1,255 @@ +package edu.university.ecs.lab.common.utils; + +import edu.university.ecs.lab.common.models.ir.*; + +import java.util.*; +import java.util.stream.Collectors; + +public class FlowUtils { + + /** + * Method for generating all possibilities + * of Flows + * + * @param microserviceSystem the microservice system to scan for flows + * @return the list of all possible flows + */ + public static List buildFlows(MicroserviceSystem microserviceSystem) { + List allFlows = new ArrayList<>(); + List baseFlows = generateNewFlows(getAllMicroserviceControllers(microserviceSystem)); + Flow flowCopy1, flowCopy2, flowCopy3, flowCopy4; + + for(Flow flow : baseFlows) { + List serviceMethodCalls = findAllServiceMethodCalls(flow); + for(MethodCall serviceMethodCall : serviceMethodCalls) { + flowCopy1 = flow; + flowCopy1.setServiceMethodCall(serviceMethodCall); + + Optional serviceField = Optional.ofNullable(findServiceField(flowCopy1)); + if (serviceField.isPresent()) { + flowCopy1.setControllerServiceField(serviceField.get()); + + List serviceClasses = findAllServices(flowCopy1); + for (JClass serviceClass : serviceClasses) { + flowCopy2 = flowCopy1; + flowCopy2.setService(serviceClass); + + Optional serviceMethod = Optional.ofNullable(findServiceMethod(flowCopy2)); + if (serviceMethod.isPresent()) { + flowCopy2.setServiceMethod(serviceMethod.get()); + + List repositoryMethodCalls = findAllRepositoryMethodCalls(flowCopy2); + for (MethodCall repositroyMethodCall : repositoryMethodCalls) { + flowCopy3 = flowCopy2; + flow.setRepositoryMethodCall(repositroyMethodCall); + + Optional repositoryField = Optional.ofNullable(findRepositoryField(flowCopy3)); + if (repositoryField.isPresent()) { + flowCopy3.setServiceRepositoryField(repositoryField.get()); + + List repositoryClasses = findAllRepositorys(flowCopy3); + for (JClass repositoryClass : repositoryClasses) { + flowCopy4 = flowCopy3; + + flowCopy4.setRepository(repositoryClass); + + Optional repositoryMethod = Optional.ofNullable(findRepositoryMethod(flowCopy4)); + if (repositoryMethod.isPresent()) { + flowCopy4.setRepositoryMethod(repositoryMethod.get()); + } + + allFlows.add(flowCopy4); + } + } else { + allFlows.add(flowCopy3); + } + + } + } else { + allFlows.add(flowCopy2); + } + + } + } else { + allFlows.add(flowCopy1); + } + + } + + if(serviceMethodCalls.isEmpty()) { + allFlows.add(flow); + } + + } + + return allFlows; + } + + /** + * This method returns a map of microservices to their controller classes + * + * @param microserviceSystem the microservice system to convert + * @return the map of microservice to JClass controllers + */ + private static Map> getAllMicroserviceControllers(MicroserviceSystem microserviceSystem) { + Map> controllerMap = new HashMap<>(); + + for (Microservice microservice : microserviceSystem.getMicroservices()) { + controllerMap.put(microservice, microservice.getControllers()); + } + + return controllerMap; + } + + /** + * This method generates the base flows + * + * @param controllerMap the controller map + * @return the base flows + */ + private static List generateNewFlows(Map> controllerMap) { + List flows = new ArrayList<>(); + Flow f; + + for (Map.Entry> controllerList : controllerMap.entrySet()) { + for (JClass controller : controllerList.getValue()) { + for (Endpoint endpoint : controller.getEndpoints()) { + f = new Flow(); + f.setController(controller); + f.setControllerMethod(endpoint); + f.setModel(controllerList.getKey()); + flows.add(f); + } + } + } + return flows; + } + + private static List generateNewFlows( + Microservice microservice, List controllers) { + List flows = new ArrayList<>(); + Flow f; + + for (JClass controller : controllers) { + for (Endpoint endpoint : controller.getEndpoints()) { + f = new Flow(); + f.setController(controller); + f.setControllerMethod(endpoint); + f.setModel(microservice); + flows.add(f); + } + } + + return flows; + } + + /** + * This method find's all method calls from the controllerMethod of a flow + * + * @param flow the flow + * @return the list of MethodCalls from the controllerMethod of the flow + */ + private static List findAllServiceMethodCalls(Flow flow) { + return flow.getController().getMethodCalls().stream() + .filter(mc -> mc.getCalledFrom().equals(flow.getControllerMethod().getName())) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * This method find's the service field affiliated with a methodCall of a flow + * + * @param flow the flow + * @return the field called from this flow's serviceMethodCall + */ + private static Field findServiceField(Flow flow) { + return flow.getController().getFields().stream() + .filter(f -> f.getName().equals(flow.getServiceMethodCall().getObjectName())) + .findFirst() + .orElse(null); + } + + /** + * This method finds any jClass affiliated with the serviceField of a flow. + * Due to polymorphism the type is not guaranteed to match one class so all + * possibilities will be considered. + * Note: This is a source of approximation -- Runtime types + * + * @param flow the flow + * @return the jClass affiliated with the serviceField + */ + private static List findAllServices(Flow flow) { + return flow.getModel().getServices().stream() + .filter( + jClass -> jClass.getImplementedTypes().contains(flow.getControllerServiceField().getType()) || jClass.getName().equals(flow.getControllerServiceField().getType())) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * This method finds the method affiliated with the serviceMethodCall of a flow + * + * @param flow the flow + * @return the method affiliated with the serviceMethodCall + */ + private static Method findServiceMethod(Flow flow) { + return flow.getService().getMethods().stream() + .filter( + method -> method.getName().equals(flow.getServiceMethodCall().getName())) + .findFirst() + .orElse(null); + } + + /** + * This method find's all method calls from the serviceMethod of a flow + * + * @param flow the flow + * @return the list of MethodCalls from the serviceMethod of the flow + */ + private static List findAllRepositoryMethodCalls(Flow flow) { + return flow.getService().getMethodCalls().stream() + .filter(mc -> mc.getCalledFrom().equals(flow.getServiceMethod().getName())) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * This method find's the repository field affiliated with a methodCall of a flow + * + * @param flow the flow + * @return the field called from this flow's repositoryMethodCall + */ + private static Field findRepositoryField(Flow flow) { + return flow.getService().getFields().stream() + .filter(f -> f.getName().equals(flow.getRepositoryMethodCall().getObjectName())) + .findFirst() + .orElse(null); + } + + /** + * This method finds any jClass affiliated with the repositoryField of a flow. + * Due to polymorphism the type is not guaranteed to match one class so all + * possibilities will be considered. + * Note: This is a source of approximation -- Runtime types + * + * @param flow the flow + * @return the jClass affiliated with the repositoryField + */ + private static List findAllRepositorys(Flow flow) { + return flow.getModel().getRepositories().stream() + .filter( + jClass -> jClass.getImplementedTypes().contains(flow.getServiceRepositoryField().getType()) || jClass.getName().equals(flow.getServiceRepositoryField().getType())) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * This method finds the method affiliated with the repositoryMethodCall of a flow + * + * @param flow the flow + * @return the method affiliated with the repositoryMethodCall + */ + private static Method findRepositoryMethod(Flow flow) { + return flow.getRepository().getMethods().stream() + .filter( + method -> method.getName().equals(flow.getRepositoryMethodCall().getName())) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/edu/university/ecs/lab/common/utils/JsonReadWriteUtils.java b/src/main/java/edu/university/ecs/lab/common/utils/JsonReadWriteUtils.java new file mode 100644 index 00000000..26967660 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/utils/JsonReadWriteUtils.java @@ -0,0 +1,82 @@ +package edu.university.ecs.lab.common.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import edu.university.ecs.lab.common.error.Error; +import edu.university.ecs.lab.common.models.ir.Method; +import edu.university.ecs.lab.common.models.ir.MethodCall; +import edu.university.ecs.lab.common.models.ir.ProjectFile; +import edu.university.ecs.lab.common.models.serialization.MethodCallDeserializer; +import edu.university.ecs.lab.common.models.serialization.MethodDeserializer; +import edu.university.ecs.lab.common.models.serialization.ProjectFileDeserializer; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +/** + * Utility class for reading and writing JSON to a file. + */ +public class JsonReadWriteUtils { + /** + * Private constructor to prevent instantiation. + */ + private JsonReadWriteUtils() { + } + + /** + * Writes an object to a JSON file at a specified path. + * + * @param the type of the object to write + * @param object the object to serialize into JSON + * @param filePath the file path where the JSON should be saved + */ + public static void writeToJSON(String filePath, T object) { + Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + try { + Path path = Paths.get(filePath); + Files.createDirectories(path.getParent()); + try (Writer writer = Files.newBufferedWriter(path)) { + gson.toJson(object, writer); + } + } catch (IOException e) { + Error.reportAndExit(Error.INVALID_JSON_WRITE, Optional.of(e)); + } + } + + /** + * Reads a JSON file from a given path and converts it into an object of the specified type. + * + * @param the type of the object to return + * @param filePath the file path to the JSON file + * @param type the Class representing the type of the object to deserialize + * @return an object of type T containing the data from the JSON file + */ + public static T readFromJSON(String filePath, Class type) { + // Register appropriate deserializers to allow compaction of data + + Gson gson = registerDeserializers(); + try (Reader reader = new BufferedReader(new FileReader(filePath))) { + return gson.fromJson(reader, type); + } catch (Exception e) { + Error.reportAndExit(Error.INVALID_JSON_READ, Optional.of(e)); + } + + return null; + } + + /** + * Function for register custom deserializers when reading JSON from a file + * @return + */ + public static Gson registerDeserializers() { + + return new GsonBuilder() + .registerTypeAdapter(Method.class, new MethodDeserializer()) + .registerTypeAdapter(MethodCall.class, new MethodCallDeserializer()) + .registerTypeAdapter(ProjectFile.class, new ProjectFileDeserializer()) + .create(); + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/utils/NonJsonReadWriteUtils.java b/src/main/java/edu/university/ecs/lab/common/utils/NonJsonReadWriteUtils.java new file mode 100644 index 00000000..ffdb556f --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/utils/NonJsonReadWriteUtils.java @@ -0,0 +1,160 @@ +package edu.university.ecs.lab.common.utils; + +import com.google.gson.*; +import edu.university.ecs.lab.common.config.Config; +import edu.university.ecs.lab.common.models.enums.FileType; +import edu.university.ecs.lab.common.models.ir.ConfigFile; +import org.json.JSONObject; +import org.json.XML; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +/** + * Utility class for reading files that don't abide by JSON format + */ +public class NonJsonReadWriteUtils { + + /** + * Private constructor to prevent instantiation. + */ + private NonJsonReadWriteUtils() { + } + + /** + * This method reads YAML from a file returning structure as JsonObject + * @param path the path to the YAML file. + * @return JsonObject YAML file structure as json object + */ + public static ConfigFile readFromYaml(String path, Config config) { + JsonObject data = null; + Yaml yaml = new Yaml(new SafeConstructor()); + Gson gson = new Gson(); + + try (FileInputStream fis = new FileInputStream(path)) { + // Parse YAML file to Map + Map yamlMap = yaml.load(fis); + + if (yamlMap == null || yamlMap.isEmpty()) { + // Handle empty file or empty YAML content + data = new JsonObject(); + } else { + String jsonString = gson.toJson(yamlMap); + data = JsonParser.parseString(jsonString).getAsJsonObject(); + } + + } catch (Exception e) { + // Handle I/O errors (file not found, etc.) + return null; + } + + return new ConfigFile(FileUtils.localPathToGitPath(path, config.getRepoName()), new File(path).getName(), data, FileType.CONFIG); + } + + public static ConfigFile readFromDocker(String path, Config config) { + List instructions = new ArrayList<>(); + JsonObject jsonObject; + try (BufferedReader br = new BufferedReader(new FileReader(path))) { + String line; + while ((line = br.readLine()) != null) { + instructions.add(line.trim()); // Add each line as an instruction + } + } catch (Exception e) { + return null; + } + jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + for (String instruction : instructions) { + jsonArray.add(instruction); + } + jsonObject.add("instructions", jsonArray); + + return new ConfigFile(FileUtils.localPathToGitPath(path, config.getRepoName()), new File(path).getName(), jsonObject, FileType.CONFIG); + } + + public static ConfigFile readFromPom(String path, Config config) { + String xmlContent = null; + JsonObject jsonObject; + try { + // Read the entire file content + xmlContent = new String(Files.readAllBytes(Paths.get(path))); + + if (xmlContent.trim().isEmpty()) { + jsonObject = new JsonObject(); + } else { + // Convert XML to JSONObject using org.json + JSONObject jsonObjectOld = XML.toJSONObject(xmlContent); + + // Convert JSONObject to Gson JsonObject + JsonElement jsonElement = JsonParser.parseString(jsonObjectOld.toString()); + jsonObject = jsonElement.getAsJsonObject(); + } + } catch (Exception e) { + return null; + } + + + return new ConfigFile(FileUtils.localPathToGitPath(path, config.getRepoName()), new File(path).getName(), jsonObject, FileType.CONFIG); + } + + public static ConfigFile readFromGradle(String path, Config config) { + JsonObject jsonObject = new JsonObject(); + Stack jsonStack = new Stack<>(); + jsonStack.push(jsonObject); + + try (BufferedReader br = new BufferedReader(new FileReader(path))) { + String line; + String currentKey = null; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + if (line.isEmpty()) { + continue; + } + + if (line.endsWith("{")) { + String key = line.substring(0, line.length() - 1).trim(); + JsonObject newObject = new JsonObject(); + jsonStack.peek().add(key, newObject); + jsonStack.push(newObject); + currentKey = key; + } else if (line.equals("}")) { + jsonStack.pop(); + currentKey = null; + } else if (line.contains("=")) { + String[] parts = line.split("=", 2); + if (parts.length == 2) { + String key = parts[0].trim(); + String value = parts[1].trim().replace("'", "\""); + jsonStack.peek().addProperty(key, value); + } + } else { + if (currentKey != null) { + JsonArray array = jsonStack.peek().has(currentKey) ? + jsonStack.peek().getAsJsonArray(currentKey) : new JsonArray(); + array.add(line); + jsonStack.peek().add(currentKey, array); + } + } + } + } catch (IOException e) { + return null; + } + + return new ConfigFile( + FileUtils.localPathToGitPath(path, config.getRepoName()), + new File(path).getName(), + jsonObject, + FileType.CONFIG + ); + } + +} diff --git a/src/main/java/edu/university/ecs/lab/common/utils/SourceToObjectUtils.java b/src/main/java/edu/university/ecs/lab/common/utils/SourceToObjectUtils.java new file mode 100644 index 00000000..5cd68f21 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/utils/SourceToObjectUtils.java @@ -0,0 +1,575 @@ +package edu.university.ecs.lab.common.utils; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.*; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.expr.*; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import com.github.javaparser.resolution.UnsolvedSymbolException; +import com.github.javaparser.symbolsolver.JavaSymbolSolver; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; +import com.github.javaparser.symbolsolver.model.resolution.TypeSolver; +import com.github.javaparser.symbolsolver.model.typesystem.ReferenceTypeImpl; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import edu.university.ecs.lab.common.config.Config; +import edu.university.ecs.lab.common.error.Error; +import edu.university.ecs.lab.common.models.enums.ClassRole; +import edu.university.ecs.lab.common.models.enums.EndpointTemplate; +import edu.university.ecs.lab.common.models.enums.HttpMethod; +import edu.university.ecs.lab.common.models.enums.RestCallTemplate; +import edu.university.ecs.lab.common.models.ir.*; +import edu.university.ecs.lab.common.services.LoggerManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Static utility class for parsing a file and returning associated models from code structure. + */ +public class SourceToObjectUtils { + private static CompilationUnit cu; + private static String microserviceName = ""; + private static String path; + private static String className; + private static String packageName; + private static String packageAndClassName; + private static CombinedTypeSolver combinedTypeSolver; + private static Config config; + + + private static void generateStaticValues(File sourceFile, Config config1) { + // Parse the highest level node being compilation unit + config = config1; + try { + cu = StaticJavaParser.parse(sourceFile); + } catch (Exception e) { + LoggerManager.warn(() -> "Failed to parse " + sourceFile.getPath()); + microserviceName = ""; + return; +// Error.reportAndExit(Error.JPARSE_FAILED, Optional.of(e)); + } + if (!cu.findAll(PackageDeclaration.class).isEmpty()) { + packageName = cu.findAll(PackageDeclaration.class).get(0).getNameAsString(); + packageAndClassName = packageName + "." + sourceFile.getName().replace(".java", ""); + } + path = FileUtils.localPathToGitPath(sourceFile.getPath(), config.getRepoName()); + + TypeSolver reflectionTypeSolver = new ReflectionTypeSolver(); + TypeSolver javaParserTypeSolver = new JavaParserTypeSolver(FileUtils.getRepositoryPath(config.getRepoName())); + + combinedTypeSolver = new CombinedTypeSolver(); + combinedTypeSolver.add(reflectionTypeSolver); + combinedTypeSolver.add(javaParserTypeSolver); + + JavaSymbolSolver symbolSolver = new JavaSymbolSolver(combinedTypeSolver); + StaticJavaParser.getConfiguration().setSymbolResolver(symbolSolver); + className = sourceFile.getName().replace(".java", ""); + + } + + /** + * This method parses a Java class file and return a JClass object. + * + * @param sourceFile the file to parse + * @return the JClass object representing the file + */ + public static JClass parseClass(File sourceFile, Config config, String microserviceName) { + // Guard condition + if(Objects.isNull(sourceFile) || FileUtils.isConfigurationFile(sourceFile.getPath())) { + LoggerManager.warn(() -> "JClass filtered " + sourceFile.getPath() + " is config or null"); + return null; + } + + generateStaticValues(sourceFile, config); + if (!microserviceName.isEmpty()) { + SourceToObjectUtils.microserviceName = microserviceName; + } + + // Calculate early to determine classrole based on annotation, filter for class based annotations only + Set classAnnotations = filterClassAnnotations(); + AnnotationExpr requestMapping = classAnnotations.stream().filter(ae -> ae.getNameAsString().equals("RequestMapping")).findFirst().orElse(null); + + ClassRole classRole = parseClassRole(classAnnotations); + + // Return unknown classRoles where annotation not found + if (classRole.equals(ClassRole.UNKNOWN)) { + LoggerManager.warn(() -> "JClass filtered " + sourceFile.getPath() + " class role unknown"); + return null; + } + + JClass jClass = null; + if(classRole == ClassRole.FEIGN_CLIENT) { + jClass = handleFeignClient(requestMapping, classAnnotations); + } else if(classRole == ClassRole.REP_REST_RSC) { + jClass = handleRepositoryRestResource(requestMapping, classAnnotations); + } else { + jClass = new JClass( + className, + path, + packageName, + classRole, + parseMethods(cu.findAll(MethodDeclaration.class), requestMapping), + parseFields(cu.findAll(FieldDeclaration.class)), + parseAnnotations(classAnnotations), + parseMethodCalls(cu.findAll(MethodDeclaration.class)), + cu.findAll(ClassOrInterfaceDeclaration.class).get(0).getImplementedTypes().stream().map(NodeWithSimpleName::getNameAsString).collect(Collectors.toSet())); + } + + // Build the JClass + return jClass; + + } + + + /** + * This method parses methodDeclarations list and returns a Set of Method models + * + * @param methodDeclarations the list of methodDeclarations to be parsed + * @return a set of Method models representing the MethodDeclarations + */ + public static Set parseMethods(List methodDeclarations, AnnotationExpr requestMapping) { + // Get params and returnType + Set methods = new HashSet<>(); + + for (MethodDeclaration methodDeclaration : methodDeclarations) { + Set parameters = new HashSet<>(); + for (Parameter parameter : methodDeclaration.getParameters()) { + parameters.add(new edu.university.ecs.lab.common.models.ir.Parameter(parameter, packageAndClassName)); + } + + Method method = new Method( + methodDeclaration.getNameAsString(), + packageAndClassName, + parameters, + methodDeclaration.getTypeAsString(), + parseAnnotations(methodDeclaration.getAnnotations()), + microserviceName, + className); + + method = convertValidEndpoints(methodDeclaration, method, requestMapping); + + methods.add(method); + } + + return methods; + } + + /** + * This method converts a valid Method to an Endpoint + * + * @param methodDeclaration the MethodDeclaration associated with Method + * @param method the Method to be converted + * @param requestMapping the class level requestMapping + * @return returns method if it is invalid, otherwise a new Endpoint + */ + public static Method convertValidEndpoints(MethodDeclaration methodDeclaration, Method method, AnnotationExpr requestMapping) { + for (AnnotationExpr ae : methodDeclaration.getAnnotations()) { + String ae_name = ae.getNameAsString(); + if (EndpointTemplate.ENDPOINT_ANNOTATIONS.contains(ae_name)) { + EndpointTemplate endpointTemplate = new EndpointTemplate(requestMapping, ae); + + // By Spring documentation, only the first valid @Mapping annotation is considered; + // And getAnnotations() return them in order, so we can return immediately + return new Endpoint(method, endpointTemplate.getUrl(), endpointTemplate.getHttpMethod()); + } + } + + return method; + } + + + + /** + * This method parses methodDeclarations list and returns a Set of MethodCall models + * + * @param methodDeclarations the list of methodDeclarations to be parsed + * @return a set of MethodCall models representing MethodCallExpressions found in the MethodDeclarations + */ + public static List parseMethodCalls(List methodDeclarations) { + List methodCalls = new ArrayList<>(); + + // loop through method calls + for (MethodDeclaration methodDeclaration : methodDeclarations) { + for (MethodCallExpr mce : methodDeclaration.findAll(MethodCallExpr.class)) { + String methodName = mce.getNameAsString(); + + String calledServiceName = getCallingObjectName(mce); + String calledServiceType = getCallingObjectType(mce); + + String parameterContents = mce.getArguments().stream().map(Objects::toString).collect(Collectors.joining(",")); + + if (Objects.nonNull(calledServiceName)) { + MethodCall methodCall = new MethodCall(methodName, packageAndClassName, calledServiceType, calledServiceName, + methodDeclaration.getNameAsString(), parameterContents, microserviceName, className); + + methodCall = convertValidRestCalls(mce, methodCall); + + methodCalls.add(methodCall); + } + } + } + + return methodCalls; + } + + /** + * This method converts a valid MethodCall to an RestCall + * + * @param methodCallExpr the MethodDeclaration associated with Method + * @param methodCall the MethodCall to be converted + * @return returns methodCall if it is invalid, otherwise a new RestCall + */ + public static MethodCall convertValidRestCalls(MethodCallExpr methodCallExpr, MethodCall methodCall) { + if ((!RestCallTemplate.REST_OBJECTS.contains(methodCall.getObjectType()) || !RestCallTemplate.REST_METHODS.contains(methodCallExpr.getNameAsString()))) { + return methodCall; + } + + RestCallTemplate restCallTemplate = new RestCallTemplate(methodCallExpr,methodCall, cu); + + if (restCallTemplate.getUrl().isEmpty()) { + return methodCall; + } + + return new RestCall(methodCall, restCallTemplate.getUrl(), restCallTemplate.getHttpMethod()); + } + + /** + * This method converts a list of FieldDeclarations to a set of Field models + * + * @param fieldDeclarations the field declarations to parse + * @return the set of Field models + */ + private static Set parseFields(List fieldDeclarations) { + Set javaFields = new HashSet<>(); + + // loop through class declarations + for (FieldDeclaration fd : fieldDeclarations) { + for (VariableDeclarator variable : fd.getVariables()) { + javaFields.add(new Field(variable.getNameAsString(), packageAndClassName, variable.getTypeAsString())); + } + + } + + return javaFields; + } + + + /** + * Get the name of the object a method is being called from (callingObj.methodName()) + * + * @return the name of the object the method is being called from + */ + private static String getCallingObjectName(MethodCallExpr mce) { + + + Expression scope = mce.getScope().orElse(null); + + if (Objects.nonNull(scope) && scope instanceof NameExpr) { + NameExpr fae = scope.asNameExpr(); + return fae.getNameAsString(); + } + + return ""; + + } + + private static String getCallingObjectType(MethodCallExpr mce) { + + Expression scope = mce.getScope().orElse(null); + + if (Objects.isNull(scope)) { + return ""; + } + + try { + // Resolve the type of the object + var resolvedType = JavaParserFacade.get(combinedTypeSolver).getType(scope); + List parts = List.of(((ReferenceTypeImpl) resolvedType).getQualifiedName().split("\\.")); + if(parts.isEmpty()) { + return ""; + } + + return parts.get(parts.size() - 1); + } catch (Exception e) { + if(e instanceof UnsolvedSymbolException && ((UnsolvedSymbolException) e).getName() != null) { + return ((UnsolvedSymbolException) e).getName(); + } + return ""; + } + } + + + /** + * This method parses a list of annotation expressions and returns a set of Annotation models + * + * @param annotationExprs the annotation expressions to parse + * @return the Set of Annotation models + */ + private static Set parseAnnotations(Iterable annotationExprs) { + Set annotations = new HashSet<>(); + + for (AnnotationExpr ae : annotationExprs) { + annotations.add(new Annotation(ae, packageAndClassName)); + } + + return annotations; + } + + /** + * This method searches a list of Annotation expressions and returns a ClassRole found + * + * @param annotations the list of annotations to search + * @return the ClassRole determined + */ + private static ClassRole parseClassRole(Set annotations) { + for (AnnotationExpr annotation : annotations) { + switch (annotation.getNameAsString()) { + case "RestController": + case "Controller": + return ClassRole.CONTROLLER; + case "Service": + return ClassRole.SERVICE; + case "Repository": + return ClassRole.REPOSITORY; + case "RepositoryRestResource": + return ClassRole.REP_REST_RSC; + case "Entity": + case "Embeddable": + return ClassRole.ENTITY; + case "FeignClient": + return ClassRole.FEIGN_CLIENT; + } + } + return ClassRole.UNKNOWN; + } + + /** + * Get the name of the microservice based on the file + * + * @param sourceFile the file we are getting microservice name for + * @return + */ + private static String getMicroserviceName(File sourceFile) { + List split = Arrays.asList(sourceFile.getPath().split(FileUtils.SPECIAL_SEPARATOR)); + return split.get(3); + } + + /** + * FeignClient represents an interface for making rest calls to a service + * other than the current one. As such this method converts feignClient + * interfaces into a service class whose methods simply contain the exact + * rest call outlined by the interface annotations. + * + * @param classAnnotations + * @return + */ + private static JClass handleFeignClient(AnnotationExpr requestMapping, Set classAnnotations) { + + // Parse the methods + Set methods = parseMethods(cu.findAll(MethodDeclaration.class), requestMapping); + + // New methods for conversion + Set newMethods = new HashSet<>(); + // New rest calls for conversion + List newRestCalls = new ArrayList<>(); + + // For each method that is detected as an endpoint convert into a Method + RestCall + for(Method method : methods) { + if(method instanceof Endpoint) { + Endpoint endpoint = (Endpoint) method; + newMethods.add(new Method(method.getName(), packageAndClassName, method.getParameters(), method.getReturnType(), method.getAnnotations(), method.getMicroserviceName(), method.getClassName())); + + StringBuilder queryParams = new StringBuilder(); + for(edu.university.ecs.lab.common.models.ir.Parameter parameter : method.getParameters()) { + for(Annotation annotation : parameter.getAnnotations()) { + if(annotation.getName().equals("RequestParam")) { + queryParams.append("&"); + if(annotation.getAttributes().containsKey("default")) { + queryParams.append(annotation.getAttributes().get("default")); + } else if(annotation.getAttributes().containsKey("name")) { + queryParams.append(annotation.getAttributes().get("name")); + } else { + queryParams.append(parameter.getName()); + } + + queryParams.append("={?}"); + } + } + } + + if(!queryParams.isEmpty()) { + queryParams.replace(0, 1, "?"); + } + + newRestCalls.add(new RestCall(new MethodCall("exchange", packageAndClassName, "RestCallTemplate", "restCallTemplate", method.getName(), "", endpoint.getMicroserviceName(), endpoint.getClassName()), endpoint.getUrl() + queryParams, endpoint.getHttpMethod())); + } else { + newMethods.add(method); + } + } + + + // Build the JClass + return new JClass( + className, + path, + packageName, + ClassRole.FEIGN_CLIENT, + newMethods, + parseFields(cu.findAll(FieldDeclaration.class)), + parseAnnotations(classAnnotations), + newRestCalls, + cu.findAll(ClassOrInterfaceDeclaration.class).get(0).getImplementedTypes().stream().map(NodeWithSimpleName::getNameAsString).collect(Collectors.toSet())); + } + + public static ConfigFile parseConfigurationFile(File file, Config config) { + if(file.getName().endsWith(".yml")) { + return NonJsonReadWriteUtils.readFromYaml(file.getPath(), config); + } else if(file.getName().equals("DockerFile")) { + return NonJsonReadWriteUtils.readFromDocker(file.getPath(), config); + } else if(file.getName().equals("pom.xml")) { + return NonJsonReadWriteUtils.readFromPom(file.getPath(), config); + } else if (file.getName().equals("build.gradle")){ + return NonJsonReadWriteUtils.readFromGradle(file.getPath(), config); + } else { + return null; + } + } + + private static Set filterClassAnnotations() { + Set classAnnotations = new HashSet<>(); + for (AnnotationExpr ae : cu.findAll(AnnotationExpr.class)) { + if (ae.getParentNode().isPresent()) { + Node n = ae.getParentNode().get(); + if (n instanceof ClassOrInterfaceDeclaration) { + classAnnotations.add(ae); + } + } + } + return classAnnotations; + } + + /** + * FeignClient represents an interface for making rest calls to a service + * other than the current one. As such this method converts feignClient + * interfaces into a service class whose methods simply contain the exact + * rest call outlined by the interface annotations. + * + * @param classAnnotations + * @return + */ + private static JClass handleRepositoryRestResource(AnnotationExpr requestMapping, Set classAnnotations) { + + // Parse the methods + Set methods = parseMethods(cu.findAll(MethodDeclaration.class), requestMapping); + + // New methods for conversion + Set newEndpoints = new HashSet<>(); + // New rest calls for conversion + List newRestCalls = new ArrayList<>(); + + // Arbitrary preURL naming scheme if not defined in the annotation + String preURL = "/" + className.toLowerCase().replace("repository", "") + "s"; + + for(AnnotationExpr annotation : classAnnotations) { + if(annotation.getNameAsString().equals("RepositoryRestResource")) { + if (requestMapping instanceof NormalAnnotationExpr) { + NormalAnnotationExpr nae = (NormalAnnotationExpr) requestMapping; + for (MemberValuePair pair : nae.getPairs()) { + if (pair.getNameAsString().equals("path")) { + preURL += pair.getValue().toString(); + break; + } + } + } else if (requestMapping instanceof SingleMemberAnnotationExpr) { + preURL += annotation.asSingleMemberAnnotationExpr().getMemberValue().toString(); + break; + } + } + } + + // For each method that is detected as an endpoint convert into a Method + RestCall + for(Method method : methods) { + + String url = "/search"; + boolean restResourceFound = false; + boolean isExported = true; + for(Annotation ae : method.getAnnotations()) { + if (requestMapping instanceof NormalAnnotationExpr) { + NormalAnnotationExpr nae = (NormalAnnotationExpr) requestMapping; + for (MemberValuePair pair : nae.getPairs()) { + if (pair.getNameAsString().equals("path")) { + preURL = pair.getValue().toString(); + restResourceFound = true; + } else if(pair.getNameAsString().equals("exported")) { + if(pair.getValue().toString().equals("false")) { + isExported = false; + } + } + } + } + } + + // This method not exported (exposed) as an Endpoint + if(!isExported) { + continue; + } + + // If no restResource annotation found we use default /search url start + if(!restResourceFound) { + url += ("/" + method.getName()); + } + + Endpoint endpoint = new Endpoint(method, preURL + url, HttpMethod.GET); + newEndpoints.add(endpoint); + } + + + // Build the JClass + return new JClass( + className, + path, + packageName, + ClassRole.REP_REST_RSC, + newEndpoints, + parseFields(cu.findAll(FieldDeclaration.class)), + parseAnnotations(classAnnotations), + newRestCalls, + cu.findAll(ClassOrInterfaceDeclaration.class).get(0).getImplementedTypes().stream().map(NodeWithSimpleName::getNameAsString).collect(Collectors.toSet())); + } + + private static JClass handleJS(String filePath) { + JClass jClass = new JClass(filePath, filePath, "", ClassRole.FEIGN_CLIENT, new HashSet<>(), new HashSet<>(), new HashSet<>(), new ArrayList<>(), new HashSet<>()); + try { + Set restCalls = new HashSet<>(); + // Command to run Node.js script + ProcessBuilder processBuilder = new ProcessBuilder("node", "scripts/parser.js"); + Process process = processBuilder.start(); + + // Capture the output + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + String[] split = line.split(";"); + restCalls.add(new RestCall(split[0], split[1], split[2], split[3], split[4], split[5], split[6], split[7])); + System.out.println("Node.js Output: " + line); + } + + // Wait for the Node.js process to complete + int exitCode = process.waitFor(); + System.out.println("Node.js process exited with code: " + exitCode); + } catch (Exception e) { + System.err.println(e); + System.exit(1); + } + + return jClass; + } +} diff --git a/src/main/java/edu/university/ecs/lab/common/utils/package-info.java b/src/main/java/edu/university/ecs/lab/common/utils/package-info.java new file mode 100644 index 00000000..1eb108d5 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/common/utils/package-info.java @@ -0,0 +1,12 @@ +/** + * Contains utility classes for file management, JSON handling, and source code parsing. + *

+ * This package includes various components necessary for managing file paths, reading/writing JSON files, + * and parsing Java source files to extract meaningful data models in the context of a microservice architecture. + *

+ * The main classes include: + * - {@link edu.university.ecs.lab.common.utils.FileUtils} - Manages file paths and conversions. + * - {@link edu.university.ecs.lab.common.utils.JsonReadWriteUtils} - Handles JSON serialization and deserialization. + * - {@link edu.university.ecs.lab.common.utils.SourceToObjectUtils} - Parses Java source files into data models. + */ +package edu.university.ecs.lab.common.utils; diff --git a/src/main/java/edu/university/ecs/lab/delta/models/Delta.java b/src/main/java/edu/university/ecs/lab/delta/models/Delta.java new file mode 100644 index 00000000..9cde49ea --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/models/Delta.java @@ -0,0 +1,112 @@ +package edu.university.ecs.lab.delta.models; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import edu.university.ecs.lab.common.models.ir.ConfigFile; +import edu.university.ecs.lab.common.models.ir.JClass; +import edu.university.ecs.lab.common.models.ir.ProjectFile; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import edu.university.ecs.lab.common.services.LoggerManager; +import edu.university.ecs.lab.common.utils.JsonReadWriteUtils; +import edu.university.ecs.lab.delta.models.enums.ChangeType; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * This class represents a single Delta change between two commits. + * In the case of ChangeType.DELETE @see {@link ChangeType} the + * classChange will respectively be null as the instance of this class + * is no longer locally present for parsing at the new commit + */ +@Data +@AllArgsConstructor +public class Delta implements JsonSerializable { + + private static final Gson gson = JsonReadWriteUtils.registerDeserializers(); + /** + * The new path to the file changed/added + * Note: The path may be null in the event of an add + */ + private String oldPath; + + /** + * The old path to the file changed/added + * Note: The path may be null in the event of an delete + */ + private String newPath; + + /** + * The type of change that occurred + */ + private ChangeType changeType; + + /** + * The changed contents, could be a changed class or + * a changed configuration file + */ + private JsonObject data; + + /** + * This method returns an instance of JClass if parsable. + * + * @return JClass instance if parsable otherwise null + */ + public JClass getClassChange() { + if(data.size() == 0) { + return null; + } + try { + if(data.get("fileType").getAsString().equals("JCLASS")) { + return gson.fromJson(data, JClass.class); + } else { + return null; + } + } catch (JsonSyntaxException e) { + if(data.get("fileType").getAsString().equals("JCLASS")) { + LoggerManager.debug(e::getMessage); + } + return null; + } + + } + + /** + * This method returns an instance of ConfigFile if parsable. + * + * @return ConfigFile instance if parsable otherwise null + */ + public ConfigFile getConfigChange() { + if(data.size() == 0) { + return null; + } + try { + if(data.get("fileType").getAsString().equals("CONFIG")) { + return gson.fromJson(data, ConfigFile.class); + } else { + return null; + } + } catch (JsonSyntaxException e) { + if(data.get("fileType").getAsString().equals("CONFIG")) { + LoggerManager.debug(e::getMessage); + } + + return null; + } + } + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("changeType", changeType.name()); + jsonObject.addProperty("oldPath", oldPath); + jsonObject.addProperty("newPath", newPath); + jsonObject.add("data", data); + + return jsonObject; + } + +} diff --git a/src/main/java/edu/university/ecs/lab/delta/models/SystemChange.java b/src/main/java/edu/university/ecs/lab/delta/models/SystemChange.java new file mode 100644 index 00000000..c60f8814 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/models/SystemChange.java @@ -0,0 +1,49 @@ +package edu.university.ecs.lab.delta.models; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.models.serialization.JsonSerializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents the overall change in the IR from oldCommit + * to newCommit as a list of Deltas see {@link Delta} + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SystemChange implements JsonSerializable { + + /** + * The old commitID + */ + private String oldCommit; + + /** + * The new commitID + */ + private String newCommit; + + /** + * List of delta changes + */ + private List changes = new ArrayList<>(); + + + /** + * see {@link JsonSerializable#toJsonObject()} + */ + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.add("changes", JsonSerializable.toJsonArray(changes)); + jsonObject.addProperty("oldCommit", oldCommit); + jsonObject.addProperty("newCommit", newCommit); + + return jsonObject; + } +} diff --git a/src/main/java/edu/university/ecs/lab/delta/models/enums/ChangeType.java b/src/main/java/edu/university/ecs/lab/delta/models/enums/ChangeType.java new file mode 100644 index 00000000..8779d0c7 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/models/enums/ChangeType.java @@ -0,0 +1,25 @@ +package edu.university.ecs.lab.delta.models.enums; + +import org.eclipse.jgit.diff.DiffEntry; + +/** + * Enumerated type for defining the types of changes used by jgit + */ +public enum ChangeType { + ADD, + MODIFY, + DELETE; + + public static ChangeType fromDiffEntry(DiffEntry entry) { + switch (entry.getChangeType()) { + case ADD: + return ADD; + case MODIFY: + return MODIFY; + case DELETE: + return DELETE; + default: + throw new IllegalArgumentException("Unknown change type: " + entry.getChangeType()); + } + } +} diff --git a/src/main/java/edu/university/ecs/lab/delta/models/package-info.java b/src/main/java/edu/university/ecs/lab/delta/models/package-info.java new file mode 100644 index 00000000..73c71182 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/models/package-info.java @@ -0,0 +1,11 @@ +/** + * This package and subpackage {@link edu.university.ecs.lab.delta.models.enums}contains models used for representing changes between two commits in a microservice system. + *

+ * It includes: + * - {@link edu.university.ecs.lab.delta.models.Delta}: Represents a single change between two commits. + * - {@link edu.university.ecs.lab.delta.models.SystemChange}: Represents the overall change in the Intermediate Representation (IR) + * from an old commit to a new commit. + * - {@link edu.university.ecs.lab.delta.models.enums.ChangeType}: Enumerates types of changes (ADD, MODIFY, DELETE). + *

+ */ +package edu.university.ecs.lab.delta.models; diff --git a/src/main/java/edu/university/ecs/lab/delta/package-info.java b/src/main/java/edu/university/ecs/lab/delta/package-info.java new file mode 100644 index 00000000..6643f8d2 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/package-info.java @@ -0,0 +1,16 @@ +/** + * This package contains classes and sub-packages related to the extraction of delta changes + * between commits in a microservice system repository. + * + *

The main components include:

+ * - The {@link edu.university.ecs.lab.delta.models} package, which holds the data models representing individual + * and overall changes between commits. + * - The {@link edu.university.ecs.lab.delta.models.enums} package, which defines enumerations used within the + * data models, such as {@link edu.university.ecs.lab.delta.models.enums.ChangeType}. + * - The {@link edu.university.ecs.lab.delta.services} package, which provides services for extracting and + * processing differences between commits, such as the {@link edu.university.ecs.lab.delta.services.DeltaExtractionService}. + * + *

The package also includes a runner class, {@link edu.university.ecs.lab.delta.DeltaExtractionRunner}, for executing + * a test delta process.

+ */ +package edu.university.ecs.lab.delta; diff --git a/src/main/java/edu/university/ecs/lab/delta/services/DeltaExtractionService.java b/src/main/java/edu/university/ecs/lab/delta/services/DeltaExtractionService.java new file mode 100644 index 00000000..e2c038fb --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/services/DeltaExtractionService.java @@ -0,0 +1,230 @@ +package edu.university.ecs.lab.delta.services; + +import com.google.gson.JsonObject; +import edu.university.ecs.lab.common.config.Config; +import edu.university.ecs.lab.common.config.ConfigUtil; +import edu.university.ecs.lab.common.models.ir.ConfigFile; +import edu.university.ecs.lab.common.models.ir.JClass; +import edu.university.ecs.lab.common.models.ir.MicroserviceSystem; +import edu.university.ecs.lab.common.services.GitService; +import edu.university.ecs.lab.common.services.LoggerManager; +import edu.university.ecs.lab.common.utils.FileUtils; +import edu.university.ecs.lab.common.utils.JsonReadWriteUtils; +import edu.university.ecs.lab.common.utils.SourceToObjectUtils; +import edu.university.ecs.lab.delta.models.Delta; +import edu.university.ecs.lab.delta.models.SystemChange; +import edu.university.ecs.lab.delta.models.enums.ChangeType; +import org.eclipse.jgit.diff.DiffEntry; + +import java.io.File; +import java.util.List; + +/** + * Service for extracting the differences between two commits of a repository. + * This class does cleaning of output so not all changes will be reflected in + * the Delta output file. + */ +public class DeltaExtractionService { + private static final String DEV_NULL = "/dev/null"; + /** + * Config object representing the contents of the config file + */ + private final Config config; + + /** + * GitService instance for interacting with the local repository + */ + private final GitService gitService; + + /** + * The old commit for comparison + */ + private final String commitOld; + + /** + * The new commit for comparison + */ + private final String commitNew; + + /** + * System change object that will be returned + */ + private SystemChange systemChange; + + /** + * The type of change that is made + */ + private ChangeType changeType; + + /** + * The path to the output file + */ + private String outputPath; + + + /** + * Constructor for the DeltaExtractionService + * + * @param configPath path to the config file + * @param outputPath output path for file + * @param commitOld old commit for comparison + * @param commitNew new commit for comparison + */ + private DeltaExtractionService(String configPath, String outputPath, String commitOld, String commitNew) { + this.config = ConfigUtil.readConfig(configPath); + this.gitService = new GitService(configPath); + this.commitOld = commitOld; + this.commitNew = commitNew; + this.outputPath = outputPath.isEmpty() ? "./Delta.json" : outputPath; + } + + /** + * Generates Delta file representing changes between commitOld and commitNew + */ + private void generateDelta() { + List differences = null; + + // Ensure we start at commitOld + gitService.resetLocal(commitOld); + + // Get the differences between commits + differences = gitService.getDifferences(commitOld, commitNew); + + // Advance the local commit for parsing + gitService.resetLocal(commitNew); + + // process/write differences to delta output + processDelta(differences); + + } + + /** + * Process differences between commits + * + * @param diffEntries list of differences + */ + private void processDelta(List diffEntries) { + // Set up a new SystemChangeObject + systemChange = new SystemChange(); + systemChange.setOldCommit(commitOld); + systemChange.setNewCommit(commitNew); + JsonObject data = null; + + + // process each difference + for (DiffEntry entry : diffEntries) { + // Git path + String path = entry.getChangeType().equals(DiffEntry.ChangeType.ADD) ? entry.getNewPath() : entry.getOldPath(); + + // Special case for root pom + if(path.equals("pom.xml")) { + continue; + } + + // Guard condition, skip invalid files + if(!FileUtils.isValidFile(path)) { + continue; + } + + // Setup oldPath, newPath for Delta + String oldPath = ""; + String newPath = ""; + + if (DiffEntry.ChangeType.DELETE.equals(entry.getChangeType())) { + oldPath = FileUtils.GIT_SEPARATOR + entry.getOldPath(); + newPath = DEV_NULL; + + } else if (DiffEntry.ChangeType.ADD.equals(entry.getChangeType())) { + oldPath = DEV_NULL; + newPath = FileUtils.GIT_SEPARATOR + entry.getNewPath(); + + } else { + oldPath = FileUtils.GIT_SEPARATOR + entry.getOldPath(); + newPath = FileUtils.GIT_SEPARATOR + entry.getNewPath(); + + } + + changeType = ChangeType.fromDiffEntry(entry); + + switch(changeType) { + case ADD: + data = add(newPath); + break; + case MODIFY: + data = add(oldPath); + break; + case DELETE: + data = delete(); + } + + systemChange.getChanges().add(new Delta(oldPath, newPath, changeType, data)); + } + + // Output the system changes + // JsonReadWriteUtils.writeToJSON(outputPath, systemChange); + + // Report + LoggerManager.info(() -> "Delta changes extracted between " + commitOld + " -> " + commitNew); + + } + + /** + * This method parses a newly added file into a JsonObject containing + * the data of the change (updated file). Returns a blank JsonObject if + * parsing fails (returns null). + * + * @param newPath git path of new file + * @return JsonObject of data of the new file + */ + private JsonObject add(String newPath) { + // Check if it is a configuration file + if(FileUtils.isConfigurationFile(newPath)) { + ConfigFile configFile = SourceToObjectUtils.parseConfigurationFile(new File(FileUtils.gitPathToLocalPath(newPath, config.getRepoName())), config); + if(configFile == null || configFile.getData() == null) { + return new JsonObject(); + } else { + return configFile.toJsonObject(); + } + + // Else it is a Java file + } else { + JClass jClass = SourceToObjectUtils.parseClass(new File(FileUtils.gitPathToLocalPath(newPath, config.getRepoName())), config, ""); + if(jClass == null) { + return new JsonObject(); + } else { + return jClass.toJsonObject(); + } + } + + } + + private SystemChange getSystemChange() { + return this.systemChange; + } + + /** + * This method returns a blank JsonObject() as there is no data to parse + * + * @return JsonObject that is empty + */ + private JsonObject delete() { + return new JsonObject(); + } + + public static SystemChange create(String configPath, String oldCommit, String newCommit) { + DeltaExtractionService extractionService = new DeltaExtractionService(configPath, "", oldCommit, newCommit); + extractionService.generateDelta(); + return extractionService.getSystemChange(); + } + + public static void createAndWrite(String configPath, String oldCommit, String newCommit, String outputPath) { + SystemChange systemChange = DeltaExtractionService.create(configPath, oldCommit, newCommit); + JsonReadWriteUtils.writeToJSON(outputPath, systemChange); + } + + public static SystemChange read(String fPath) { + SystemChange systemChange = JsonReadWriteUtils.readFromJSON(fPath, SystemChange.class); + return systemChange; + } + +} diff --git a/src/main/java/edu/university/ecs/lab/delta/services/package-info.java b/src/main/java/edu/university/ecs/lab/delta/services/package-info.java new file mode 100644 index 00000000..16e638fd --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/delta/services/package-info.java @@ -0,0 +1,8 @@ +/** + * This package provides services for extracting and processing delta changes between + * commits in a repository. + * + *

The main service in this package handles the extraction of differences between two specified commits. It utilizes various utility classes + * and models from the common library to achieve this.

+ */ +package edu.university.ecs.lab.delta.services; diff --git a/src/main/java/edu/university/ecs/lab/intermediate/create/package-info.java b/src/main/java/edu/university/ecs/lab/intermediate/create/package-info.java new file mode 100644 index 00000000..1f87bb35 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/create/package-info.java @@ -0,0 +1,8 @@ +/** + * This package contains the classes responsible for creating the intermediate representation (IR) from remote repositories. + * + *

This package involves using the {@link edu.university.ecs.lab.intermediate.create.services.IRExtractionService} + * to clone repositories, scan them for REST endpoints and calls, and then write the extracted data to an intermediate representation file. + * {@link edu.university.ecs.lab.intermediate.create.IRExtractionRunner} is available as a test runner.

+ */ +package edu.university.ecs.lab.intermediate.create; diff --git a/src/main/java/edu/university/ecs/lab/intermediate/create/services/IRExtractionService.java b/src/main/java/edu/university/ecs/lab/intermediate/create/services/IRExtractionService.java new file mode 100644 index 00000000..75ae656e --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/create/services/IRExtractionService.java @@ -0,0 +1,274 @@ +package edu.university.ecs.lab.intermediate.create.services; + +import edu.university.ecs.lab.common.config.Config; +import edu.university.ecs.lab.common.config.ConfigUtil; +import edu.university.ecs.lab.common.error.Error; +import edu.university.ecs.lab.common.models.ir.ConfigFile; +import edu.university.ecs.lab.common.models.ir.JClass; +import edu.university.ecs.lab.common.models.ir.Microservice; +import edu.university.ecs.lab.common.models.ir.MicroserviceSystem; +import edu.university.ecs.lab.common.services.GitService; +import edu.university.ecs.lab.common.services.LoggerManager; +import edu.university.ecs.lab.common.utils.FileUtils; +import edu.university.ecs.lab.common.utils.JsonReadWriteUtils; +import edu.university.ecs.lab.common.utils.SourceToObjectUtils; +import edu.university.ecs.lab.delta.models.SystemChange; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import lombok.extern.java.Log; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + + +/** + * Top-level service for extracting intermediate representation from remote repositories. Methods + * are allowed to exit the program with an error code if an error occurs. + */ +public class IRExtractionService { + /** + * Service to handle cloning from git + */ + private final GitService gitService; + + /** + * Configuration object + */ + private final Config config; + + /** + * CommitID of IR Extraction + */ + private final String commitID; + + /** + * This constructor initializes a new IRExtractionService and instantiates a + * GitService object for repository manipulation + * + * @param configPath path to configuration file + * @param commitID optional commitID for extraction, if empty resolves to HEAD + * @see GitService + */ + public IRExtractionService(String configPath, Optional commitID) { + gitService = new GitService(configPath); + + if(commitID.isPresent()) { + this.commitID = commitID.get(); + gitService.resetLocal(this.commitID); + } else { + this.commitID = gitService.getHeadCommit(); + } + + config = ConfigUtil.readConfig(configPath); + } + + /** + * Intermediate extraction runner, generates IR from remote repository and writes to file. + * + * @param fileName name of output file for IR extraction + */ + public void generateIR(String fileName) { + // Clone remote repositories and scan through each cloned repo to extract endpoints + Set microservices = cloneAndScanServices(); + + if (microservices.isEmpty()) { + LoggerManager.info(() -> "No microservices were found during IR Extraction!"); + } + + // Write each service and endpoints to IR + writeToFile(microservices, fileName); + + } + + /** + * Clone remote repositories and scan through each local repo and extract endpoints/calls + * + * @return a map of services and their endpoints + */ + public Set cloneAndScanServices() { + Set microservices = new HashSet<>(); + + // Clone the repository present in the configuration file + gitService.cloneRemote(); + + // Start scanning from the root directory + List rootDirectories = findRootDirectories(FileUtils.getRepositoryPath(config.getRepoName())); + List rootDirectoriesCopy = List.copyOf(rootDirectories); + + // Filter more/less specific + for(String s1 : rootDirectoriesCopy) { + for(String s2 : rootDirectoriesCopy) { + if(s1.equals(s2)) { + continue; + } else if(s1.matches(s2.replace(FileUtils.SYS_SEPARATOR, FileUtils.SPECIAL_SEPARATOR) + FileUtils.SPECIAL_SEPARATOR + ".*")) { + rootDirectories.remove(s2); + } else if(s2.matches(s1.replace(FileUtils.SYS_SEPARATOR, FileUtils.SPECIAL_SEPARATOR) + FileUtils.SPECIAL_SEPARATOR + ".*")) { + rootDirectories.remove(s1); + } + } + } + + // Scan each root directory for microservices + for (String rootDirectory : rootDirectories) { + Microservice microservice = recursivelyScanFiles(rootDirectory); + if (microservice != null) { + microservices.add(microservice); + } + } + + return microservices; + } + + /** + * Recursively search for directories containing a microservice (pom.xml file) + * + * @param directory the directory to start the search from + * @return a list of directory paths containing pom.xml + */ + private List findRootDirectories(String directory) { + List rootDirectories = new ArrayList<>(); + File root = new File(directory); + if (root.exists() && root.isDirectory()) { + // Check if the current directory contains a Dockerfile + File[] files = root.listFiles(); + boolean containsPom = false; + boolean containsGradle = false; + if (files != null) { + for (File file : files) { + if (file.isFile() && file.getName().equals("pom.xml")) { + try { + + // Create a DocumentBuilder + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + + // Parse the XML file + Document document = builder.parse(file); + + // Normalize the XML Structure + document.getDocumentElement().normalize(); + + // Get all elements with the specific tag name + NodeList nodeList = document.getElementsByTagName("modules"); + // Check if the tag is present + if (nodeList.getLength() == 0) { + containsPom = true; + } + } catch (Exception e) { + throw new RuntimeException("Error parsing pom.xml"); + } + } else if(file.isFile() && file.getName().equals("build.gradle")) { + containsGradle = true; + } else if (file.isDirectory()) { + rootDirectories.addAll(findRootDirectories(file.getPath())); + } + } + } + if (containsPom) { + rootDirectories.add(root.getPath()); + return rootDirectories; + } else if (containsGradle){ + rootDirectories.add(root.getPath()); + return rootDirectories; + } + } + return rootDirectories; + } + + + /** + * Write each service and endpoints to intermediate representation + * + * @param microservices a list of microservices extracted from repository + * @param fileName the name of the output file for IR + */ + private void writeToFile(Set microservices, String fileName) { + + MicroserviceSystem microserviceSystem = new MicroserviceSystem(config.getSystemName(), commitID, microservices, new HashSet<>()); + + JsonReadWriteUtils.writeToJSON(fileName, microserviceSystem.toJsonObject()); + + LoggerManager.info(() -> "Successfully extracted IR at " + commitID); + } + + /** + * Recursively scan the files in the given repository path and extract the endpoints and + * dependencies for a single microservice. + * + * @return model of a single service containing the extracted endpoints and dependencies + */ + public Microservice recursivelyScanFiles(String rootMicroservicePath) { + // Validate path exists and is a directory + File localDir = new File(rootMicroservicePath); + if (!localDir.exists() || !localDir.isDirectory()) { + Error.reportAndExit(Error.INVALID_REPO_PATHS, Optional.empty()); + } + + + Microservice model = new Microservice(FileUtils.getMicroserviceNameFromPath(rootMicroservicePath), + FileUtils.localPathToGitPath(rootMicroservicePath, config.getRepoName())); + scanDirectory(localDir, model); + + LoggerManager.info(() -> "Done scanning directory " + rootMicroservicePath); + return model; + } + + /** + * Recursively scan the given directory for files and extract the endpoints and dependencies. + * + * @param directory the directory to scan + */ + public void scanDirectory( + File directory, + Microservice microservice) { + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + scanDirectory(file, microservice); + } else if (FileUtils.isValidFile(file.getPath())) { + + if(FileUtils.isConfigurationFile(file.getPath())) { + ConfigFile configFile = SourceToObjectUtils.parseConfigurationFile(file, config); + if(configFile != null) { + microservice.getFiles().add(configFile); + } + + } else { + JClass jClass = SourceToObjectUtils.parseClass(file, config, microservice.getName()); + if (jClass != null) { + microservice.addJClass(jClass); + } + } + + + } + } + } + } + + public static MicroserviceSystem create(String configPath) { + IRExtractionService extractionService = new IRExtractionService(configPath, Optional.empty()); + Set microservices = extractionService.cloneAndScanServices(); + MicroserviceSystem microserviceSystem = new MicroserviceSystem(extractionService.config.getSystemName(), extractionService.commitID, microservices, new HashSet<>()); + return microserviceSystem; + } + + public static void createAndWrite(String configPath, String outputPath) { + MicroserviceSystem microserviceSystem = create(configPath); + JsonReadWriteUtils.writeToJSON(outputPath, microserviceSystem.toJsonObject()); + } + + public static MicroserviceSystem read(String fPath) { + MicroserviceSystem microserviceSystem = JsonReadWriteUtils.readFromJSON(fPath, MicroserviceSystem.class); + return microserviceSystem; + } + + +} diff --git a/src/main/java/edu/university/ecs/lab/intermediate/create/services/package-info.java b/src/main/java/edu/university/ecs/lab/intermediate/create/services/package-info.java new file mode 100644 index 00000000..deaffc13 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/create/services/package-info.java @@ -0,0 +1,8 @@ +/** + * This package provides services for extracting intermediate representations (IR) from remote repositories. + * + *

The main service in this package is the {@link edu.university.ecs.lab.intermediate.create.services.IRExtractionService}, + * which handles the entire process of cloning repositories, scanning through each repository to extract REST endpoints and calls, + * and writing the extracted information into an intermediate representation file.

+ */ +package edu.university.ecs.lab.intermediate.create.services; diff --git a/src/main/java/edu/university/ecs/lab/intermediate/merge/package-info.java b/src/main/java/edu/university/ecs/lab/intermediate/merge/package-info.java new file mode 100644 index 00000000..6f34a46c --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/merge/package-info.java @@ -0,0 +1,8 @@ +/** + * This package contains the classes responsible for merging intermediate representations (IR) with delta changes. + * + *

The primary class in this package is {@link edu.university.ecs.lab.intermediate.merge.IRMergeRunner}, which serves + * as the main entry point for initiating the IR merge process. This process involves using the {@link edu.university.ecs.lab.intermediate.merge.services.MergeService} + * to merge an existing intermediate representation with delta changes to produce an updated intermediate representation.

+ */ +package edu.university.ecs.lab.intermediate.merge; diff --git a/src/main/java/edu/university/ecs/lab/intermediate/merge/services/MergeService.java b/src/main/java/edu/university/ecs/lab/intermediate/merge/services/MergeService.java new file mode 100644 index 00000000..5d006d6f --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/merge/services/MergeService.java @@ -0,0 +1,336 @@ +package edu.university.ecs.lab.intermediate.merge.services; + +import edu.university.ecs.lab.common.config.Config; +import edu.university.ecs.lab.common.config.ConfigUtil; +import edu.university.ecs.lab.common.models.ir.*; +import edu.university.ecs.lab.common.services.LoggerManager; +import edu.university.ecs.lab.common.utils.FileUtils; +import edu.university.ecs.lab.common.utils.JsonReadWriteUtils; +import edu.university.ecs.lab.delta.models.Delta; +import edu.university.ecs.lab.delta.models.SystemChange; +import edu.university.ecs.lab.delta.models.enums.ChangeType; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + + +/** + * This class is used for creating new IR's from old IR + Delta + * and provides all functionality related to updating the old + * IR + */ +public class MergeService { + private final Config config; + private final MicroserviceSystem microserviceSystem; + private final SystemChange systemChange; + private final String outputPath; + + // TODO handle exceptions here + public MergeService( + String intermediatePath, + String deltaPath, + String configPath, + String outputPath) { + this.config = ConfigUtil.readConfig(configPath); + this.microserviceSystem = JsonReadWriteUtils.readFromJSON(Path.of(intermediatePath).toAbsolutePath().toString(), MicroserviceSystem.class); + this.systemChange = JsonReadWriteUtils.readFromJSON(Path.of(deltaPath).toAbsolutePath().toString(), SystemChange.class); + this.outputPath = outputPath.isEmpty() ? "./NewIR.json" : outputPath; + } + + /** + * This method generates the new IR from the old IR + Delta file + */ + public void generateMergeIR(String newCommitID) { + + // If no changes are present we will write back out same IR + if (Objects.isNull(systemChange.getChanges())) { + LoggerManager.debug(() -> "No changes found at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + // JsonReadWriteUtils.writeToJSON(outputPath, microserviceSystem); + return; + } + + + // First we make necessary changes to microservices + updateMicroservices(); + + for (Delta d : systemChange.getChanges()) { + + switch (d.getChangeType()) { + case ADD: + addFile(d); + break; + case MODIFY: + removeFile(d); + addFile(d); + break; + case DELETE: + removeFile(d); + break; + } + } + + microserviceSystem.setCommitID(systemChange.getNewCommit()); + + LoggerManager.info(() -> "Merged to new IR at " + systemChange.getNewCommit()); + // JsonReadWriteUtils.writeToJSON(outputPath, microserviceSystem); + } + + + /** + * This method adds a JClass based on a Delta change + * + * @param delta the delta change for adding + */ + public void addFile(Delta delta) { + // Check for unparsable files + if(delta.getClassChange() == null && delta.getConfigChange() == null) { + LoggerManager.warn(() -> "[Filtered] An added file has no change information " + delta.getNewPath()); + return; + } + + Microservice ms = microserviceSystem.findMicroserviceByPath(delta.getNewPath()); + + // If no ms is found, it will be held in orphans + if (Objects.isNull(ms)) { + if(delta.getClassChange() != null) { + microserviceSystem.getOrphans().add(delta.getClassChange()); + } else if(delta.getConfigChange() != null) { + microserviceSystem.getOrphans().add(delta.getConfigChange()); + } + + LoggerManager.debug(() -> "[File added] " + delta.getNewPath() + " to orphans at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + return; + } + + // If we found it's ms + if(delta.getConfigChange() != null) { + ms.getFiles().add(delta.getConfigChange()); + } else { + // Add the JClass, the microservice name is updated see addJClass() + ms.addJClass(delta.getClassChange()); + } + + LoggerManager.debug(() -> "[File added] " + delta.getNewPath() + " to microservice " + ms.getPath() + " at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + + + } + + /** + * This method removes a JClass based on a Delta change + * Note it might not be found, so it will handle this gracefully + * + * @param delta the delta change for removal + */ + public void removeFile(Delta delta) { + Microservice ms = microserviceSystem.findMicroserviceByPath(delta.getOldPath()); + + // If we are removing a file and it's microservice doesn't exist + if (Objects.isNull(ms)) { + // Check the orphan pool + for (ProjectFile orphan : microserviceSystem.getOrphans()) { + // If found remove it and return + if (orphan.getPath().equals(delta.getOldPath())) { + microserviceSystem.getOrphans().remove(orphan); + LoggerManager.debug(() -> "[File removed] " + delta.getOldPath() + " from orphans at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + return; + } + } + LoggerManager.debug(() -> "[File not found] " + delta.getOldPath() + " in orphans at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + + return; + } + + // Remove the file depending on which is null, skips gracefully if not found in microservice + // see removeProjectFile() + // see removeProjectFile() + ms.removeProjectFile(delta.getOldPath()); + + LoggerManager.debug(() -> "[File removed] " + delta.getOldPath() + " from microservice " + ms.getPath() + " at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + + + } + + + /** + * Method for updating MicroserviceSystem structure (microservices) based on + * pom.xml changes in Delta file + * + */ + private void updateMicroservices() { + + // See filterBuildDeltas() + List buildDeltas = filterBuildDeltas(); + + if(buildDeltas.isEmpty()) { + return; + } + + // Loop through changes to pom.xml files + for (Delta delta : buildDeltas) { + + + Microservice microservice; + String[] tokens; + + String path = delta.getOldPath().equals("/dev/null") ? delta.getNewPath() : delta.getOldPath(); + tokens = path.split("/"); + + // Skip a pom that is in the root + if (tokens.length <= 2) { + LoggerManager.debug(() -> "Tokens check still needed?"); + } + + match: { + switch (delta.getChangeType()) { + case ADD: + + // If a delta is more/less specific than an active microservice + Microservice removeMicroservice = null; + for (Microservice compareMicroservice : microserviceSystem.getMicroservices()) { + // If delta is more specific than compareMicroservice, we remove this one + if (delta.getNewPath().replace("/pom.xml", "").replace("/build.gradle", "").matches(compareMicroservice.getPath() + "/.*")) { + removeMicroservice = compareMicroservice; + + // If a microservice already exists that is more specific, skip the addition + } else if (compareMicroservice.getPath().matches(delta.getNewPath().replace("/pom.xml", "").replace("/build.gradle", "") + "/.*")) { + break match; + } + } + + // If a match was found, orphanize and remove. They will be adopted below + if (Objects.nonNull(removeMicroservice)) { + microserviceSystem.getMicroservices().remove(removeMicroservice); + microserviceSystem.orphanize(removeMicroservice); + } + + microservice = new Microservice(tokens[tokens.length - 2], delta.getNewPath().replace("/pom.xml", "").replace("/build.gradle", "")); + // Here we must check if any orphans are waiting on this creation + microserviceSystem.adopt(microservice); + microserviceSystem.getMicroservices().add(microservice); + LoggerManager.debug(() -> "[Microservice added] " + microservice.getName() + " " + microservice.getPath() + " at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + break; + + + case DELETE: + microservice = microserviceSystem.findMicroserviceByPath(delta.getOldPath().replace("/pom.xml", "").replace("/build.gradle", "")); + + // If a less + if (microservice == null) { + LoggerManager.error(() -> "[Microservice not found] " + delta.getOldPath() + " at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit(), Optional.of(new RuntimeException("Fail"))); + } + + // Here we must orphan all the classes of this microservice + microserviceSystem.getMicroservices().remove(microservice); + microserviceSystem.orphanize(microservice); + LoggerManager.debug(() -> "[Microservice removed] " + microservice.getName() + " " + microservice.getPath() + " at " + systemChange.getOldCommit() + " -> " + systemChange.getNewCommit()); + break; + + } + } + + } + + + } + + /** + * Filter's the delta files that deal with building project + * so either pom.xml or build.gradle + * + * @return a list of system changes that deal with build files that aren't modifications + */ + private List filterBuildDeltas() { + // deltaChanges.stream().filter(delta -> (delta.getOldPath() == null || delta.getOldPath().isEmpty() ? delta.getNewPath() : delta.getOldPath()).endsWith("/pom.xml")).collect(Collectors.toUnmodifiableList()); + List filteredDeltas = new ArrayList<>(systemChange.getChanges()); + + // Remove non build related files + filteredDeltas.removeIf(delta -> !(delta.getOldPath().endsWith("/pom.xml") || delta.getNewPath().endsWith("/pom.xml")) && !(delta.getOldPath().endsWith("/build.gradle") || delta.getNewPath().endsWith("/build.gradle"))); + + // Remove modified files, doesn't change microservice structure + filteredDeltas.removeIf(delta -> delta.getChangeType().equals(ChangeType.MODIFY)); + + // Remove deleted files, if their microservice doesn't exist (they were less specific and were filtered out) + filteredDeltas.removeIf(delta -> (delta.getChangeType().equals(ChangeType.DELETE) && microserviceSystem.findMicroserviceByPath(delta.getOldPath()) == null)); + + // Remove more specific build deltas in the same system change + List filteredDeltasCopy = new ArrayList<>(List.copyOf(filteredDeltas)); + + + // If a delta is more specific than another in same SystemChange, + // we need to remove the more general option in case of add + List addDeltas = filteredDeltas.stream().filter(d -> d.getChangeType().equals(ChangeType.ADD)).collect(Collectors.toList()); + boolean deletedFirst = false; + for(Delta delta1 : addDeltas) { + for(Delta delta2 : addDeltas) { + // If they are equal or they aren't both additions + if(delta1.equals(delta2) || !delta1.getChangeType().equals(ChangeType.ADD) || !delta2.getChangeType().equals(ChangeType.ADD)) { + continue; + } + String delta1Path = delta1.getNewPath().replace("/pom.xml", "").replace("/build.gradle", ""); + String delta2Path = delta2.getNewPath().replace("/pom.xml", "").replace("/build.gradle", ""); + if(delta1Path.equals(delta2Path) && !deletedFirst) { + LoggerManager.debug(() -> "[Filtered] Duplicates deltas detected for " + delta1.getNewPath() + " and " + delta2.getNewPath()); + filteredDeltas.remove(delta1); + deletedFirst = true; + continue; + } + + // Check if paths are more/less specific + if(delta1Path.matches(delta2Path + "/.*")) { + LoggerManager.debug(() -> "[Filtered] Delta " + delta1.getNewPath() + " more specific than " + delta2.getNewPath()); + filteredDeltasCopy.remove(delta2); + } else if(delta2Path.matches(delta1Path + "/.*")) { + LoggerManager.debug(() -> "[Filtered] Delta " + delta2.getNewPath() + " more specific than " + delta1.getNewPath()); + filteredDeltasCopy.remove(delta1); + } + } + } + + deletedFirst = false; + // Remove duplicate deletes (pom.xml and build.gradle) of the same microservice + List deleteDeltas = filteredDeltas.stream().filter(d -> d.getChangeType().equals(ChangeType.DELETE)).collect(Collectors.toList()); + for(Delta delta1 : deleteDeltas) { + for(Delta delta2 : deleteDeltas) { + String delta1Path = delta1.getOldPath().replace("/pom.xml", "").replace("/build.gradle", ""); + String delta2Path = delta2.getOldPath().replace("/pom.xml", "").replace("/build.gradle", ""); + + + // If they are equal and they aren't both additions, arbitrarily remove one of them + if(delta1Path.equals(delta2Path) && !delta1.getOldPath().equals(delta2.getOldPath()) && !deletedFirst) { + LoggerManager.debug(() -> "[Filtered] Duplicates deltas detected for " + delta1.getOldPath() + " and " + delta2.getOldPath()); + filteredDeltasCopy.remove(delta1); + deletedFirst = true; + } + } + } + + + return filteredDeltasCopy; + } + + private String getMicroserviceNameFromPath(String path) { + for (Microservice microservice : microserviceSystem.getMicroservices()) { + if (path.contains(microservice.getPath())) { + return microservice.getName(); + } + } + + return null; + } + + private MicroserviceSystem getMicroserviceSystem() { + return this.microserviceSystem; + } + + public static MicroserviceSystem create(String configPath, String intermediatePath, String deltaPath, String newCommitID) { + MergeService mergeService = new MergeService(intermediatePath, deltaPath, configPath, ""); + mergeService.generateMergeIR(newCommitID); + return mergeService.getMicroserviceSystem(); + } + + public static void createAndWrite(String configPath, String intermediatePath, String deltaPath, String newCommitID, String outputPath) { + MicroserviceSystem microserviceSystem = create(configPath, intermediatePath, deltaPath, newCommitID); + JsonReadWriteUtils.writeToJSON(outputPath, microserviceSystem); + } + +} diff --git a/src/main/java/edu/university/ecs/lab/intermediate/merge/services/package-info.java b/src/main/java/edu/university/ecs/lab/intermediate/merge/services/package-info.java new file mode 100644 index 00000000..6ca3a371 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/merge/services/package-info.java @@ -0,0 +1,8 @@ +/** + * This package provides services for merging intermediate representations (IR) with delta changes. + * + *

The main service in this package is the {@link edu.university.ecs.lab.intermediate.merge.services.MergeService}, + * which handles the entire process of merging an existing IR with delta changes to generate an updated IR.

+ * + */ +package edu.university.ecs.lab.intermediate.merge.services; diff --git a/src/main/java/edu/university/ecs/lab/intermediate/package-info.java b/src/main/java/edu/university/ecs/lab/intermediate/package-info.java new file mode 100644 index 00000000..453705d4 --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/package-info.java @@ -0,0 +1,13 @@ +/** + * This package contains the classes and sub-packages responsible for the intermediate representation extraction and merging processes. + * + *

The main sub-packages within this package are:

+ * - {@link edu.university.ecs.lab.intermediate.create}: Includes the classes responsible for generating the intermediate representation from the source code. + * - {@link edu.university.ecs.lab.intermediate.merge}: Includes the classes responsible for merging the intermediate representation with delta changes. + * - {@link edu.university.ecs.lab.intermediate.utils}: Includes utility classes used throughout the intermediate representation processes. + * + *

The intermediate extraction process involves cloning remote services, scanning through each local repository to extract REST endpoints and calls, and writing the extracted data into an intermediate representation.

+ * + *

The intermediate merging process involves taking an existing intermediate representation and applying changes based on delta files to generate an updated intermediate representation.

+ */ +package edu.university.ecs.lab.intermediate; diff --git a/src/main/java/edu/university/ecs/lab/intermediate/utils/StringParserUtils.java b/src/main/java/edu/university/ecs/lab/intermediate/utils/StringParserUtils.java new file mode 100644 index 00000000..e3bbaa3f --- /dev/null +++ b/src/main/java/edu/university/ecs/lab/intermediate/utils/StringParserUtils.java @@ -0,0 +1,40 @@ +package edu.university.ecs.lab.intermediate.utils; + +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.PackageDeclaration; +import org.apache.commons.io.FilenameUtils; + +/** + * Utility class for parsing strings. + */ +public class StringParserUtils { + /** + * Private constructor to prevent instantiation. + */ + private StringParserUtils() { + } + + /** + * Remove start/end quotations from the given string. + * + * + * @param s the string to remove quotations from + * @return the string with quotations removed + */ + public static String removeOuterQuotations(String s) { + if (s != null && s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) { + return s.substring(1, s.length() - 1); + } + return s; + } + + /** + * Simplifies all path arguments to {?}. + * + * @param url the endpoint URL + * @return the simplified endpoint URL + */ + public static String simplifyEndpointURL(String url) { + return url.replaceAll("\\{[^{}]*\\}", "{?}"); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 00000000..cb6ab38b --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/test/java/IRExtractionTest.java b/src/test/java/IRExtractionTest.java new file mode 100644 index 00000000..29982aaa --- /dev/null +++ b/src/test/java/IRExtractionTest.java @@ -0,0 +1,14 @@ +import org.junit.jupiter.api.Test; + +import edu.university.ecs.lab.common.models.ir.MicroserviceSystem; +import edu.university.ecs.lab.intermediate.create.services.IRExtractionService; + +public class IRExtractionTest { + + public static final String TEST_CONFIG_PATH = "/resources/test_config.json"; + + @Test + void testCreate() { + MicroserviceSystem newSystem = IRExtractionService.create(TEST_CONFIG_PATH); + } +} diff --git a/src/test/resources/test_config.json b/src/test/resources/test_config.json new file mode 100644 index 00000000..9faf8c60 --- /dev/null +++ b/src/test/resources/test_config.json @@ -0,0 +1,5 @@ +{ + "systemName": "spring-cloud-movie-recommendation", + "repositoryURL": "https://github.com/mdeket/spring-cloud-movie-recommendation.git", + "baseBranch": "master" + } \ No newline at end of file