├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── README.md ├── copilot-instructions.md ├── dependabot.yml └── workflows │ ├── integration-tests.yml │ ├── quick-build.yml │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── workspace.code-workspace ├── Dockerfile ├── LICENSE ├── pom.xml └── src ├── main ├── java │ └── us │ │ └── fatehi │ │ └── schemacrawler │ │ └── webapp │ │ ├── AsyncConfiguration.java │ │ ├── MultipartFileConverter.java │ │ ├── SchemaCrawlerWebApplication.java │ │ ├── controller │ │ ├── DiagramRequestController.java │ │ ├── DiagramResultController.java │ │ ├── ErrorController.java │ │ └── URIConstants.java │ │ ├── model │ │ ├── DiagramKey.java │ │ ├── DiagramRequest.java │ │ └── DiagramRequestUtility.java │ │ └── service │ │ ├── WebappConfig.java │ │ ├── notification │ │ ├── AmazonSESNotificationConfig.java │ │ ├── AmazonSESNotificationService.java │ │ └── NotificationService.java │ │ ├── processing │ │ └── ProcessingService.java │ │ └── storage │ │ ├── AWSConfig.java │ │ ├── AmazonS3StorageConfig.java │ │ ├── AmazonS3StorageService.java │ │ ├── FileExtensionType.java │ │ └── StorageService.java └── resources │ ├── api │ └── schemacrawler-web-application.yaml │ ├── application.yaml │ ├── banner.txt │ ├── static │ ├── css │ │ └── style.css │ └── images │ │ └── schemacrawler_logo.svg │ └── templates │ ├── SchemaCrawlerDiagram.html │ ├── SchemaCrawlerDiagramForm.html │ ├── SchemaCrawlerDiagramResult.html │ ├── error.html │ └── fragments │ ├── footer.html │ ├── head.html │ └── header.html └── test ├── java └── us │ └── fatehi │ └── schemacrawler │ └── webapp │ └── test │ ├── ControllerRoundtripTest.java │ ├── DiagramRequestUtilityTest.java │ ├── LocalStackS3BucketTest.java │ ├── RequestControllerAPITest.java │ ├── RequestControllerTest.java │ ├── RequestControllerWithS3Test.java │ ├── ResultControllerAPITest.java │ ├── ResultControllerTest.java │ ├── service │ ├── notification │ │ └── LogNotificationService.java │ └── storage │ │ ├── FileSystemStorageConfig.java │ │ └── FileSystemStorageService.java │ └── utility │ ├── S3ServiceControllerTestConfig.java │ └── TestUtility.java ├── postman ├── how-to-run-locally.md ├── schemacrawler-web-application.postman_collection.json ├── test.data └── test.db └── resources └── test.db /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/java:0-8 2 | 3 | ARG MAVEN_VERSION="" 4 | RUN \ 5 | su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\"" 6 | 7 | RUN \ 8 | apt-get update && \ 9 | export DEBIAN_FRONTEND=noninteractive && \ 10 | apt-get -y install --no-install-recommends gnupg2 graphviz fonts-freefont-ttf && \ 11 | apt-get clean && \ 12 | apt-get autoremove --purge && \ 13 | rm -rf /var/lib/apt/lists/* 14 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SchemaCrawler - Web Application", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "INSTALL_MAVEN": "true", 7 | "INSTALL_GRADLE": "true", 8 | "NODE_VERSION": "none" 9 | } 10 | }, 11 | "settings": { 12 | "workbench.startupEditor": "none", 13 | "java.home": "/docker-java-home", 14 | "java.import.gradle.java.home": "/usr/local/sdkman/candidates/java/current", 15 | "java.configuration.runtimes": [ 16 | { 17 | "default": true, 18 | "name": "JavaSE-1.8", 19 | "path": "/usr/local/sdkman/candidates/java/current" 20 | } 21 | ], 22 | "files.exclude": { 23 | "workspace": true 24 | }, 25 | "java.configuration.updateBuildConfiguration": "automatic", 26 | "java.format.settings.profile": "GoogleStyle", 27 | "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml", 28 | "editor.tabSize": 2, 29 | "editor.foldingImportsByDefault": true, 30 | "files.trimTrailingWhitespace": true, 31 | "git.enableCommitSigning": true 32 | }, 33 | "extensions": [ 34 | "vscjava.vscode-java-pack", 35 | "shengchen.vscode-checkstyle", 36 | "editorconfig.editorconfig", 37 | "pivotal.vscode-boot-dev-pack", 38 | "pivotal.vscode-spring-boot", 39 | "redhat.vscode-xml", 40 | "jebbs.markdown-extended", 41 | "davidanson.vscode-markdownlint" 42 | ], 43 | "remoteUser": "vscode" 44 | } 45 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | !target 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sualeh @schemacrawler 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sualeh 2 | custom: 'https://www.paypal.me/sualeh' 3 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | [![Quick Build](https://github.com/schemacrawler/SchemaCrawler-Web-Application/workflows/Quick%20Build/badge.svg)](https://github.com/schemacrawler/SchemaCrawler-Web-Application/actions?query=workflow%3A%22Quick+Build%22) 2 | [![Integration Tests](https://github.com/schemacrawler/SchemaCrawler-Web-Application/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/schemacrawler/SchemaCrawler-Web-Application/actions/workflows/integration-tests.yml) 3 | [![codecov](https://codecov.io/gh/schemacrawler/SchemaCrawler-Web-Application/branch/main/graph/badge.svg)](https://app.codecov.io/gh/schemacrawler/SchemaCrawler-Web-Application) 4 | 5 | 6 | 7 | # SchemaCrawler Web Application 8 | 9 | > **Note**: Please see the [SchemaCrawler website](https://www.schemacrawler.com/) for more details. 10 | 11 | 12 | ## Technologies 13 | 14 | This is a Spring Boot web application with a Bootstrap user interface, with source code control in GitHub, which is automatically built on every commit by GitHub Actions using a Maven build, tests are run, and coverage measured with JaCoCo and Codecov.io, and then immediately deployed to Heroku using a Docker image, which generates an crows-foot ERD of a SQLite database. 15 | 16 | 17 | ## Build and Run 18 | 19 | ### Build 20 | 21 | - Install [Graphviz](https://www.graphviz.org), which is a prerequisite for SchemaCrawler 22 | - Install Docker 23 | - Build application from Maven, run `mvn clean package` 24 | 25 | 26 | ### Build Docker Image 27 | 28 | - Follow the steps above 29 | - Install Docker 30 | - Build application and Docker image from Maven, run `mvn -Ddocker.skip=false clean package` 31 | 32 | 33 | ### Start the Server 34 | 35 | - Set the following environmental variables locally 36 | - AWS_ACCESS_KEY_ID 37 | - AWS_SECRET_ACCESS_KEY 38 | - AWS_S3_BUCKET 39 | - Do one of the steps below to start the web application locally on your system 40 | - Start the application from Maven, run 41 | `mvn -Dspring-boot.run.fork=false spring-boot:run` 42 | - Start application from the jar file, run 43 | `java -jar target/schemacrawler-webapp-16.26.1-1.jar` 44 | - Start the application from the local image in a Docker container, run 45 | `docker run -d --rm --env AWS_ACCESS_KEY_ID=xxxxx --env AWS_SECRET_ACCESS_KEY=xxxxx --env AWS_S3_BUCKET=xxxxx -p 8080:8080 -t schemacrawler/schemacrawler-webapp` 46 | 47 | 48 | ### Use the Application 49 | 50 | Then, after you ensure that the web server is running, either from the command-line, 51 | or the Docker container, open a browser to 52 | [https://localhost:8080](https://localhost:8080) 53 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Instructions for Java Projects Using Apache Maven ## General Coding Guidelines - Prefer **immutability** and use the `final` keyword for fields, parameters, and local variables when appropriate. - Follow **Java 21 best practices**, including usage of `Optional`, `Streams`, and functional programming where applicable. - Ensure **thread safety** by avoiding mutable shared state and using synchronized wrappers or concurrency utilities when necessary. - Use **meaningful names** for classes, methods, and variables to improve code readability. - Follow **SOLID principles** to enhance maintainability and scalability. - Write meaningful **javadocs** for functions and classes. ## Project Structure - Organize packages based on functionality (e.g., `service`, `repository`, `controller`). - Use **Apache Maven** for build management and dependency resolution. - Maintain a **consistent project structure**, following standard conventions. ## Dependencies and Versions - Define **explicit versions** for dependencies to prevent compatibility issues. - Prefer **dependency management** using `dependencyManagement` in `pom.xml` for centralized version control. - Use **dependency exclusions** where necessary to avoid unwanted transitive dependencies. ## Testing and Quality - Write **unit tests** for business logic using JUnit 5 with Hamcrest matchers. - Use **Mockito** for mocking dependencies in tests. - Maintain **high test coverage** to ensure reliability. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | schedule: 7 | - cron: '12 0/11 * * *' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | name: Integration Tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | # SETUP BUILD ENVIRONMENT 20 | - id: prepare-maven-build 21 | name: Prepare Maven build 22 | uses: sualeh/prepare-maven-build@v1.4.0 23 | with: 24 | java-version: 21 25 | - id: install-graphviz 26 | name: Install Graphviz 27 | uses: sualeh/install-graphviz@v1.0.3 28 | - id: setup-node 29 | name: Set up Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '20.x' 33 | - id: install-newman 34 | name: Install newman 35 | shell: bash 36 | run: | 37 | # Install newman 38 | npm install -g newman 39 | newman -v 40 | 41 | # BUILD DEPENDENCIES 42 | - id: checkout-schemacrawler 43 | name: Checkout SchemaCrawler 44 | uses: actions/checkout@v4 45 | with: 46 | repository: schemacrawler/SchemaCrawler 47 | path: SchemaCrawler 48 | - id: build-schemacrawler 49 | name: Build SchemaCrawler for local Maven repository 50 | shell: bash 51 | run: | 52 | # Build SchemaCrawler 53 | cd SchemaCrawler 54 | mvn \ 55 | --no-transfer-progress \ 56 | --batch-mode \ 57 | -DskipTests=true \ 58 | clean install 59 | 60 | # BUILD AND TEST 61 | - id: build-test 62 | name: Build and run tests 63 | shell: bash 64 | run: | 65 | # Build 66 | mvn \ 67 | --no-transfer-progress \ 68 | --batch-mode \ 69 | package 70 | 71 | # RUN INTEGRATION TESTS WITH NEWMAN 72 | - id: run-postman 73 | name: Run Postman tests for API 74 | env: 75 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 76 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 77 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 78 | shell: bash 79 | run: | 80 | # Build 81 | timeout 120s mvn spring-boot:run > application.log 2>&1 & 82 | sleep 15 83 | newman run \ 84 | --verbose \ 85 | --color on \ 86 | --delay-request 1000 \ 87 | --working-dir src/test/postman \ 88 | --env-var "url=http://localhost:8080" \ 89 | src/test/postman/schemacrawler-web-application.postman_collection.json 90 | - id: upload-application-log 91 | name: Upload application log 92 | uses: actions/upload-artifact@v4 93 | if: always() 94 | with: 95 | name: application-log 96 | path: ./application.log 97 | retention-days: 5 98 | -------------------------------------------------------------------------------- /.github/workflows/quick-build.yml: -------------------------------------------------------------------------------- 1 | name: Quick Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | build: 18 | name: Quick build 19 | runs-on: ubuntu-latest 20 | 21 | # Service containers to run with workflow 22 | services: 23 | swagger-editor: 24 | image: swaggerapi/swagger-editor 25 | ports: 26 | - 80:8080 27 | 28 | 29 | steps: 30 | 31 | # SETUP BUILD ENVIRONMENT 32 | - id: prepare-maven-build 33 | name: Prepare Maven build 34 | uses: sualeh/prepare-maven-build@v1.4.0 35 | with: 36 | java-version: 21 37 | - id: install-graphviz 38 | name: Install Graphviz 39 | uses: sualeh/install-graphviz@v1.0.3 40 | 41 | # BUILD DEPENDENCIES 42 | - id: checkout-schemacrawler 43 | name: Checkout SchemaCrawler 44 | uses: actions/checkout@v4 45 | with: 46 | repository: schemacrawler/SchemaCrawler 47 | path: SchemaCrawler 48 | - id: build-schemacrawler 49 | name: Build SchemaCrawler for local Maven repository 50 | shell: bash 51 | run: | 52 | # Build SchemaCrawler 53 | cd SchemaCrawler 54 | mvn \ 55 | --no-transfer-progress \ 56 | --batch-mode \ 57 | -DskipTests=true \ 58 | clean install 59 | 60 | # VALIDATE OPENAPI SPECIFICATION 61 | - id: validate-oas 62 | name: Validate OpenAPI definition 63 | uses: swaggerexpert/swagger-editor-validate@v1 64 | with: 65 | swagger-editor-url: http://localhost/ 66 | definition-file: src/main/resources/api/schemacrawler-web-application.yaml 67 | 68 | # BUILD AND TEST 69 | - id: build-test 70 | name: Build and run tests 71 | shell: bash 72 | run: | 73 | # Build 74 | mvn \ 75 | --no-transfer-progress \ 76 | --batch-mode \ 77 | clean package 78 | - id: publish-test-results 79 | name: Upload results and coverage 80 | if: contains(github.ref, 'main') 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | CODECOV_TOKEN: 350b716c-8f65-451b-b438-072f0662637d 84 | shell: bash 85 | run: | 86 | bash <(curl -s https://codecov.io/bash) 87 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | name: Create Release 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | # VALIDATE TAGS 20 | - id: validate-semver 21 | name: Validate tag against semantic versioning 22 | if: startsWith(github.ref, 'refs/tags/') 23 | shell: bash 24 | run: | 25 | SEMVER_PATTERN="^refs/tags/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$" 26 | if [[ ! $GITHUB_REF =~ $SEMVER_PATTERN ]] 27 | then 28 | echo "Tag $GITHUB_REF does not follow semantic versioning" 29 | exit 1 30 | fi 31 | 32 | # SETUP BUILD ENVIRONMENT 33 | - id: prepare-maven-build 34 | name: Prepare Maven build 35 | uses: sualeh/prepare-maven-build@v1.4.0 36 | with: 37 | java-version: 21 38 | - id: install-graphviz 39 | name: Install Graphviz 40 | uses: sualeh/install-graphviz@v1.0.3 41 | 42 | # BUILD FOR DISTRIBUTION 43 | # To build an image using Cloud Native Buildpacks for Spring Boot, provide 44 | # and additional -Ddocker.skip=false argument to the Maven build. 45 | - id: build 46 | name: Build and test for distribution 47 | shell: bash 48 | run: | 49 | # Build 50 | mvn \ 51 | --no-transfer-progress \ 52 | --batch-mode \ 53 | clean install 54 | 55 | # CREATE GITHUB RELEASE AND ADD ASSETS 56 | - id: create-release 57 | name: Create GitHub release 58 | uses: actions/create-release@latest 59 | if: startsWith(github.ref, 'refs/tags/') 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | tag_name: ${{ github.ref }} 64 | release_name: ${{ github.ref }} 65 | body: | 66 | SchemaCrawler Webapp Release ${{ github.sha }} 67 | draft: false 68 | prerelease: false 69 | - id: upload-release-zip 70 | name: Upload SchemaCrawler Webapp distribution 71 | uses: actions/upload-release-asset@latest 72 | if: startsWith(github.ref, 'refs/tags/') 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | upload_url: ${{ steps.create-release.outputs.upload_url }} 77 | asset_path: ./target/schemacrawler-webapp-16.26.1-1.jar 78 | asset_name: schemacrawler-webapp-16.26.1-1.jar 79 | asset_content_type: application/zip 80 | 81 | # BUILD AND PUBLISH DOCKER IMAGE 82 | - id: setup-qemu 83 | name: Setup QEMU 84 | uses: docker/setup-qemu-action@v3 85 | 86 | - id: setup-buildx 87 | name: Setup Docker Buildx 88 | uses: docker/setup-buildx-action@v3 89 | 90 | - name: Log into Docker Hub 91 | uses: docker/login-action@v3 92 | with: 93 | username: ${{ secrets.DOCKER_USERNAME }} 94 | password: ${{ secrets.DOCKER_PASSWORD }} 95 | 96 | - name: Build and push Docker image 97 | uses: docker/build-push-action@v6 98 | with: 99 | file: ./Dockerfile 100 | context: . 101 | platforms: |- 102 | linux/amd64 103 | linux/arm64 104 | tags: |- 105 | schemacrawler/schemacrawler-webapp:v16.26.1-1 106 | schemacrawler/schemacrawler-webapp:latest 107 | sbom: true 108 | provenance: true 109 | push: true 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | ~/ 4 | *.versionsBackup 5 | tree.txt 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | **/*.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | nbproject/private/ 23 | build/ 24 | nbbuild/ 25 | dist/ 26 | nbdist/ 27 | .nb-gradle/ 28 | /uploaded-files/ 29 | keys.txt 30 | *.db 31 | *.png 32 | **/launch.json 33 | 34 | /src/test/postman/production.postman_environment.json 35 | application.log 36 | tree.txt 37 | /src/test/postman/api-*.json 38 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vscjava.vscode-java-debug" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "scm.showInputActionButton": false, 3 | "git.showActionButton": { 4 | "commit": false, 5 | "publish": false, 6 | "sync": false 7 | }, 8 | "git.postCommitCommand": "sync", 9 | "java.configuration.updateBuildConfiguration": "automatic", 10 | "java.compile.nullAnalysis.mode": "automatic", 11 | "editor.formatOnSave": false, 12 | "editor.formatOnType": true 13 | } -------------------------------------------------------------------------------- /.vscode/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ], 7 | "settings": { 8 | "workbench.startupEditor": "none", 9 | "files.exclude": { 10 | "workspace": true 11 | }, 12 | "java.configuration.updateBuildConfiguration": "automatic", 13 | "java.format.settings.profile": "GoogleStyle", 14 | "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml", 15 | "java.test.config": [ 16 | { 17 | "name": "Standard", 18 | "workingDirectory": "${workspaceFolder}" 19 | }, 20 | { 21 | "name": "Run Heavy Database Tests", 22 | "vmargs": [ 23 | "-Dheavydb" 24 | ] 25 | } 26 | ], 27 | "editor.tabSize": 2, 28 | "editor.foldingImportsByDefault": true, 29 | "files.trimTrailingWhitespace": true, 30 | "git.enableCommitSigning": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # SchemaCrawler 3 | # http://www.schemacrawler.com 4 | # Copyright (c) 2000-2025, Sualeh Fatehi . 5 | # All rights reserved. 6 | # ------------------------------------------------------------------------ 7 | # 8 | # SchemaCrawler is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 11 | # 12 | # SchemaCrawler and the accompanying materials are made available under 13 | # the terms of the Eclipse Public License v1.0, GNU General Public License 14 | # v3 or GNU Lesser General Public License v3. 15 | # 16 | # You may elect to redistribute this code under any of these licenses. 17 | # 18 | # The Eclipse Public License is available at: 19 | # http://www.eclipse.org/legal/epl-v10.html 20 | # 21 | # The GNU General Public License v3 and the GNU Lesser General Public 22 | # License v3 are available at: 23 | # http://www.gnu.org/licenses/ 24 | # 25 | # ======================================================================== 26 | 27 | FROM eclipse-temurin:21-jdk-alpine 28 | 29 | ARG SCHEMACRAWLER_VERSION=16.26.1 30 | ARG SCHEMACRAWLER_WEBAPP_VERSION=16.26.1-1 31 | 32 | LABEL \ 33 | "maintainer"="Sualeh Fatehi " \ 34 | "org.opencontainers.image.authors"="Sualeh Fatehi " \ 35 | "org.opencontainers.image.title"="SchemaCrawler Web Application" \ 36 | "org.opencontainers.image.description"="Free database schema discovery and comprehension tool" \ 37 | "org.opencontainers.image.url"="https://www.schemacrawler.com/" \ 38 | "org.opencontainers.image.source"="https://github.com/schemacrawler/SchemaCrawler-Web-Application" \ 39 | "org.opencontainers.image.vendor"="SchemaCrawler" \ 40 | "org.opencontainers.image.license"="(GPL-3.0 OR OR LGPL-3.0+ EPL-1.0)" 41 | 42 | 43 | # Install Graphviz as root user 44 | RUN \ 45 | apk add --update --no-cache \ 46 | bash \ 47 | bash-completion \ 48 | graphviz \ 49 | ttf-freefont 50 | 51 | # Copy SchemaCrawler Web Application files for the current user 52 | COPY \ 53 | ./target/schemacrawler-webapp-${SCHEMACRAWLER_WEBAPP_VERSION}.jar \ 54 | schemacrawler-webapp.jar 55 | 56 | # Expose the port the application will run on 57 | EXPOSE 8080 58 | 59 | # Run the web-application. Define default port and java options as environment variables 60 | ENV JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom" 61 | ENV PORT=8080 62 | 63 | # Default command to run the application 64 | ENTRYPOINT java $JAVA_OPTS -Dserver.port=$PORT -jar schemacrawler-webapp.jar 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | us.fatehi 8 | schemacrawler-webapp 9 | 16.26.1-1 10 | jar 11 | 12 | SchemaCrawler Web Application 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 3.4.1 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | true 25 | 21 26 | 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-dependencies 33 | 3.5.0 34 | pom 35 | import 36 | 37 | 38 | org.junit 39 | junit-bom 40 | 5.13.0 41 | pom 42 | import 43 | 44 | 45 | software.amazon.awssdk 46 | bom 47 | 2.31.54 48 | pom 49 | import 50 | 51 | 52 | com.atlassian.oai 53 | swagger-request-validator 54 | 2.44.8 55 | pom 56 | import 57 | 58 | 59 | us.fatehi 60 | schemacrawler-parent 61 | 16.26.1 62 | import 63 | pom 64 | 65 | 66 | org.testcontainers 67 | testcontainers-bom 68 | 1.21.1 69 | pom 70 | import 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-web 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-thymeleaf 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-starter-validation 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-starter-actuator 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-devtools 96 | runtime 97 | true 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-starter-test 102 | test 103 | 104 | 105 | 106 | commons-io 107 | commons-io 108 | 2.19.0 109 | 110 | 111 | com.google.code.gson 112 | gson 113 | 114 | 115 | software.amazon.awssdk 116 | s3 117 | 118 | 119 | com.sun.mail 120 | jakarta.mail 121 | 2.0.1 122 | 123 | 124 | 125 | org.eclipse.angus 126 | jakarta.mail 127 | 128 | 129 | software.amazon.awssdk 130 | ses 131 | 132 | 133 | org.apache.commons 134 | commons-lang3 135 | 136 | 137 | 138 | us.fatehi 139 | schemacrawler-sqlite 140 | 16.26.1 141 | 142 | 143 | 144 | 145 | org.apache.tika 146 | tika-core 147 | 3.2.0 148 | 149 | 150 | 151 | org.testcontainers 152 | junit-jupiter 153 | test 154 | 155 | 156 | nl.jqno.equalsverifier 157 | equalsverifier 158 | 4.0 159 | test 160 | 161 | 162 | org.testcontainers 163 | localstack 164 | test 165 | 166 | 167 | 168 | 169 | com.atlassian.oai 170 | swagger-request-validator-mockmvc 171 | test 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | true 183 | org.apache.maven.plugins 184 | maven-enforcer-plugin 185 | 3.5.2 186 | 187 | 188 | org.apache.maven.plugins 189 | maven-clean-plugin 190 | 3.5.0 191 | 192 | 193 | org.apache.maven.plugins 194 | maven-resources-plugin 195 | 3.3.1 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-source-plugin 200 | 3.3.1 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-compiler-plugin 205 | 3.14.0 206 | 207 | 208 | org.apache.maven.plugins 209 | maven-surefire-plugin 210 | 3.5.3 211 | 212 | 213 | org.apache.maven.plugins 214 | maven-jar-plugin 215 | 3.4.2 216 | 217 | 218 | org.apache.maven.plugins 219 | maven-javadoc-plugin 220 | 3.11.2 221 | 222 | 21 223 | 224 | 225 | 226 | org.apache.maven.plugins 227 | maven-install-plugin 228 | 3.1.4 229 | 230 | 231 | org.apache.maven.plugins 232 | maven-gpg-plugin 233 | 3.2.7 234 | 235 | 236 | org.apache.maven.plugins 237 | maven-assembly-plugin 238 | 3.7.1 239 | 240 | 241 | org.apache.maven.plugins 242 | maven-shade-plugin 243 | 3.6.0 244 | 245 | 246 | org.apache.maven.plugins 247 | maven-deploy-plugin 248 | 3.1.4 249 | 250 | 251 | org.sonatype.plugins 252 | nexus-staging-maven-plugin 253 | 1.7.0 254 | 255 | 256 | org.codehaus.mojo 257 | exec-maven-plugin 258 | 3.5.1 259 | 260 | 261 | 262 | 263 | 264 | org.apache.maven.plugins 265 | maven-toolchains-plugin 266 | 3.2.0 267 | 268 | 269 | 270 | toolchain 271 | 272 | 273 | 274 | 275 | 276 | 277 | 21 278 | temurin 279 | 280 | 281 | 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-compiler-plugin 286 | 287 | 21 288 | 21 289 | 290 | 291 | 292 | org.springframework.boot 293 | spring-boot-maven-plugin 294 | 295 | 296 | repackage 297 | package 298 | 299 | repackage 300 | 301 | 302 | 303 | 304 | 305 | org.jacoco 306 | jacoco-maven-plugin 307 | 0.8.13 308 | 309 | 310 | 311 | prepare-agent 312 | 313 | 314 | 315 | report 316 | test 317 | 318 | report 319 | 320 | 321 | 322 | 323 | 324 | org.codehaus.mojo 325 | exec-maven-plugin 326 | 327 | 328 | docker-build 329 | package 330 | 331 | exec 332 | 333 | 334 | ${docker.skip} 335 | docker 336 | ${project.basedir} 337 | 338 | build 339 | -f 340 | ${project.basedir}/Dockerfile 341 | -t 342 | schemacrawler/schemacrawler-webapp:v${project.version} 343 | -t 344 | schemacrawler/schemacrawler-webapp:latest 345 | ${project.basedir} 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/AsyncConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | 29 | package us.fatehi.schemacrawler.webapp; 30 | 31 | import org.apache.commons.lang3.exception.ExceptionUtils; 32 | import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; 33 | import org.springframework.context.annotation.Configuration; 34 | import org.springframework.scheduling.annotation.AsyncConfigurer; 35 | import org.springframework.scheduling.annotation.EnableAsync; 36 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 37 | import java.util.Arrays; 38 | import java.util.concurrent.Executor; 39 | import java.util.logging.Logger; 40 | 41 | @Configuration 42 | @EnableAsync 43 | public class AsyncConfiguration implements AsyncConfigurer { 44 | 45 | private static final Logger logger = Logger.getLogger(AsyncConfiguration.class.getName()); 46 | 47 | @Override 48 | public Executor getAsyncExecutor() { 49 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 50 | executor.setCorePoolSize(2); 51 | executor.setMaxPoolSize(5); 52 | executor.setQueueCapacity(500); 53 | executor.setThreadNamePrefix("AsyncExecutor-"); 54 | executor.initialize(); 55 | return executor; 56 | } 57 | 58 | @Override 59 | public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { 60 | return (throwable, method, parameters) -> { 61 | final StringBuilder buffer = new StringBuilder(); 62 | buffer.append("Thread, " + Thread.currentThread().getName()).append(System.lineSeparator()); 63 | buffer.append("Method, " + method).append(System.lineSeparator()); 64 | buffer.append("Parameters, " + Arrays.asList(parameters)).append(System.lineSeparator()); 65 | buffer.append(ExceptionUtils.getStackTrace(throwable)).append(System.lineSeparator()); 66 | 67 | logger.warning(buffer.toString()); 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/MultipartFileConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp; 29 | 30 | import org.springframework.core.convert.converter.Converter; 31 | import org.springframework.web.multipart.MultipartFile; 32 | 33 | /** 34 | * Converts a multi-part file to a string, which is the name of the uploaded file. This is needed 35 | * for input form validation, and also for saving details of the request. 36 | */ 37 | public class MultipartFileConverter implements Converter { 38 | 39 | /** Converts a multi-part file to a string, which is the name of the uploaded file. */ 40 | @Override 41 | public String convert(final MultipartFile file) { 42 | if (file == null) { 43 | return null; 44 | } else { 45 | return file.getOriginalFilename(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/SchemaCrawlerWebApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp; 29 | 30 | import org.springframework.boot.CommandLineRunner; 31 | import org.springframework.boot.SpringApplication; 32 | import org.springframework.boot.autoconfigure.SpringBootApplication; 33 | import org.springframework.format.FormatterRegistry; 34 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 35 | 36 | @SpringBootApplication 37 | public class SchemaCrawlerWebApplication implements WebMvcConfigurer, CommandLineRunner { 38 | 39 | /** 40 | * Spring Boot entry point. 41 | * 42 | * @param args Spring Boot arguments. 43 | */ 44 | public static void main(final String[] args) { 45 | SpringApplication.run(SchemaCrawlerWebApplication.class, args); 46 | } 47 | 48 | @Override 49 | public void addFormatters(final FormatterRegistry registry) { 50 | registry.addConverter(new MultipartFileConverter()); 51 | } 52 | 53 | @Override 54 | public void run(final String... args) throws Exception { 55 | // Do something, if needed 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/controller/DiagramRequestController.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | 29 | package us.fatehi.schemacrawler.webapp.controller; 30 | 31 | import static java.nio.charset.StandardCharsets.UTF_8; 32 | import static java.nio.file.StandardOpenOption.READ; 33 | import static org.apache.commons.io.IOUtils.toInputStream; 34 | import static us.fatehi.schemacrawler.webapp.controller.URIConstants.API_PREFIX; 35 | import static us.fatehi.schemacrawler.webapp.controller.URIConstants.UI_PREFIX; 36 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.DATA; 37 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.JSON; 38 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.LOG; 39 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.SQLITE_DB; 40 | import java.io.IOException; 41 | import java.io.InputStream; 42 | import java.io.PrintWriter; 43 | import java.io.StringWriter; 44 | import java.net.URI; 45 | import java.nio.file.Files; 46 | import java.nio.file.Path; 47 | import java.util.Collections; 48 | import java.util.List; 49 | import java.util.Optional; 50 | import java.util.stream.Collectors; 51 | import org.apache.tika.Tika; 52 | import org.apache.tika.mime.MimeType; 53 | import org.apache.tika.mime.MimeTypeException; 54 | import org.apache.tika.mime.MimeTypes; 55 | import org.slf4j.Logger; 56 | import org.slf4j.LoggerFactory; 57 | import org.springframework.http.MediaType; 58 | import org.springframework.http.ResponseEntity; 59 | import org.springframework.stereotype.Controller; 60 | import org.springframework.ui.Model; 61 | import org.springframework.util.DigestUtils; 62 | import org.springframework.validation.BindingResult; 63 | import org.springframework.web.bind.annotation.GetMapping; 64 | import org.springframework.web.bind.annotation.ModelAttribute; 65 | import org.springframework.web.bind.annotation.PostMapping; 66 | import org.springframework.web.bind.annotation.RequestParam; 67 | import org.springframework.web.bind.annotation.ResponseBody; 68 | import org.springframework.web.multipart.MultipartFile; 69 | import static us.fatehi.utility.Utility.isBlank; 70 | import jakarta.validation.Valid; 71 | import jakarta.validation.constraints.NotNull; 72 | import schemacrawler.schemacrawler.exceptions.ExecutionRuntimeException; 73 | import us.fatehi.schemacrawler.webapp.model.DiagramKey; 74 | import us.fatehi.schemacrawler.webapp.model.DiagramRequest; 75 | import us.fatehi.schemacrawler.webapp.service.notification.NotificationService; 76 | import us.fatehi.schemacrawler.webapp.service.processing.ProcessingService; 77 | import us.fatehi.schemacrawler.webapp.service.storage.StorageService; 78 | 79 | @Controller 80 | public class DiagramRequestController { 81 | 82 | private static final Logger LOGGER = LoggerFactory.getLogger(DiagramRequestController.class); 83 | 84 | private final StorageService storageService; 85 | private final ProcessingService processingService; 86 | private final NotificationService notificationService; 87 | 88 | public DiagramRequestController( 89 | @NotNull(message = "Storage service not provided") final StorageService storageService, 90 | @NotNull(message = "Processing service not provided") 91 | final ProcessingService processingService, 92 | @NotNull(message = "Notification service not provided") 93 | final NotificationService notificationService) { 94 | this.storageService = storageService; 95 | this.processingService = processingService; 96 | this.notificationService = notificationService; 97 | } 98 | 99 | @GetMapping(UI_PREFIX) 100 | public String diagramRequestForm(@NotNull(message = "Model not provided") final Model model) { 101 | model.addAttribute("diagramRequest", new DiagramRequest()); 102 | return "SchemaCrawlerDiagramForm"; 103 | } 104 | 105 | // http://stackoverflow.com/questions/30297719/cannot-get-validation-working-with-spring-boot-and-thymeleaf 106 | @PostMapping(value = UI_PREFIX) 107 | public String diagramRequestFormSubmit( 108 | @ModelAttribute("diagramRequest") @NotNull(message = "Diagram request not provided") @Valid 109 | final DiagramRequest diagramRequest, 110 | final BindingResult bindingResult, 111 | @RequestParam("file") final MultipartFile file) 112 | throws Exception { 113 | 114 | if (bindingResult.hasErrors()) { 115 | // Save validation errors 116 | saveBindingResultLogFile(diagramRequest.getKey(), bindingResult); 117 | saveDiagramRequest(diagramRequest); 118 | return "SchemaCrawlerDiagramForm"; 119 | } 120 | 121 | generateSchemaCrawlerDiagram(diagramRequest, file); 122 | notificationService.notify(diagramRequest); 123 | 124 | return "SchemaCrawlerDiagramResult"; 125 | } 126 | 127 | @PostMapping(value = API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE) 128 | @ResponseBody 129 | public ResponseEntity diagramRequestFormSubmitApi( 130 | @ModelAttribute("diagramRequest") @NotNull(message = "Diagram request not provided") @Valid 131 | final DiagramRequest diagramRequest, 132 | final BindingResult bindingResult, 133 | @RequestParam("file") final Optional file) { 134 | 135 | // Check for bad requests 136 | if (!file.isPresent()) { 137 | diagramRequest.setError("No SQLite file upload provided"); 138 | // Save validation errors 139 | saveDiagramRequest(diagramRequest); 140 | } else if (bindingResult.hasErrors()) { 141 | final List errors = 142 | bindingResult.getFieldErrors().stream() 143 | .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) 144 | .collect(Collectors.toList()); 145 | Collections.sort(errors); 146 | diagramRequest.setError(String.join("; ", errors)); 147 | // Save validation errors 148 | saveBindingResultLogFile(diagramRequest.getKey(), bindingResult); 149 | saveDiagramRequest(diagramRequest); 150 | } 151 | 152 | if (diagramRequest.hasLogMessage()) { 153 | return ResponseEntity.badRequest().body(diagramRequest); 154 | } 155 | 156 | // Generate diagram 157 | URI location = null; 158 | try { 159 | generateSchemaCrawlerDiagram(diagramRequest, file.get()); 160 | location = new URI("./" + diagramRequest.getKey()); 161 | } catch (final Exception e) { 162 | LOGGER.error(String.format("%s%n%s", e.getMessage(), diagramRequest)); 163 | LOGGER.trace(e.getMessage(), e); 164 | diagramRequest.setError(e.getMessage()); 165 | } 166 | 167 | if (diagramRequest.hasLogMessage()) { 168 | return ResponseEntity.internalServerError().body(diagramRequest); 169 | } 170 | return ResponseEntity.created(location).body(diagramRequest); 171 | } 172 | 173 | @GetMapping(value = "/") 174 | public String index() { 175 | return "redirect:/schemacrawler"; 176 | } 177 | 178 | private void checkMimeType(final DiagramRequest diagramRequest, final Path localPath) 179 | throws ExecutionRuntimeException { 180 | try { 181 | final String detectedMimeType = new Tika().detect(localPath); 182 | if (!"application/x-sqlite3".equals(detectedMimeType)) { 183 | final MimeType mimeType = MimeTypes.getDefaultMimeTypes().forName(detectedMimeType); 184 | 185 | final StringBuffer exceptionMessage = new StringBuffer(); 186 | exceptionMessage.append("Expected a SQLite database file, but got a "); 187 | if (!isBlank(mimeType.getDescription())) { 188 | exceptionMessage.append(mimeType.getDescription()).append(" file"); 189 | } else if (isBlank(mimeType.getDescription()) && !isBlank(detectedMimeType)) { 190 | exceptionMessage.append("file of type ").append(detectedMimeType); 191 | } else { 192 | exceptionMessage.append("file of an unknown type"); 193 | } 194 | throw new ExecutionRuntimeException(exceptionMessage.toString()); 195 | } 196 | } catch (final MimeTypeException | IOException | NullPointerException e) { 197 | LOGGER.error(String.format("%s%n%s", e.getMessage(), diagramRequest)); 198 | LOGGER.trace(e.getMessage(), e); 199 | } 200 | } 201 | 202 | private void generateSchemaCrawlerDiagram( 203 | final DiagramRequest diagramRequest, final MultipartFile file) throws Exception { 204 | 205 | final DiagramKey key = diagramRequest.getKey(); 206 | try { 207 | 208 | // Store the uploaded database file locally, so it can be processed 209 | final Path localPath = storageService.storeLocal(file, key, SQLITE_DB); 210 | try (InputStream is = Files.newInputStream(localPath, READ)) { 211 | final String md5DigestHex = DigestUtils.md5DigestAsHex(is); 212 | diagramRequest.setFileHash(md5DigestHex); 213 | } 214 | 215 | checkMimeType(diagramRequest, localPath); 216 | 217 | // Make asynchronous call to generate diagram 218 | processingService.generateSchemaCrawlerDiagram(diagramRequest, localPath); 219 | } catch (final Exception e) { 220 | LOGGER.error(String.format("%s%n%s", e.getMessage(), diagramRequest)); 221 | LOGGER.warn(e.getMessage(), e); 222 | saveExceptionLogFile(key, e); 223 | diagramRequest.setError(e.getMessage()); 224 | // Save a copy of the uploaded file, which may not be a SQLite database 225 | storageService.store(file, key, DATA); 226 | throw e; 227 | } finally { 228 | // Diagram request may contain an error message due to exceptions thrown 229 | saveDiagramRequest(diagramRequest); 230 | } 231 | } 232 | 233 | private void saveBindingResultLogFile(final DiagramKey key, final BindingResult bindingResult) { 234 | try { 235 | // Write out stack trace to a log file, and save it 236 | final StringWriter stackTraceWriter = new StringWriter(); 237 | stackTraceWriter.write(bindingResult.toString()); 238 | final String stackTrace = stackTraceWriter.toString(); 239 | storageService.store(() -> toInputStream(stackTrace, UTF_8), key, LOG); 240 | } catch (final Exception e) { 241 | LOGGER.error(String.format("<%s>: %s", key, e.getMessage())); 242 | LOGGER.warn(e.getMessage(), e); 243 | } 244 | } 245 | 246 | private void saveDiagramRequest(final DiagramRequest diagramRequest) { 247 | if (diagramRequest == null) { 248 | return; 249 | } 250 | final DiagramKey key = diagramRequest.getKey(); 251 | try { 252 | storageService.store(() -> toInputStream(diagramRequest.toJson(), UTF_8), key, JSON); 253 | } catch (final Exception e) { 254 | LOGGER.error( 255 | String.format("Could not save diagram request%n%s%n%s", e.getMessage(), diagramRequest)); 256 | } 257 | } 258 | 259 | private void saveExceptionLogFile(final DiagramKey key, final Exception exception) { 260 | try { 261 | // Write out stack trace to a log file, and save it 262 | final StringWriter stackTraceWriter = new StringWriter(); 263 | exception.printStackTrace(new PrintWriter(stackTraceWriter)); 264 | final String stackTrace = stackTraceWriter.toString(); 265 | storageService.store(() -> toInputStream(stackTrace, UTF_8), key, LOG); 266 | } catch (final Exception e) { 267 | LOGGER.error(String.format("<%s>: %s", key, e.getMessage())); 268 | LOGGER.warn(e.getMessage(), e); 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/controller/DiagramResultController.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | 29 | package us.fatehi.schemacrawler.webapp.controller; 30 | 31 | import static java.nio.charset.StandardCharsets.UTF_8; 32 | import static java.nio.file.Files.newBufferedReader; 33 | import static us.fatehi.schemacrawler.webapp.controller.URIConstants.API_PREFIX; 34 | import static us.fatehi.schemacrawler.webapp.controller.URIConstants.UI_RESULTS_PREFIX; 35 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.JSON; 36 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.PNG; 37 | import java.nio.file.Path; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | import org.springframework.core.io.PathResource; 41 | import org.springframework.core.io.Resource; 42 | import org.springframework.http.MediaType; 43 | import org.springframework.http.ResponseEntity; 44 | import org.springframework.stereotype.Controller; 45 | import org.springframework.ui.Model; 46 | import org.springframework.web.bind.annotation.GetMapping; 47 | import org.springframework.web.bind.annotation.PathVariable; 48 | import org.springframework.web.bind.annotation.ResponseBody; 49 | import jakarta.validation.constraints.NotNull; 50 | import schemacrawler.schemacrawler.exceptions.ExecutionRuntimeException; 51 | import us.fatehi.schemacrawler.webapp.model.DiagramKey; 52 | import us.fatehi.schemacrawler.webapp.model.DiagramRequest; 53 | import us.fatehi.schemacrawler.webapp.service.processing.ProcessingService; 54 | import us.fatehi.schemacrawler.webapp.service.storage.StorageService; 55 | 56 | @Controller 57 | public class DiagramResultController { 58 | 59 | private static final Logger LOGGER = LoggerFactory.getLogger(DiagramResultController.class); 60 | 61 | private final StorageService storageService; 62 | 63 | public DiagramResultController( 64 | @NotNull(message = "StorageService not provided") final StorageService storageService, 65 | @NotNull(message = "ProcessingService not provided") 66 | final ProcessingService processingService) { 67 | this.storageService = storageService; 68 | } 69 | 70 | @GetMapping( 71 | value = {API_PREFIX + "/{key}/diagram", UI_RESULTS_PREFIX + "/{key}/diagram"}, 72 | produces = MediaType.IMAGE_PNG_VALUE) 73 | @ResponseBody 74 | public Resource diagramImage( 75 | @PathVariable @NotNull(message = "Key not provided") final DiagramKey key) throws Exception { 76 | return retrieveDiagramLocal(key); 77 | } 78 | 79 | /** 80 | * Retrieve results as HTML using a rendered Thymeleaf template. 81 | * 82 | * @param key Diagram key for the results. 83 | * @return Diagram request data, including the key 84 | * @throws Exception On an exception 85 | */ 86 | @GetMapping(value = UI_RESULTS_PREFIX + "/{key}") 87 | public String retrieveResults( 88 | final Model model, @PathVariable @NotNull(message = "Key not provided") final DiagramKey key) 89 | throws Exception { 90 | 91 | final DiagramRequest diagramRequest = retrieveResults(key); 92 | model.addAttribute("diagramRequest", diagramRequest); 93 | 94 | if (diagramRequest.hasLogMessage()) { 95 | throw new ExecutionRuntimeException(diagramRequest.toString()); 96 | } 97 | 98 | return "SchemaCrawlerDiagram"; 99 | } 100 | 101 | /** 102 | * Retrieve results as a JSON object. 103 | * 104 | * @param key Diagram key for the results. 105 | * @return Diagram request data, including the key 106 | * @throws Exception On an exception 107 | */ 108 | @GetMapping(value = API_PREFIX + "/{key}", produces = MediaType.APPLICATION_JSON_VALUE) 109 | @ResponseBody 110 | public ResponseEntity retrieveResultsApi( 111 | @PathVariable @NotNull(message = "Key not provided") final DiagramKey key) throws Exception { 112 | 113 | final DiagramRequest diagramRequest; 114 | try { 115 | diagramRequest = retrieveResults(key); 116 | } catch (final Exception e) { 117 | LOGGER.error(String.format("<%s>: %s", key, e.getMessage())); 118 | LOGGER.trace(e.getMessage(), e); 119 | return ResponseEntity.notFound().build(); 120 | } 121 | 122 | return ResponseEntity.ok(diagramRequest); 123 | } 124 | 125 | private Resource retrieveDiagramLocal(final DiagramKey key) throws Exception { 126 | return storageService 127 | .retrieveLocal(key, PNG) 128 | .map(PathResource::new) 129 | .orElseThrow( 130 | () -> new ExecutionRuntimeException(String.format("Cannot find key <%s>", key))); 131 | } 132 | 133 | private DiagramRequest retrieveResults(final DiagramKey key) throws Exception { 134 | final Path jsonFile = 135 | storageService 136 | .retrieveLocal(key, JSON) 137 | .orElseThrow( 138 | () -> 139 | new ExecutionRuntimeException( 140 | String.format("Cannot find request for <%s>", key))); 141 | final DiagramRequest diagramRequest = 142 | DiagramRequest.fromJson(newBufferedReader(jsonFile, UTF_8)); 143 | return diagramRequest; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/controller/ErrorController.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.controller; 29 | 30 | import org.apache.commons.lang3.exception.ExceptionUtils; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | import org.springframework.http.ResponseEntity; 34 | import org.springframework.stereotype.Controller; 35 | import org.springframework.web.bind.annotation.ControllerAdvice; 36 | import org.springframework.web.bind.annotation.ExceptionHandler; 37 | import org.springframework.web.context.request.WebRequest; 38 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 39 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 40 | 41 | @Controller 42 | @ControllerAdvice 43 | public class ErrorController { 44 | 45 | private static final Logger LOGGER = LoggerFactory.getLogger(ErrorController.class); 46 | 47 | @ExceptionHandler(Throwable.class) 48 | public String handleException( 49 | final Throwable throwable, final RedirectAttributes redirectAttributes) { 50 | LOGGER.error(throwable.getMessage(), throwable); 51 | 52 | final String errorMessage = ExceptionUtils.getRootCauseMessage(throwable); 53 | redirectAttributes.addFlashAttribute("errorMessage", errorMessage); 54 | 55 | return "redirect:/error"; 56 | } 57 | 58 | @ExceptionHandler({MethodArgumentTypeMismatchException.class}) 59 | public ResponseEntity handleMethodArgumentTypeMismatch( 60 | final MethodArgumentTypeMismatchException ex, final WebRequest request) { 61 | return ResponseEntity.badRequest().build(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/controller/URIConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.controller; 29 | 30 | public class URIConstants { 31 | 32 | public static final String API_PREFIX = "/diagrams"; 33 | public static final String UI_PREFIX = "/schemacrawler"; 34 | public static final String UI_RESULTS_PREFIX = "/schemacrawler/results"; 35 | 36 | private URIConstants() { 37 | // Prevent instantiation 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/model/DiagramKey.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.model; 29 | 30 | import java.io.Serializable; 31 | import java.util.Objects; 32 | 33 | import jakarta.validation.constraints.Pattern; 34 | import jakarta.validation.constraints.Size; 35 | 36 | import org.apache.commons.lang3.RandomStringUtils; 37 | import org.apache.commons.lang3.StringUtils; 38 | 39 | import schemacrawler.schemacrawler.exceptions.InternalRuntimeException; 40 | 41 | public class DiagramKey implements Serializable { 42 | 43 | private static final long serialVersionUID = 3453873731406876293L; 44 | 45 | @Pattern(regexp = "[a-z0-9]{12}") 46 | @Size(min = 12, max = 12, message = "Invalid key length") 47 | private final String key; 48 | 49 | public DiagramKey() { 50 | key = RandomStringUtils.secure().nextAlphanumeric(12).toLowerCase(); 51 | } 52 | 53 | public DiagramKey(final String key) { 54 | this.key = validateKey(key); 55 | } 56 | 57 | @Override 58 | public boolean equals(final Object obj) { 59 | if (this == obj) { 60 | return true; 61 | } 62 | if (obj == null) { 63 | return false; 64 | } 65 | if (getClass() != obj.getClass()) { 66 | return false; 67 | } 68 | final DiagramKey other = (DiagramKey) obj; 69 | if (!Objects.equals(key, other.key)) { 70 | return false; 71 | } 72 | return true; 73 | } 74 | 75 | public String getKey() { 76 | return key; 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | final int prime = 31; 82 | int result = 1; 83 | result = prime * result + (key == null ? 0 : key.hashCode()); 84 | return result; 85 | } 86 | 87 | @Override 88 | public String toString() { 89 | return key; 90 | } 91 | 92 | /** 93 | * Prevent malicious injection attacks. 94 | * 95 | * @param key Key 96 | * @throws Exception On a badly constructed key. 97 | */ 98 | private String validateKey(final String key) throws RuntimeException { 99 | if (StringUtils.length(key) != 12 || !StringUtils.isAlphanumeric(key)) { 100 | throw new InternalRuntimeException(String.format("Invalid key <%s>", key)); 101 | } 102 | return key.toLowerCase(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/model/DiagramRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.model; 29 | 30 | import static java.util.Objects.requireNonNull; 31 | import static us.fatehi.utility.Utility.isBlank; 32 | 33 | import java.io.Reader; 34 | import java.io.Serializable; 35 | import java.time.Instant; 36 | 37 | import jakarta.validation.constraints.NotNull; 38 | import jakarta.validation.constraints.Pattern; 39 | import jakarta.validation.constraints.Size; 40 | 41 | import org.apache.commons.lang3.builder.EqualsBuilder; 42 | import org.apache.commons.lang3.builder.HashCodeBuilder; 43 | 44 | public class DiagramRequest implements Serializable { 45 | 46 | private static final long serialVersionUID = 2065519510282344200L; 47 | 48 | /** 49 | * Factory method to deserialize a JSON request. 50 | * 51 | * @param jsonReader JSON serialized request reader. 52 | * @return Deserialized Java request. 53 | */ 54 | public static DiagramRequest fromJson(final Reader jsonReader) { 55 | requireNonNull(jsonReader, "No reader provided"); 56 | return new DiagramRequestUtility().readDiagramRequest(jsonReader); 57 | } 58 | 59 | private final DiagramKey key; 60 | private final Instant timestamp; 61 | private String error; 62 | 63 | @NotNull(message = "Name is required") 64 | @Size(min = 2, max = 255, message = "Please enter your full name") 65 | private String name; 66 | 67 | @NotNull(message = "Email is required") 68 | @Pattern( 69 | regexp = 70 | "[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*", 71 | message = "Please enter a valid email address") 72 | @Size(min = 5, max = 255, message = "Please enter a valid email address") 73 | private String email; 74 | 75 | @NotNull(message = "A SQLite database file needs to be uploaded") 76 | @Size(min = 1, max = 255, message = "Please select a file to upload") 77 | private String file; 78 | 79 | @Size(min = 0, max = 255, message = "Please enter a valid title") 80 | private String title; 81 | 82 | @Size(min = 32, max = 32) 83 | private String fileHash; 84 | 85 | /** Public constructor. Generates a random key, and sets the creation timestamp. */ 86 | public DiagramRequest() { 87 | timestamp = Instant.now(); 88 | key = new DiagramKey(); 89 | } 90 | 91 | /** {@inheritDoc} */ 92 | @Override 93 | public boolean equals(final Object obj) { 94 | return EqualsBuilder.reflectionEquals(this, obj); 95 | } 96 | 97 | /** 98 | * Returns the email address of the requester. 99 | * 100 | * @return Email address of the requester. 101 | */ 102 | public String getEmail() { 103 | return email; 104 | } 105 | 106 | /** 107 | * Returns the exception message. 108 | * 109 | * @return Error message 110 | */ 111 | public String getError() { 112 | return error; 113 | } 114 | 115 | /** 116 | * Returns the uploaded file name. 117 | * 118 | * @return Uploaded file name. 119 | */ 120 | public String getFile() { 121 | return file; 122 | } 123 | 124 | public String getFileHash() { 125 | return fileHash; 126 | } 127 | 128 | /** 129 | * Returns a randomized unique key for the request. 130 | * 131 | * @return Unique key for the request. 132 | */ 133 | public DiagramKey getKey() { 134 | return key; 135 | } 136 | 137 | /** 138 | * Returns the name of the requester. 139 | * 140 | * @return Name of the requester. 141 | */ 142 | public String getName() { 143 | return name; 144 | } 145 | 146 | /** 147 | * Returns the request creation timestamp. 148 | * 149 | * @return Request creation timestamp. 150 | */ 151 | public Instant getTimestamp() { 152 | return timestamp; 153 | } 154 | 155 | public String getTitle() { 156 | return title; 157 | } 158 | 159 | /** {@inheritDoc} */ 160 | @Override 161 | public int hashCode() { 162 | return HashCodeBuilder.reflectionHashCode(this, false); 163 | } 164 | 165 | public boolean hasLogMessage() { 166 | return !isBlank(error); 167 | } 168 | 169 | public void setEmail(final String email) { 170 | this.email = email; 171 | } 172 | 173 | /** 174 | * Set error message. 175 | * 176 | * @param Error message 177 | */ 178 | public void setError(final String error) { 179 | this.error = error; 180 | } 181 | 182 | /** 183 | * Sets the uploaded file name. 184 | * 185 | * @param file Uploaded file name. 186 | */ 187 | public void setFile(final String file) { 188 | this.file = file; 189 | } 190 | 191 | public void setFileHash(final String fileHash) { 192 | this.fileHash = fileHash; 193 | } 194 | 195 | /** 196 | * Sets the name of the requester. 197 | * 198 | * @param name Name of the requester. 199 | */ 200 | public void setName(final String name) { 201 | this.name = name; 202 | } 203 | 204 | public void setTitle(final String title) { 205 | this.title = title; 206 | } 207 | 208 | /** 209 | * Converts this object to JSON. 210 | * 211 | * @return JSON string 212 | */ 213 | public String toJson() { 214 | return new DiagramRequestUtility().diagramRequestToJson(this); 215 | } 216 | 217 | /** {@inheritDoc} */ 218 | @Override 219 | public String toString() { 220 | return toJson(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/model/DiagramRequestUtility.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.model; 29 | 30 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; 31 | import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; 32 | import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS; 33 | import static com.fasterxml.jackson.databind.SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID; 34 | import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_ENUMS_USING_TO_STRING; 35 | import static java.util.Objects.requireNonNull; 36 | 37 | import java.io.IOException; 38 | import java.io.Reader; 39 | 40 | import org.springframework.context.annotation.Bean; 41 | import org.springframework.context.annotation.Configuration; 42 | import org.springframework.context.annotation.Primary; 43 | 44 | import com.fasterxml.jackson.annotation.JsonInclude; 45 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 46 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 47 | import com.fasterxml.jackson.core.JsonProcessingException; 48 | import com.fasterxml.jackson.databind.ObjectMapper; 49 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 50 | import com.fasterxml.jackson.databind.SerializationFeature; 51 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 52 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 53 | 54 | import schemacrawler.schemacrawler.exceptions.ExecutionRuntimeException; 55 | 56 | @Configuration 57 | public class DiagramRequestUtility { 58 | 59 | @Bean 60 | @Primary 61 | public ObjectMapper objectMapper() { 62 | 63 | @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) 64 | @JsonInclude(JsonInclude.Include.NON_NULL) 65 | @JsonPropertyOrder({ 66 | "key", 67 | "timestamp", 68 | "name", 69 | "email", 70 | "file", 71 | "title", 72 | "error", 73 | }) 74 | abstract class JacksonAnnotationMixIn { 75 | @JsonUnwrapped public DiagramKey key; 76 | } 77 | 78 | final ObjectMapper mapper = new ObjectMapper(); 79 | mapper.enable( 80 | ORDER_MAP_ENTRIES_BY_KEYS, 81 | INDENT_OUTPUT, 82 | USE_EQUALITY_FOR_OBJECT_ID, 83 | WRITE_ENUMS_USING_TO_STRING); 84 | mapper.setSerializationInclusion(NON_NULL); 85 | mapper.addMixIn(DiagramRequest.class, JacksonAnnotationMixIn.class); 86 | mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 87 | mapper.registerModule(new JavaTimeModule()); 88 | return mapper; 89 | } 90 | 91 | String diagramRequestToJson(final DiagramRequest diagramRequest) { 92 | requireNonNull(diagramRequest, "No diagram request provided"); 93 | try { 94 | return objectMapper().writeValueAsString(diagramRequest); 95 | } catch (final JsonProcessingException e) { 96 | throw new ExecutionRuntimeException("Cannot serialize diagram request", e); 97 | } 98 | } 99 | 100 | DiagramRequest readDiagramRequest(final Reader diagramRequestReader) { 101 | requireNonNull(diagramRequestReader, "No diagram request reader provided"); 102 | try { 103 | return objectMapper().readValue(diagramRequestReader, DiagramRequest.class); 104 | } catch (final IOException e) { 105 | throw new ExecutionRuntimeException( 106 | String.format("Cannot deserialize diagram request%n%s", diagramRequestReader), e); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/WebappConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.service; 29 | 30 | import org.apache.commons.lang3.StringUtils; 31 | import org.springframework.beans.factory.annotation.Value; 32 | import org.springframework.context.annotation.Bean; 33 | import org.springframework.context.annotation.Configuration; 34 | 35 | import schemacrawler.schemacrawler.exceptions.InternalRuntimeException; 36 | 37 | @Configuration 38 | public class WebappConfig { 39 | 40 | @Value("${SC_WEB_APP_URI:none}") 41 | private String webAppUri; 42 | 43 | @Bean(name = "webAppUri") 44 | public String webAppUri() { 45 | if (StringUtils.isBlank(webAppUri)) { 46 | throw new InternalRuntimeException("No web application URI provided"); 47 | } 48 | if (webAppUri.equals("none")) { 49 | return "http://localhost:8080"; 50 | } 51 | return webAppUri; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/notification/AmazonSESNotificationConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.service.notification; 29 | 30 | import org.springframework.context.annotation.Bean; 31 | import org.springframework.context.annotation.Configuration; 32 | import org.springframework.context.annotation.Profile; 33 | 34 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 35 | import software.amazon.awssdk.regions.Region; 36 | import software.amazon.awssdk.services.ses.SesClient; 37 | 38 | @Configuration 39 | @Profile("production") 40 | public class AmazonSESNotificationConfig { 41 | 42 | @Bean(name = "sesClient") 43 | public SesClient sesClient(final AwsCredentialsProvider awsCredentials, final Region awsRegion) { 44 | final SesClient sesClient = 45 | SesClient.builder().credentialsProvider(awsCredentials).region(awsRegion).build(); 46 | return sesClient; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/notification/AmazonSESNotificationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | 29 | package us.fatehi.schemacrawler.webapp.service.notification; 30 | 31 | import java.io.ByteArrayOutputStream; 32 | import java.io.IOException; 33 | import java.io.UnsupportedEncodingException; 34 | import java.nio.ByteBuffer; 35 | import java.util.Properties; 36 | 37 | import jakarta.mail.Message; 38 | import jakarta.mail.MessagingException; 39 | import jakarta.mail.Session; 40 | import jakarta.mail.internet.InternetAddress; 41 | import jakarta.mail.internet.MimeBodyPart; 42 | import jakarta.mail.internet.MimeMessage; 43 | import jakarta.mail.internet.MimeMultipart; 44 | import jakarta.validation.constraints.NotBlank; 45 | import jakarta.validation.constraints.NotNull; 46 | 47 | import org.slf4j.Logger; 48 | import org.slf4j.LoggerFactory; 49 | import org.springframework.context.annotation.Profile; 50 | import org.springframework.lang.NonNull; 51 | import org.springframework.stereotype.Service; 52 | 53 | import software.amazon.awssdk.awscore.exception.AwsErrorDetails; 54 | import software.amazon.awssdk.core.SdkBytes; 55 | import software.amazon.awssdk.services.ses.SesClient; 56 | import software.amazon.awssdk.services.ses.model.RawMessage; 57 | import software.amazon.awssdk.services.ses.model.SendRawEmailRequest; 58 | import software.amazon.awssdk.services.ses.model.SesException; 59 | import us.fatehi.schemacrawler.webapp.model.DiagramRequest; 60 | 61 | @Service("amazonSESNotificationService") 62 | @Profile("production") 63 | public class AmazonSESNotificationService implements NotificationService { 64 | 65 | private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSESNotificationService.class); 66 | 67 | private final SesClient sesClient; 68 | private final InternetAddress sender; 69 | private final String webAppUri; 70 | 71 | public AmazonSESNotificationService( 72 | @NonNull final SesClient sesClient, @NotBlank final String webAppUri) { 73 | this.sesClient = sesClient; 74 | this.webAppUri = webAppUri; 75 | 76 | try { 77 | sender = new InternetAddress("webapp@schemacrawler.com", "SchemaCrawler Web Application"); 78 | } catch (final UnsupportedEncodingException e) { 79 | throw new IllegalArgumentException("Could not create sender", e); 80 | } 81 | } 82 | 83 | @Override 84 | public void notify(@NotNull final DiagramRequest diagramRequest) { 85 | 86 | final String recipient = diagramRequest.getEmail(); 87 | final String subject = String.format("SchemaCrawler Diagram: %s", diagramRequest.getTitle()); 88 | final String key = diagramRequest.getKey().getKey(); 89 | final String resultsUrl = String.format("%s/schemacrawler/results/%s", webAppUri, key); 90 | 91 | final String bodyText = String.format("Your SchemaCrawler diagram is ready at %s", resultsUrl); 92 | final String bodyHTML = 93 | String.format("Your SchemaCrawler diagram is ready", resultsUrl); 94 | 95 | try { 96 | final MimeMessage message = createMessage(recipient, subject, bodyText, bodyHTML); 97 | send(message); 98 | } catch (MessagingException | IOException e) { 99 | LOGGER.warn( 100 | String.format("Error sending email for %s: %s", diagramRequest.getKey(), e.getMessage())); 101 | } catch (final SesException e) { 102 | final AwsErrorDetails awsErrorDetails = e.awsErrorDetails(); 103 | LOGGER.warn( 104 | String.format( 105 | "Error sending email for %s: %s - %s", 106 | diagramRequest.getKey(), 107 | awsErrorDetails.errorCode(), 108 | awsErrorDetails.errorMessage())); 109 | } 110 | } 111 | 112 | private MimeMessage createMessage( 113 | final String recipient, final String subject, final String bodyText, final String bodyHTML) 114 | throws MessagingException { 115 | final Session session = Session.getDefaultInstance(new Properties()); 116 | final MimeMessage message = new MimeMessage(session); 117 | 118 | // Add subject, from and to lines 119 | message.setSubject(subject, "UTF-8"); 120 | message.setFrom(sender); 121 | message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipient)); 122 | 123 | // Create a multipart/alternative child container 124 | final MimeMultipart msgBody = new MimeMultipart("alternative"); 125 | 126 | // Create a wrapper for the HTML and text parts 127 | final MimeBodyPart wrap = new MimeBodyPart(); 128 | 129 | // Define the text part 130 | final MimeBodyPart textPart = new MimeBodyPart(); 131 | textPart.setContent(bodyText, "text/plain; charset=UTF-8"); 132 | 133 | // Define the HTML part 134 | final MimeBodyPart htmlPart = new MimeBodyPart(); 135 | htmlPart.setContent(bodyHTML, "text/html; charset=UTF-8"); 136 | 137 | // Add the text and HTML parts to the child container 138 | msgBody.addBodyPart(textPart); 139 | msgBody.addBodyPart(htmlPart); 140 | 141 | // Add the child container to the wrapper object 142 | wrap.setContent(msgBody); 143 | 144 | // Create a multipart/mixed parent container 145 | final MimeMultipart msg = new MimeMultipart("mixed"); 146 | 147 | // Add the parent container to the message 148 | message.setContent(msg); 149 | 150 | // Add the multipart/alternative part to the message 151 | msg.addBodyPart(wrap); 152 | return message; 153 | } 154 | 155 | private void send(final MimeMessage message) throws MessagingException, IOException { 156 | 157 | final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 158 | message.writeTo(outputStream); 159 | final ByteBuffer buf = ByteBuffer.wrap(outputStream.toByteArray()); 160 | 161 | final byte[] arr = new byte[buf.remaining()]; 162 | buf.get(arr); 163 | 164 | final SdkBytes data = SdkBytes.fromByteArray(arr); 165 | final RawMessage rawMessage = RawMessage.builder().data(data).build(); 166 | 167 | final SendRawEmailRequest rawEmailRequest = 168 | SendRawEmailRequest.builder().rawMessage(rawMessage).build(); 169 | 170 | sesClient.sendRawEmail(rawEmailRequest); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/notification/NotificationService.java: -------------------------------------------------------------------------------- 1 | package us.fatehi.schemacrawler.webapp.service.notification; 2 | 3 | import us.fatehi.schemacrawler.webapp.model.DiagramRequest; 4 | 5 | public interface NotificationService { 6 | 7 | void notify(DiagramRequest diagramRequest); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/processing/ProcessingService.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | 29 | package us.fatehi.schemacrawler.webapp.service.processing; 30 | 31 | import static schemacrawler.tools.command.text.diagram.options.DiagramOutputFormat.png; 32 | import static schemacrawler.tools.sqlite.SchemaCrawlerSQLiteUtility.executeForOutput; 33 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.PNG; 34 | import static us.fatehi.schemacrawler.webapp.service.storage.FileExtensionType.SQLITE_DB; 35 | 36 | import java.nio.file.Path; 37 | import java.util.logging.Logger; 38 | 39 | import jakarta.validation.constraints.NotNull; 40 | 41 | import org.springframework.core.io.PathResource; 42 | import org.springframework.scheduling.annotation.Async; 43 | import org.springframework.stereotype.Service; 44 | 45 | import us.fatehi.schemacrawler.webapp.model.DiagramKey; 46 | import us.fatehi.schemacrawler.webapp.model.DiagramRequest; 47 | import us.fatehi.schemacrawler.webapp.service.storage.StorageService; 48 | 49 | @Service 50 | public class ProcessingService { 51 | 52 | private static final Logger logger = Logger.getLogger(ProcessingService.class.getName()); 53 | 54 | private final StorageService storageService; 55 | 56 | public ProcessingService( 57 | @NotNull(message = "StorageService not provided") final StorageService storageService) { 58 | this.storageService = storageService; 59 | } 60 | 61 | @Async 62 | public void generateSchemaCrawlerDiagram( 63 | @NotNull(message = "Diagram request not provided") final DiagramRequest diagramRequest, 64 | @NotNull(message = "Local path not provided") final Path localPath) 65 | throws Exception { 66 | 67 | logger.info( 68 | String.format( 69 | "Processing in thread %s%n%s", Thread.currentThread().getName(), diagramRequest)); 70 | 71 | final DiagramKey key = diagramRequest.getKey(); 72 | 73 | // Store the uploaded database file 74 | storageService.store(new PathResource(localPath), key, SQLITE_DB); 75 | 76 | final String title = diagramRequest.getTitle(); 77 | // Generate a database integration, and store the generated image 78 | final Path schemaCrawlerDiagram = executeForOutput(localPath, title, png); 79 | storageService.store(new PathResource(schemaCrawlerDiagram), key, PNG); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/storage/AWSConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.service.storage; 29 | 30 | import jakarta.validation.constraints.NotNull; 31 | 32 | import org.apache.commons.lang3.StringUtils; 33 | import org.springframework.beans.factory.annotation.Value; 34 | import org.springframework.context.annotation.Bean; 35 | import org.springframework.context.annotation.Configuration; 36 | import org.springframework.context.annotation.Profile; 37 | 38 | import schemacrawler.schemacrawler.exceptions.InternalRuntimeException; 39 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 40 | import software.amazon.awssdk.auth.credentials.AwsCredentials; 41 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 42 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 43 | import software.amazon.awssdk.regions.Region; 44 | 45 | @Configuration 46 | @Profile("production") 47 | public class AWSConfig { 48 | 49 | @Value("${AWS_ACCESS_KEY_ID}") 50 | @NotNull(message = "AWS_ACCESS_KEY_ID not provided") 51 | private String accessKeyId; 52 | 53 | @Value("${AWS_SECRET_ACCESS_KEY}") 54 | @NotNull(message = "AWS_SECRET_ACCESS_KEY not provided") 55 | private String secretAccessKey; 56 | 57 | @Value("${AWS_REGION:us-east-1}") 58 | @NotNull(message = "AWS_REGION not provided") 59 | private String awsRegion; 60 | 61 | @Bean(name = "awsCredentials") 62 | public AwsCredentialsProvider awsCredentials() { 63 | if (StringUtils.isAnyBlank(accessKeyId, secretAccessKey)) { 64 | throw new InternalRuntimeException("No AWS credentials provided"); 65 | } 66 | final AwsCredentials awsCredentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey); 67 | return StaticCredentialsProvider.create(awsCredentials); 68 | } 69 | 70 | @Bean(name = "awsRegion") 71 | public Region awsRegion() { 72 | if (StringUtils.isBlank(awsRegion)) { 73 | throw new InternalRuntimeException("No AWS region provided"); 74 | } 75 | return Region.of(awsRegion); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/storage/AmazonS3StorageConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.service.storage; 29 | 30 | import jakarta.validation.constraints.NotNull; 31 | 32 | import org.apache.commons.lang3.StringUtils; 33 | import org.springframework.beans.factory.annotation.Value; 34 | import org.springframework.context.annotation.Bean; 35 | import org.springframework.context.annotation.Configuration; 36 | import org.springframework.context.annotation.Profile; 37 | 38 | import schemacrawler.schemacrawler.exceptions.InternalRuntimeException; 39 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 40 | import software.amazon.awssdk.regions.Region; 41 | import software.amazon.awssdk.services.s3.S3Client; 42 | 43 | @Configuration 44 | @Profile("production") 45 | public class AmazonS3StorageConfig { 46 | 47 | @Value("${AWS_S3_BUCKET}") 48 | @NotNull(message = "AWS_S3_BUCKET not provided") 49 | private String s3Bucket; 50 | 51 | @Bean(name = "s3Bucket") 52 | public String s3Bucket() { 53 | if (StringUtils.isAnyBlank(s3Bucket)) { 54 | throw new InternalRuntimeException("No Amazon S3 bucket provided"); 55 | } 56 | return s3Bucket; 57 | } 58 | 59 | @Bean(name = "s3Client") 60 | public S3Client s3Client(final AwsCredentialsProvider awsCredentials, final Region awsRegion) { 61 | final S3Client s3Client = 62 | S3Client.builder().credentialsProvider(awsCredentials).region(awsRegion).build(); 63 | return s3Client; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/storage/AmazonS3StorageService.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | 29 | package us.fatehi.schemacrawler.webapp.service.storage; 30 | 31 | import static java.nio.file.Files.copy; 32 | import static java.nio.file.Files.createTempFile; 33 | import static java.nio.file.Files.delete; 34 | import static java.nio.file.Files.size; 35 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 36 | import static org.apache.commons.io.IOUtils.copy; 37 | import java.io.InputStream; 38 | import java.io.OutputStream; 39 | import java.nio.file.Files; 40 | import java.nio.file.Path; 41 | import java.nio.file.Paths; 42 | import java.util.Optional; 43 | import java.util.UUID; 44 | import org.slf4j.Logger; 45 | import org.slf4j.LoggerFactory; 46 | import org.springframework.context.annotation.Profile; 47 | import org.springframework.core.io.InputStreamSource; 48 | import org.springframework.lang.NonNull; 49 | import org.springframework.stereotype.Service; 50 | import jakarta.annotation.PostConstruct; 51 | import schemacrawler.schemacrawler.exceptions.InternalRuntimeException; 52 | import software.amazon.awssdk.services.s3.S3Client; 53 | import us.fatehi.schemacrawler.webapp.model.DiagramKey; 54 | 55 | @Service("amazonS3StorageService") 56 | @Profile("production") 57 | public class AmazonS3StorageService implements StorageService { 58 | 59 | private static final Logger LOGGER = LoggerFactory.getLogger(AmazonS3StorageService.class); 60 | 61 | private final String s3Bucket; 62 | private final S3Client s3Client; 63 | 64 | public AmazonS3StorageService(@NonNull final S3Client s3Client, @NonNull final String s3Bucket) { 65 | this.s3Client = s3Client; 66 | this.s3Bucket = s3Bucket; 67 | } 68 | 69 | @Override 70 | @PostConstruct 71 | public void init() { 72 | final boolean bucketExists = 73 | s3Client.headBucket(b -> b.bucket(s3Bucket)).sdkHttpResponse().isSuccessful(); 74 | if (!bucketExists) { 75 | throw new InternalRuntimeException( 76 | String.format("Amazon S3 bucket <%s> does not exist", s3Bucket)); 77 | } 78 | } 79 | 80 | /** {@inheritDoc} */ 81 | @Override 82 | public Optional retrieveLocal(final DiagramKey key, final FileExtensionType extension) 83 | throws Exception { 84 | 85 | if (extension == null) { 86 | return Optional.empty(); 87 | } 88 | 89 | try { 90 | final String filename = key + "." + extension.getExtension(); 91 | final Path tempFilePath = 92 | Paths.get( 93 | System.getProperty("java.io.tmpdir"), 94 | String.format( 95 | "sc-webapp-%s-%s.%s", key, UUID.randomUUID(), extension.getExtension())); 96 | 97 | // Download file from S3 to a local temporary file 98 | // IMPORTANT: Temporary file should not exist 99 | s3Client.getObject(b -> b.bucket(s3Bucket).key(filename), tempFilePath); 100 | 101 | // Check that the file is not empty 102 | if (size(tempFilePath) == 0) { 103 | delete(tempFilePath); 104 | LOGGER.warn(String.format("No data for file <%s.%s>", key, extension)); 105 | return Optional.empty(); 106 | } 107 | 108 | return Optional.of(tempFilePath); 109 | 110 | } catch (final Exception e) { 111 | LOGGER.warn(String.format("Could not retrieve file <%s.%s>", key, extension), e); 112 | return Optional.empty(); 113 | } 114 | } 115 | 116 | /** {@inheritDoc} */ 117 | @Override 118 | public void store( 119 | @NonNull final InputStreamSource streamSource, 120 | @NonNull final DiagramKey key, 121 | @NonNull final FileExtensionType extension) 122 | throws Exception { 123 | 124 | try { 125 | 126 | // Save stream to a temporary file, so the Amazon S3 API can get length of data and MD5 127 | // checksum, and avoid ResetException 128 | final String filename = key + "." + extension.getExtension(); 129 | final Path tempFilePath = createTempFile(null, filename).toAbsolutePath(); 130 | try (final InputStream inputStream = streamSource.getInputStream(); 131 | final OutputStream outputStream = Files.newOutputStream(tempFilePath); ) { 132 | copy(inputStream, outputStream); 133 | } 134 | 135 | // Upload local temporary file to S3 136 | s3Client.putObject(b -> b.bucket(s3Bucket).key(filename), tempFilePath); 137 | 138 | } catch (final Exception e) { 139 | LOGGER.warn(String.format("Could not store <%s.%s>", key, extension), e); 140 | } 141 | } 142 | 143 | /** {@inheritDoc} */ 144 | @Override 145 | public Path storeLocal( 146 | @NonNull final InputStreamSource streamSource, 147 | @NonNull final DiagramKey key, 148 | @NonNull final FileExtensionType extension) 149 | throws Exception { 150 | 151 | // Save stream to a local temporary file 152 | final Path tempFilePath = createTempFile("sc-webapp.", "." + extension.getExtension()); 153 | copy(streamSource.getInputStream(), tempFilePath, REPLACE_EXISTING); 154 | 155 | // Check that the file is not empty 156 | if (size(tempFilePath) == 0) { 157 | delete(tempFilePath); 158 | throw new Exception(String.format("No data for file <%s.%s>", key, extension)); 159 | } 160 | 161 | return tempFilePath; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/storage/FileExtensionType.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.service.storage; 29 | 30 | public enum FileExtensionType { 31 | SQLITE_DB("db", "application/x-sqlite3"), 32 | PNG("png", "image/png"), 33 | JSON("json", "application/json"), 34 | LOG("log", "text/plain"), 35 | DATA("data", "application/octet-stream"); 36 | 37 | private final String extension; 38 | private final String mimeType; 39 | 40 | FileExtensionType(final String extension, final String mimeType) { 41 | this.extension = extension; 42 | this.mimeType = mimeType; 43 | } 44 | 45 | public String getExtension() { 46 | return extension; 47 | } 48 | 49 | public String getMimeType() { 50 | return mimeType; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return getExtension(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/us/fatehi/schemacrawler/webapp/service/storage/StorageService.java: -------------------------------------------------------------------------------- 1 | /* 2 | ======================================================================== 3 | SchemaCrawler 4 | http://www.schemacrawler.com 5 | Copyright (c) 2000-2025, Sualeh Fatehi . 6 | All rights reserved. 7 | ------------------------------------------------------------------------ 8 | 9 | SchemaCrawler is distributed in the hope that it will be useful, but 10 | WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | SchemaCrawler and the accompanying materials are made available under 14 | the terms of the Eclipse Public License v1.0, GNU General Public License 15 | v3 or GNU Lesser General Public License v3. 16 | 17 | You may elect to redistribute this code under any of these licenses. 18 | 19 | The Eclipse Public License is available at: 20 | http://www.eclipse.org/legal/epl-v10.html 21 | 22 | The GNU General Public License v3 and the GNU Lesser General Public 23 | License v3 are available at: 24 | http://www.gnu.org/licenses/ 25 | 26 | ======================================================================== 27 | */ 28 | package us.fatehi.schemacrawler.webapp.service.storage; 29 | 30 | import java.nio.file.Path; 31 | import java.util.Optional; 32 | 33 | import org.springframework.core.io.InputStreamSource; 34 | 35 | import us.fatehi.schemacrawler.webapp.model.DiagramKey; 36 | 37 | /** Service to store files. */ 38 | public interface StorageService { 39 | 40 | /** 41 | * Initializes the service. Called via a Spring @PostConstruct. 42 | * 43 | * @throws Exception On an exception. 44 | */ 45 | void init() throws Exception; 46 | 47 | /** 48 | * Resolves a filename key and extension into a local default file-system path to a file. 49 | * 50 | * @param key Key. 51 | * @param extension Filename extension. 52 | * @return a local file-system path to a file, if one is found. 53 | * @throws Exception Exception resolving a path. 54 | */ 55 | Optional retrieveLocal(DiagramKey key, FileExtensionType extension) throws Exception; 56 | 57 | /** 58 | * Stores a stream given a key and extension. 59 | * 60 | * @param stream Input stream 61 | * @param key Key. 62 | * @param extension Filename extension. 63 | * @throws Exception Exception storing a file. 64 | */ 65 | void store(InputStreamSource stream, DiagramKey key, FileExtensionType extension) 66 | throws Exception; 67 | 68 | /** 69 | * Stores a stream given a filename key and extension locally in a temporary file. 70 | * 71 | * @param stream Input stream 72 | * @param key Key. 73 | * @param extension Filename extension. 74 | * @throws Exception Exception storing a file. 75 | */ 76 | Path storeLocal(InputStreamSource stream, DiagramKey key, FileExtensionType extension) 77 | throws Exception; 78 | } 79 | -------------------------------------------------------------------------------- /src/main/resources/api/schemacrawler-web-application.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: SchemaCrawler Web Application 4 | description: Create a SchemaCrawler schema diagram from an uploaded SQLite database file 5 | version: '16.26.1-1' 6 | license: 7 | name: Eclipse Public License (EPL-2.0) 8 | url: https://www.eclipse.org/legal/epl-2.0/ 9 | contact: 10 | name: SchemaCrawler 11 | email: sualeh@schemacrawler.com 12 | url: https://www.schemacrawler.com 13 | tags: 14 | - name: diagram-requests 15 | description: Create a SchemaCrawler schema diagram 16 | - name: diagram-results 17 | description: Retrieve a SchemaCrawler schema diagram 18 | paths: 19 | /diagrams: 20 | post: 21 | summary: Create a SchemaCrawler schema diagram 22 | description: Makes a request to create a SchemaCrawler schema diagram 23 | operationId: diagram-request 24 | tags: 25 | - diagram-requests 26 | requestBody: 27 | content: 28 | multipart/form-data: 29 | schema: 30 | allOf: 31 | - $ref: '#/components/schemas/DiagramRequest' 32 | - $ref: '#/components/schemas/OnRequestParameters' 33 | examples: 34 | diagram-request-basic: 35 | $ref: '#/components/examples/diagram-request-basic' 36 | diagram-request-complete: 37 | $ref: '#/components/examples/diagram-request-complete' 38 | responses: 39 | '201': 40 | $ref: '#/components/responses/created-diagram' 41 | '400': 42 | $ref: '#/components/responses/bad-request' 43 | '500': 44 | $ref: '#/components/responses/server-error' 45 | /diagrams/{key}: 46 | description: Retrieve a SchemaCrawler schema diagram request 47 | get: 48 | tags: 49 | - diagram-results 50 | operationId: retrieve-results 51 | parameters: 52 | - $ref: '#/components/parameters/key' 53 | responses: 54 | '200': 55 | $ref: '#/components/responses/retrieved-request' 56 | '400': 57 | description: Bad request 58 | '404': 59 | description: Results not found 60 | /diagrams/{key}/diagram: 61 | description: Retrieve a SchemaCrawler schema diagram 62 | get: 63 | tags: 64 | - diagram-results 65 | operationId: retrieve-diagram-image 66 | parameters: 67 | - $ref: '#/components/parameters/key' 68 | responses: 69 | '200': 70 | description: OK 71 | content: 72 | image/png: 73 | schema: 74 | type: string 75 | format: binary 76 | components: 77 | schemas: 78 | DiagramKey: 79 | description: Unique key identifying each request 80 | type: string 81 | minLength: 12 82 | maxLength: 12 83 | pattern: '[a-z0-9]{12}' 84 | readOnly: true # Returned by GET, not used in POST/PUT/PATCH 85 | DiagramRequest: 86 | type: object 87 | properties: 88 | key: 89 | $ref: '#/components/schemas/DiagramKey' 90 | name: 91 | description: Name of the user 92 | type: string 93 | minLength: 2 94 | maxLength: 255 95 | email: 96 | description: Email of the user 97 | type: string 98 | format: email 99 | minLength: 5 100 | maxLength: 255 101 | title: 102 | description: Diagram title 103 | type: string 104 | minLength: 0 105 | maxLength: 255 106 | file: 107 | description: Uploaded SQLite database file 108 | type: string 109 | format: binary 110 | file-hash: 111 | description: MD5 hash of SQLite database file contents to identify duplicate requests 112 | type: string 113 | minLength: 32 114 | maxLength: 32 115 | error: 116 | description: Error message 117 | type: string 118 | readOnly: true # Returned by GET, not used in POST/PUT/PATCH 119 | timestamp: 120 | description: Request timestamp in UTC 121 | type: string 122 | format: date-time 123 | readOnly: true # Returned by GET, not used in POST/PUT/PATCH 124 | OnRequestParameters: 125 | type: object 126 | required: 127 | - name 128 | - email 129 | - file 130 | OnResponseParameters: 131 | type: object 132 | required: 133 | - key 134 | - timestamp 135 | OnErrorParameters: 136 | type: object 137 | required: 138 | - error 139 | parameters: 140 | key: 141 | name: key 142 | description: Diagram key for lookups 143 | in: path 144 | required: true 145 | schema: 146 | $ref: '#/components/schemas/DiagramKey' 147 | responses: 148 | retrieved-request: 149 | description: Good diagram request information 150 | content: 151 | application/json: 152 | schema: 153 | allOf: 154 | - $ref: '#/components/schemas/DiagramRequest' 155 | - $ref: '#/components/schemas/OnResponseParameters' 156 | examples: 157 | diagram-response-good: 158 | $ref: '#/components/examples/diagram-response-good' 159 | links: 160 | retrieve-diagram-image-by-key: 161 | $ref: '#/components/links/retrieve-results-by-key' 162 | created-diagram: 163 | description: Created diagram request information 164 | headers: 165 | Location: 166 | description: Location of a newly created resource 167 | schema: 168 | type: string 169 | format: uri 170 | content: 171 | application/json: 172 | schema: 173 | allOf: 174 | - $ref: '#/components/schemas/DiagramRequest' 175 | - $ref: '#/components/schemas/OnResponseParameters' 176 | examples: 177 | diagram-response-good: 178 | $ref: '#/components/examples/diagram-response-good' 179 | links: 180 | retrieve-results-by-key: 181 | $ref: '#/components/links/retrieve-results-by-key' 182 | retrieve-diagram-image-by-key: 183 | $ref: '#/components/links/retrieve-results-by-key' 184 | bad-request: 185 | description: Diagram request information with error 186 | content: 187 | application/json: 188 | schema: 189 | allOf: 190 | - $ref: '#/components/schemas/DiagramRequest' 191 | - $ref: '#/components/schemas/OnResponseParameters' 192 | - $ref: '#/components/schemas/OnErrorParameters' 193 | examples: 194 | diagram-response-good: 195 | $ref: '#/components/examples/diagram-response-error' 196 | server-error: 197 | description: Diagram request information with error 198 | content: 199 | application/json: 200 | schema: 201 | allOf: 202 | - $ref: '#/components/schemas/DiagramRequest' 203 | - $ref: '#/components/schemas/OnResponseParameters' 204 | - $ref: '#/components/schemas/OnErrorParameters' 205 | examples: 206 | diagram-response-good: 207 | $ref: '#/components/examples/diagram-response-error' 208 | links: 209 | retrieve-results-by-key: 210 | operationId: retrieve-results 211 | parameters: 212 | key: '$response.body#/key' 213 | description: The 'key' from the response can be used as a path parameter 214 | retrieve-diagram-image-by-key: 215 | operationId: retrieve-diagram-image 216 | parameters: 217 | key: '$response.body#/key' 218 | description: The 'key' from the response can be used as a path parameter 219 | examples: 220 | diagram-request-basic: 221 | value: 222 | name: Sualeh Fatehi 223 | email: sualeh@hotmail.com 224 | file: test.db 225 | diagram-request-complete: 226 | value: 227 | name: Sualeh Fatehi 228 | email: sualeh@hotmail.com 229 | title: Test schema diagram 230 | file: test.db 231 | diagram-response-good: 232 | value: 233 | key: 38yyfzj0f5mk 234 | name: Sualeh Fatehi 235 | email: sualeh@hotmail.com 236 | file: test.db 237 | timestamp: '2021-12-11T04:28:59.237Z' 238 | diagram-response-error: 239 | value: 240 | key: 38xxfzj0f5mk 241 | name: Sualeh Fatehi 242 | email: sualeh@hotmail.com 243 | file: test.db 244 | timestamp: '2021-12-11T04:28:59.237Z' 245 | error: Expected SQLite database, but got content of text/plain 246 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | org: 4 | springframework: WARN 5 | root: WARN 6 | 7 | spring: 8 | servlet: 9 | multipart: 10 | max-request-size: 1024KB 11 | max-file-size: 1024KB 12 | thymeleaf: 13 | cache: 'false' 14 | profiles: 15 | active: production 16 | 17 | management: 18 | endpoints: 19 | web: 20 | exposure: 21 | include: info, health, metrics 22 | 23 | springdoc: 24 | api-docs: 25 | path: /openapi 26 | enabled: 'true' 27 | writer-with-default-pretty-printer: 'true' 28 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | SchemaCrawler Web Application v16.26.1-1 4 | Spring Boot ${spring-boot.version} 5 | -------------------------------------------------------------------------------- /src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-left: 3rem; 3 | padding-right: 3rem; 4 | padding-bottom: 3rem; 5 | } 6 | 7 | @media (max-width: 767px) { 8 | body { 9 | padding-left: 0; 10 | padding-right: 0; 11 | } 12 | } 13 | 14 | #page-content h1 { 15 | font-size: 2rem; 16 | margin-top: 2rem; 17 | color: rgb(92, 92, 92); 18 | } 19 | 20 | #page-content h2 { 21 | font-size: 1.5rem; 22 | margin-top: 1.5rem; 23 | color: rgb(92, 92, 92); 24 | } 25 | 26 | #page-content h3 { 27 | font-size: 1.2rem; 28 | margin-top: 1.2rem; 29 | color: rgb(92, 92, 92); 30 | } 31 | 32 | a:link { text-decoration: none; } 33 | a:visited { text-decoration: none; } 34 | -------------------------------------------------------------------------------- /src/main/resources/static/images/schemacrawler_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | -------------------------------------------------------------------------------- /src/main/resources/templates/SchemaCrawlerDiagram.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
8 | 9 |

SchemaCrawler Diagram

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
Name:
Email:
Database:
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |