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:
+ *
+ * a model file (suffix {@code .use}) - this file provides the (possible empty) model used
+ * for the tests
+ * 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 *}
+ * any other used file from the command line, e.g., ASSL- or command-files.
+ *
+ *
+ * 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:
+ *
+ * {@code {testcasename}.expected - The expected output calculated from the {testcase}.in}-file
+ * {@code {testcasename}.actual} - The output captured while running the test.
+ *
+ * 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:
+ *
+ * a command file and the expected output are created by examining the input file (via {@code createCommandFile}.
+ * USE is executed using the {@code useFile and the created command file (runUSE}).
+ * The output of USE is compared to the expected output created in 1. ({@code validateOutput}).
+ *
+ *
+ * @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);
+ }
+}