From e530007f2e178b6a7ff59e7c0290def6091c38d5 Mon Sep 17 00:00:00 2001 From: Jamil RAICHOUNI Date: Tue, 13 Aug 2024 20:12:52 +0200 Subject: [PATCH] feat: Implement REST API --- .github/workflows/build-addon.yml | 135 -------- .github/workflows/code-quality.yml | 28 ++ .github/workflows/setup-build-release.yml | 131 ++++++++ CONTRIBUTING.md | 19 -- capella_addons/__main__.py | 56 +++- models-from-directory-importer/pom.xml | 2 +- pyproject.toml | 7 +- rest-api/.classpath.license | 2 + rest-api/.gitignore | 16 + rest-api/.openapi-generator-ignore | 18 + rest-api/.project | 35 ++ rest-api/.project.license | 2 + rest-api/META-INF/MANIFEST.MF | 3 +- rest-api/bin/jaxrs-jersey.json | 10 + rest-api/bin/jaxrs-jersey.json.license | 2 + rest-api/bin/jaxrs-jersey.zsh | 10 + rest-api/build.properties | 2 + rest-api/build.properties.license | 2 + rest-api/openapi/custom.yaml | 317 ++++++++++++++++++ rest-api/plugin.xml | 9 + rest-api/plugin.xml.license | 2 + rest-api/pom.xml | 159 +++++++++ rest-api/pom.xml.license | 2 + rest-api/src/com/db/capella/Application.java | 16 + rest-api/src/com/db/capella/Main.java | 45 +++ .../db/capella/api/JacksonJsonProvider.java | 34 ++ .../api/impl/ProjectsApiServiceImpl.java | 314 +++++++++++++++++ .../capella/integration/ReflectionUtils.java | 96 ++++++ .../integration/WorkspaceProjectInt.java | 100 ++++++ 29 files changed, 1402 insertions(+), 172 deletions(-) delete mode 100644 .github/workflows/build-addon.yml create mode 100644 .github/workflows/code-quality.yml create mode 100644 .github/workflows/setup-build-release.yml delete mode 100644 CONTRIBUTING.md create mode 100644 rest-api/.classpath.license create mode 100644 rest-api/.gitignore create mode 100644 rest-api/.openapi-generator-ignore create mode 100644 rest-api/.project create mode 100644 rest-api/.project.license create mode 100644 rest-api/bin/jaxrs-jersey.json create mode 100644 rest-api/bin/jaxrs-jersey.json.license create mode 100755 rest-api/bin/jaxrs-jersey.zsh create mode 100644 rest-api/build.properties create mode 100644 rest-api/build.properties.license create mode 100644 rest-api/openapi/custom.yaml create mode 100644 rest-api/plugin.xml create mode 100644 rest-api/plugin.xml.license create mode 100644 rest-api/pom.xml create mode 100644 rest-api/pom.xml.license create mode 100644 rest-api/src/com/db/capella/Application.java create mode 100644 rest-api/src/com/db/capella/Main.java create mode 100644 rest-api/src/com/db/capella/api/JacksonJsonProvider.java create mode 100644 rest-api/src/com/db/capella/api/impl/ProjectsApiServiceImpl.java create mode 100644 rest-api/src/com/db/capella/integration/ReflectionUtils.java create mode 100644 rest-api/src/com/db/capella/integration/WorkspaceProjectInt.java diff --git a/.github/workflows/build-addon.yml b/.github/workflows/build-addon.yml deleted file mode 100644 index 5c66c72..0000000 --- a/.github/workflows/build-addon.yml +++ /dev/null @@ -1,135 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: CC0-1.0 -name: Build addons - -on: - pull_request: - push: - branches: ["main"] - tags: ["v*.*.*"] - -env: - registry: ghcr.io/dsd-dbs/capella-addons/ - addons: rest-api - JVM_DIR: /usr/lib/jvm - -jobs: - # test: - # name: Test - # runs-on: ubuntu-latest - # steps: - # - name: Test - # run: |- - quality: - name: Ensure code quality - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install pre-commit - run: |- - python -m pip install pre-commit - - name: Run pre-commit - run: |- - pre-commit run --all-files - setup-build-environment: - strategy: - matrix: - capella: - - version: "6.0.0" - jdk: - name: "jdk-17.0.6+10" - url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.6%2B10/OpenJDK17U-jdk_aarch64_linux_hotspot_17.0.6_10.tar.gz" - # url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.6%2B10/OpenJDK17U-jdk_x64_linux_hotspot_17.0.6_10.tar.gz" - # - version: "6.1.0" - # jdk: - # name: "jdk-17.0.6+10" - # url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.6%2B10/OpenJDK17U-jdk_aarch64_linux_hotspot_17.0.6_10.tar.gz" - # - version: "7.0.0" - # jdk: - # name: "jdk-17.0.11+9" - # url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.11%2B9/OpenJDK17U-jdk_x64_linux_hotspot_17.0.11_9.tar.gz" - name: Setup build environment for Capella ${{ matrix.capella.version }} - # needs: [quality] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install `capella-addons` CLI tool - run: |- - pip install . - pip show capella-addons - - name: Install Capella from capelladocker-images - run: |- - docker pull ghcr.io/dsd-dbs/capella-dockerimages/capella/remote:${{ matrix.capella.version }}-selected-dropins-main - if [ ! -d /tmp/capella_${{ matrix.capella.version }} ]; then - docker run --rm -v /tmp:/tmp --entrypoint="" --user=root \ - ghcr.io/dsd-dbs/capella-dockerimages/capella/remote:${{ matrix.capella.version }}-selected-dropins-main \ - bash -c "cp -r /opt/capella /tmp/capella_${{ matrix.capella.version }}" - fi - - name: Install JDKs - run: |- - JVM_DIR=/usr/lib/jvm - TMP_JDK=/tmp/jdk.tar.gz - mkdir -p $JVM_DIR - cd $JVM_DIR - URL="${{ matrix.capella.jdk.url }}" - [[ -f $TMP_JDK ]] && rm $TMP_JDK - curl -Lo $TMP_JDK $URL - JDK_DIR_NAME=$(tar tf $TMP_JDK | head -n 1) - [[ -d $JDK_DIR_NAME ]] && rm -rf $JDK_DIR_NAME - tar xzf $TMP_JDK - mv $JVM_DIR/jdk-* /usr/lib/jvm/jdk - rm $TMP_JDK - # done - - name: Install Eclipse JDT language server - run: |- - if [ ! -d /tmp/jdtls ]; then mkdir /tmp/jdtls; fi - cd /tmp/jdtls - if [ ! -f jdtls.tar.gz ]; then - curl -Lo jdtls.tar.gz \ - https://download.eclipse.org/jdtls/milestones/1.40.0/jdt-language-server-1.40.0-202409261450.tar.gz - fi - tar xzf jdtls.tar.gz - rm *.tar.gz - build-addons: - name: Build addons - needs: [setup-build-environment] - runs-on: ubuntu-latest - strategy: - matrix: - capella_version: - - '6.0.0' - # - '6.1.0' - # - '7.0.0' - addon: - - rest-api - steps: - - name: Build `.classpath` file - run: |- - cd ${{ matrix.addon }} - python -m capella_addons build-classpath \ - --java-execution-environment=JavaSE-17 \ - $(find . -type f -name "Main.java") \ - /tmp/capella_${{ matrix.capella_version }} - cat .classpath - - name: Build workspace - run: |- - cd ${{ matrix.addon }} - rm -rf target - python -m capella_addons -v \ - build-workspace \ - --java-execution-environment=JavaSE-17 \ - /usr/lib/jvm/jdk-17.0.6+10 \ - /tmp/jdtls diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..74a85b5 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: CC0-1.0 +on: + push: + branches: + - main + - "*/v*.*.*" # Match version tags for releases + pull_request: + branches: + - main + +jobs: + code-quality: + runs-on: ubuntu-latest + name: Check code quality + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install pre-commit + run: python -m pip install pre-commit + - name: Run pre-commit + run: pre-commit run --all-files diff --git a/.github/workflows/setup-build-release.yml b/.github/workflows/setup-build-release.yml new file mode 100644 index 0000000..187ef5b --- /dev/null +++ b/.github/workflows/setup-build-release.yml @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: CC0-1.0 +on: + push: + tags: ["*/v*.*.*"] # tags must be /v + pull_request: + branches: [main] +jobs: + setup-build-release: + name: Setup, build, and release + runs-on: ubuntu-latest + strategy: + matrix: + capella-version: [6.0.0, 7.0.0] + include: + - capella-version: 6.0.0 + java-execution-environment: JavaSE-17 + jdk-version: 17.0.6+10 + - capella-version: 7.0.0 + java-execution-environment: JavaSE-17 + jdk-version: 17.0.11+9 + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up `capella-addons` CLI tool + run: pip install . && pip show capella-addons + - name: Identify addon and version + id: identify_addon_and_version + run: | + echo "GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME}" + echo "GITHUB_REF_NAME=${GITHUB_REF_NAME}" + VERSION="" + if [ ${GITHUB_EVENT_NAME} = "pull_request" ]; then + ADDON=${GITHUB_HEAD_REF} + elif [ ${GITHUB_EVENT_NAME} = "push" ]; then + if [[ ${GITHUB_REF_TYPE} == "tag" ]]; then + echo "This push is a tag: ${GITHUB_REF_NAME}" + ADDON=$(echo "${GITHUB_REF_NAME}" | sed 's|/.*||') # `sed` with `|` as delimiter + VERSION=$(echo "${GITHUB_REF_NAME}" | sed 's|[^/.]*/v||') + else + echo "This push is not a tag." + ADDON=${GITHUB_REF_NAME} + VERSION=$(python -m capella_addons --version | cut -d' ' -f2) + fi + fi + echo "addon=${ADDON}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Identified addon: ${ADDON}" + - name: Set addon version + run: | + sed -i 's|VERSION|${{ steps.identify_addon_and_version.outputs.version }}|g' ${{ steps.identify_addon_and_version.outputs.addon }}/pom.xml + sed -i 's/^Bundle-Version.*/Bundle-Version: ${{ steps.identify_addon_and_version.outputs.version }}/' ${{ steps.identify_addon_and_version.outputs.addon }}/META-INF/MANIFEST.MF + - name: Set up Capella from ghcr.io/dsd-dbs/capella-dockerimages/capella/remote:${{ matrix.capella-version }}-selected-dropins-main + run: | + docker pull ghcr.io/dsd-dbs/capella-dockerimages/capella/remote:${{ matrix.capella-version }}-selected-dropins-main + if [ ! -d /tmp/capella_${{ matrix.capella-version }} ]; then + docker run --platform=linux/x86_64 --rm -v /tmp:/tmp --entrypoint="" --user=root \ + ghcr.io/dsd-dbs/capella-dockerimages/capella/remote:${{ matrix.capella-version }}-selected-dropins-main \ + bash -c "cp -r /opt/capella /tmp/capella_${{ matrix.capella-version }}" + fi + - name: Build `.classpath` file + run: | + cd ${{ steps.identify_addon_and_version.outputs.addon }} + python -m capella_addons build-classpath \ + --java-execution-environment=${{ matrix.java-execution-environment }} \ + $(find src -type f -name "Main.java") \ + /tmp/capella_${{ matrix.capella-version }} + - name: Set up OpenAPI generator + if: steps.identify_addon_and_version.outputs.addon == 'rest-api' + run: | + curl -Lo /opt/openapi-generator.jar \ + https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.9.0/openapi-generator-cli-7.9.0.jar + echo "java -jar /opt/openapi-generator.jar \$@" > /usr/local/bin/openapi-generator + chmod +x /usr/local/bin/openapi-generator + - name: Run OpenAPI generator + if: steps.identify_addon_and_version.outputs.addon == 'rest-api' + run: | + cd ${{ steps.identify_addon_and_version.outputs.addon }} + bash bin/jaxrs-jersey.zsh + rm -rf /tmp/src; cp -r src /tmp + - name: Set up Eclipse Temurin JDK ${{ matrix.jdk-version }} + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.jdk-version }} + distribution: 'temurin' + java-package: "jdk" + - name: Set up Eclipse JDT language server 1.40.0 + run: | + if [ ! -d /tmp/jdtls ]; then mkdir /tmp/jdtls; fi + cd /tmp/jdtls + if [ ! -f jdtls.tar.gz ]; then + curl -Lo jdtls.tar.gz \ + https://download.eclipse.org/jdtls/milestones/1.40.0/jdt-language-server-1.40.0-202409261450.tar.gz + fi + tar xzf jdtls.tar.gz + rm *.tar.gz + - name: Build workspace + run: | + cd ${{ steps.identify_addon_and_version.outputs.addon }} + rm -rf target + python -m capella_addons -v \ + build-workspace \ + --java-execution-environment=${{ matrix.java-execution-environment }} \ + $JAVA_HOME /tmp/jdtls + - name: Package addon + run: | + cd ${{ steps.identify_addon_and_version.outputs.addon }} + python -m capella_addons -v \ + package \ + $JAVA_HOME \ + /tmp/capella_${{ matrix.capella-version }} + - name: Include Capella version in JAR file name + run: | + cd ${{ steps.identify_addon_and_version.outputs.addon }} + for file in target/*.jar; do + mv "$file" "${file%.jar}_capella_${{ matrix.capella-version }}.jar" + done + - name: Create release + if: github.event_name == 'push' && github.ref_type == 'tag' + uses: ncipollo/release-action@v1.14.0 + with: + name: ${{ steps.identify_addon_and_version.outputs.addon }} ${{ steps.identify_addon_and_version.outputs.version }} + artifacts: ${{ steps.identify_addon_and_version.outputs.addon }}/target/*.jar + allowUpdates: true + tag: ${{ github.ref_name }} + draft: false + prerelease: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 1573de0..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,19 +0,0 @@ - - -# Contributing - -Capella addons are developed on separated branches. To contribute, please -follow these steps: - -1. Fork the repository -1. Create a new branch from the `main` branch and name it according to the - Capella addon (name as defined in the next step) you plan to contribute. -1. Change into the root directory for your local checkout of the present - repository. -1. Create the new addon project using the [project template] which has - according instructions. - -[project template]: https://github.com/DSD-DBS/cookiecutter-dbs-eclipse-addon diff --git a/capella_addons/__main__.py b/capella_addons/__main__.py index 265c16e..41af231 100644 --- a/capella_addons/__main__.py +++ b/capella_addons/__main__.py @@ -6,6 +6,7 @@ import logging import os import pathlib +import platform import shutil import subprocess import sys @@ -61,11 +62,6 @@ class BuildWorkspaceStatus(Enum): @click.group() -@click.version_option( - version=capella_addons.__version__, - prog_name="eclipse-plugin-builders", - message="%(prog)s %(version)s", -) @click.option( "-v", "--verbose", @@ -77,6 +73,11 @@ class BuildWorkspaceStatus(Enum): logging.DEBUG if x else logging.INFO ), ) +@click.version_option( + version=capella_addons.__version__, + prog_name="capella-addons", + message="%(prog)s %(version)s", +) def main() -> None: """Console tools to develop, build, pack, and deploy Capella addons. @@ -109,8 +110,7 @@ def _third_party_lib_paths() -> list[pathlib.Path]: """Return the paths to the third-party libraries.""" classpath_root = _read_xml_file(".classpath") third_party_lib_paths = classpath_root.xpath( - 'classpathentry[@kind="lib" and ' - 'not(starts-with(@path, "/opt/capella_6.0.0"))]/@path' + 'classpathentry[@kind="lib"]/@path' ) return sorted([pathlib.Path(p) for p in third_party_lib_paths]) @@ -126,7 +126,26 @@ def compute_jar_name() -> str: group_id = group_id[0].text if group_id else "unknown" artifact_id = artifact_id[0].text if artifact_id else "unknown" version = version[0].text if version else "unknown" - return f"{group_id}.{artifact_id}_{version}.jar" + + # Determine the operating system + os_name = platform.system().lower() + if os_name == "darwin": + os_name = "macos" + + # Determine the architecture + architecture = platform.machine().lower() + if architecture in ["x86_64", "amd64"]: + architecture = "x64" + elif architecture in ["i386", "i686"]: + architecture = "x86" + elif architecture.startswith("arm"): + architecture = "arm" + + jar_name = ( + f"{group_id}.{artifact_id}_{version}_{os_name}_{architecture}.jar" + ) + logger.debug("Computed jar name: %s", jar_name) + return jar_name def _output_and_jar_path() -> tuple[pathlib.Path, pathlib.Path]: @@ -615,6 +634,7 @@ def build_workspace( status = response.get("result", BuildWorkspaceStatus.FAILED.value) if status == BuildWorkspaceStatus.SUCCEED.value: click.echo("Build of workspace succeeded.") + sys.exit(0) elif status == BuildWorkspaceStatus.CANCELLED.value: click.echo("Build of workspace cancelled.") elif status == BuildWorkspaceStatus.WITH_ERROR.value: @@ -651,7 +671,7 @@ def deploy(target_path: pathlib.Path) -> None: def _get_bundle_classpath(third_party_lib_paths: list[pathlib.Path]) -> str: - lib_paths = sorted([p.name for p in _third_party_lib_paths()]) + lib_paths = sorted([p.name for p in third_party_lib_paths]) value = "." if third_party_lib_paths: value = ".,\n" @@ -689,7 +709,12 @@ def _update_bundle_classpath( @main.command() @click.argument("java_home", type=click.Path(exists=True, dir_okay=True)) -def package(java_home: pathlib.Path) -> None: +@click.argument( + "target_platform_path", type=click.Path(exists=True, dir_okay=True) +) +def package( + java_home: pathlib.Path, target_platform_path: pathlib.Path +) -> None: """Package the eclipse plugin. \b @@ -697,12 +722,20 @@ def package(java_home: pathlib.Path) -> None: --------- java_home : pathlib.Path The path to the Java home directory. + target_platform_path + The installation directory of an Eclipse/ Capella application + that will be referenced as target platform to build the + classpath. """ # noqa: D301 lib_dir = pathlib.Path("lib") if lib_dir.is_dir(): shutil.rmtree(lib_dir) lib_dir.mkdir() - third_party_lib_paths = _third_party_lib_paths() + third_party_lib_paths = [ + p + for p in _third_party_lib_paths() + if not p.is_relative_to(target_platform_path) + ] if third_party_lib_paths: for path in third_party_lib_paths: dest = lib_dir / path.name @@ -714,6 +747,7 @@ def package(java_home: pathlib.Path) -> None: click.echo(f"`{path}` file not found.") sys.exit(1) output_path, jar_path = _output_and_jar_path() + logger.info("Packaging the addon into `%s`...", jar_path) jar_path.unlink(missing_ok=True) jar = pathlib.Path(java_home) / "bin" / "jar" jar_cmd = [ diff --git a/models-from-directory-importer/pom.xml b/models-from-directory-importer/pom.xml index 66c3617..8473abd 100644 --- a/models-from-directory-importer/pom.xml +++ b/models-from-directory-importer/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.deutschebahn models-from-directory-importer - 0.0.1 + VERSION 2.7.5 diff --git a/pyproject.toml b/pyproject.toml index 3351b4c..fbb7ff4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ # SPDX-License-Identifier: Apache-2.0 [build-system] -requires = ["setuptools>=64", "setuptools_scm[toml]>=3.4", "wheel"] +requires = ["setuptools>=64", "wheel"] build-backend = "setuptools.build_meta" [project] -dynamic = ["version"] +version = "0.1.0" name = "capella-addons" description = "Tools to develop Capella addons" @@ -162,6 +162,3 @@ zip-safe = false [tool.setuptools.packages.find] include = ["capella_addons", "capella_addons.*"] - -[tool.setuptools_scm] -# This section must exist for setuptools_scm to work diff --git a/rest-api/.classpath.license b/rest-api/.classpath.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/rest-api/.classpath.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/rest-api/.gitignore b/rest-api/.gitignore new file mode 100644 index 0000000..b17d0d1 --- /dev/null +++ b/rest-api/.gitignore @@ -0,0 +1,16 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: CC0-1.0 + +*.class +*.jar +.factorypath +.mypy_cache/ +.openapi-generator/ +.settings/ +lib/ +openapitools.json +src/com/db/capella/api/*.java +src/com/db/capella/api/factories/ +!src/com/db/capella/api/JacksonJsonProvider.java +src/com/db/capella/model/ +target/ diff --git a/rest-api/.openapi-generator-ignore b/rest-api/.openapi-generator-ignore new file mode 100644 index 0000000..f25fd2e --- /dev/null +++ b/rest-api/.openapi-generator-ignore @@ -0,0 +1,18 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: CC0-1.0 + +# keep our custom files +README.md +pom.xml +src/com/db/capella/Main.java + +# keep our customized JacksonJsonProvider with +# indention and ordered keys for the JSON output returned by the API +src/com/db/capella/api/JacksonJsonProvider.java + +# do not overwrite code where we implement the magic! +# src/com/db/capella/api/impl/ +src/com/db/capella/api/impl/ProjectsApiServiceImpl.java + +# do not generate src/main/webapp/ +src/main/ diff --git a/rest-api/.project b/rest-api/.project new file mode 100644 index 0000000..0ada5ec --- /dev/null +++ b/rest-api/.project @@ -0,0 +1,35 @@ + + + REST API + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + + + + 1723576096526 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/rest-api/.project.license b/rest-api/.project.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/rest-api/.project.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/rest-api/META-INF/MANIFEST.MF b/rest-api/META-INF/MANIFEST.MF index 81f1041..9fef623 100644 --- a/rest-api/META-INF/MANIFEST.MF +++ b/rest-api/META-INF/MANIFEST.MF @@ -78,4 +78,5 @@ Bundle-ClassPath: ., lib/validation-api-1.1.0.Final.jar Import-Package: org.eclipse.ui, org.eclipse.ui.part, - org.eclipse.core.resources + org.eclipse.core.resources, + org.eclipse.core.runtime diff --git a/rest-api/bin/jaxrs-jersey.json b/rest-api/bin/jaxrs-jersey.json new file mode 100644 index 0000000..131a0ad --- /dev/null +++ b/rest-api/bin/jaxrs-jersey.json @@ -0,0 +1,10 @@ +{ + "apiPackage": "com.db.capella.api", + "hideGenerationTimestamp": true, + "implFolder": "src", + "invokerPackage": "com.db.capella.api", + "library": "jersey3", + "modelPackage": "com.db.capella.model", + "sourceFolder": "src", + "useJakartaEe": true +} diff --git a/rest-api/bin/jaxrs-jersey.json.license b/rest-api/bin/jaxrs-jersey.json.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/rest-api/bin/jaxrs-jersey.json.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/rest-api/bin/jaxrs-jersey.zsh b/rest-api/bin/jaxrs-jersey.zsh new file mode 100755 index 0000000..e99b55b --- /dev/null +++ b/rest-api/bin/jaxrs-jersey.zsh @@ -0,0 +1,10 @@ +#!/usr/bin/env zsh +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: CC0-1.0 +SCRIPT_PATH=$(realpath -s $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) +openapi-generator generate \ + -i openapi/custom.yaml \ + -c $SCRIPT_DIR/jaxrs-jersey.json \ + -g jaxrs-jersey \ + -o . diff --git a/rest-api/build.properties b/rest-api/build.properties new file mode 100644 index 0000000..7ab72b9 --- /dev/null +++ b/rest-api/build.properties @@ -0,0 +1,2 @@ +source.. = src/ +bin.includes = .,plugin.xml,META-INF/ diff --git a/rest-api/build.properties.license b/rest-api/build.properties.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/rest-api/build.properties.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/rest-api/openapi/custom.yaml b/rest-api/openapi/custom.yaml new file mode 100644 index 0000000..8945509 --- /dev/null +++ b/rest-api/openapi/custom.yaml @@ -0,0 +1,317 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +openapi: 3.0.3 +info: + title: Capella API + description: API to access live data and modify Capella projects + version: 0.0.1 +servers: + - url: http://localhost:5007/api/v1 + description: Embedded Capella REST API server +tags: + - name: Diagrams + description: > + Operations related to diagrams in a Capella model + - name: Logical Architectures + description: > + Operations related to logical architecture layers in a Capella model + - name: Projects + description: > + Operations related to projects in the current workspace in Capella +paths: + /projects: + get: + tags: + - Projects + summary: List all projects in the workspace + operationId: listProjects + responses: + '200': + description: A list of projects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WorkspaceProject' + post: + tags: + - Projects + summary: Import a project into the workspace + operationId: importProject + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - location + properties: + location: + type: string + description: > + Absolute path of the project folder in the local file + system + responses: + '201': + description: Project imported successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceProject' + '400': + description: Invalid request + '404': + description: Project not found + '409': + description: Project already exists in the workspace + /projects/{project_name}: + get: + tags: + - Projects + summary: Get a project by name + operationId: getProjectByName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceProject' + '404': + description: Project not found + delete: + tags: + - Projects + summary: Delete a project by name + operationId: deleteProjectByName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project to be deleted + schema: + type: string + - name: deleteContents + in: query + required: false + description: > + Whether to delete the project contents on disk (cannot be undone) + or not + schema: + type: boolean + default: false + responses: + '200': + description: Project deleted successfully + '404': + description: Project not found + /projects/{project_name}/close: + post: + tags: + - Projects + summary: Close a project by name + operationId: closeProjectByName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project to be closed + schema: + type: string + responses: + '200': + description: Project closed successfully + '404': + description: Project not found + /projects/{project_name}/open: + post: + tags: + - Projects + summary: Open a project by name + operationId: openProjectByName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project to be opened + schema: + type: string + responses: + '200': + description: Project opened successfully + '404': + description: Project not found + /projects/{project_name}/save: + post: + tags: + - Projects + summary: Save a project by name + operationId: saveProjectByName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project to be saved + schema: + type: string + responses: + '200': + description: Project saved successfully + '404': + description: Project not found + /projects/{project_name}/diagram-editors: + get: + tags: + - Diagrams + summary: Get a list of all open diagram editors for a project by name + operationId: getDiagramEditorsByProjectName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project + schema: + type: string + responses: + '200': + description: A list of open diagram editors + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiagramEditor' + '404': + description: Project not found + /projects/{project_name}/diagrams: + get: + tags: + - Diagrams + summary: Get a list of all diagrams for a project by name + operationId: getDiagramsByProjectName + parameters: + - name: project_name + in: path + required: true + description: Unique name of the project + schema: + type: string + responses: + '200': + description: A list of diagrams + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Diagram' + '404': + description: Project not found +components: + schemas: + Diagram: + type: object + description: A diagram + properties: + id: + type: string + description: Unique identifier of the diagram + name: + type: string + description: Name of the diagram + description: + type: string + description: Description of the diagram + documentation: + type: string + description: Documentation of the diagram + DiagramEditor: + type: object + description: A diagram editor in Capella + properties: + id: + type: string + description: Unique identifier of the diagram + name: + type: string + description: Name of the diagram + uri: + type: string + description: URI of the diagram + EObject: + type: object + description: An abstract model object + properties: + DUMMY: + type: string + description: Dummy property + Element: + type: object + description: An element + allOf: + - $ref: '#/components/schemas/EObject' + ExtensibleElement: + type: object + description: An extensible element + allOf: + - $ref: '#/components/schemas/Element' + ModelElement: + type: object + description: "Common base for all capella elements\r\n[source: capella study]\n\ + \nUsed in levels: operational,system,logical,physical,epbs" + allOf: + - $ref: '#/components/schemas/ExtensibleElement' + properties: + id: + type: string + description: "The unique identifier for this element\r\n[source: capella\ + \ study]" + sid: + type: string + description: The unique system identifier for this element + Resource: + type: object + description: A generic resource + properties: + fullPath: + type: string + description: > + Full, absolute path of the resource relative to the workspace. A + resource's full path indicates the route from the root of the + workspace to the resource. Within a workspace, there is exactly + one such path for any given resource. The first segment of these + paths name a project; remaining segments, folders and/or files + within that project. The returned path never has a trailing + separator. + location: + type: string + description: Location of the resource in the file system + name: + type: string + default: '' + description: > + Name of the resource. The workspace root's name is the empty + string. + WorkspaceProject: + type: object + description: A workspace project + allOf: + - $ref: '#/components/schemas/Resource' + properties: + open: + type: boolean + description: Whether the project is open or not + modified: + type: boolean + description: Whether the project is in a dirty (unsaved changes) state or not + default: false diff --git a/rest-api/plugin.xml b/rest-api/plugin.xml new file mode 100644 index 0000000..c15b3b7 --- /dev/null +++ b/rest-api/plugin.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/rest-api/plugin.xml.license b/rest-api/plugin.xml.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/rest-api/plugin.xml.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/rest-api/pom.xml b/rest-api/pom.xml new file mode 100644 index 0000000..4d80008 --- /dev/null +++ b/rest-api/pom.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + com.deutschebahn + rest-api + VERSION + + 2.7.5 + 17 + ${java.version} + ${java.version} + 2.2.15 + 3.0.2 + 11.0.15 + 3.1.6 + 2.15.2 + 4.13.2 + 1.4.14 + 5.0.0 + UTF-8 + + + + + + + io.swagger.core.v3 + swagger-jaxrs2-jakarta + compile + ${swagger-core-version} + + + io.swagger.core.v3 + swagger-jaxrs2-servlet-initializer-v2-jakarta + ${swagger-core-version} + + + ch.qos.logback + logback-classic + ${logback-version} + compile + + + ch.qos.logback + logback-core + ${logback-version} + compile + + + junit + junit + ${junit-version} + test + + + jakarta.servlet + jakarta.servlet-api + ${servlet-api-version} + + + org.glassfish.jersey.containers + jersey-container-servlet-core + ${jersey3-version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey3-version} + + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey3-version} + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + ${jersey3-version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-version} + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + ${jackson-version} + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + 2.15.2 + + + + com.brsanthu + migbase64 + 2.2 + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.0 + + + com.sun.xml.bind + jaxb-impl + 4.0.3 + runtime + + + + jakarta.validation + jakarta.validation-api + ${beanvalidation-version} + provided + + + io.swagger + swagger-jaxrs + 1.6.14 + + + io.swagger + swagger-annotations + 1.6.14 + + + jakarta.ws.rs + jakarta.ws.rs-api + 3.1.0 + + + + + + Artifactory - Maven Central Repository + https://bahnhub.tech.rz.db.de:443/artifactory/Maven-Central-remote/ + + + + src + + + org.eclipse.tycho + tycho-packaging-plugin + ${tycho-version} + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + + diff --git a/rest-api/pom.xml.license b/rest-api/pom.xml.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/rest-api/pom.xml.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/rest-api/src/com/db/capella/Application.java b/rest-api/src/com/db/capella/Application.java new file mode 100644 index 0000000..9d4c265 --- /dev/null +++ b/rest-api/src/com/db/capella/Application.java @@ -0,0 +1,16 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +package com.db.capella; + +import org.glassfish.jersey.server.ResourceConfig; + +import com.db.capella.api.JacksonJsonProvider; +import com.db.capella.api.ProjectsApi; + +public class Application extends ResourceConfig { + public Application() { + register(ProjectsApi.class); + register(JacksonJsonProvider.class); + } +} diff --git a/rest-api/src/com/db/capella/Main.java b/rest-api/src/com/db/capella/Main.java new file mode 100644 index 0000000..0f197b1 --- /dev/null +++ b/rest-api/src/com/db/capella/Main.java @@ -0,0 +1,45 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +package com.db.capella; + +import java.net.URI; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.ui.IStartup; +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.server.ResourceConfig; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +public class Main implements IStartup { + public static final String BASE_URI = "http://0.0.0.0:5007/api/v1"; + + public static void log(int severity, String message, Throwable exception) { + Bundle bundle = FrameworkUtil.getBundle(Main.class); + ILog log = Platform.getLog(bundle); + IStatus status = new Status(severity, bundle.getSymbolicName(), + message, exception); + log.log(status); + } + + @Override + public void earlyStartup() { + final ResourceConfig resourceConfig = new Application(); + try { + final HttpServer server = GrizzlyHttpServerFactory.createHttpServer( + URI.create(BASE_URI), + resourceConfig, true); + log(IStatus.INFO, "Capella REST API server listens on " + BASE_URI + " ...", null); + } catch (Exception e) { + String msg = "There was an error while starting Capella REST API server."; + log(IStatus.ERROR, msg, null); + System.err.println(msg); + e.printStackTrace(); + } + } +} diff --git a/rest-api/src/com/db/capella/api/JacksonJsonProvider.java b/rest-api/src/com/db/capella/api/JacksonJsonProvider.java new file mode 100644 index 0000000..e14648f --- /dev/null +++ b/rest-api/src/com/db/capella/api/JacksonJsonProvider.java @@ -0,0 +1,34 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +package com.db.capella.api; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import com.fasterxml.jackson.datatype.jsr310.*; + +import com.fasterxml.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; + +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Produces({MediaType.APPLICATION_JSON}) +public class JacksonJsonProvider extends JacksonXmlBindJsonProvider { + + public JacksonJsonProvider() { + + ObjectMapper objectMapper = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .enable(SerializationFeature.INDENT_OUTPUT) + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .registerModule(new JavaTimeModule()) + .setDateFormat(new RFC3339DateFormat()); + + setMapper(objectMapper); + } +} diff --git a/rest-api/src/com/db/capella/api/impl/ProjectsApiServiceImpl.java b/rest-api/src/com/db/capella/api/impl/ProjectsApiServiceImpl.java new file mode 100644 index 0000000..10557af --- /dev/null +++ b/rest-api/src/com/db/capella/api/impl/ProjectsApiServiceImpl.java @@ -0,0 +1,314 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +package com.db.capella.api.impl; + +import java.io.File; +import java.util.Collection; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.emf.common.util.URI; +import org.eclipse.sirius.business.api.dialect.DialectManager; +import org.eclipse.sirius.business.api.session.Session; +import org.eclipse.sirius.business.api.session.SessionManager; +import org.eclipse.sirius.diagram.ui.part.SiriusDiagramEditor; +import org.eclipse.sirius.ui.business.api.session.SessionEditorInput; +import org.eclipse.sirius.viewpoint.DRepresentationDescriptor; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; + +import com.db.capella.api.ApiException; +import com.db.capella.api.ApiResponseMessage; +import com.db.capella.api.NotFoundException; +import com.db.capella.api.ProjectsApiService; +import com.db.capella.integration.WorkspaceProjectInt; +import com.db.capella.model.Diagram; +import com.db.capella.model.DiagramEditor; +import com.db.capella.model.ImportProjectRequest; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.7.0") +public class ProjectsApiServiceImpl extends ProjectsApiService { + private static URI getEMFURIForAirdFile(IPath directoryPath) { + File directory = directoryPath.toFile(); + if (directory.isDirectory()) { + File[] files = directory.listFiles((dir, name) -> name.endsWith(".aird")); + if (files != null && files.length > 0) { + // Assuming there's only one .aird file in the directory + File airdFile = files[0]; + return URI.createFileURI(airdFile.getAbsolutePath()); + } + } + return null; + } + + private Session getSession(IProject project) throws ApiException { + URI airdFileName = getEMFURIForAirdFile(project.getLocation()); + if (airdFileName == null) { + throw new ApiException(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), + "An .aird file is required to get the SystemEngineering"); + } + final Session session = SessionManager.INSTANCE.getSession(airdFileName, new NullProgressMonitor()); + if (session == null) { + throw new ApiException(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), + "Cannot get session for project"); + } + return session; + } + + private IProject getWorkspaceProjectByName(String name) throws ApiException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject workspaceProject = workspace.getRoot().getProject(name); + if (!workspaceProject.exists()) { + throw new ApiException(Response.Status.NOT_FOUND.getStatusCode(), "Project named '" + name + "' not found"); + } + return workspaceProject; + } + + @Override + public Response closeProjectByName(String projectName, SecurityContext securityContext) throws NotFoundException { + IProject workspaceProject; + try { + workspaceProject = getWorkspaceProjectByName(projectName); + } catch (ApiException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + try { + workspaceProject.close(null); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + return Response.ok() + .entity(new ApiResponseMessage(ApiResponseMessage.OK, "Project '" + projectName + "' is closed.")) + .build(); + } + + @Override + public Response deleteProjectByName(String projectName, + Boolean deleteContents, + SecurityContext securityContext) + throws NotFoundException { + IProject workspaceProject = null; + try { + workspaceProject = getWorkspaceProjectByName(projectName); + } catch (ApiException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + try { + workspaceProject.delete(deleteContents, true, null); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "Project deleted")).build(); + } + + @Override + public Response getDiagramEditorsByProjectName(String projectName, SecurityContext securityContext) + throws NotFoundException { + java.util.List diagramEditorList = new java.util.ArrayList(); + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow[] windows = workbench.getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + IWorkbenchPage[] pages = window.getPages(); + for (IWorkbenchPage page : pages) { + IEditorReference[] editorReferences = page.getEditorReferences(); + for (IEditorReference editorReference : editorReferences) { + // IEditorPart editorPart = editorReference.getEditor(false); + // if (editorPart instanceof SiriusDiagramEditor) { + try { + IEditorInput input = editorReference.getEditorInput(); + if (input instanceof SessionEditorInput) { + SessionEditorInput sessionEditorInput = (SessionEditorInput) input; + URI uri = sessionEditorInput.getURI(); + IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFile(new Path(uri.toPlatformString(true))); + IProject project = file.getProject(); + if (project.getName().equals(projectName)) { + DiagramEditor diagramEditor = new DiagramEditor(); + diagramEditor.setName(sessionEditorInput.getName()); + diagramEditor.setUri(sessionEditorInput.getURI().toString()); + String repDesUri = sessionEditorInput.getRepDescUri().toString(); + diagramEditor.setId(repDesUri.split("#")[1]); + diagramEditorList.add(diagramEditor); + if (editorReference.getEditor(false) == null) { + System.out.println(sessionEditorInput.getName() + " is null"); + } + } + } + } catch (PartInitException e) { + } + } + } + } + + return Response.ok().entity(diagramEditorList).build(); + } + + @Override + public Response getDiagramsByProjectName(String projectName, SecurityContext securityContext) + throws NotFoundException { + IProject workspaceProject = null; + try { + workspaceProject = getWorkspaceProjectByName(projectName); + } catch (ApiException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + Session session = null; + try { + session = getSession(workspaceProject); + } catch (ApiException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + Collection representationDescriptors = DialectManager.INSTANCE + .getAllRepresentationDescriptors(session); + java.util.List diagramList = new java.util.ArrayList(); + if (true) { + for (DRepresentationDescriptor representationDescriptor : representationDescriptors) { + Diagram diagram = new Diagram(); + diagram.setId(representationDescriptor.getUid()); + diagram.setName(representationDescriptor.getName()); + // DRepresentation representation = + // representationDescriptor.getRepresentation(); + // DDiagram dDiagram = (DDiagram) representation; + // for (DRepresentationElement representationElement : + // representation.getRepresentationElements()) { + // if (representationElement.getTarget() instanceof ModelElement) { + // ModelElement modelElement = (ModelElement) representationElement.getTarget(); + // System.out.println(modelElement.getLabel()); + // System.out.println(modelElement.getFullLabel()); + // } + // } + // diagram.setId(representation.getUid()); + // diagram.setDescription(representationDescriptor.getDescription()); + // diagram.setName(representation.getName()); + diagramList.add(diagram); + } + } + return Response.ok().entity(diagramList).build(); + } + + @Override + public Response getProjectByName(String projectName, + SecurityContext securityContext) + throws NotFoundException { + // do some magic! + return Response.ok() + .entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")) + .build(); + } + + @Override + public Response importProject(ImportProjectRequest body, SecurityContext securityContext) + throws NotFoundException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + String projectFolderPath = body.getLocation(); + File projectFolder = new File(projectFolderPath); + if (!projectFolder.exists()) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Project folder not found")).build(); + } + File dotProjectFile = new File(projectFolder, ".project"); + if (!dotProjectFile.exists()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, + "Project folder does not contain .project file")) + .build(); + } + IPath projectLocation = new Path(dotProjectFile.getAbsolutePath()); + try { + IProjectDescription projectDescription = workspace + .loadProjectDescription(projectLocation); + IProject project = workspace.getRoot().getProject(projectDescription.getName()); + if (project.exists()) { + return Response.status(Response.Status.CONFLICT) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Project already exists")).build(); + } + project.create(projectDescription, null); + project.open(null); + WorkspaceProjectInt projectInt = new WorkspaceProjectInt( + (org.eclipse.core.internal.resources.Project) project); + // todo: URL in response header is `http://localhost:5007/projects/(...)` which + // is wrong + return Response.created(new java.net.URI("/projects/" + projectInt.getName())) + .build(); + } catch (Exception e) { + e.printStackTrace(); + } + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Failed to import project")).build(); + } + + @Override + public Response listProjects(SecurityContext securityContext) + throws NotFoundException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject[] projects = workspace.getRoot().getProjects(); + java.util.List projectIntList = new java.util.ArrayList(); + for (IProject project : projects) { + WorkspaceProjectInt projectInt = new WorkspaceProjectInt( + (org.eclipse.core.internal.resources.Project) project); + projectIntList.add(projectInt); + } + return Response.ok().entity(projectIntList).build(); + } + + @Override + public Response openProjectByName(String projectName, SecurityContext securityContext) throws NotFoundException { + IProject workspaceProject; + try { + workspaceProject = getWorkspaceProjectByName(projectName); + } catch (ApiException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + try { + workspaceProject.open(null); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + return Response.ok() + .entity(new ApiResponseMessage(ApiResponseMessage.OK, "Project '" + projectName + "' is opened.")) + .build(); + } + + @Override + public Response saveProjectByName(String projectName, + SecurityContext securityContext) + throws NotFoundException { + IProject workspaceProject; + try { + workspaceProject = getWorkspaceProjectByName(projectName); + } catch (ApiException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, e.getMessage())).build(); + } + WorkspaceProjectInt projectInt = new WorkspaceProjectInt( + (org.eclipse.core.internal.resources.Project) workspaceProject); + projectInt.save(); + return Response.ok() + .entity(new ApiResponseMessage(ApiResponseMessage.OK, "Project '" + projectName + "' is saved.")) + .build(); + } +} diff --git a/rest-api/src/com/db/capella/integration/ReflectionUtils.java b/rest-api/src/com/db/capella/integration/ReflectionUtils.java new file mode 100644 index 0000000..a671665 --- /dev/null +++ b/rest-api/src/com/db/capella/integration/ReflectionUtils.java @@ -0,0 +1,96 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +package com.db.capella.integration; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +public class ReflectionUtils { + public static void copyProperties(Object source, Object target) { + Class sourceClass = source.getClass(); + Class targetClass = target.getClass(); + + Map getters = getGetters(sourceClass); + Map setters = getSetters(targetClass); + + for (Map.Entry getterEntry : getters.entrySet()) { + String propName = getterEntry.getKey(); + Method getterMethod = getterEntry.getValue(); + if (setters.containsKey(propName)) { + Method setterMethod = setters.get(propName); + try { + Object value = getterMethod.invoke(source); + setterMethod.invoke(target, value); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + public static Map getGetters(Class clazz) { + Map getters = new HashMap<>(); + populateGetters(getters, clazz); + return getters; + } + + public static Map getSetters(Class clazz) { + Map setters = new HashMap<>(); + populateSetters(setters, clazz); + return setters; + } + + public static void populateGetters(Map getters, Class clazz) { + if (clazz == null || clazz == Object.class) { + return; + } + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (isGetter(method)) { + String propName = extractPropertyName(method.getName()); + getters.putIfAbsent(propName, method); + } + } + populateGetters(getters, clazz.getSuperclass()); + } + + public static void populateSetters(Map setters, Class clazz) { + if (clazz == null || clazz == Object.class) { + return; + } + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (isSetter(method)) { + String propName = extractPropertyName(method.getName()); + setters.putIfAbsent(propName, method); + } + } + populateSetters(setters, clazz.getSuperclass()); + } + + public static boolean isGetter(Method method) { + return method.getParameterTypes().length == 0 + && (method.getName().startsWith("get") || method.getName().startsWith("is")); + } + + public static boolean isSetter(Method method) { + return method.getName().startsWith("set") && + method.getParameterTypes().length == 1; + } + + public static String extractPropertyName(String methodName) { + String prefix = ""; + if (methodName.startsWith("get")) { + prefix = "get"; + } else if (methodName.startsWith("is")) { + prefix = "is"; + } else if (methodName.startsWith("set")) { + prefix = "set"; + } + String propName = methodName.substring(prefix.length(), prefix.length() + + 1).toLowerCase() + methodName.substring(prefix.length() + 1); + return propName; + } +} diff --git a/rest-api/src/com/db/capella/integration/WorkspaceProjectInt.java b/rest-api/src/com/db/capella/integration/WorkspaceProjectInt.java new file mode 100644 index 0000000..c499adb --- /dev/null +++ b/rest-api/src/com/db/capella/integration/WorkspaceProjectInt.java @@ -0,0 +1,100 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +package com.db.capella.integration; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.FileEditorInput; + +import com.db.capella.model.WorkspaceProject; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.ALWAYS) +public class WorkspaceProjectInt extends WorkspaceProject { + org.eclipse.core.resources.IProject obj; + + public WorkspaceProjectInt(org.eclipse.core.resources.IProject obj) { + super(); + this.obj = obj; + ReflectionUtils.copyProperties(obj, this); + this.computeModified(); + } + + public void computeModified() { + this.setModified(false); + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow[] windows = workbench.getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + IWorkbenchPage[] pages = window.getPages(); + for (IWorkbenchPage page : pages) { + IEditorPart[] editors = page.getDirtyEditors(); + for (IEditorPart editor : editors) { + if (editor.getEditorInput() instanceof FileEditorInput) { + FileEditorInput input = (FileEditorInput) editor.getEditorInput(); + IResource resource = input.getFile(); + if (resource.getProject().equals(this.obj)) { + // Found a dirty editor in the project + this.setModified(true); + return; + } + } + } + } + } + } + + public void save() { + org.eclipse.core.resources.IProject obj = this.obj; + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow[] workbenchWindows = workbench.getWorkbenchWindows(); + + for (IWorkbenchWindow window : workbenchWindows) { + IWorkbenchPage[] pages = window.getPages(); + for (IWorkbenchPage page : pages) { + IEditorPart[] editors = page.getDirtyEditors(); + for (IEditorPart editor : editors) { + if (editor.getEditorInput() instanceof FileEditorInput) { + FileEditorInput input = (FileEditorInput) editor.getEditorInput(); + IResource resource = input.getFile(); + org.eclipse.core.resources.IProject editorProject = resource.getProject(); + + // Check if the editor is associated with the given + // project + if (obj.equals(editorProject)) { + // Save the editor + page.saveEditor(editor, false); + } + } + } + } + } + } + }); + } + + public void setFullPath(IPath fullPath) { + if (fullPath == null) { + this.setFullPath(""); + } else { + this.setFullPath(fullPath.toString()); + } + } + + public void setLocation(IPath location) { + if (location == null) { + this.setLocation(""); + } else { + this.setLocation(location.toString()); + } + } +}