diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 054da7ebb..f93ed0507 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,37 +1,111 @@ name: Java CI with Maven -on: +on: push jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'adopt' - cache: maven - - name: Build with Maven - run: | - start_time=$(date +%s) - mvn --batch-mode --update-snapshots verify | tee build_output.log - end_time=$(date +%s) - build_time=$((end_time - start_time)) - echo "BUILD_TIME_SECONDS=$build_time" >> build_output.log - - name: Upload build result - run: mkdir staging && cp use-assembly/target/*.zip staging - - uses: actions/upload-artifact@v4 - with: - name: Package - path: staging - - uses: actions/upload-artifact@v4 - with: - name: build-log - path: build_output.log - - uses: actions/upload-artifact@v4 - with: - name: failure-reports - path: | - docs/archunit-results/cycles-current-failure-report.txt - docs/archunit-results/layers-current-failure-report.txt + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'adopt' + cache: maven + - name: Build with Maven + run: | + start_time=$(date +%s) + mvn --batch-mode --update-snapshots verify | tee build_output.log + end_time=$(date +%s) + build_time=$((end_time - start_time)) + echo "BUILD_TIME_SECONDS=$build_time" >> build_output.log + - name: Upload build result + run: mkdir staging && cp use-assembly/target/*.zip staging + - uses: actions/upload-artifact@v4 + with: + name: Package + path: staging + - uses: actions/upload-artifact@v4 + with: + name: build-log + path: build_output.log + - uses: actions/upload-artifact@v4 + with: + name: failure-reports + path: | + docs/archunit-results/cycles-current-failure-report.txt + docs/archunit-results/layers-current-failure-report.txt + + - name: Upload build result use-api + run: mkdir testphase && cp use-api/target/use-api-7.1.1.jar testphase + - uses: actions/upload-artifact@v4 + with: + name: constructed_use-api + path: testphase + + - name: Upload postman tests + run: mkdir postmantests && cp use-api/src/it/java/org.tzi.use/postman_collection/use-webapi.postman_collection.json postmantests + - uses: actions/upload-artifact@v4 + with: + name: postman_tests + path: postmantests + + - name: Upload docker image + run: mkdir docker && cp use-api/docker-compose.yml docker && cp use-api/Dockerfile docker + - uses: actions/upload-artifact@v4 + with: + name: docker_image + path: docker + + + + test: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'adopt' + + + - name: Download Artifact use-api jar + uses: actions/download-artifact@v4 + with: + name: constructed_use-api + + - name: Download Artifact postman tests + uses: actions/download-artifact@v4 + with: + name: postman_tests + + - name: Download Aritfact Dockerimage + uses: actions/download-artifact@v4 + with: + name: docker_image + + - name: move the jar + run: mkdir target && cp use-api-7.1.1.jar target + + - name: Run docker compose + run: docker compose up -d --wait + env: + MONGODB_USERNAME: ${{ secrets.MONGODB_USERNAME }} + MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }} + MONGODB_DATABASE: ${{ secrets.MONGODB_DATABASE }} + MONGODB_HOST: mongodb_cicd + + + - name: Wait for docker to be ready + run: sleep 20 + + - name: Run Postman tests + run: | + npm install -g newman + newman run use-webapi.postman_collection.json + + - name: Stop services + if: always() + run: docker compose down diff --git a/.gitignore b/.gitignore index 0d379eafe..4cd4fa805 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ fabric.properties /use-assembly/target **/.DS_Store +/.idea +/use-api/target +use-api/.env \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 000000000..9ef790c41 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,17 @@ + + + + + mongo + true + com.dbschema.MongoJdbcDriver + mongodb://localhost:27017 + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 882bb50c6..27b0cfc26 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -3,6 +3,8 @@ + + diff --git a/pom.xml b/pom.xml index 16a851563..bfeadb9ec 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ use-assembly use-core use-gui + use-api diff --git a/use-api/.env.example b/use-api/.env.example new file mode 100644 index 000000000..d9a26a940 --- /dev/null +++ b/use-api/.env.example @@ -0,0 +1,5 @@ +MONGODB_USERNAME=your_username +MONGODB_PASSWORD=your_password +MONGODB_DATABASE=use-database +MONGODB_PORT=27017 +MONGODB_HOST=localhost \ No newline at end of file diff --git a/use-api/Dockerfile b/use-api/Dockerfile new file mode 100644 index 000000000..dc090e9b3 --- /dev/null +++ b/use-api/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:21-jdk + +WORKDIR /app + +COPY target/use-api-*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java","-jar","app.jar"] diff --git a/use-api/docker-compose.yml b/use-api/docker-compose.yml new file mode 100644 index 000000000..2083f6602 --- /dev/null +++ b/use-api/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + mongodb: + image: mongo:latest + container_name: mongodb_cicd + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGODB_USERNAME:-rootuser} + - MONGO_INITDB_ROOT_PASSWORD=${MONGODB_PASSWORD:-rootpass} + - MONGO_INITDB_DATABASE=${MONGODB_DATABASE:-use-database} + + use-api: + build: . + container_name: use-api + ports: + - "8080:8080" + depends_on: + - mongodb + environment: + - SPRING_DATA_MONGODB_HOST=${MONGODB_HOST:-mongodb_cicd} + - SPRING_DATA_MONGODB_PORT=${MONGODB_PORT:-27017} + - SPRING_DATA_MONGODB_USERNAME=${MONGODB_USERNAME:-rootuser} + - SPRING_DATA_MONGODB_PASSWORD=${MONGODB_PASSWORD:-rootpass} + - SPRING_DATA_MONGODB_DATABASE=${MONGODB_DATABASE:-use-database} + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080/actuator/health" ] + interval: 5s + timeout: 5s + retries: 10 + start_period: 30s + +volumes: + mongodb_data: diff --git a/use-api/pom.xml b/use-api/pom.xml new file mode 100644 index 000000000..b16598b15 --- /dev/null +++ b/use-api/pom.xml @@ -0,0 +1,258 @@ + + + + use + org.tzi.use + 7.5.0 + + 4.0.0 + + use-api + + + + 21 + 21 + UTF-8 + + + + + + org.springframework.boot + spring-boot-dependencies + 3.1.5 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + use + org.tzi.use + 7.1.1 + pom + compile + + + org.springframework.boot + spring-boot-starter-graphql + + + org.projectlombok + lombok + 1.18.30 + provided + + + org.springframework.boot + spring-boot-starter-hateoas + + + org.mapstruct + mapstruct + 1.6.3 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + + + org.springframework.boot + spring-boot-starter-web + + + + com.h2database + h2 + runtime + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework + spring-webflux + test + + + org.springframework.graphql + spring-graphql-test + test + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + io.rest-assured + rest-assured + 5.5.0 + test + + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + org.tzi.use + use-core + 7.1.1 + compile + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + org.tzi.use.UseWebAPIApplication + + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + org.projectlombok + lombok + 1.18.30 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + -Amapstruct.defaultComponentModel=spring + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/use-api/src/it/java/org.tzi.use/postman_collection/use-webapi.postman_collection.json b/use-api/src/it/java/org.tzi.use/postman_collection/use-webapi.postman_collection.json new file mode 100644 index 000000000..74fdff445 --- /dev/null +++ b/use-api/src/it/java/org.tzi.use/postman_collection/use-webapi.postman_collection.json @@ -0,0 +1,1341 @@ +{ + "info": { + "_postman_id": "c1a2b3c4-d5e6-f7g8-h9i0-j1k2l3m4n5o6", + "name": "use-webapi-system-tests", + "description": "Comprehensive test collection for USE Web API with thorough test cases.", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "Model Operations", + "item": [ + { + "name": "Create Model - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test('Response contains model name', function () {", + " const json = pm.response.json();", + " pm.expect(json.name).to.equal('ComprehensiveTestModel');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"ComprehensiveTestModel\"}" + }, + "url": "localhost:8080/api/model" + } + }, + { + "name": "Create Model - Duplicate Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 409 Conflict for duplicate', function () {", + " pm.response.to.have.status(409);", + "});", + "pm.test('Error message indicates duplicate', function () {", + " const json = pm.response.json();", + " pm.expect(json.message).to.include('already exists');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"ComprehensiveTestModel\"}" + }, + "url": "localhost:8080/api/model" + } + }, + { + "name": "Create Model - Empty Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"\"}" + }, + "url": "localhost:8080/api/model" + } + }, + { + "name": "Create Model - Null Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 or 500', function () {", + " pm.expect([400, 500]).to.include(pm.response.code);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": null}" + }, + "url": "localhost:8080/api/model" + } + }, + { + "name": "Get Model by Name - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response contains model data', function () {", + " const json = pm.response.json();", + " pm.expect(json.name).to.equal('ComprehensiveTestModel');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel" + } + }, + { + "name": "Get Model by Name - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request for not found', function () {", + " pm.response.to.have.status(400);", + "});", + "pm.test('Error message indicates not found', function () {", + " const json = pm.response.json();", + " pm.expect(json.message).to.include('not found');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/NonExistentModel123" + } + }, + { + "name": "Get All Models", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response time is acceptable', function () {", + " pm.expect(pm.response.responseTime).to.be.below(1000);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models" + } + } + ] + }, + { + "name": "Class Operations", + "item": [ + { + "name": "Create Class - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test('Response contains class name', function () {", + " const json = pm.response.json();", + " pm.expect(json.name).to.equal('Person');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"Person\", \"attributes\": [{\"name\": \"firstName\", \"type\": \"String\"}, {\"name\": \"age\", \"type\": \"Integer\"}], \"operations\": []}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/class" + } + }, + { + "name": "Create Class - Duplicate Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 409 Conflict', function () {", + " pm.response.to.have.status(409);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"Person\", \"attributes\": [], \"operations\": []}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/class" + } + }, + { + "name": "Create Class - Empty Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"\", \"attributes\": [], \"operations\": []}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/class" + } + }, + { + "name": "Create Class - Non-existent Model", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});", + "pm.test('Error indicates model not found', function () {", + " const json = pm.response.json();", + " pm.expect(json.message).to.include('not found');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"TestClass\", \"attributes\": [], \"operations\": []}" + }, + "url": "localhost:8080/api/model/FakeModel999/class" + } + }, + { + "name": "Get Classes", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel/classes" + } + }, + { + "name": "Get Class by Name - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response contains class data', function () {", + " const json = pm.response.json();", + " pm.expect(json.name).to.equal('Person');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person" + } + }, + { + "name": "Get Class by Name - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/NonExistentClass" + } + } + ] + }, + { + "name": "Attribute Operations", + "item": [ + { + "name": "Add Attribute - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test('Response contains attribute', function () {", + " const json = pm.response.json();", + " pm.expect(json.name).to.equal('email');", + " pm.expect(json.type).to.equal('String');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"email\", \"type\": \"String\"}" + }, + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute" + } + }, + { + "name": "Add Attribute - Empty Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"\", \"type\": \"String\"}" + }, + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute" + } + }, + { + "name": "Add Attribute - Invalid Type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"badAttr\", \"type\": \"InvalidType\"}" + }, + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute" + } + }, + { + "name": "Get Attributes", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attributes" + } + }, + { + "name": "Get Attribute by Name - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response contains attribute name', function () {", + " const json = pm.response.json();", + " pm.expect(json.name).to.equal('firstName');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute/firstName" + } + }, + { + "name": "Get Attribute by Name - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute/nonExistentAttr" + } + } + ] + }, + { + "name": "Operation (Method) Tests", + "item": [ + { + "name": "Add Operation - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test('Response contains operation', function () {", + " const json = pm.response.json();", + " pm.expect(json.operationName).to.equal('getInfo');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"operationName\": \"getInfo\", \"parameter\": [], \"returnType\": \"String\"}" + }, + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/operation" + } + }, + { + "name": "Add Operation - Empty Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"operationName\": \"\", \"parameter\": [], \"returnType\": \"String\"}" + }, + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/operation" + } + }, + { + "name": "Get Operations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/operations" + } + }, + { + "name": "Get Operation by Name - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/operation/nonExistentOp" + } + } + ] + }, + { + "name": "Association Operations", + "item": [ + { + "name": "Create Second Class for Association", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"name\": \"Company\", \"attributes\": [{\"name\": \"companyName\", \"type\": \"String\"}], \"operations\": []}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/class" + } + }, + { + "name": "Create Association - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test('Response contains association name', function () {", + " const json = pm.response.json();", + " pm.expect(json.associationName).to.equal('WorksFor');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"associationName\": \"WorksFor\", \"end1ClassName\": \"Person\", \"end1RoleName\": \"employee\", \"end1Multiplicity\": \"*\", \"end1Aggregation\": \"NONE\", \"end2ClassName\": \"Company\", \"end2RoleName\": \"employer\", \"end2Multiplicity\": \"0..1\", \"end2Aggregation\": \"NONE\"}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/association" + } + }, + { + "name": "Create Association - Non-existent Class", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"associationName\": \"BadAssoc\", \"end1ClassName\": \"NonExistent\", \"end1RoleName\": \"role1\", \"end1Multiplicity\": \"*\", \"end1Aggregation\": \"NONE\", \"end2ClassName\": \"Person\", \"end2RoleName\": \"role2\", \"end2Multiplicity\": \"1\", \"end2Aggregation\": \"NONE\"}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/association" + } + }, + { + "name": "Get Associations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel/associations" + } + } + ] + }, + { + "name": "Invariant Operations", + "item": [ + { + "name": "Create Invariant - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test('Response contains invariant data', function () {", + " const json = pm.response.json();", + " pm.expect(json.invName).to.equal('PositiveAge');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"invName\": \"PositiveAge\", \"invBody\": \"self.age > 0\", \"isExistential\": false}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/Person/invariant" + } + }, + { + "name": "Create Invariant - Empty Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"invName\": \"\", \"invBody\": \"self.age > 0\", \"isExistential\": false}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/Person/invariant" + } + }, + { + "name": "Create Invariant - Empty Body", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"invName\": \"EmptyBodyInv\", \"invBody\": \"\", \"isExistential\": false}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/Person/invariant" + } + }, + { + "name": "Create Invariant - Non-existent Class", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"invName\": \"SomeInv\", \"invBody\": \"true\", \"isExistential\": false}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/FakeClass999/invariant" + } + }, + { + "name": "Get Invariants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel/invariants" + } + } + ] + }, + { + "name": "PrePostCondition Operations", + "item": [ + { + "name": "Create PreCondition - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\"operationName\": \"getInfo\", \"name\": \"validPerson\", \"condition\": \"self.age > 0\", \"isPre\": true}" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/Person/prepostcondition" + } + }, + { + "name": "Get PrePostConditions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200 OK', function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel/prepostconditions" + } + } + ] + }, + { + "name": "Error Handling", + "item": [ + { + "name": "Invalid JSON Body", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{invalid json}" + }, + "url": "localhost:8080/api/model" + } + }, + { + "name": "Missing Content-Type Header", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 or 415', function () {", + " pm.expect([400, 415]).to.include(pm.response.code);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\"name\": \"TestModel\"}" + }, + "url": "localhost:8080/api/model" + } + }, + { + "name": "Empty Request Body", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "" + }, + "url": "localhost:8080/api/model/ComprehensiveTestModel/class" + } + } + ] + }, + { + "name": "Performance Tests", + "item": [ + { + "name": "GET Models Performance", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response time below 500ms', function () {", + " pm.expect(pm.response.responseTime).to.be.below(500);", + "});", + "pm.test('Response time below 1000ms', function () {", + " pm.expect(pm.response.responseTime).to.be.below(1000);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/models" + } + }, + { + "name": "GET Model by Name Performance", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response time below 300ms', function () {", + " pm.expect(pm.response.responseTime).to.be.below(300);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel" + } + } + ] + }, + { + "name": "Delete Operations", + "item": [ + { + "name": "Delete Attribute - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute/email" + } + }, + { + "name": "Delete Attribute - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/attribute/nonExistentAttr" + } + }, + { + "name": "Delete Operation - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/operation/getInfo" + } + }, + { + "name": "Delete Operation - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/models/ComprehensiveTestModel/class/Person/operation/nonExistentOp" + } + }, + { + "name": "Delete PrePostCondition - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/prepostcondition/Person::getInfovalidPerson" + } + }, + { + "name": "Delete PrePostCondition - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/prepostcondition/nonExistent" + } + }, + { + "name": "Delete Invariant - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/invariant/Person" + } + }, + { + "name": "Delete Invariant - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/invariant/nonExistent" + } + }, + { + "name": "Delete Association - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/association/Person" + } + }, + { + "name": "Delete Association - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/association/nonExistent" + } + }, + { + "name": "Delete Class - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/class/Company" + } + }, + { + "name": "Delete Class - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel/class/nonExistent" + } + }, + { + "name": "Delete Model - Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204 No Content', function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/ComprehensiveTestModel" + } + }, + { + "name": "Get Model After Delete - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": "localhost:8080/api/model/ComprehensiveTestModel" + } + }, + { + "name": "Delete Model - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400 Bad Request', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "url": "localhost:8080/api/model/NonExistentModel" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/use-api/src/main/java/org/tzi/use/DTO/AggregationTypeDTO.java b/use-api/src/main/java/org/tzi/use/DTO/AggregationTypeDTO.java new file mode 100644 index 000000000..43f8e7a3f --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/AggregationTypeDTO.java @@ -0,0 +1,18 @@ +package org.tzi.use.DTO; + +public enum AggregationTypeDTO { + NONE("association"), + AGGREGATION("aggregation"), + COMPOSITION("composition"); + + private final String displayName; + + AggregationTypeDTO(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + +} diff --git a/use-api/src/main/java/org/tzi/use/DTO/AssociationDTO.java b/use-api/src/main/java/org/tzi/use/DTO/AssociationDTO.java new file mode 100644 index 000000000..f6e96e901 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/AssociationDTO.java @@ -0,0 +1,25 @@ +package org.tzi.use.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AssociationDTO { + + private String associationName; + + private String end1ClassName; + private String end1RoleName; + private String end1Multiplicity; + private AggregationTypeDTO end1Aggregation; + + private String end2ClassName; + private String end2RoleName; + private String end2Multiplicity; + private AggregationTypeDTO end2Aggregation; +} + + diff --git a/use-api/src/main/java/org/tzi/use/DTO/AttributeDTO.java b/use-api/src/main/java/org/tzi/use/DTO/AttributeDTO.java new file mode 100644 index 000000000..8640f9a3c --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/AttributeDTO.java @@ -0,0 +1,15 @@ +package org.tzi.use.DTO; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AttributeDTO { + + private String name; + private String type; + +} diff --git a/use-api/src/main/java/org/tzi/use/DTO/ClassDTO.java b/use-api/src/main/java/org/tzi/use/DTO/ClassDTO.java new file mode 100644 index 000000000..e9d194f5f --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/ClassDTO.java @@ -0,0 +1,17 @@ +package org.tzi.use.DTO; + +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ClassDTO { + + private String name; + private List attributes = new ArrayList<>(); + private List operations = new ArrayList<>(); + private List associations = new ArrayList<>(); +} diff --git a/use-api/src/main/java/org/tzi/use/DTO/InvariantDTO.java b/use-api/src/main/java/org/tzi/use/DTO/InvariantDTO.java new file mode 100644 index 000000000..5a4d51a05 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/InvariantDTO.java @@ -0,0 +1,16 @@ +package org.tzi.use.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InvariantDTO { + + private String invName; + private String invBody; + private boolean isExistential; + +} diff --git a/use-api/src/main/java/org/tzi/use/DTO/ModelDTO.java b/use-api/src/main/java/org/tzi/use/DTO/ModelDTO.java new file mode 100644 index 000000000..9e61a79aa --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/ModelDTO.java @@ -0,0 +1,27 @@ +package org.tzi.use.DTO; + +import lombok.*; +import org.tzi.use.entities.AssociationNTT; +import org.tzi.use.entities.InvariantNTT; +import org.tzi.use.entities.PrePostConditionNTT; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ModelDTO { + private String name; + private List classes = new ArrayList<>(); + private Map associations = new TreeMap<>(); + private Map invariants = new TreeMap<>(); + private Map prePostConditions = new TreeMap<>(); + + public ModelDTO(String name) { + this.name = name; + } + +} diff --git a/use-api/src/main/java/org/tzi/use/DTO/OperationDTO.java b/use-api/src/main/java/org/tzi/use/DTO/OperationDTO.java new file mode 100644 index 000000000..e7d59c0c2 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/OperationDTO.java @@ -0,0 +1,16 @@ +package org.tzi.use.DTO; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OperationDTO { + + private String operationName; + private String[][] parameter; + private String returnType; + +} diff --git a/use-api/src/main/java/org/tzi/use/DTO/PrePostConditionDTO.java b/use-api/src/main/java/org/tzi/use/DTO/PrePostConditionDTO.java new file mode 100644 index 000000000..ce810346c --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/DTO/PrePostConditionDTO.java @@ -0,0 +1,17 @@ +package org.tzi.use.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PrePostConditionDTO { + + private String operationName; + private String name; + private String condition; + private boolean isPre; + +} diff --git a/use-api/src/main/java/org/tzi/use/GlobalExceptionHandler.java b/use-api/src/main/java/org/tzi/use/GlobalExceptionHandler.java new file mode 100644 index 000000000..38025857b --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/GlobalExceptionHandler.java @@ -0,0 +1,94 @@ +package org.tzi.use; + +import org.springframework.dao.DuplicateKeyException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.tzi.use.api.UseApiException; + +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Global exception handler for the REST API. Catches specific exceptions and returns appropriate HTTP responses. + */ +@ControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + /** + * Handle IllegalArgumentException - typically thrown when invalid input is provided + * (e.g., model element without name) + * Returns 400 Bad Request + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException( + IllegalArgumentException ex, WebRequest request) { + + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + body.put("error", "Bad Request"); + body.put("message", ex.getMessage()); + body.put("path", request.getDescription(false).replace("uri=", "")); + + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + + + @ExceptionHandler(DuplicateKeyException.class) + public ResponseEntity handleDuplicateKeyException( + DuplicateKeyException ex, WebRequest request) { + + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.CONFLICT.value()); + body.put("error", "Conflict"); + body.put("message", ex.getMessage()); + body.put("path", request.getDescription(false).replace("uri=", "")); + + return new ResponseEntity<>(body, HttpStatus.CONFLICT); + } + + /** + * Handle UseApiException - thrown by the USE API when operations fail + * Returns 400 Bad Request + */ + @ExceptionHandler(UseApiException.class) + public ResponseEntity handleUseApiException( + UseApiException ex, WebRequest request) { + + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + body.put("error", "Bad Request"); + body.put("message", ex.getMessage()); + body.put("path", request.getDescription(false).replace("uri=", "")); + + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + + /** + * Handle all other unexpected exceptions + * Returns 500 Internal Server Error + */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException( + Exception ex, WebRequest request) { + + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); + body.put("error", "Internal Server Error"); + body.put("message", "An unexpected error occurred"); + body.put("path", request.getDescription(false).replace("uri=", "")); + + // Log the full exception for debugging (in production, this goes to logs) + logger.error("Unexpected exception occurred", ex); + + return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/use-api/src/main/java/org/tzi/use/OpenApiConfig.java b/use-api/src/main/java/org/tzi/use/OpenApiConfig.java new file mode 100644 index 000000000..ce8f2cf92 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/OpenApiConfig.java @@ -0,0 +1,29 @@ +package org.tzi.use; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.servers.Server; + +@OpenAPIDefinition( + info = @Info( + title = "USE API", + description = "OpenAPI Dokumentation für die USE API", + contact = @Contact( + name = "Hüseyin Akkiran", + email = "Hueseyin.Akkiran@haw-hamburg.de" + ), + version = "1.0", + license = @License( + name = "GPL-2.0 license", + url = "https://github.com/useocl/use/blob/master/COPYING")), + servers = { + @Server( + description = "Lokale Umgebung", + url = "http://localhost:8080" + ) + } +) +public class OpenApiConfig { +} \ No newline at end of file diff --git a/use-api/src/main/java/org/tzi/use/UseModelFacade.java b/use-api/src/main/java/org/tzi/use/UseModelFacade.java new file mode 100644 index 000000000..11720a3e8 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/UseModelFacade.java @@ -0,0 +1,137 @@ +package org.tzi.use; + +import org.springframework.stereotype.Component; +import org.tzi.use.api.UseApiException; +import org.tzi.use.api.UseModelApi; +import org.tzi.use.entities.*; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class UseModelFacade { + + private final Map umaCache = new HashMap<>(); + + public void createModel(String modelName) { + new UseModelApi().createModel(modelName); + } + + public void createClass(ModelNTT modelNTT, ClassNTT classNTT) throws UseApiException { + UseModelApi uma = getUMAfromModelNTT(modelNTT); + uma.createClass(classNTT.getName(), false); + + for (AttributeNTT attribute : classNTT.getAttributes()) { + uma.createAttribute(classNTT.getName(), attribute.getName(), attribute.getType()); + } + for (OperationNTT operation : classNTT.getOperations()) { + uma.createOperation(classNTT.getName(), operation.getOperationName(), operation.getParameter(), operation.getReturnType()); + } + } + + public void createInvariant(ModelNTT modelNTT, InvariantNTT invariant, String className) throws UseApiException { + UseModelApi uma = getUMAfromModelNTT(modelNTT); + uma.createInvariant(invariant.getInvName(), className, invariant.getInvBody(), invariant.isExistential()); + } + + public void createAssociation(ModelNTT modelNTT, AssociationNTT association) throws UseApiException { + UseModelApi uma = getUMAfromModelNTT(modelNTT); + uma.createAssociation( + association.getAssociationName(), + association.getEnd1ClassName(), + association.getEnd1RoleName(), + association.getEnd1Multiplicity(), + association.getEnd1Aggregation().ordinal(), + association.getEnd2ClassName(), + association.getEnd2RoleName(), + association.getEnd2Multiplicity(), + association.getEnd2Aggregation().ordinal() + ); + } + + public void createAttribute(ModelNTT modelNTT, String className, AttributeNTT attributeNTT) throws UseApiException { + UseModelApi uma = getUMAfromModelNTT(modelNTT); + uma.createAttribute(className, attributeNTT.getName(), attributeNTT.getType()); + } + + public void createOperation(ModelNTT modelNTT, String className, OperationNTT operationNTT) throws UseApiException { + UseModelApi uma = getUMAfromModelNTT(modelNTT); + uma.createOperation(className, operationNTT.getOperationName(), operationNTT.getParameter(), operationNTT.getReturnType()); + } + + public void createPrePostCondition(ModelNTT modelNTT, PrePostConditionNTT ppc, String className) throws UseApiException { + UseModelApi uma = getUMAfromModelNTT(modelNTT); + uma.createPrePostCondition(className, ppc.getOperationName(), ppc.getName(), ppc.getCondition(), ppc.isPre()); + } + + + /* Delete */ + + public void deleteModel(String modelName) { + umaCache.remove(modelName); + } + + /* Helper Methods */ + + private static String extractClassName(String concatenatedClassName) { + String[] parts = concatenatedClassName.split("::"); + return parts[0]; + } + + private UseModelApi getUMAfromModelNTT(ModelNTT modelNTT) throws UseApiException { + UseModelApi cached = umaCache.get(modelNTT.getName()); + if (cached != null) { + return cached; + } + + UseModelApi result = new UseModelApi(modelNTT.getName()); + for (ClassNTT aClass : modelNTT.getClasses()) { + result.createClass(aClass.getName(), false); + for (AttributeNTT attribute : aClass.getAttributes()) { + result.createAttribute(aClass.getName(), attribute.getName(), attribute.getType()); + } + for (OperationNTT operation : aClass.getOperations()) { + result.createOperation(aClass.getName(), operation.getOperationName(), operation.getParameter(), operation.getReturnType()); + } + } + for (Map.Entry ppcEntry : modelNTT.getPrePostConditions().entrySet()) { + PrePostConditionNTT ppc = ppcEntry.getValue(); + result.createPrePostCondition( + extractClassName(ppcEntry.getKey()), + ppc.getOperationName(), + ppc.getName(), + ppc.getCondition(), + ppc.isPre() + ); + } + + for (Map.Entry assocEntry : modelNTT.getAssociations().entrySet()) { + AssociationNTT association = assocEntry.getValue(); + result.createAssociation( + association.getAssociationName(), + association.getEnd1ClassName(), + association.getEnd1RoleName(), + association.getEnd1Multiplicity(), + association.getEnd1Aggregation().ordinal(), + association.getEnd2ClassName(), + association.getEnd2RoleName(), + association.getEnd2Multiplicity(), + association.getEnd2Aggregation().ordinal() + ); + } + + for (Map.Entry invariantEntry : modelNTT.getInvariants().entrySet()) { + InvariantNTT invariant = invariantEntry.getValue(); + result.createInvariant( + invariant.getInvName(), + extractClassName(invariantEntry.getKey()), + invariant.getInvBody(), + invariant.isExistential() + ); + } + + umaCache.put(modelNTT.getName(), result); + return result; + } + +} diff --git a/use-api/src/main/java/org/tzi/use/UseWebAPIApplication.java b/use-api/src/main/java/org/tzi/use/UseWebAPIApplication.java new file mode 100644 index 000000000..0ba75b84e --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/UseWebAPIApplication.java @@ -0,0 +1,11 @@ +package org.tzi.use; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UseWebAPIApplication { + public static void main(String[] args) { + SpringApplication.run(UseWebAPIApplication.class, args); + } +} diff --git a/use-api/src/main/java/org/tzi/use/entities/AggregationTypeNTT.java b/use-api/src/main/java/org/tzi/use/entities/AggregationTypeNTT.java new file mode 100644 index 000000000..283875f75 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/AggregationTypeNTT.java @@ -0,0 +1,18 @@ +package org.tzi.use.entities; + +public enum AggregationTypeNTT { + NONE("association"), + AGGREGATION("aggregation"), + COMPOSITION("composition"); + + private final String displayName; + + AggregationTypeNTT(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + +} diff --git a/use-api/src/main/java/org/tzi/use/entities/AssociationNTT.java b/use-api/src/main/java/org/tzi/use/entities/AssociationNTT.java new file mode 100644 index 000000000..ae7956db2 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/AssociationNTT.java @@ -0,0 +1,23 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AssociationNTT { + private String associationName; + + private String end1ClassName; + private String end1RoleName; + private String end1Multiplicity; + private AggregationTypeNTT end1Aggregation; + + private String end2ClassName; + private String end2RoleName; + private String end2Multiplicity; + private AggregationTypeNTT end2Aggregation; +} + diff --git a/use-api/src/main/java/org/tzi/use/entities/AttributeNTT.java b/use-api/src/main/java/org/tzi/use/entities/AttributeNTT.java new file mode 100644 index 000000000..16bf70284 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/AttributeNTT.java @@ -0,0 +1,15 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AttributeNTT { + + private String name; + private String type; + +} diff --git a/use-api/src/main/java/org/tzi/use/entities/ClassNTT.java b/use-api/src/main/java/org/tzi/use/entities/ClassNTT.java new file mode 100644 index 000000000..75b734956 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/ClassNTT.java @@ -0,0 +1,18 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ClassNTT { + private String name; + private List attributes = new ArrayList<>(); + private List operations = new ArrayList<>(); + private List associations = new ArrayList<>(); +} diff --git a/use-api/src/main/java/org/tzi/use/entities/InvariantNTT.java b/use-api/src/main/java/org/tzi/use/entities/InvariantNTT.java new file mode 100644 index 000000000..8b0ef79c8 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/InvariantNTT.java @@ -0,0 +1,14 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class InvariantNTT { + private String invName; + private String invBody; + private boolean isExistential; +} diff --git a/use-api/src/main/java/org/tzi/use/entities/ModelNTT.java b/use-api/src/main/java/org/tzi/use/entities/ModelNTT.java new file mode 100644 index 000000000..7c7646bbe --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/ModelNTT.java @@ -0,0 +1,30 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.tzi.use.DTO.ClassDTO; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +@Document("model") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ModelNTT { + @Id + private String name; + private List classes = new ArrayList<>(); + private Map associations = new TreeMap<>(); + private Map invariants = new TreeMap<>(); + private Map prePostConditions = new TreeMap<>(); + + public ModelNTT(String name) { + this.name = name; + } +} diff --git a/use-api/src/main/java/org/tzi/use/entities/OperationNTT.java b/use-api/src/main/java/org/tzi/use/entities/OperationNTT.java new file mode 100644 index 000000000..bc9c3db98 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/OperationNTT.java @@ -0,0 +1,17 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OperationNTT { + + private String operationName; + private String[][] parameter; + private String returnType; + +} + diff --git a/use-api/src/main/java/org/tzi/use/entities/PrePostConditionNTT.java b/use-api/src/main/java/org/tzi/use/entities/PrePostConditionNTT.java new file mode 100644 index 000000000..f30784495 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/entities/PrePostConditionNTT.java @@ -0,0 +1,15 @@ +package org.tzi.use.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PrePostConditionNTT { + private String operationName; + private String name; + private String condition; + private boolean isPre; +} diff --git a/use-api/src/main/java/org/tzi/use/mapper/AssociationMapper.java b/use-api/src/main/java/org/tzi/use/mapper/AssociationMapper.java new file mode 100644 index 000000000..7dd6e922a --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/AssociationMapper.java @@ -0,0 +1,13 @@ +package org.tzi.use.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.AssociationDTO; +import org.tzi.use.entities.AssociationNTT; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface AssociationMapper { + AssociationDTO toDTO(AssociationNTT entity); + + AssociationNTT toEntity(AssociationDTO dto); +} diff --git a/use-api/src/main/java/org/tzi/use/mapper/AttributeMapper.java b/use-api/src/main/java/org/tzi/use/mapper/AttributeMapper.java new file mode 100644 index 000000000..6fc5a3bbb --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/AttributeMapper.java @@ -0,0 +1,14 @@ +package org.tzi.use.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.AttributeDTO; +import org.tzi.use.entities.AttributeNTT; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface AttributeMapper { + AttributeDTO toDTO(AttributeNTT entity); + + AttributeNTT toEntity(AttributeDTO dto); +} + diff --git a/use-api/src/main/java/org/tzi/use/mapper/ClassMapper.java b/use-api/src/main/java/org/tzi/use/mapper/ClassMapper.java new file mode 100644 index 000000000..bdd664046 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/ClassMapper.java @@ -0,0 +1,14 @@ +package org.tzi.use.mapper; + + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.ClassDTO; +import org.tzi.use.entities.ClassNTT; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ClassMapper { + ClassDTO toDTO(ClassNTT entity); + + ClassNTT toEntity(ClassDTO dto); +} diff --git a/use-api/src/main/java/org/tzi/use/mapper/InvariantMapper.java b/use-api/src/main/java/org/tzi/use/mapper/InvariantMapper.java new file mode 100644 index 000000000..ec9931cbc --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/InvariantMapper.java @@ -0,0 +1,14 @@ +package org.tzi.use.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.InvariantDTO; +import org.tzi.use.entities.InvariantNTT; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) + +public interface InvariantMapper { + InvariantDTO toDTO(InvariantNTT entity); + + InvariantNTT toEntity(InvariantDTO dto); +} diff --git a/use-api/src/main/java/org/tzi/use/mapper/ModelMapper.java b/use-api/src/main/java/org/tzi/use/mapper/ModelMapper.java new file mode 100644 index 000000000..ae90f231a --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/ModelMapper.java @@ -0,0 +1,18 @@ +package org.tzi.use.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.ModelDTO; +import org.tzi.use.entities.ModelNTT; + +/** + * MapStruct mapper to convert between ModelNTT (entity) and ModelDTO (data transfer object). + * Implementation will be generated by MapStruct at compile time and registered as a Spring bean. + */ +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ModelMapper { + + ModelDTO toDTO(ModelNTT entity); + + ModelNTT toEntity(ModelDTO dto); +} diff --git a/use-api/src/main/java/org/tzi/use/mapper/OperationMapper.java b/use-api/src/main/java/org/tzi/use/mapper/OperationMapper.java new file mode 100644 index 000000000..ba73ba378 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/OperationMapper.java @@ -0,0 +1,14 @@ +package org.tzi.use.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.OperationDTO; +import org.tzi.use.entities.OperationNTT; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface OperationMapper { + OperationDTO toDTO(OperationNTT entity); + + OperationNTT toEntity(OperationDTO dto); +} + diff --git a/use-api/src/main/java/org/tzi/use/mapper/PrePostConditionMapper.java b/use-api/src/main/java/org/tzi/use/mapper/PrePostConditionMapper.java new file mode 100644 index 000000000..ca571acd6 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/mapper/PrePostConditionMapper.java @@ -0,0 +1,13 @@ +package org.tzi.use.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.tzi.use.DTO.PrePostConditionDTO; +import org.tzi.use.entities.PrePostConditionNTT; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface PrePostConditionMapper { + PrePostConditionDTO toDTO(PrePostConditionNTT entity); + + PrePostConditionNTT toEntity(PrePostConditionDTO dto); +} diff --git a/use-api/src/main/java/org/tzi/use/repository/ModelRepo.java b/use-api/src/main/java/org/tzi/use/repository/ModelRepo.java new file mode 100644 index 000000000..0ded7ac3b --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/repository/ModelRepo.java @@ -0,0 +1,10 @@ +package org.tzi.use.repository; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.tzi.use.entities.ModelNTT; + +import java.util.Optional; + +public interface ModelRepo extends MongoRepository { + Optional findByClassesName(String className); +} diff --git a/use-api/src/main/java/org/tzi/use/rest/controller/ClassController.java b/use-api/src/main/java/org/tzi/use/rest/controller/ClassController.java new file mode 100644 index 000000000..f3ee5e2fd --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/rest/controller/ClassController.java @@ -0,0 +1,161 @@ +package org.tzi.use.rest.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.tzi.use.DTO.AttributeDTO; +import org.tzi.use.DTO.ClassDTO; +import org.tzi.use.DTO.OperationDTO; +import org.tzi.use.api.UseApiException; +import org.tzi.use.rest.services.ClassService; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.CollectionModel; + +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@RestController +@RequestMapping("/api/models/{modelName}") +@RequiredArgsConstructor +public class ClassController { + private final ClassService classService; + + @Operation(summary = "Get a class by name", description = "Returns one class of the model") + @GetMapping("/class/{className}") + public ResponseEntity> getClassByName(@PathVariable String modelName, @PathVariable String className) throws UseApiException { + ClassDTO classDTO = classService.getClassByName(modelName, className); + + EntityModel entityModels = EntityModel.of(classDTO); + + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getAttributes(modelName, className)).withRel("attributes")); + entityModels.add(linkTo(methodOn(ClassController.class).getOperations(modelName, className)).withRel("operations")); + entityModels.add(linkTo(methodOn(ModelController.class).getClasses(modelName)).withRel("classes")); + + return ResponseEntity.ok(entityModels); + } + + @Operation(summary = "List attributes of a class", description = "Lists every attribute of the class") + @GetMapping("/class/{className}/attributes") + public ResponseEntity>> getAttributes(@PathVariable String modelName, @PathVariable String className) throws UseApiException { + List attributes = classService.getAttributes(modelName, className); + List> attributeModels = new ArrayList<>(); + for (AttributeDTO attr : attributes) { + EntityModel attributeDTOEntityModel = EntityModel.of(attr); + + attributeDTOEntityModel.add(linkTo(methodOn(ClassController.class).getAttributeByName(modelName, className, attr.getName())).withSelfRel()); + + attributeModels.add(attributeDTOEntityModel); + } + + CollectionModel> entityModels = CollectionModel.of(attributeModels); + entityModels.add(linkTo(methodOn(ClassController.class).getAttributes(modelName, className)).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withRel("class")); + + return ResponseEntity.ok(entityModels); + } + + @Operation(summary = "Get an attribute by name", description = "Returns a single attribute of the class") + @GetMapping("/class/{className}/attribute/{attributeName}") + public ResponseEntity> getAttributeByName(@PathVariable String modelName, @PathVariable String className, @PathVariable String attributeName) throws UseApiException { + AttributeDTO attribute = classService.getAttributeByName(modelName, className, attributeName); + + EntityModel entityModels = EntityModel.of(attribute); + + entityModels.add(linkTo(methodOn(ClassController.class).getAttributeByName(modelName, className, attributeName)).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getAttributes(modelName, className)).withRel("attributes")); + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withRel("class")); + + return ResponseEntity.ok(entityModels); + } + + + @Operation(summary = "List operations of a class", description = "Lists operations defined on the class") + @GetMapping("/class/{className}/operations") + public ResponseEntity>> getOperations(@PathVariable String modelName, @PathVariable String className) throws UseApiException { + List operations = classService.getOperations(modelName, className); + List> operationModels = new ArrayList<>(); + for (OperationDTO op : operations) { + EntityModel operationDTOEntityModel = EntityModel.of(op); + + operationDTOEntityModel.add(linkTo(methodOn(ClassController.class).getOperationByName(modelName, className, op.getOperationName())).withSelfRel()); + + operationModels.add(operationDTOEntityModel); + } + + CollectionModel> entityModels = CollectionModel.of(operationModels); + + entityModels.add(linkTo(methodOn(ClassController.class).getOperations(modelName, className)).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withRel("class")); + + return ResponseEntity.ok(entityModels); + } + + @Operation(summary = "Get an operation by name", description = "Returns a specific operation of the class") + @GetMapping("/class/{className}/operation/{operationName}") + public ResponseEntity> getOperationByName(@PathVariable String modelName, @PathVariable String className, @PathVariable String operationName) throws UseApiException { + OperationDTO operation = classService.getOperationByName(modelName, className, operationName); + + EntityModel entityModels = EntityModel.of(operation); + + entityModels.add(linkTo(methodOn(ClassController.class).getOperationByName(modelName, className, operationName)).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getOperations(modelName, className)).withRel("operations")); + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withRel("class")); + + + return ResponseEntity.ok(entityModels); + } + + + + @Operation(summary = "Add an attribute to a class", description = "Creates an attribute for the class") + @PostMapping("/class/{className}/attribute") + public ResponseEntity> addAttribute(@PathVariable String modelName, @PathVariable String className, @RequestBody AttributeDTO attributeDTO) throws UseApiException { + AttributeDTO newAttribute = classService.createAttribute(modelName, className, attributeDTO); + EntityModel entityModels = EntityModel.of(newAttribute); + + entityModels.add(linkTo(methodOn(ClassController.class).getAttributeByName(modelName, className, newAttribute.getName())).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withRel("class")); + + + return new ResponseEntity<>(entityModels, HttpStatus.CREATED); + } + + @Operation(summary = "Add an operation to a class", description = "Creates an operation on the class") + @PostMapping("/class/{className}/operation") + public ResponseEntity> addOperation(@PathVariable String modelName, @PathVariable String className, @RequestBody OperationDTO operationDTO) throws UseApiException { + OperationDTO newOperation = classService.createOperation(modelName, className, operationDTO); + + EntityModel entityModels = EntityModel.of(newOperation); + + entityModels.add(linkTo(methodOn(ClassController.class).getOperationByName(modelName, className, newOperation.getOperationName())).withSelfRel()); + entityModels.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, className)).withRel("class")); + + return new ResponseEntity<>(entityModels, HttpStatus.CREATED); + } + + @Operation(summary = "Delete an attribute from a class", description = "Deletes the attribute from the class") + @DeleteMapping("/class/{className}/attribute/{attributeName}") + public ResponseEntity deleteAttribute(@PathVariable String modelName, @PathVariable String className, @PathVariable String attributeName) { + classService.deleteAttribute(modelName, className, attributeName); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "Delete an operation from a class", description = "Deletes the operation from the class") + @DeleteMapping("/class/{className}/operation/{operationName}") + public ResponseEntity deleteOperation(@PathVariable String modelName, @PathVariable String className, @PathVariable String operationName) { + classService.deleteOperation(modelName, className, operationName); + return ResponseEntity.noContent().build(); + } +} diff --git a/use-api/src/main/java/org/tzi/use/rest/controller/ModelController.java b/use-api/src/main/java/org/tzi/use/rest/controller/ModelController.java new file mode 100644 index 000000000..efe98212a --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/rest/controller/ModelController.java @@ -0,0 +1,335 @@ +package org.tzi.use.rest.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.tzi.use.DTO.*; +import org.tzi.use.api.UseApiException; +import org.tzi.use.rest.services.ModelService; + +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ModelController { + + private final ModelService modelService; + + // ======================================== + // GET Mappings + // ======================================== + + @Operation(summary = "Get a model by name", description = "Returns the requested model and related metadata") + @GetMapping("/model/{modelName}") + public ResponseEntity> getModelByName(@PathVariable String modelName) throws UseApiException { + ModelDTO modelDTO = modelService.getModelByName(modelName); + + EntityModel entityModel = EntityModel.of(modelDTO); + + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getClasses(modelName)).withRel("classes")); + entityModel.add(linkTo(methodOn(ModelController.class).getAssociations(modelName)).withRel("associations")); + entityModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelName)).withRel("invariants")); + entityModel.add(linkTo(methodOn(ModelController.class).getPrePostConditions(modelName)).withRel("prePostConditions")); + + return new ResponseEntity<>(entityModel, HttpStatus.OK); + } + + @Operation(summary = "Get an association by model and name", description = "Returns a single association of the given model") + @GetMapping("/model/{modelName}/association/{associationName}") + public ResponseEntity> getModelAssociationByName(@PathVariable String modelName, @PathVariable String associationName) throws UseApiException { + AssociationDTO association = modelService.getAssociationByName(modelName, associationName); + + EntityModel entityModel = EntityModel.of(association); + + entityModel.add(linkTo(methodOn(ModelController.class).getModelAssociationByName(modelName, associationName)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ModelController.class).getAssociations(modelName)).withRel("associations")); + + return new ResponseEntity<>(entityModel, HttpStatus.OK); + } + + @Operation(summary = "Get an invariant by model and name", description = "Returns a specific invariant of the model") + @GetMapping("/model/{modelName}/invariant/{invariantName}") + public ResponseEntity> getModelInvariantByName(@PathVariable String modelName, @PathVariable String invariantName) throws UseApiException { + InvariantDTO invariant = modelService.getInvariantByName(modelName, invariantName); + + EntityModel entityModel = EntityModel.of(invariant); + + entityModel.add(linkTo(methodOn(ModelController.class).getModelInvariantByName(modelName, invariantName)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelName)).withRel("invariants")); + + return new ResponseEntity<>(entityModel, HttpStatus.OK); + } + + @Operation(summary = "List all models", description = "Returns all available models with their metadata") + @GetMapping("/models") + public ResponseEntity>> getModels() throws UseApiException { + List models = modelService.getAllModels(); + + List> modelEntities = new ArrayList<>(); + for (ModelDTO modelDTO : models) { + EntityModel entityModel = EntityModel.of(modelDTO); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelDTO.getName())).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getClasses(modelDTO.getName())).withRel("classes")); + entityModel.add(linkTo(methodOn(ModelController.class).getAssociations(modelDTO.getName())).withRel("associations")); + entityModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelDTO.getName())).withRel("invariants")); + entityModel.add(linkTo(methodOn(ModelController.class).getPrePostConditions(modelDTO.getName())).withRel("prePostConditions")); + + modelEntities.add(entityModel); + } + + CollectionModel> collectionModel = CollectionModel.of(modelEntities); + collectionModel.add(linkTo(methodOn(ModelController.class).getModels()).withSelfRel()); + collectionModel.add(linkTo(methodOn(ModelController.class).createModel(null)).withRel("create model")); + + + return new ResponseEntity<>(collectionModel, HttpStatus.OK); + } + + + @Operation(summary = "List all classes of a model", description = "Lists every class of the model") + @GetMapping("/model/{modelName}/classes") + public ResponseEntity getClasses(@PathVariable String modelName) throws UseApiException { + List modelClasses = modelService.getModelClasses(modelName); + + List> classEntities = new ArrayList<>(); + for (ClassDTO classOfModelName : modelClasses) { + EntityModel entityModel = EntityModel.of(classOfModelName); + + entityModel.add(linkTo(methodOn(ClassController.class).getClassByName(modelName, null)).withRel("class by name")); + entityModel.add(linkTo(methodOn(ClassController.class).addAttribute(modelName,null,null)).withRel("add attribute")); + entityModel.add(linkTo(methodOn(ClassController.class).addOperation(modelName,null,null)).withRel("add operation")); + + classEntities.add(entityModel); + } + + CollectionModel> collectionModel = CollectionModel.of(classEntities); + collectionModel.add(linkTo(methodOn(ModelController.class).getClasses(modelName)).withSelfRel()); + collectionModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + collectionModel.add(linkTo(methodOn(ModelController.class).createClass(modelName, null)).withRel("create class")); + + return new ResponseEntity<>(collectionModel, HttpStatus.OK); + } + + + @Operation(summary = "List all associations of a model", description = "Lists associations defined in the model") + @GetMapping("/model/{modelName}/associations") + public ResponseEntity>> getAssociations(@PathVariable String modelName) throws UseApiException { + List associations = modelService.getModelAssociations(modelName); + + List> associationEntities = new ArrayList<>(); + for (AssociationDTO associationDTO : associations) { + EntityModel entityModel = EntityModel.of(associationDTO); + + entityModel.add(linkTo(methodOn(ModelController.class).getModelAssociationByName(modelName, associationDTO.getAssociationName())).withSelfRel()); + associationEntities.add(entityModel); + } + + CollectionModel> collectionModel = CollectionModel.of(associationEntities); + + collectionModel.add(linkTo(methodOn(ModelController.class).getAssociations(modelName)).withSelfRel()); + collectionModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + collectionModel.add(linkTo(methodOn(ModelController.class).createAssociation(modelName, null)).withRel("create association")); + + + return new ResponseEntity<>(collectionModel, HttpStatus.OK); + } + + + @Operation(summary = "List all invariants of a model", description = "Lists invariants defined in the model") + @GetMapping("/model/{modelName}/invariants") + public ResponseEntity>> getInvariants(@PathVariable String modelName) throws UseApiException { + List invariants = modelService.getModelInvariants(modelName); + + + List> invariantEntities = new ArrayList<>(); + for (InvariantDTO invariantDTO : invariants) { + EntityModel entityModel = EntityModel.of(invariantDTO); + + entityModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelName)).withSelfRel()); + + invariantEntities.add(entityModel); + } + + CollectionModel> collectionModel = CollectionModel.of(invariantEntities); + + collectionModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelName)).withSelfRel()); + collectionModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + collectionModel.add(linkTo(methodOn(ModelController.class).createInvariant(modelName,null,null)).withRel("create invariant")); + + + return new ResponseEntity<>(collectionModel, HttpStatus.OK); + } + + @Operation(summary = "List all pre/post conditions of a model", description = "Lists pre/post conditions defined in the model") + @GetMapping("/model/{modelName}/prepostconditions") + public ResponseEntity>> getPrePostConditions(@PathVariable String modelName) throws UseApiException { + List prePostConditions = modelService.getModelPrePostConditions(modelName); + + + List> prePostConditionEntities = new ArrayList<>(); + for (PrePostConditionDTO prePostConditionDTO : prePostConditions) { + EntityModel entityModel = EntityModel.of(prePostConditionDTO); + + entityModel.add(linkTo(methodOn(ModelController.class).getPrePostConditions(modelName)).withSelfRel()); + + prePostConditionEntities.add(entityModel); + } + + CollectionModel> collectionModel = CollectionModel.of(prePostConditionEntities); + + collectionModel.add(linkTo(methodOn(ModelController.class).getPrePostConditions(modelName)).withSelfRel()); + collectionModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + collectionModel.add(linkTo(methodOn(ModelController.class).createPrePostCondition(modelName,null,null)).withRel("create prepostcondition")); + + return new ResponseEntity<>(collectionModel, HttpStatus.OK); + } + + @Operation(summary = "Get a pre/post condition by model and name", description = "Returns the named pre/post condition of the model") + @GetMapping("/model/{modelName}/prepostcondition/{prePostConditionName}") + public ResponseEntity> getModelPrePostCondByName(@PathVariable String modelName, @PathVariable String prePostConditionName) throws UseApiException { + PrePostConditionDTO prePostCondition = modelService.getPrePostConditionByName(modelName, prePostConditionName); + + EntityModel entityModel = EntityModel.of(prePostCondition); + + entityModel.add(linkTo(methodOn(ModelController.class).getModelPrePostCondByName(modelName, prePostConditionName)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ModelController.class).getPrePostConditions(modelName)).withRel("prePostConditions")); + + return new ResponseEntity<>(entityModel, HttpStatus.OK); + } + + // ======================================== + // POST Mappings + // ======================================== + + + @Operation(summary = "Create a model", description = "Creates a new model") + @PostMapping("/model") + public ResponseEntity> createModel(@RequestBody ModelDTO modelDTO) throws UseApiException { + ModelDTO createdModel = modelService.createModel(modelDTO); + + EntityModel entityModel = EntityModel.of(createdModel); + + entityModel.add(linkTo(methodOn(ModelController.class).createModel(createdModel)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModels()).withRel("models")); + entityModel.add(linkTo(methodOn(ModelController.class).createClass(modelDTO.getName(),null)).withRel("create class")); + entityModel.add(linkTo(methodOn(ModelController.class).createAssociation(modelDTO.getName(), null)).withRel("create association")); + entityModel.add(linkTo(methodOn(ModelController.class).createInvariant(modelDTO.getName(),null,null)).withRel("create invariant")); + entityModel.add(linkTo(methodOn(ModelController.class).createPrePostCondition(modelDTO.getName(), null,null)).withRel("create prepostcondition")); + + return new ResponseEntity<>(entityModel, HttpStatus.CREATED); + } + + @Operation(summary = "Create a class in a model", description = "Adds a class to the given model") + @PostMapping("/model/{modelName}/class") + public ResponseEntity> createClass(@PathVariable String modelName, @RequestBody ClassDTO classDTO) throws UseApiException { + ClassDTO createdClass = modelService.createClass(modelName, classDTO); + + EntityModel entityModel = EntityModel.of(createdClass); + + entityModel.add(linkTo(methodOn(ModelController.class).createClass(modelName,classDTO)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ClassController.class).addAttribute(modelName,classDTO.getName(),null)).withRel("add attribute")); + entityModel.add(linkTo(methodOn(ClassController.class).addOperation(modelName,classDTO.getName(),null)).withRel("add operation")); + entityModel.add(linkTo(methodOn(ModelController.class).getClasses(modelName)).withRel("classes")); + + + return new ResponseEntity<>(entityModel, HttpStatus.CREATED); + } + + @Operation(summary = "Create an association in a model", description = "Creates an association in the model") + @PostMapping("/model/{modelName}/association") + public ResponseEntity> createAssociation(@PathVariable String modelName, @RequestBody AssociationDTO association) throws UseApiException { + AssociationDTO createdAssociation = modelService.createAssociation(modelName, association); + + EntityModel entityModel = EntityModel.of(createdAssociation); + + entityModel.add(linkTo(methodOn(ModelController.class).createAssociation(modelName, createdAssociation)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ModelController.class).getAssociations(modelName)).withRel("associations")); + entityModel.add(linkTo(methodOn(ModelController.class).getModelAssociationByName(modelName,association.getAssociationName())).withRel("association by name")); + + return new ResponseEntity<>(entityModel, HttpStatus.CREATED); + } + + @Operation(summary = "Create an invariant in a class", description = "Adds an invariant scoped to a class within the model") + @PostMapping("/model/{modelName}/{className}/invariant") + public ResponseEntity> createInvariant(@PathVariable String modelName, @PathVariable String className, @RequestBody InvariantDTO invariantDTO) throws UseApiException { + InvariantDTO createdInvariant = modelService.createInvariant(modelName, invariantDTO, className); + + EntityModel entityModel = EntityModel.of(createdInvariant); + + entityModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelName)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ModelController.class).getInvariants(modelName)).withRel("invariants")); + entityModel.add(linkTo(methodOn(ModelController.class).getModelInvariantByName(modelName, invariantDTO.getInvName())).withRel("invariant by name")); + + return new ResponseEntity<>(entityModel, HttpStatus.CREATED); + } + + @Operation(summary = "Create a pre/post condition in a class", description = "Adds a pre/post condition to a class") + @PostMapping("/model/{modelName}/{className}/prepostcondition") + public ResponseEntity> createPrePostCondition(@PathVariable String modelName, @PathVariable String className, @RequestBody PrePostConditionDTO prePostConditionDTO) throws UseApiException { + PrePostConditionDTO createdPrePostCondition = modelService.createPrePostCondition(modelName, prePostConditionDTO, className); + + EntityModel entityModel = EntityModel.of(createdPrePostCondition); + + entityModel.add(linkTo(methodOn(ModelController.class).getClasses(modelName)).withSelfRel()); + entityModel.add(linkTo(methodOn(ModelController.class).getModelByName(modelName)).withRel("model")); + entityModel.add(linkTo(methodOn(ModelController.class).getPrePostConditions(modelName)).withRel("prePostConditions")); + entityModel.add(linkTo(methodOn(ModelController.class).getModelPrePostCondByName(modelName, prePostConditionDTO.getName())).withRel("prepostcondition by name")); + + return new ResponseEntity<>(entityModel, HttpStatus.CREATED); + } + + // ======================================== + // DELETE Mappings + // ======================================== + + @Operation(summary = "Delete a model", description = "Deletes the model and all its classes, associations, invariants and prepostconditions") + @DeleteMapping("/model/{modelName}") + public ResponseEntity deleteModel(@PathVariable String modelName) { + modelService.deleteModel(modelName); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "Delete a class from a model", description = "Deletes the class and its attributes, operations and associations referencing it") + @DeleteMapping("/model/{modelName}/class/{className}") + public ResponseEntity deleteClass(@PathVariable String modelName, @PathVariable String className) { + modelService.deleteClass(modelName, className); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "Delete an association from a model", description = "Deletes the association from the model") + @DeleteMapping("/model/{modelName}/association/{associationName}") + public ResponseEntity deleteAssociation(@PathVariable String modelName, @PathVariable String associationName) { + modelService.deleteAssociation(modelName, associationName); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "Delete an invariant from a model", description = "Deletes the invariant from the model") + @DeleteMapping("/model/{modelName}/invariant/{invariantName}") + public ResponseEntity deleteInvariant(@PathVariable String modelName, @PathVariable String invariantName) { + modelService.deleteInvariant(modelName, invariantName); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "Delete a pre/post condition from a model", description = "Deletes the pre/post condition from the model") + @DeleteMapping("/model/{modelName}/prepostcondition/{prePostConditionName}") + public ResponseEntity deletePrePostCondition(@PathVariable String modelName, @PathVariable String prePostConditionName) { + modelService.deletePrePostCondition(modelName, prePostConditionName); + return ResponseEntity.noContent().build(); + } +} diff --git a/use-api/src/main/java/org/tzi/use/rest/services/ClassService.java b/use-api/src/main/java/org/tzi/use/rest/services/ClassService.java new file mode 100644 index 000000000..798dc5747 --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/rest/services/ClassService.java @@ -0,0 +1,117 @@ +package org.tzi.use.rest.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.tzi.use.DTO.AttributeDTO; +import org.tzi.use.DTO.ClassDTO; +import org.tzi.use.DTO.OperationDTO; +import org.tzi.use.UseModelFacade; +import org.tzi.use.api.UseApiException; +import org.tzi.use.entities.AttributeNTT; +import org.tzi.use.entities.ClassNTT; +import org.tzi.use.entities.ModelNTT; +import org.tzi.use.entities.OperationNTT; +import org.tzi.use.mapper.*; +import org.tzi.use.repository.ModelRepo; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ClassService { + private final ModelRepo modelRepo; + private final ClassMapper classMapper; + private final AttributeMapper attributeMapper; + private final OperationMapper operationMapper; + private final UseModelFacade useModelFacade; + private final ModelService modelService; + + /* Create */ + public AttributeDTO createAttribute(String modelName, String className, AttributeDTO attributeDTO) throws UseApiException { + AttributeNTT attributeNTT = attributeMapper.toEntity(attributeDTO); + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + + useModelFacade.createAttribute(modelNTT, className, attributeNTT); + + classNTT.getAttributes().add(attributeNTT); + modelRepo.save(modelNTT); + return attributeMapper.toDTO(attributeNTT); + } + + public OperationDTO createOperation(String modelName, String className, OperationDTO operationDTO) throws UseApiException { + OperationNTT operationNTT = operationMapper.toEntity(operationDTO); + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + + useModelFacade.createOperation(modelNTT, className, operationNTT); + + classNTT.getOperations().add(operationNTT); + modelRepo.save(modelNTT); + return operationMapper.toDTO(operationNTT); + } + + /* Get */ + public ClassDTO getClassByName(String modelName, String className) { + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + return classMapper.toDTO(classNTT); + } + + public List getAttributes(String modelName, String className) { + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + return classNTT.getAttributes().stream().map(attributeMapper::toDTO).toList(); + } + + public List getOperations(String modelName, String className) { + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + return classNTT.getOperations().stream().map(operationMapper::toDTO).toList(); + } + + public OperationDTO getOperationByName(String modelName, String className, String operationName) { + return getOperations(modelName,className).stream() + .filter(op -> op.getOperationName().equals(operationName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Operation not found: " + operationName)); + } + + public AttributeDTO getAttributeByName(String modelName, String className, String attributeName) { + return getAttributes(modelName, className).stream() + .filter(attr -> attr.getName().equals(attributeName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Attribute not found: " + attributeName)); + } + + /* Delete */ + public void deleteAttribute(String modelName, String className, String attributeName) { + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + + boolean removed = classNTT.getAttributes().removeIf(attr -> attr.getName().equals(attributeName)); + if (!removed) { + throw new IllegalArgumentException("Attribute not found: " + attributeName); + } + modelRepo.save(modelNTT); + } + + public void deleteOperation(String modelName, String className, String operationName) { + ModelNTT modelNTT = modelService.findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + + boolean removed = classNTT.getOperations().removeIf(op -> op.getOperationName().equals(operationName)); + if (!removed) { + throw new IllegalArgumentException("Operation not found: " + operationName); + } + modelRepo.save(modelNTT); + } + + /* Helper Methods */ + private ClassNTT findClassByNameOrThrow(ModelNTT modelNTT, String className) { + return modelNTT.getClasses().stream() + .filter(c -> c.getName().equals(className)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Class not found: " + className)); + } +} diff --git a/use-api/src/main/java/org/tzi/use/rest/services/ModelService.java b/use-api/src/main/java/org/tzi/use/rest/services/ModelService.java new file mode 100644 index 000000000..d039ea74e --- /dev/null +++ b/use-api/src/main/java/org/tzi/use/rest/services/ModelService.java @@ -0,0 +1,209 @@ +package org.tzi.use.rest.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.tzi.use.DTO.*; +import org.tzi.use.UseModelFacade; +import org.tzi.use.api.UseApiException; +import org.tzi.use.entities.*; +import org.tzi.use.mapper.*; +import org.tzi.use.repository.ModelRepo; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ModelService { + + private final ModelRepo modelRepo; + private final ModelMapper modelMapper; + private final ClassMapperImpl classMapperImpl; + private final InvariantMapperImpl invariantMapperImpl; + private final AssociationMapperImpl associationMapperImpl; + private final PrePostConditionMapper prePostConditionMapper; + private final UseModelFacade useModelFacade; + private final InvariantMapper invariantMapper; + private final AssociationMapper associationMapper; + private final ClassMapper classMapper; + private final PrePostConditionMapperImpl prePostConditionMapperImpl; + + /* Create */ + public ModelDTO createModel(ModelDTO modelDTO) { + if (modelRepo.findById(modelDTO.getName()).isPresent()) { + throw new DuplicateKeyException("Model name already exists"); + } + ModelNTT modelNTT = modelMapper.toEntity(modelDTO); + + useModelFacade.createModel(modelNTT.getName()); + + modelRepo.save(modelNTT); + return modelMapper.toDTO(modelNTT); + } + + public ClassDTO createClass(String modelName, ClassDTO classDTO) throws UseApiException { + + ClassNTT classNTT = classMapper.toEntity(classDTO); + + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + + boolean classExists = modelNTT.getClasses().stream().anyMatch(c -> c.getName().equals(classDTO.getName())); + if (classExists) { + throw new DuplicateKeyException("Class name already exists in model: " + modelName); + } + + useModelFacade.createClass(modelNTT, classNTT); + modelNTT.getClasses().add(classNTT); + modelRepo.save(modelNTT); + + return classMapper.toDTO(classNTT); + } + + public PrePostConditionDTO createPrePostCondition(String modelName, PrePostConditionDTO prePostConditionDTO, String className) throws UseApiException { + PrePostConditionNTT prePostConditionNTT = prePostConditionMapper.toEntity(prePostConditionDTO); + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + + useModelFacade.createPrePostCondition(modelNTT, prePostConditionNTT, className); + String name = className + "::" + prePostConditionDTO.getOperationName() + prePostConditionDTO.getName(); + + modelNTT.getPrePostConditions().put(name, prePostConditionNTT); + modelRepo.save(modelNTT); + return prePostConditionMapper.toDTO(prePostConditionNTT); + } + + public InvariantDTO createInvariant(String modelName, InvariantDTO invariantDTO, String className) throws UseApiException { + InvariantNTT invariantNTT = invariantMapper.toEntity(invariantDTO); + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + + useModelFacade.createInvariant(modelNTT, invariantNTT, className); + modelNTT.getInvariants().put(className, invariantNTT); + modelRepo.save(modelNTT); + return invariantMapper.toDTO(invariantNTT); + } + + public AssociationDTO createAssociation(String modelName, AssociationDTO association) throws UseApiException { + AssociationNTT associationNTT = associationMapper.toEntity(association); + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + + useModelFacade.createAssociation(modelNTT, associationNTT); + modelNTT.getAssociations().put(associationNTT.getEnd1ClassName(), associationNTT); + modelRepo.save(modelNTT); + return associationMapper.toDTO(associationNTT); + } + + /* Get */ + public ModelDTO getModelByName(String modelName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + return modelMapper.toDTO(modelNTT); + } + + public List getAllModels() { + return modelRepo.findAll().stream().map(modelMapper::toDTO).toList(); + } + + + public List getModelClasses(String modelName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + return modelNTT.getClasses().stream().map(classMapperImpl::toDTO).toList(); + } + + public List getModelAssociations(String modelName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + return modelNTT.getAssociations().values().stream().map(associationMapperImpl::toDTO).toList(); + } + + public List getModelInvariants(String modelName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + return modelNTT.getInvariants().values().stream().map(invariantMapperImpl::toDTO).toList(); + } + + public List getModelPrePostConditions(String modelName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + return modelNTT.getPrePostConditions().values().stream().map(prePostConditionMapperImpl::toDTO).toList(); + } + + public AssociationDTO getAssociationByName(String modelName, String associationName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + AssociationNTT associationNTT = modelNTT.getAssociations().get(associationName); + return associationMapper.toDTO(associationNTT); + } + + public InvariantDTO getInvariantByName(String modelName, String invariantName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + InvariantNTT invariantNTT = modelNTT.getInvariants().get(invariantName); + return invariantMapper.toDTO(invariantNTT); + } + + public PrePostConditionDTO getPrePostConditionByName(String modelName, String prePostConditionName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + PrePostConditionNTT prePostConditionNTT = modelNTT.getPrePostConditions().get(prePostConditionName); + return prePostConditionMapper.toDTO(prePostConditionNTT); + } + + /* Delete */ + + // Recursively deletes model with all its classes, associations, invariants and prepostconditions + public void deleteModel(String modelName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + modelNTT.getClasses().clear(); + modelNTT.getAssociations().clear(); + modelNTT.getInvariants().clear(); + modelNTT.getPrePostConditions().clear(); + useModelFacade.deleteModel(modelName); + modelRepo.delete(modelNTT); + } + + // Recursively deletes class with its attributes, operations and associations referencing the class + public void deleteClass(String modelName, String className) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + ClassNTT classNTT = findClassByNameOrThrow(modelNTT, className); + + classNTT.getAttributes().clear(); + classNTT.getOperations().clear(); + + modelNTT.getAssociations().entrySet().removeIf(entry -> { + AssociationNTT assoc = entry.getValue(); + return assoc.getEnd1ClassName().equals(className) || assoc.getEnd2ClassName().equals(className); + }); + + modelNTT.getClasses().remove(classNTT); + modelRepo.save(modelNTT); + } + + public void deleteAssociation(String modelName, String associationName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + if (modelNTT.getAssociations().remove(associationName) == null) { + throw new IllegalArgumentException("Association not found: " + associationName); + } + modelRepo.save(modelNTT); + } + + public void deleteInvariant(String modelName, String invariantName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + if (modelNTT.getInvariants().remove(invariantName) == null) { + throw new IllegalArgumentException("Invariant not found: " + invariantName); + } + modelRepo.save(modelNTT); + } + + public void deletePrePostCondition(String modelName, String prePostConditionName) { + ModelNTT modelNTT = findModelByNameOrThrow(modelName); + if (modelNTT.getPrePostConditions().remove(prePostConditionName) == null) { + throw new IllegalArgumentException("PrePostCondition not found: " + prePostConditionName); + } + modelRepo.save(modelNTT); + } + + /* Helper Methods */ + ModelNTT findModelByNameOrThrow(String modelName) { + return modelRepo.findById(modelName).orElseThrow(() -> new IllegalArgumentException("Model not found: " + modelName)); + } + + private ClassNTT findClassByNameOrThrow(ModelNTT modelNTT, String className) { + return modelNTT.getClasses().stream() + .filter(c -> c.getName().equals(className)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Class not found: " + className)); + } + +} diff --git a/use-api/src/main/resources/application.properties b/use-api/src/main/resources/application.properties new file mode 100644 index 000000000..cdb952633 --- /dev/null +++ b/use-api/src/main/resources/application.properties @@ -0,0 +1,24 @@ +spring.application.name=usewebapi +spring.jpa.hibernate.ddl-auto=create-drop +spring.graphql.graphiql.enabled=true +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.main.allow-bean-definition-overriding=true +server.port=8080 + +# /api-docs endpoint custom path +springdoc.swagger-ui.path=/docs +springdoc.swagger-ui.operationsSorter=method +springdoc.swagger-ui.filter=true + +# mongoDB connection +spring.data.mongodb.authentication-database=admin +spring.data.mongodb.username=${MONGODB_USERNAME:rootuser} +spring.data.mongodb.password=${MONGODB_PASSWORD:rootpass} +spring.data.mongodb.database=${MONGODB_DATABASE:use-database} +spring.data.mongodb.port=${MONGODB_PORT:27017} +spring.data.mongodb.host=${MONGODB_HOST:localhost} + +# Actuator configuration +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=when-authorized diff --git a/use-api/src/main/resources/docker-compose.yml b/use-api/src/main/resources/docker-compose.yml new file mode 100644 index 000000000..9222b6e7c --- /dev/null +++ b/use-api/src/main/resources/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3.8" +services: + mongodb: + image: mongo + container_name: mongodb + ports: + - 27017:27017 + volumes: + - data:/data + environment: + - MONGO_INITDB_ROOT_USERNAME=rootuser + - MONGO_INITDB_ROOT_PASSWORD=rootpass + mongo-express: + image: mongo-express + container_name: mongo-express + restart: always + ports: + - 8081:8081 + environment: + - ME_CONFIG_MONGODB_ADMINUSERNAME=rootuser + - ME_CONFIG_MONGODB_ADMINPASSWORD=rootpass + - ME_CONFIG_MONGODB_SERVER=mongodb +volumes: + data: {} + +networks: + default: + name: mongodb_network \ No newline at end of file diff --git a/use-api/src/test/java/org/tzi/use/RestApiControllerTest.java b/use-api/src/test/java/org/tzi/use/RestApiControllerTest.java new file mode 100644 index 000000000..1d11e9b5b --- /dev/null +++ b/use-api/src/test/java/org/tzi/use/RestApiControllerTest.java @@ -0,0 +1,149 @@ +package org.tzi.use; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; + +@Deprecated +public class RestApiControllerTest { +// +// @BeforeAll +// public static void setup() { +// // Set the base URI for RestAssured +// RestAssured.baseURI = "http://localhost:8080/api"; // Adjust port if necessary +// } +// +// @Test +// public void testCreateMClass() { +// // Create a sample ClassDTO JSON payload +// String requestBody = "{\n" + +// " \"name_mclass\": \"Class1\",\n" + +// " \"attributes\": [\n" + +// " {\n" + +// " \"name_attr\": \"Attribute1\",\n" + +// " \"type\": \"String\"\n" + +// " }\n" + +// " ],\n" + +// " \"operations\": [\n" + +// " {\n" + +// " \"head\": \"Operation1\",\n" + +// " \"body\": \"Operation details\"\n" + +// " }\n" + +// " ]\n" + +// "}"; +// +// // Send POST request and validate response +// given() +// .contentType("application/json") +// .body(requestBody) +// .when() +// .post("/mclass") +// .then() +// .statusCode(201) // Expect HTTP 201 Created +// .body("name_mclass", equalTo("Class1")) +// .body("attributes.size()", equalTo(1)) +// .body("attributes[0].name_attr", equalTo("Attribute1")) +// .body("operations.size()", equalTo(1)) +// .body("operations[0].head", equalTo("Operation1")); +// } +// +// @Test +// public void testCreateDuplicateMClass() { +// // Create first ClassDTO with name "SameClass" +// String requestBody = "{\n" + +// " \"name_mclass\": \"SameClass\"\n" + +// "}"; +// +// // Create the first class +// given() +// .contentType("application/json") +// .body(requestBody) +// .when() +// .post("/mclass") +// .then() +// .statusCode(201); // Expect HTTP 201 Created +// +// // Attempt to create another class with the same name +// given() +// .contentType("application/json") +// .body(requestBody) +// .when() +// .post("/mclass") +// .then() +// .statusCode(400); // Expect HTTP 400 Bad Request or appropriate error code +// } +// +// @Test +// public void testCreateMClassWithoutName() { +// // Create ClassDTO with no name but with attributes and operations +// String requestBody = "{\n" + +// " \"attributes\": [\n" + +// " {\n" + +// " \"name_attr\": \"Attribute1\",\n" + +// " \"type\": \"String\"\n" + +// " }\n" + +// " ],\n" + +// " \"operations\": [\n" + +// " {\n" + +// " \"head\": \"Operation1\",\n" + +// " \"body\": \"Operation details\"\n" + +// " }\n" + +// " ]\n" + +// "}"; +// +// given() +// .contentType("application/json") +// .body(requestBody) +// .when() +// .post("/mclass") +// .then() +// .statusCode(400); // Expect HTTP 400 Bad Request due to missing name_mclass +// } +// +// @Test +// public void testCreateMClassWithEmptyPayload() { +// // Create ClassDTO with empty payload +// String requestBody = "{}"; +// +// given() +// .contentType("application/json") +// .body(requestBody) +// .when() +// .post("/mclass") +// .then() +// .statusCode(400); // Expect HTTP 400 Bad Request due to empty payload +// } +// +// @Test +// public void testCreateMClassWithInvalidJson() { +// // Create ClassDTO with invalid JSON structure +// String requestBody = "{\n" + +// " \"name_mclass\": \"Class1\",\n" + +// " \"attributes\": [\n" + +// " {\n" + +// " \"name_attr\": \"Attribute1\",\n" + +// " \"type\": \"String\"\n" + +// " },\n" + +// " ],\n" + // Extra comma here to create invalid JSON +// " \"operations\": [\n" + +// " {\n" + +// " \"head\": \"Operation1\",\n" + +// " \"body\": \"Operation details\"\n" + +// " }\n" + +// " ]\n" + +// "}"; +// +// given() +// .contentType("application/json") +// .body(requestBody) +// .when() +// .post("/mclass") +// .then() +// .statusCode(400); // Expect HTTP 400 Bad Request due to invalid JSON +// } + +} diff --git a/use-core/src/test/java/org/tzi/use/util/AbstractBagTest.java b/use-core/src/test/java/org/tzi/use/util/AbstractBagTest.java new file mode 100644 index 000000000..08edc1345 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/AbstractBagTest.java @@ -0,0 +1,307 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +import junit.framework.TestCase; + +import org.tzi.use.api.UseApiException; +import org.tzi.use.api.UseModelApi; +import org.tzi.use.api.UseSystemApi; +import org.tzi.use.uml.mm.MClass; +import org.tzi.use.uml.ocl.type.TypeFactory; +import org.tzi.use.uml.ocl.value.BagValue; +import org.tzi.use.uml.ocl.value.IntegerValue; +import org.tzi.use.uml.ocl.value.ObjectValue; +import org.tzi.use.uml.ocl.value.Value; +import org.tzi.use.uml.sys.MSystem; + + +/** + * Test comparing Bags with each other. + * + * @author Fabian Gutsche + */ +public class AbstractBagTest extends TestCase { + + private MSystem system; + private MClass a; + private MClass b; + private MClass c; + + /** + * Tests if the equals method returns false if the bags are not the + * same size. (values are primitiv) + */ + public void testBagWithoutSameSize() { + Value[] valuesForBag1 = { IntegerValue.valueOf(1) }; + + Value[] valuesForBag2 = { IntegerValue.valueOf(0), + IntegerValue.valueOf(1) }; + + BagValue bagValue1 = new BagValue( TypeFactory.mkInteger(), + valuesForBag1 ); + BagValue bagValue2 = new BagValue( TypeFactory.mkInteger(), + valuesForBag2 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + + Value[] valuesForBag3 = { IntegerValue.valueOf(0), + IntegerValue.valueOf(1) }; + Value[] valuesForBag4 = { IntegerValue.valueOf(1) }; + + bagValue1 = new BagValue( TypeFactory.mkInteger(), + valuesForBag3 ); + bagValue2 = new BagValue( TypeFactory.mkInteger(), + valuesForBag4 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + } + + /** + * Tests if the equals method returns false if the values of the + * two bags are not the same. (values are primitiv) + */ + public void testSameBagSize() { + Value[] valuesForBag1 = { IntegerValue.valueOf(1), + IntegerValue.valueOf(1) }; + + Value[] valuesForBag2 = { IntegerValue.valueOf(0), + IntegerValue.valueOf(1) }; + + BagValue bagValue1 = new BagValue( TypeFactory.mkInteger(), + valuesForBag1 ); + BagValue bagValue2 = new BagValue( TypeFactory.mkInteger(), + valuesForBag2 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + + Value[] valuesForBag3 = { IntegerValue.valueOf(0), + IntegerValue.valueOf(1) }; + Value[] valuesForBag4 = { IntegerValue.valueOf(1), + IntegerValue.valueOf(1) }; + + bagValue1 = new BagValue( TypeFactory.mkInteger(), + valuesForBag3 ); + bagValue2 = new BagValue( TypeFactory.mkInteger(), + valuesForBag4 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + } + + + /** + * Tests if the equals method returns true if the values of the + * two bags are the same. (values are primitiv) + */ + public void testSameBagSizeWithSameValues() { + Value[] valuesForBag1 = { IntegerValue.valueOf(1), + IntegerValue.valueOf(1) }; + + Value[] valuesForBag2 = { IntegerValue.valueOf(1), + IntegerValue.valueOf(1) }; + + BagValue bagValue1 = new BagValue( TypeFactory.mkInteger(), + valuesForBag1 ); + BagValue bagValue2 = new BagValue( TypeFactory.mkInteger(), + valuesForBag2 ); + + assertTrue( bagValue1.equals( bagValue2 ) ); + + Value[] valuesForBag3 = { IntegerValue.valueOf(3), + IntegerValue.valueOf(0), + IntegerValue.valueOf(1) }; + Value[] valuesForBag4 = { IntegerValue.valueOf(0), + IntegerValue.valueOf(1), + IntegerValue.valueOf(3) }; + + bagValue1 = new BagValue( TypeFactory.mkInteger(), + valuesForBag3 ); + bagValue2 = new BagValue( TypeFactory.mkInteger(), + valuesForBag4 ); + + assertTrue( bagValue1.equals( bagValue2 ) ); + } + + + + + + + + /** + * Tests if the equals method returns false if the bags are not the + * same size. (values are objects) + */ + public void testBagWithoutSameSizeWithObjects() { + createModel(); + ObjectValue o1 = new ObjectValue( a, + system.state().objectByName("a1") ); + Value[] valuesForBag1 = { o1 }; + + ObjectValue ov1 = new ObjectValue( a, + system.state().objectByName("a1") ); + ObjectValue ov2 = new ObjectValue( b, + system.state().objectByName("b1") ); + Value[] valuesForBag2 = { ov1, ov2 }; + + BagValue bagValue1 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag1 ); + BagValue bagValue2 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag2 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + + o1 = new ObjectValue( a, system.state().objectByName("a1") ); + + ObjectValue o2 = new ObjectValue( a, system.state().objectByName("b1") ); + Value[] valuesForBag3 = { o1, o2 }; + + ov1 = new ObjectValue( a, system.state().objectByName("a1") ); + Value[] valuesForBag4 = { ov1 }; + + bagValue1 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag3 ); + bagValue2 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag4 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + } + + + /** + * Tests if the equals method returns false if the values of the + * two bags are not the same. (values are objects) + */ + public void testSameBagSizeObjects() { + createModel(); + ObjectValue o1 = new ObjectValue( a, + system.state().objectByName("a1") ); + ObjectValue o2 = new ObjectValue( a, + system.state().objectByName("a1") ); + Value[] valuesForBag1 = { o1, o2 }; + + ObjectValue ov1 = new ObjectValue( a, + system.state().objectByName("a1") ); + ObjectValue ov2 = new ObjectValue( b, + system.state().objectByName("b1") ); + Value[] valuesForBag2 = { ov1, ov2 }; + + BagValue bagValue1 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag1 ); + BagValue bagValue2 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag2 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + + o1 = new ObjectValue( a, + system.state().objectByName("a1") ); + o2 = new ObjectValue( b, + system.state().objectByName("b1") ); + Value[] valuesForBag3 = { o1, o2 }; + + ov1 = new ObjectValue( a, + system.state().objectByName("a1") ); + ov2 = new ObjectValue( a, + system.state().objectByName("a1") ); + Value[] valuesForBag4 = { ov1, ov2 }; + + bagValue1 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag3 ); + bagValue2 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag4 ); + + assertFalse( bagValue1.equals( bagValue2 ) ); + } + + + /** + * Tests if the equals method returns true if the values of the + * two bags are the same. (values are objects) + */ + public void testSameBagSizeWithObjectsValuesAreTheSame() { + createModel(); + ObjectValue o1 = new ObjectValue( a, + system.state().objectByName("a1") ); + ObjectValue o2 = new ObjectValue( a, + system.state().objectByName("a1") ); + Value[] valuesForBag1 = { o1, o2 }; + + ObjectValue ov1 = new ObjectValue( a, + system.state().objectByName("a1") ); + ObjectValue ov2 = new ObjectValue( b, + system.state().objectByName("a1") ); + Value[] valuesForBag2 = { ov1, ov2 }; + + BagValue bagValue1 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag1 ); + BagValue bagValue2 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag2 ); + + assertTrue( bagValue1.equals( bagValue2 ) ); + + o1 = new ObjectValue( a, + system.state().objectByName("a1") ); + o2 = new ObjectValue( b, + system.state().objectByName("b1") ); + ObjectValue o3 = new ObjectValue( c, + system.state().objectByName("c1") ); + Value[] valuesForBag3 = { o1, o2, o3 }; + + ov1 = new ObjectValue( c, + system.state().objectByName("c1") ); + ov2 = new ObjectValue( a, + system.state().objectByName("a1") ); + ObjectValue ov3 = new ObjectValue( b, + system.state().objectByName("b1") ); + Value[] valuesForBag4 = { ov1, ov2, ov3 }; + + bagValue1 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag3 ); + bagValue2 = new BagValue( TypeFactory.mkOclAny(), + valuesForBag4 ); + + assertTrue( bagValue1.equals( bagValue2 ) ); + } + + + + /** + * Creates the model and system every test is working with. + */ + private void createModel() { + UseModelApi mApi = new UseModelApi("Test"); + + try { + a = mApi.createClass("A", false); + b = mApi.createClass("B", false); + c = mApi.createClass("C", false); + + UseSystemApi sApi = UseSystemApi.create(mApi.getModel(), false); + + sApi.createObjectsEx(a, "a1"); + sApi.createObjectsEx(b, "b1"); + sApi.createObjectsEx(c, "c1"); + + system = sApi.getSystem(); + } catch ( UseApiException ex ) { + fail( ex.getMessage() ); + } + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/AllTests.java b/use-core/src/test/java/org/tzi/use/util/AllTests.java new file mode 100644 index 000000000..a32dc28e5 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/AllTests.java @@ -0,0 +1,44 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Runs all test in package org.tzi.use.util. + * + * @author Hanna Bauerdick + * @author Fabian Gutsche + */ +public class AllTests { + + private AllTests(){} + + public static Test suite() { + final TestSuite test = new TestSuite( "All util tests" ); + test.addTestSuite( org.tzi.use.util.AbstractBagTest.class ); + test.addTestSuite( org.tzi.use.util.ReportTest.class ); + test.addTestSuite( org.tzi.use.util.StringUtilTest.class ); + test.addTestSuite( org.tzi.use.util.CombinationTest.class ); + test.addTest(org.tzi.use.util.soil.AllTests.suite()); + return test; + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/CombinationTest.java b/use-core/src/test/java/org/tzi/use/util/CombinationTest.java new file mode 100644 index 000000000..598176a75 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/CombinationTest.java @@ -0,0 +1,74 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +// $Id: AbstractBagTest.java 2409 2011-07-27 09:45:00Z lhamann $ + +package org.tzi.use.util; + +import java.util.ArrayList; +import java.util.List; + +import org.tzi.use.util.collections.CollectionUtil; +import org.tzi.use.util.collections.MinCombinationsIterator; +import org.tzi.use.util.collections.CollectionUtil.UniqueList; + +import junit.framework.TestCase; + + +/** + * Test comparing Bags with each other. + * + * @author Fabian Gutsche + */ +public class CombinationTest extends TestCase { + + private List getList(int offSet, int numElements) { + List result = new ArrayList(numElements); + for (int index = 1; index <= numElements; ++index) { + result.add(new String(new char[]{(char)(index + offSet + 64)})); + } + return result; + } + + public void testCombination() { + List l1 = getList(0, 3); + List l2 = getList(3, 3); + + List>> result = CollectionUtil.combinationsOne(l1, l2, UniqueList.SECOND_IS_UNIQUE); + assertEquals(64, result.size()); + } + + + public void testCombinationIterator() { + List l1 = getList(0,3); + List l2 = getList(3,3); + + List>> result = CollectionUtil.combinationsOne(l1, l2, UniqueList.SECOND_IS_UNIQUE); + MinCombinationsIterator iter = new MinCombinationsIterator(l1, l2, UniqueList.SECOND_IS_UNIQUE); + + int num = 0; + while(iter.hasNext()) { + List> elem = iter.next(); + assertTrue(result.contains(elem)); + ++num; + } + + assertEquals(result.size(), num); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/ReportTest.java b/use-core/src/test/java/org/tzi/use/util/ReportTest.java new file mode 100644 index 000000000..a9ce98103 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/ReportTest.java @@ -0,0 +1,70 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; +import java.io.PrintWriter; +import java.io.StringWriter; + +import junit.framework.TestCase; + +/** + * Test Report class. + * + * @author Mark Richters + */ + +public class ReportTest extends TestCase { + + public void test1() { + Report r = new Report(4, "[ $c = $r, $r, $l ]"); + r.addRuler('-'); + r.addRow(); + r.addCell("foo"); + r.addCell(Integer.valueOf(3)); + r.addCell(Double.valueOf(1.2)); + r.addCell(Boolean.FALSE); + + r.addRow(); + r.addCell("foobar"); + r.addCell(Integer.valueOf(453453)); + r.addCell(Double.valueOf(-1.245345345)); + r.addCell(Boolean.TRUE); + + r.addRow(); + r.addCell("line"); + r.addCell(Integer.valueOf(555)); + r.addCell(Double.valueOf(999.0)); + r.addCell(Boolean.TRUE); + r.addRuler('='); + + StringWriter sw1 = new StringWriter(); + PrintWriter p1 = new PrintWriter(sw1); + r.printOn(p1); + p1.flush(); + StringWriter sw2 = new StringWriter(); + PrintWriter p2 = new PrintWriter(sw2); + p2.println("----------------------------------------"); + p2.println("[ foo = 3, 1.2, false ]"); + p2.println("[ foobar = 453453, -1.245345345, true ]"); + p2.println("[ line = 555, 999.0, true ]"); + p2.println("========================================"); + p2.flush(); + assertEquals(sw1.toString(), sw2.toString()); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/StringUtilTest.java b/use-core/src/test/java/org/tzi/use/util/StringUtilTest.java new file mode 100644 index 000000000..0e3c65a8e --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/StringUtilTest.java @@ -0,0 +1,62 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; +import junit.framework.TestCase; + +/** + * Test StringUtil class. + * + * @author Mark Richters + */ + +public class StringUtilTest extends TestCase { + + public StringUtilTest(String name) { + super(name); + } + + public void testNthIndexOf() { + assertEquals(-1, StringUtil.nthIndexOf("abbbb", 0, "bb")); + assertEquals(1, StringUtil.nthIndexOf("abbbb", 1, "bb")); + assertEquals(3, StringUtil.nthIndexOf("abbbb", 2, "bb")); + assertEquals(-1, StringUtil.nthIndexOf("abbbb", 3, "bb")); + assertEquals(7, StringUtil.nthIndexOf("abbbbaabb", 3, "bb")); + assertEquals(-1, StringUtil.nthIndexOf("abbbb", 0, 'b')); + assertEquals(1, StringUtil.nthIndexOf("abbbb", 1, 'b')); + assertEquals(3, StringUtil.nthIndexOf("abbbb", 3, 'b')); + } + + public void testPad() { + assertEquals("a", StringUtil.pad("a", 1)); + assertEquals("a ", StringUtil.pad("a", 2)); + } + + public void testCenter() { + assertEquals(" a ", StringUtil.center("a", 3)); + assertEquals(" a ", StringUtil.center("a", 3)); + } + + public void testEscapeChar() { + assertEquals("a", StringUtil.escapeChar('a', '"')); + assertEquals("\\344", StringUtil.escapeChar('\344', '"')); + assertEquals("\\u1234", StringUtil.escapeChar('\u1234', '"')); + assertEquals("\\t", StringUtil.escapeChar('\t', '"')); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/soil/AllTests.java b/use-core/src/test/java/org/tzi/use/util/soil/AllTests.java new file mode 100644 index 000000000..695500e62 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/soil/AllTests.java @@ -0,0 +1,55 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.soil; + + +import junit.framework.Test; +import junit.framework.TestSuite; + + +/** + * Collection of all soil utility tests + * + * @author Daniel Gent + */ +public class AllTests { + + /** + * no instances required + */ + private AllTests() { + + } + + + /** + * builds the suite of all soil utility tests + * + * @return the suite of all soil utility tests + */ + public static Test suite() { + final TestSuite testSuite = new TestSuite("All soil util tests"); + testSuite.addTestSuite(VariableEnvironmentTest.class); + testSuite.addTestSuite(StateChangesTest.class); + testSuite.addTestSuite(VariableSetTest.class); + testSuite.addTestSuite(SymbolTableTest.class); + return testSuite; + } +} \ No newline at end of file diff --git a/use-core/src/test/java/org/tzi/use/util/soil/StateChangesTest.java b/use-core/src/test/java/org/tzi/use/util/soil/StateChangesTest.java new file mode 100644 index 000000000..7dedab553 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/soil/StateChangesTest.java @@ -0,0 +1,307 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.soil; + + +import java.util.Arrays; + +import junit.framework.TestCase; + +import org.junit.Before; +import org.junit.Test; +import org.tzi.use.uml.mm.MAssociation; +import org.tzi.use.uml.mm.MAssociationClass; +import org.tzi.use.uml.mm.MClass; +import org.tzi.use.uml.mm.MModel; +import org.tzi.use.uml.mm.TestModelUtil; +import org.tzi.use.uml.sys.MLink; +import org.tzi.use.uml.sys.MLinkObject; +import org.tzi.use.uml.sys.MObject; +import org.tzi.use.uml.sys.MSystem; +import org.tzi.use.uml.sys.MSystemException; +import org.tzi.use.uml.sys.MSystemState; + + +/** + * Test cases for various methods of {@code StateChanges} + * + * @author Daniel Gent + * @see StateDifference + */ +public class StateChangesTest extends TestCase { + /** the test subject */ + private StateDifference fSC; + /** an arbitrary object */ + private MObject fObject; + /** an arbitrary link */ + private MLink fLink; + /** an arbitrary link object */ + private MLinkObject fLinkObject; + + + /** + * builds the fixture consisting of the {@code StateChanges} object + * + the objects to insert + */ + @Override + @Before + public void setUp() { + + fSC = new StateDifference(); + MModel model = TestModelUtil.getInstance().createComplexModel(); + MClass person = model.getClass("Person"); + MClass company = model.getClass("Company"); + MAssociation isBoss = model.getAssociation("isBoss"); + MAssociationClass job = model.getAssociationClass("Job"); + + MSystem system = new MSystem(model); + MSystemState state = system.state(); + try { + fObject = state.createObject(person, "P"); + MObject c = state.createObject(company, "C"); + fLink = state.createLink(isBoss, Arrays.asList(fObject, fObject), null); + fLinkObject = state.createLinkObject(job, "J", Arrays.asList(fObject, c), null); + } catch (MSystemException e) { + fail(e.getMessage()); + } + } + + + /** + * tests clear, isEmpty + *

+ * - initially the object is empty
+ * - invoking any {@code add} method on an empty {@code StateChanges} + * results in it being not empty anymore
+ * - invoking {@code clear} results in the {@code StateChanges} being + * empty
+ */ + @Test + public void testClearIsEmpty() { + assertTrue(fSC.isEmpty()); + fSC.addNewObject(fObject); + assertFalse(fSC.isEmpty()); + fSC.clear(); + assertTrue(fSC.isEmpty()); + fSC.addModifiedObject(fObject); + assertFalse(fSC.isEmpty()); + fSC.clear(); + assertTrue(fSC.isEmpty()); + fSC.addDeletedObject(fObject); + assertFalse(fSC.isEmpty()); + fSC.clear(); + assertTrue(fSC.isEmpty()); + fSC.addNewLink(fLink); + assertFalse(fSC.isEmpty()); + fSC.clear(); + assertTrue(fSC.isEmpty()); + fSC.addDeletedLink(fLink); + assertFalse(fSC.isEmpty()); + fSC.clear(); + assertTrue(fSC.isEmpty()); + } + + + /** + * tests addNewObject, addModifiedObject, addDeletedObject with a + * non-link-object object + *

+ * - after invoking any one method, the supplied object is either + * in exactly one or in none of the new-, modified- or deleted sets
+ * - two consecutive invocations of different {@code add...Object} methods + * with the same object lead to certain outcomes
+ */ + @Test + public void testAddObject() { + // adding objects to an empty StateChanges object + fSC.addNewObject(fObject); + assertTrue(isOnlyNew(fObject)); + fSC.clear(); + fSC.addModifiedObject(fObject); + assertTrue(isOnlyModified(fObject)); + fSC.clear(); + fSC.addDeletedObject(fObject); + assertTrue(isOnlyDeleted(fObject)); + fSC.clear(); + + // invocations of the add...Object methods in the context of + // previous add...Object calls + + // new, modified = new + fSC.addNewObject(fObject); + fSC.addModifiedObject(fObject); + assertTrue(isOnlyNew(fObject)); + fSC.clear(); + + // new, deleted = empty + fSC.addNewObject(fObject); + fSC.addDeletedObject(fObject); + assertTrue(fSC.isEmpty()); + fSC.clear(); + + // modified, new = new + // hypothetical case, shouldn't be possible to achieve + fSC.addModifiedObject(fObject); + fSC.addNewObject(fObject); + assertTrue(isOnlyNew(fObject)); + fSC.clear(); + + // modified, deleted = deleted + fSC.addModifiedObject(fObject); + fSC.addDeletedObject(fObject); + assertTrue(isOnlyDeleted(fObject)); + fSC.clear(); + + // deleted, new = modified + fSC.addDeletedObject(fObject); + fSC.addNewObject(fObject); + assertTrue(isOnlyModified(fObject)); + fSC.clear(); + + // deleted, modified = modified + // hypothetical case, shouldn't be possible to achieve + fSC.addDeletedObject(fObject); + fSC.addModifiedObject(fObject); + assertTrue(isOnlyModified(fObject)); + fSC.clear(); + } + + + /** + * tests addNewLink, addDeletedLink with a non-link-object link + *

+ * - after invoking one of the methods, the link is either in the set of + * new links or deleted links or in neither of those sets, but not in + * both
+ * - adding a new link which was previously deleted leads to that link + * being in neither the set of new links nor the set of deleted links
+ * - the same holds true for adding a deleted link which was previously + * new
+ */ + @Test + public void testAddLink() { + fSC.addNewLink(fLink); + assertTrue(fSC.getNewLinks().contains(fLink)); + fSC.clear(); + fSC.addDeletedLink(fLink); + assertTrue(fSC.getDeletedLinks().contains(fLink)); + fSC.clear(); + + // new, deleted = empty + fSC.addNewLink(fLink); + fSC.addDeletedLink(fLink); + assertFalse(fSC.getNewLinks().contains(fLink)); + assertFalse(fSC.getDeletedLinks().contains(fLink)); + fSC.clear(); + + // deleted, new = empty + fSC.addDeletedLink(fLink); + fSC.addNewLink(fLink); + assertFalse(fSC.getNewLinks().contains(fLink)); + assertFalse(fSC.getDeletedLinks().contains(fLink)); + fSC.clear(); + } + + + /** + * tests addNew(Link)(Object), addDeleted(Link)(Object) + *

+ * - invoking any of the {@code addNew...} or {@code addDeleted...} methods + * with a link object results in the link object being treated as an + * object and a link, i.E. it doesn't matter which version is used
+ */ + @Test + public void testAddLinkObject() { + fSC.addNewLinkObject(fLinkObject); + assertTrue(fSC.getNewObjects().contains(fLinkObject)); + assertTrue(fSC.getNewLinks().contains(fLinkObject)); + fSC.clear(); + + fSC.addDeletedLinkObject(fLinkObject); + assertTrue(fSC.getDeletedObjects().contains(fLinkObject)); + assertTrue(fSC.getDeletedLinks().contains(fLinkObject)); + fSC.clear(); + + fSC.addNewObject(fLinkObject); + assertTrue(fSC.getNewObjects().contains(fLinkObject)); + assertTrue(fSC.getNewLinks().contains(fLinkObject)); + fSC.clear(); + + fSC.addDeletedObject(fLinkObject); + assertTrue(fSC.getDeletedObjects().contains(fLinkObject)); + assertTrue(fSC.getDeletedLinks().contains(fLinkObject)); + fSC.clear(); + + fSC.addNewLink(fLinkObject); + assertTrue(fSC.getNewObjects().contains(fLinkObject)); + assertTrue(fSC.getNewLinks().contains(fLinkObject)); + fSC.clear(); + + fSC.addDeletedLink(fLinkObject); + assertTrue(fSC.getDeletedObjects().contains(fLinkObject)); + assertTrue(fSC.getDeletedLinks().contains(fLinkObject)); + fSC.clear(); + } + + + /** + * returns true if object is a member of the newObjects set, and not member + * of the other sets + * + * @param object the object to test + * @return true if {@code object} is only in the set of new objects + */ + private boolean isOnlyNew(MObject object) { + return + fSC.getNewObjects().contains(object) + && !fSC.getModifiedObjects().contains(object) + && !fSC.getDeletedObjects().contains(object); + } + + + /** + * returns true if object is a member of the modifiedObjects set, and not + * member of the other sets + * + * @param object the object to test + * @return true if {@code object} is only in the set of modified objects + */ + private boolean isOnlyModified(MObject object) { + return + !fSC.getNewObjects().contains(object) + && fSC.getModifiedObjects().contains(object) + && !fSC.getDeletedObjects().contains(object); + } + + + /** + * returns true if object is a member of the deletedObjects set, and not + * member of the other sets + * + * @param object the object to test + * @return true if {@code object} is only in the set of deleted objects + */ + private boolean isOnlyDeleted(MObject object) { + return + !fSC.getNewObjects().contains(object) + && !fSC.getModifiedObjects().contains(object) + && fSC.getDeletedObjects().contains(object); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/soil/SymbolTableTest.java b/use-core/src/test/java/org/tzi/use/util/soil/SymbolTableTest.java new file mode 100644 index 000000000..6127a2d98 --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/soil/SymbolTableTest.java @@ -0,0 +1,130 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.soil; + +import junit.framework.TestCase; + +import org.junit.Before; +import org.junit.Test; +import org.tzi.use.parser.soil.ast.ASTEmptyStatement; +import org.tzi.use.parser.soil.ast.ASTStatement; +import org.tzi.use.uml.ocl.type.Type; +import org.tzi.use.uml.ocl.type.TypeFactory; + + +/** + * Test cases for various methods of {@code SymbolTable}s + * + * @author Daniel Gent + * @see SymbolTable + */ +public class SymbolTableTest extends TestCase { + /** the test subject */ + private SymbolTable fST; + /** arbitrary name */ + private String fVariableName; + /** the ocl Real type */ + private Type fRealType; + /** the ocl Integer type */ + private Type fIntegerType; + /** the ocl String type */ + private Type fStringType; + /** arbitrary statement */ + private ASTStatement fStatement; + + + + /** + * constructs the fixture + */ + @Override + @Before + public void setUp() { + fST = new SymbolTable(); + fVariableName = "name"; + fRealType = TypeFactory.mkReal(); + fIntegerType = TypeFactory.mkInteger(); + fStringType = TypeFactory.mkString(); + fStatement = new ASTEmptyStatement(); + + assertTrue(fIntegerType.conformsTo(fRealType)); + } + + + /** + * tests type setting and getting + * + * @see SymbolTable#getType(String) + * @see SymbolTable#setType(String, Type) + */ + @Test + public void testGetSet() { + assertNull(fST.getType(fVariableName)); + fST.setType(fVariableName, fIntegerType); + assertEquals(fST.getType(fVariableName), fIntegerType); + fST.setType(fVariableName, fRealType); + assertEquals(fST.getType(fVariableName), fRealType); + } + + + /** + * tests state storing and restoring + * + * @see SymbolTable#storeState() + * @see SymbolTable#restoreState(ASTStatement) + */ + @Test + public void testStoreRestore() { + // check if the stack works correctly + fST.setType(fVariableName, fIntegerType); + assertEquals(fST.getType(fVariableName), fIntegerType); + fST.storeState(); + fST.setType(fVariableName, fRealType); + assertEquals(fST.getType(fVariableName), fRealType); + fST.storeState(); + fST.setType(fVariableName, fStringType); + assertEquals(fST.getType(fVariableName), fStringType); + fST.restoreState(fStatement); + assertEquals(fST.getType(fVariableName), fRealType); + fST.restoreState(fStatement); + assertEquals(fST.getType(fVariableName), fIntegerType); + fST.clear(); + + // test dirty-bit + fST.setType(fVariableName, fIntegerType); + fST.storeState(); + fST.setType(fVariableName, fStringType); + fST.restoreState(fStatement); + // the type does not change + assertEquals(fST.getType(fVariableName), fIntegerType); + // but the variable is now dirty + assertTrue(fST.isDirty(fVariableName)); + assertEquals(fST.getCause(fVariableName), fStatement); + fST.clear(); + + fST.setType(fVariableName, fRealType); + fST.storeState(); + fST.setType(fVariableName, fIntegerType); + fST.restoreState(fStatement); + assertEquals(fST.getType(fVariableName), fRealType); + // not dirty, since Integer is a sub-type of Real + assertFalse(fST.isDirty(fVariableName)); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/soil/VariableEnvironmentTest.java b/use-core/src/test/java/org/tzi/use/util/soil/VariableEnvironmentTest.java new file mode 100644 index 000000000..f98cb91fd --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/soil/VariableEnvironmentTest.java @@ -0,0 +1,326 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.soil; + + +import junit.framework.TestCase; + +import org.junit.Before; +import org.junit.Test; +import org.tzi.use.TestSystem; +import org.tzi.use.uml.mm.MClass; +import org.tzi.use.uml.mm.MInvalidModelException; +import org.tzi.use.uml.mm.MModel; +import org.tzi.use.uml.mm.ModelFactory; +import org.tzi.use.uml.ocl.value.IntegerValue; +import org.tzi.use.uml.ocl.value.UndefinedValue; +import org.tzi.use.uml.ocl.value.Value; +import org.tzi.use.uml.ocl.value.VarBindings; +import org.tzi.use.uml.sys.MObject; +import org.tzi.use.uml.sys.MSystem; +import org.tzi.use.uml.sys.MSystemException; + + +/** + * Test cases for various methods of {@code VariableEnvironment}s + * + * @author Daniel Gent + * @see VariableEnvironment + */ +public class VariableEnvironmentTest extends TestCase { + private VariableEnvironment ve; + private String n1; + private String n2; + private String n3; + private Value v1; + private Value v2; + private Value v3; + private Value vUnassigned; + + + /** + * constructs fixture + */ + @Override + @Before + public void setUp() throws Exception { + TestSystem testSystem = new TestSystem(); + ve = new VariableEnvironment(testSystem.getState()); + n1 = "n1"; + n2 = "n2"; + n3 = "n3"; + v1 = IntegerValue.valueOf(1); + v2 = IntegerValue.valueOf(2); + v3 = IntegerValue.valueOf(3); + vUnassigned = null; + } + + @Test + public void testIdentity() { + assertEquals(1,1); + } + + /** + * tests clear, isEmpty + *

+ * - empty means one empty frame
+ * - calling clear results in the variable environment being empty
+ */ + @Test + public void testClearIsEmpty() { + // [ ] + assertTrue(ve.isEmpty()); + // [n1 -> v1] + ve.assign(n1, v1); + assertFalse(ve.isEmpty()); + // [ ] + ve.clear(); + assertTrue(ve.isEmpty()); + // [ ][ ] + ve.pushFrame(false); + assertFalse(ve.isEmpty()); + // [ ] + ve.popFrame(); + assertTrue(ve.isEmpty()); + } + + + /** + * tests pushFrame, popFrame + *

+ * - after pushing a new frame, new assignments happen there
+ * - mappings in a popped frame are gone
+ */ + @Test + public void testPushPopFrame() { + // [ ][ ] + ve.pushFrame(false); + // [ ][n1 -> v1] + ve.assign(n1, v1); + assertEquals(ve.lookUp(n1), v1); + // [ ] + ve.popFrame(); + assertNull(ve.lookUp(n1)); + // [n1 -> v1] + ve.assign(n1, v1); + assertEquals(ve.lookUp(n1), v1); + } + + + /** + * tests assign and lookup methods + *

+ * - assignments should only occur on the most recent level
+ * - previous assignments on that level should be updated in the most + * recent frame
+ */ + @Test + public void testAssignLookUp() { + // [n1 -> v1] + ve.assign(n1, v1); + assertEquals(ve.lookUp(n1), v1); + // [n1 -> v2] + ve.assign(n1, v2); + assertEquals(ve.lookUp(n1), v2); + // [n1 -> v2, n2 -> v2] + ve.assign(n2, v2); + assertEquals(ve.lookUp(n1), v2); + assertEquals(ve.lookUp(n2), v2); + // [n1 -> v2, n2 -> v2][n1 -> v3, n2 -> v3, n3 -> v1] + ve.pushFrame(false); + ve.assign(n1, v3); + ve.assign(n2, v3); + ve.assign(n3, v1); + assertEquals(ve.lookUp(n1), v3); + assertEquals(ve.lookUp(n2), v3); + assertEquals(ve.lookUp(n3), v1); + // [n1 -> v2, n2 -> v2] + ve.popFrame(); + assertEquals(ve.lookUp(n1), v2); + assertEquals(ve.lookUp(n2), v2); + assertEquals(ve.lookUp(n3), vUnassigned); + } + + + /** + * tests undefineReferencesTo, getTopLevelReferencesTo + *

+ * - all variables - disregarding the containing the level or frame - + * reference to the undefined value after undefineReferencesTo
+ * - getTopLevelReferencesTo returns the names of all variables in the + * first frame of the first level which refer to the specified object
+ */ + @Test + public void testObjectReferences() { + MObject object = null; + ModelFactory mf = new ModelFactory(); + MModel model = mf.createModel("m"); + MClass cls = mf.createClass("c", false); + try { + model.addClass(cls); + } catch (MInvalidModelException e) { + fail(e.getMessage()); + } + MSystem system = new MSystem(model); + try { + object = system.state().createObject(cls, "o"); + } catch (MSystemException e) { + fail(e.getMessage()); + } + + Value vO = object.value(); + Value vU = UndefinedValue.instance; + + // [n1 -> vO1] + ve.assign(n1, vO); + assertTrue(ve.getTopLevelReferencesTo(object).contains(n1)); + // [n1 -> vU] + ve.undefineReferencesTo(object); + assertEquals(ve.lookUp(n1), vU); + // [n1 -> vO1, n2 -> vO1] + ve.assign(n1, vO); + ve.assign(n2, vO); + assertTrue(ve.getTopLevelReferencesTo(object).contains(n1)); + assertTrue(ve.getTopLevelReferencesTo(object).contains(n2)); + // [n1 -> vU, n2 -> vU] + ve.undefineReferencesTo(object); + assertEquals(ve.lookUp(n1), vU); + assertEquals(ve.lookUp(n2), vU); + // [n1 -> vO1][n2 -> vO1] + // [n1 -> vO1, n2 -> vO1] + ve.assign(n1, vO); + ve.assign(n2, vO); + ve.pushFrame(false); + ve.assign(n1, vO); + ve.pushFrame(false); + ve.assign(n2, vO); + // [n1 -> vU][n2 -> vU] + // [n1 -> vU, n2 -> vU] + ve.undefineReferencesTo(object); + assertEquals(ve.lookUp(n2), vU); + // [n1 -> vU] + // [n1 -> vU, n2 -> vU] + ve.popFrame(); + assertEquals(ve.lookUp(n1), vU); + // [n1 -> vU, n2 -> vU] + ve.popFrame(); + assertEquals(ve.lookUp(n1), vU); + assertEquals(ve.lookUp(n2), vU); + } + + + /** + * tests remove + *

+ * - removes a mapping in the most recent frame
+ */ + @Test + public void testRemove() { + // [n1 -> v1] + ve.assign(n1, v1); + assertEquals(ve.lookUp(n1), v1); + // [ ] + ve.remove(n1); + assertEquals(ve.lookUp(n1), vUnassigned); + // [n1 -> v1][n1 -> v2] + ve.assign(n1, v1); + ve.pushFrame(false); + ve.assign(n1, v2); + assertEquals(ve.lookUp(n1), v2); + // [n -> v1][ ] + ve.remove(n1); + assertEquals(ve.lookUp(n1), vUnassigned); + // [n -> v1] + ve.popFrame(); + assertEquals(ve.lookUp(n1), v1); + } + + + /** + * test constructSymbolTable + *

+ * - name -> value mappings are transformed to corresponding + * name -> TypeOf(value) mappings
+ * - the symbol table in constructed only by mappings in the most recent + * frame
+ */ + @Test + public void testConstructSymbolTable() { + // VE + // [n1 -> v1, n2 -> v2, n3 -> v3] + ve.assign(n1, v1); + ve.assign(n2, v2); + ve.assign(n3, v3); + // ST + // [n1 -> Type(v1), n2 -> Type(v2), n3 -> Type(v3)] + SymbolTable st = ve.constructSymbolTable(); + assertEquals(st.getType(n1), v1.type()); + assertEquals(st.getType(n2), v2.type()); + assertEquals(st.getType(n3), v3.type()); + // VE + // [n1 -> v1, n2 -> v2, n3 -> v3][n1 -> v2, n2 -> v3, n3 -> v1] + ve.pushFrame(false); + ve.assign(n1, v2); + ve.assign(n2, v3); + ve.assign(n3, v1); + // ST + // [n1 -> Type(v2), n2 -> Type(v3), n3 -> Type(v1)] + st = ve.constructSymbolTable(); + assertEquals(st.getType(n1), v2.type()); + assertEquals(st.getType(n2), v3.type()); + assertEquals(st.getType(n3), v1.type()); + } + + + /** + * tests constructVarBindings + *

+ * - all mappings in the current frame get copied
+ */ + @Test + public void testConstructVarBindings() { + VarBindings vb; + // VE + // [n1 -> v1, n2 -> v2, n3 -> v3] + ve.assign(n1, v1); + ve.assign(n2, v2); + ve.assign(n3, v3); + // VB + // [n1 -> v1, n2 -> v2, n3 -> v3] + vb = ve.constructVarBindings(); + assertEquals(vb.getValue(n1), v1); + assertEquals(vb.getValue(n2), v2); + assertEquals(vb.getValue(n3), v3); + // VE + // [n1 -> v1, n2 -> v2, n3 -> v3][n1 -> v2, n2 -> v3, n3 -> v1] + ve.pushFrame(false); + ve.assign(n1, v2); + ve.assign(n2, v3); + ve.assign(n3, v1); + // VB + // [n1 -> v2, n2 -> v3, n3 -> v1] + vb = ve.constructVarBindings(); + assertEquals(vb.getValue(n1), v2); + assertEquals(vb.getValue(n2), v3); + assertEquals(vb.getValue(n3), v1); + } + + // TODO lookup, constructVBetc with visible objects +} diff --git a/use-core/src/test/java/org/tzi/use/util/soil/VariableSetTest.java b/use-core/src/test/java/org/tzi/use/util/soil/VariableSetTest.java new file mode 100644 index 000000000..2bafb505f --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/soil/VariableSetTest.java @@ -0,0 +1,243 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.soil; + +import java.util.Random; + +import junit.framework.TestCase; + +import org.junit.Before; +import org.junit.Test; +import org.tzi.use.uml.ocl.type.Type; +import org.tzi.use.uml.ocl.type.TypeFactory; + + +/** + * Test cases for the special set operations of {@code VariableSet}s + * + * @author Daniel Gent + * @see VariableSet + */ +public class VariableSetTest extends TestCase { + /** {@code VariableSet} A */ + private VariableSet fA; + /** {@code VariableSet} B */ + private VariableSet fB; + + + /** + * constructs the fixtures + */ + @Override + @Before + public void setUp() { + + // fill A and B with some random data + Type[] types = { + TypeFactory.mkInteger(), + TypeFactory.mkReal(), + TypeFactory.mkString(), + TypeFactory.mkBoolean(), + TypeFactory.mkOclAny(), + }; + + String[] names = { + "v00", "v01", "v02", "v03", "v04", "v05", "v06", "v07", "v08", + "v09", "v10", "v11", "v12", "v13", "v14", "v15", "v16", "v17", + "v18", "v19" + }; + + int numElems = 10; + Random random = new Random(); + + fA = new VariableSet(); + for (int i = 0; i < numElems; ++i) { + fA.add( + // the last 5 names are exclusively in B + names[random.nextInt(names.length - 5)], + types[random.nextInt(types.length)]); + } + + fB = new VariableSet(); + for (int i = 0; i < numElems; ++i) { + fB.add( + // the first 5 names are exclusively in A + names[5 + random.nextInt(names.length - 5)], + types[random.nextInt(types.length)]); + } + + // make sure there is an element in one set, which is not in the other + fA.add("aExclusive", types[random.nextInt(types.length)]); + fB.add("bExclusive", types[random.nextInt(types.length)]); + + // make sure A and B have at least one element in common + Type commonType = types[random.nextInt(types.length)]; + fA.add("common", commonType); + fB.add("common", commonType); + + // something to do for polydiff2... + Type integerType = TypeFactory.mkInteger(); + Type realType = TypeFactory.mkReal(); + // just to make sure... + assertTrue(integerType.conformsTo(realType)); + + fA.add("aInt_bReal", integerType); + fB.add("aInt_bReal", realType); + + fA.add("aReal_bInt", realType); + fB.add("aReal_bInt", integerType); + } + + + /** + * tests {@link VariableSet#add(VariableSet) Union} respectively + * {@link VariableSet#add(VariableSet) add} + */ + @Test + public void testUnion() { + VariableSet result = VariableSet.union(fA, fB); + + // everything in A must be in the result + assertTrue(result.containsAll(fA)); + + // everything in B must be in the result + assertTrue(result.containsAll(fB)); + + // everything in the result must be in either A or B + for (String name : result.getNames()) { + for (Type type : result.getTypes(name)) { + assertTrue(fA.contains(name, type) || fB.contains(name, type)); + } + } + } + + + /** + * tests {@link VariableSet#difference(VariableSet, VariableSet) Difference} + * respectively {@link VariableSet#add(VariableSet) remove} + */ + @Test + public void testDifference() { + VariableSet diffAB = VariableSet.difference(fA, fB); + + // everything in the difference must be in A + assertTrue(fA.containsAll(diffAB)); + + // nothing in B might be in the difference + for (String name : fB.getNames()) { + for (Type type : fB.getTypes(name)) { + assertFalse(diffAB.contains(name, type)); + } + } + + VariableSet diffBA = VariableSet.difference(fB, fA); + + // everything in the difference must be in B + assertTrue(fB.containsAll(diffBA)); + + // nothing in A might be in the difference + for (String name : fA.getNames()) { + for (Type type : fA.getTypes(name)) { + assertFalse(diffBA.contains(name, type)); + } + } + } + + + /** + * tests + * {@link VariableSet#polymorphicDifference1(VariableSet, VariableSet) + * PolymorphicDifference1} + * respectively + * {@link VariableSet#removePolymorphic1(VariableSet) removePolymorphic1} + */ + @Test + public void testPolymorphicDifference1() { + VariableSet pDiff1 = VariableSet.polymorphicDifference1(fA, fB); + + // everything in pDiff1 must be in A + assertTrue(fA.containsAll(pDiff1)); + + // the normal difference is a subset of the first polymorphic difference + // (it removes everything a normal difference would + possibly some more) + // we want to take a look everything that gets only removed by the + // first polymorphic difference + + VariableSet diff = VariableSet.difference(fA, fB); + VariableSet pDiffExcl = VariableSet.difference(diff, pDiff1); + + // for each element in pDiffExcl the following must hold: + // - element of vA + // - not element of vB + // - vB has a variable with that name + for (String name : pDiffExcl.getNames()) { + for (Type type : pDiffExcl.getTypes(name)) { + assertTrue(fA.contains(name, type)); + assertFalse(fB.contains(name, type)); + assertTrue(fB.contains(name)); + } + } + } + + + /** + * tests + * {@link VariableSet#polymorphicDifference2(VariableSet, VariableSet) + * PolymorphicDifference2} + * respectively + * {@link VariableSet#removePolymorphic2(VariableSet) removePolymorphic2} + */ + @Test + public void testPolymorphicDifference2() { + VariableSet pDiff2 = VariableSet.polymorphicDifference2(fA, fB); + + // everything in pDiff2 must be in A + assertTrue(fA.containsAll(pDiff2)); + + // the normal difference is a subset of the second polymorphic difference + // (it removes everything a normal difference would + possibly some more) + // we want to take a look everything that gets only removed by the + // second polymorphic difference + + VariableSet diff = VariableSet.difference(fA, fB); + VariableSet pDiffExcl = VariableSet.difference(diff, pDiff2); + + // for each element in pDiffExcl the following must hold: + // - element of A + // - not element of B + // - B has a variable with that name + // - one of its types must be a subtype of the current elements type + for (String name : pDiffExcl.getNames()) { + for (Type type : pDiffExcl.getTypes(name)) { + assertTrue(fA.contains(name, type)); + assertFalse(fB.contains(name, type)); + assertTrue(fB.contains(name)); + boolean containsSubType = false; + for (Type otherType : fB.getTypes(name)) { + if (otherType.conformsTo(type)) { + containsSubType = true; + break; + } + } + assertTrue(containsSubType); + } + } + } +} diff --git a/use-gui/src/it/java/org/tzi/use/main/ShellIT.java b/use-gui/src/it/java/org/tzi/use/main/ShellIT.java new file mode 100644 index 000000000..df4992dde --- /dev/null +++ b/use-gui/src/it/java/org/tzi/use/main/ShellIT.java @@ -0,0 +1,304 @@ +package org.tzi.use.main; + +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRowGenerator; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.tzi.use.config.Options; +import org.tzi.use.util.USEWriter; + +import java.io.ByteArrayOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This class implements the shell integration tests. + * + *

These tests represent nearly the exact behavior of the + * USE shell. Only some whitespace differences and time outputs are ignored.

+ * + *

Each test consists of the following files: + *

    + *
  1. a model file (suffix {@code .use}) - this file provides the (possible empty) model used + * for the tests
  2. + *
  3. an input file (suffix: {@code .in}) - this file can contain any commands USE supports. + * The expected output must be specified by starting a line with a star {@code *}
  4. + *
  5. any other used file from the command line, e.g., ASSL- or command-files.
  6. + *

+ * + *

All {@code .use and .in} files must share the same name, e.g., t555.use and t555.in for test + * case 555. These files must be placed in the folder {@code it/resources/testfiles/shell}.

+ * + *

If an integration test fails, two additional files are created: + *

    + *
  1. {@code {testcasename}.expected - The expected output calculated from the {testcase}.in}-file
  2. + *
  3. {@code {testcasename}.actual} - The output captured while running the test.
  4. + *
+ * These files can be used to easily diff expected and current output.

+ */ +public class ShellIT { + + /** + * This TestFactory enumerates the {@code .in
-files in th folder testfiles/shell}. + * For each file a {@code DynamicTest} is created with the name of the file. + * + * @return A {@code Stream with one DynamicTest for each *.in}-file. + */ + @TestFactory + public Stream evaluateExpressionFiles() { + URL testDirURL = getClass().getClassLoader().getResource("testfiles/shell"); + Path testDirPath = null; + + if (testDirURL == null) { + fail("Directory for shell integration tests not found!"); + } + + try { + testDirPath = Path.of(testDirURL.toURI()); + } catch (URISyntaxException e) { + fail("Directory for shell integration tests not found!"); + } + + try { + return Files.walk(testDirPath).filter( + path -> path.getFileName().toString().endsWith(".in") + ).map(mapInFileToTest()); + } catch (IOException e) { + fail("Error iterating shell integration test input files!"); + } + + return Stream.empty(); + } + + /** + * This {@code Function} is used to map + * a given testinput-file given as a {@code Path} + * to a {@code DynamicTest}. + * + * @return A {@code DynamicTest that uses the function assertShellExpression} + * to test the given testinput file. + */ + private Function mapInFileToTest() { + return path -> { + final String modelFilename = path.getFileName().toString().replace(".in", ".use"); + final Path modelPath = path.resolveSibling(modelFilename); + + return DynamicTest.dynamicTest(path.getFileName().toString(), path.toUri(), () -> assertShellExpressions(path, modelPath)); + }; + } + + /** + *

This function controls the overall process for test for a single testfile.

+ * + *

The process is as follows: + *

    + *
  1. a command file and the expected output are created by examining the input file (via {@code createCommandFile}.
  2. + *
  3. USE is executed using the {@code useFile and the created command file (runUSE}).
  4. + *
  5. The output of USE is compared to the expected output created in 1. ({@code validateOutput}).
  6. + *

+ * + * @param testFile {@code Path} to the test input file to execute. + * @param useFile {@code Path} to the USE file containing the model to load for the test. + */ + private void assertShellExpressions(Path testFile, Path useFile) { + + Path cmdFile = testFile.resolveSibling(testFile.getFileName() + ".cmd"); + + List expectedOutput = createCommandFile(testFile, cmdFile); + + List actualOutput = runUSE(useFile, cmdFile).collect(Collectors.toList()); + + validateOutput(testFile, expectedOutput, actualOutput); + } + + /** + * Compares the two lists of strings {@code expectedOutput} + * and {@code actualOutput}. + * If they differ, two files are written at the location of the + * {@code testFile. One with the expected output (.expected}) + * and one with the actual output ({@code .actual}). + * + * @param testFile The {@code Path to the testFile} + * @param expectedOutput List of strings with the expected output (one String per line) + * @param actualOutput List of strings with the actual output (one String per line) + */ + private void validateOutput(Path testFile, List expectedOutput, List actualOutput) { + //create a configured DiffRowGenerator + DiffRowGenerator generator = DiffRowGenerator.create() + .showInlineDiffs(true) + .mergeOriginalRevised(true) + .inlineDiffByWord(true) + .ignoreWhiteSpaces(true) + .lineNormalizer( (s) -> s ) // No normalization required + .oldTag((f, start) -> start ? "-\033[9m" : "\033[m-") //introduce markdown style for strikethrough + .newTag((f, start) -> start ? "+\033[97;42m" : "\033[m+") //introduce markdown style for bold + .build(); + + //compute the differences for two test texts. + List rows = generator.generateDiffRows(expectedOutput, actualOutput); + Predicate filter = d -> d.getTag() != DiffRow.Tag.EQUAL; + + if (rows.stream().anyMatch(filter)) { + StringBuilder diffMsg = new StringBuilder("USE output does not match expected output!").append(System.lineSeparator()); + + diffMsg.append("Testfile: ").append(testFile).append(System.lineSeparator()); + + diffMsg.append(System.lineSeparator()).append("Note: the position is not the position in the input file!"); + diffMsg.append(System.lineSeparator()).append(System.lineSeparator()); + + rows.stream().filter(filter).forEach( + row ->diffMsg.append(System.lineSeparator()).append(row.getOldLine()) + ); + + writeToFile(expectedOutput, testFile.getParent().resolve(testFile.getFileName().toString() + ".expected")); + writeToFile(actualOutput, testFile.getParent().resolve(testFile.getFileName().toString() + ".actual")); + + fail(diffMsg.toString()); + } + } + + /** + *

Helper method that writes the list of strings {@code data} + * to the file located by the {@code Path} {@code file}.

+ * + *

If the file is not accessible, i.e., an IOException is thrown, + * the exceptions is caught and the test case fails.

+ * @param data The {@code List} of string (lines) to write. + * @param file The path to the file to write (file is overwritten). + */ + private void writeToFile(List data, Path file) { + try (FileWriter writer = new FileWriter(file.toFile())) { + + for (String line : data) { + writer.write(line); + writer.write(System.lineSeparator()); + } + } catch (IOException e) { + fail("Testoutput could not be written!", e); + } + } + + /** + * Creates a USE-command file at the position located by the path {@code cmdFile}. + * The file contains all commands that are specified in the {@code inFile}. + * The expected output, i.e., lines starting with a {@code *} are added to the list {@code expectedOutput}. + * + * @param inFile The {@code Path} to the test input file. + * @param cmdFile The {@code Path} where to create the command file. + * @return A {@code List} which is filled with the expected output of USE. + */ + private List createCommandFile(Path inFile, Path cmdFile) { + List expectedOutput = new LinkedList<>(); + + // Build USE command file and build expected output + try ( + Stream linesStream = Files.lines(inFile, StandardCharsets.UTF_8); + FileWriter cmdWriter = new FileWriter(cmdFile.toFile(), StandardCharsets.UTF_8, false) + ) { + + linesStream.forEach(inputLine -> { + + // Ignore empty lines in expected, since they are also suppressed in actual output + if (inputLine.isBlank()) + return; + + if ((inputLine.startsWith("*") || inputLine.startsWith("#")) + && inputLine.substring(1).isBlank()) { + return; + } + + if (inputLine.startsWith("*")) { + // Input line minus prefix(*) is expected output + expectedOutput.add(inputLine.substring(1).trim()); + } else if (!inputLine.startsWith("#")) { // Not a comment + try { + cmdWriter.write(inputLine); + cmdWriter.write(System.lineSeparator()); + + // Multi line commands (backslash and dot) are ignored + if (!inputLine.matches("^[\\\\.]$")) { + expectedOutput.add(inputLine); + } + } catch (IOException e1) { + fail("Could not write USE command file for test!", e1); + } + } + }); + } catch (IOException e) { + fail("Could not write USE command file for test!", e); + } + + return expectedOutput; + } + + /** + * Executes USE with the given {@code useFile} as the model + * and the {@code cmdFile} to execute commands. + * The output is captured from the output and error streams. + * + * @param useFile Path to the USE model to load on startup + * @param cmdFile Path to the commands file to execute + * @return A {@code List} of strings. Each string is one line of output. + */ + private Stream runUSE(Path useFile, Path cmdFile) { + + // We need to specify a concrete locale to always get the same formatted result + Locale.setDefault(new Locale("en", "US")); + + Options.resetOptions(); + USEWriter.getInstance().clearLog(); + + String homeDir = null; + try { + homeDir = useFile.getParent().resolve("../../../../../use-core/target/classes").toFile().getCanonicalPath(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + String[] args = new String[] { + "-nogui", + "-nr", + "-t", + "-it", + "-q", + "-oclAnyCollectionsChecks:E", + "-extendedTypeSystemChecks:E", + /* This is currently an unstable workaround + USE determines the plugin and the extensions to OCL by fixed paths. + For now, the use-core module contains the directories including the extensions + and an empty plugins folder. + The folder is located: use/use-core/target/classes + Therefore, this is used as the USE home + */ + "-H=" + homeDir, + useFile.toString(), + cmdFile.toString()}; + + Main.main(args); + + try (ByteArrayOutputStream protocol = new ByteArrayOutputStream();) { + USEWriter.getInstance().writeProtocolFile(protocol); + String output = protocol.toString(); + return output.lines().filter(l -> !l.isBlank()); + } catch (IOException e) { + fail(e); + } + + return Collections.emptyList().stream(); + } +} diff --git a/use-gui/src/main/java/org/tzi/use/main/Main.java b/use-gui/src/main/java/org/tzi/use/main/Main.java new file mode 100644 index 000000000..5a70ef8ad --- /dev/null +++ b/use-gui/src/main/java/org/tzi/use/main/Main.java @@ -0,0 +1,259 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2004 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.main; + +import org.tzi.use.config.Options; +import org.tzi.use.gui.main.MainWindow; +import org.tzi.use.main.runtime.IRuntime; +import org.tzi.use.main.shell.Shell; +import org.tzi.use.parser.use.USECompiler; +import org.tzi.use.uml.mm.MMPrintVisitor; +import org.tzi.use.uml.mm.MMVisitor; +import org.tzi.use.uml.mm.MModel; +import org.tzi.use.uml.mm.ModelFactory; +import org.tzi.use.uml.ocl.extension.ExtensionManager; +import org.tzi.use.uml.sys.MSystem; +import org.tzi.use.util.Log; +import org.tzi.use.util.USEWriter; + +import javax.swing.*; +import javax.swing.plaf.FontUIResource; +import javax.swing.plaf.metal.DefaultMetalTheme; +import javax.swing.plaf.metal.MetalLookAndFeel; +import java.awt.*; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.nio.file.Path; + +/** + * Main class. + * + * @author Mark Richters + */ +public final class Main { + + // utility class + private Main() { + } + + private static void initGUIdefaults() { + MetalLookAndFeel.setCurrentTheme(new MyTheme()); + } + + public static void main(String[] args) { + // set System.out to the OldUSEWriter to protocol the output. + System.setOut(USEWriter.getInstance().getOut()); + // set System.err to the OldUSEWriter to protocol the output. + System.setErr(USEWriter.getInstance().getErr()); + + // read and set global options, setup application properties + Options.processArgs(args); + if (Options.doGUI) { + initGUIdefaults(); + } + + Session session = new Session(); + IRuntime pluginRuntime = null; + MModel model = null; + MSystem system = null; + + if (!Options.disableExtensions) { + ExtensionManager.EXTENSIONS_FOLDER = Options.homeDir + Options.FILE_SEPARATOR + + "oclextensions"; + ExtensionManager.getInstance().loadExtensions(); + } + + // Plugin Framework + if (Options.doPLUGIN) { + // create URL from plugin directory + Path pluginDirURL = Options.pluginDir; + Log.verbose("Plugin path: [" + pluginDirURL + "]"); + Class mainPluginRuntimeClass = null; + try { + mainPluginRuntimeClass = Class + .forName("org.tzi.use.runtime.MainPluginRuntime"); + } catch (ClassNotFoundException e) { + Log + .error("Could not load PluginRuntime. Probably use-runtime-...jar is missing.\n" + + "Try starting use with -noplugins switch.\n" + + e.getMessage()); + System.exit(1); + } + try { + Method run = mainPluginRuntimeClass.getMethod("run", + new Class[] { Path.class }); + pluginRuntime = (IRuntime) run.invoke(null, + new Object[] { pluginDirURL }); + Log.debug("Starting plugin runtime, got class [" + + pluginRuntime.getClass() + "]"); + } catch (Exception e) { + e.printStackTrace(); + Log.error("FATAL ERROR."); + System.exit(1); + } + } + + // compile spec if filename given as argument + if (Options.specFilename != null) { + Path file = Path.of(Options.specFilename); + + try (FileInputStream specStream = new FileInputStream(Options.specFilename)){ + Log.verbose("compiling specification..."); + model = USECompiler.compileSpecification(specStream, + file.getFileName().toString(), new PrintWriter(System.err), + new ModelFactory()); + } catch (FileNotFoundException e) { + Log.error("File `" + Options.specFilename + "' not found."); + if (Options.integrationTestMode) { + return; + } else { + System.exit(1); + } + } catch (IOException e1) { + // close failed + } + + // compile errors? + if (model == null) { + if (Options.integrationTestMode) { + return; + } else { + System.exit(1); + } + } + + if(!Options.quiet){ + Options.setLastDirectory(new java.io.File(Options.specFilename).getAbsoluteFile().toPath().getParent()); + } + if (!Options.testMode) + Options.getRecentFiles().push(Options.specFilename); + + if (Options.compileOnly) { + Log.verbose("no errors."); + if (Options.compileAndPrint) { + MMVisitor v = new MMPrintVisitor(new PrintWriter( + System.out, true)); + model.processWithVisitor(v); + } + System.exit(0); + } + + // print some info about model + Log.verbose(model.getStats()); + + // create system + system = new MSystem(model); + } + session.setSystem(system); + + if (Options.doGUI) { + if (pluginRuntime == null) { + Log.debug("Starting gui without plugin runtime!"); + MainWindow.create(session); + } else { + Log.debug("Starting gui with plugin runtime."); + MainWindow.create(session, pluginRuntime); + } + } + + // create thread for shell + Shell.createInstance(session, pluginRuntime); + Shell sh = Shell.getInstance(); + Thread t = new Thread(sh); + t.start(); + + // wait on exit from shell (this thread never returns) + try { + t.join(); + } catch (InterruptedException ex) { + // ignored + } + } +} + +/** + * A theme with full control over fonts and customized tree display. + */ +class MyTheme extends DefaultMetalTheme { + private FontUIResource controlFont; + + private FontUIResource systemFont; + + private FontUIResource userFont; + + private FontUIResource smallFont; + + MyTheme() { + // System.out.println("font: " + Font.getFont("use.gui.controlFont")); + controlFont = new FontUIResource(Font.getFont("use.gui.controlFont", + super.getControlTextFont())); + systemFont = new FontUIResource(Font.getFont("use.gui.systemFont", + super.getSystemTextFont())); + userFont = new FontUIResource(Font.getFont("use.gui.userFont", super + .getUserTextFont())); + smallFont = new FontUIResource(Font.getFont("use.gui.smallFont", super + .getSubTextFont())); + } + + public String getName() { + return "USE"; + } + + public FontUIResource getControlTextFont() { + return controlFont; + } + + public FontUIResource getSystemTextFont() { + return systemFont; + } + + public FontUIResource getUserTextFont() { + return userFont; + } + + public FontUIResource getMenuTextFont() { + return controlFont; + } + + public FontUIResource getWindowTitleFont() { + return controlFont; + } + + public FontUIResource getSubTextFont() { + return smallFont; + } + + public void addCustomEntriesToTable(UIDefaults table) { + initIcon(table, "Tree.expandedIcon", "TreeExpanded.gif"); + initIcon(table, "Tree.collapsedIcon", "TreeCollapsed.gif"); + initIcon(table, "Tree.leafIcon", "TreeLeaf.gif"); + initIcon(table, "Tree.openIcon", "TreeOpen.gif"); + initIcon(table, "Tree.closedIcon", "TreeClosed.gif"); + table.put("Desktop.background", table.get("Menu.background")); + } + + private void initIcon(UIDefaults table, String property, String iconFilename) { + table.put(property, new ImageIcon(getClass().getResource("/images/" + iconFilename))); + } + +} diff --git a/use-gui/src/main/java/org/tzi/use/util/input/ShellReadline.java b/use-gui/src/main/java/org/tzi/use/util/input/ShellReadline.java new file mode 100644 index 000000000..03c17e035 --- /dev/null +++ b/use-gui/src/main/java/org/tzi/use/util/input/ShellReadline.java @@ -0,0 +1,71 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util.input; + +import java.io.IOException; + +import org.tzi.use.main.shell.Shell; + +/** + * A {@link Readline} implementation that reads input from the {@link Shell} + * using the current readline that is on top of the readline stack. This might + * either be interactive or from an open soil file. + * + * @author Frank Hilken + */ +public class ShellReadline implements Readline { + + private Shell shell; + + public ShellReadline(Shell useShell) { + shell = useShell; + } + + @Override + public String readline(String prompt) throws IOException { + if(shell == null){ + throw new IOException("Stream closed"); + } + return shell.readline(prompt); + } + + @Override + public void usingHistory() { + } + + @Override + public void readHistory(String filename) throws IOException { + } + + @Override + public void writeHistory(String filename) throws IOException { + } + + @Override + public void close() throws IOException { + shell = null; + } + + @Override + public boolean doEcho() { + return false; + } + +} diff --git a/use-gui/src/main/resources/images/use1.gif b/use-gui/src/main/resources/images/use1.gif new file mode 100644 index 000000000..1b947ed71 Binary files /dev/null and b/use-gui/src/main/resources/images/use1.gif differ diff --git a/use-gui/src/test/java/org/tzi/use/util/DiagramUtilTest.java b/use-gui/src/test/java/org/tzi/use/util/DiagramUtilTest.java new file mode 100644 index 000000000..d84d8fe02 --- /dev/null +++ b/use-gui/src/test/java/org/tzi/use/util/DiagramUtilTest.java @@ -0,0 +1,130 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2010 Mark Richters, University of Bremen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import junit.framework.TestCase; + +import org.junit.Test; +import org.tzi.use.gui.views.diagrams.util.Util; + +/** + * Test for diagram utilities (mainly geometry) + * @author Lars Hamann + * + */ +public class DiagramUtilTest extends TestCase { + @Test + public void testCircleIntersection() { + Ellipse2D circle = new Ellipse2D.Double(-4, -4, 8, 8); + Point2D res; + Point2D.Double expected = new Point2D.Double(); + + expected.x = 4; + expected.y = 0; + res = Util.intersectionPoint(circle, new Point2D.Double(4, 0)); + assertEquals(expected, res); + + expected.x = 0; + expected.y = 4; + res = Util.intersectionPoint(circle, new Point2D.Double(0, 4)); + assertEquals(expected, res); + + expected.x = -4; + expected.y = 0; + res = Util.intersectionPoint(circle, new Point2D.Double(-4, 0)); + assertEquals(expected, res); + + expected.x = 0; + expected.y = -4; + res = Util.intersectionPoint(circle, new Point2D.Double(0, -4)); + assertEquals(expected, res); + + res = Util.intersectionPoint(circle, new Point2D.Double(1.5, -2.5)); + Point2D res2 = Util.intersectionPoint(circle, res); + assertEquals(res, res2); + } + + public void testRectangleInterception() { + Rectangle2D.Double r = new Rectangle2D.Double(); + r.x = 0; + r.y = 0; + r.width = 1; + r.height = 1; + + Line2D.Double l = new Line2D.Double(); + l.x1 = 0.5; + l.y1 = 0.5; + + l.x2 = 1.5; + l.y2 = 0.5; + + Point2D res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(1.0, res.getX()); + assertEquals(0.5, res.getY()); + + l.x2 = 0.5; + l.y2 = 1.5; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(0.5, res.getX()); + assertEquals(1.0, res.getY()); + + l.x2 = -0.5; + l.y2 = 0.5; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(0.0, res.getX()); + assertEquals(50, Math.round(res.getY() * 100)); + + l.x2 = 0.5; + l.y2 = -1.5; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(0.5, res.getX()); + assertEquals(0.0, res.getY() * 100); + + // Test enlarge + l.x2 = 0.6; + l.y2 = 0.5; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(1.0, res.getX()); + assertEquals(0.5, res.getY()); + + l.x2 = 0.5; + l.y2 = 0.6; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(0.5, res.getX()); + assertEquals(1.0, res.getY()); + + l.x2 = 0.4; + l.y2 = 0.5; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(0.0, res.getX()); + assertEquals(50, Math.round(res.getY() * 100)); + + l.x2 = 0.5; + l.y2 = 0.4; + res = Util.intersectionPoint(r, l.getP1(), l.getP2(), true); + assertEquals(0.5, res.getX()); + assertEquals(0.0, res.getY() * 100); + } +}