├── .gitattributes ├── .github ├── auto_assign.yml ├── check-md-links.json ├── dependabot.yml ├── labels.yml ├── linkspector.yml ├── release-drafter.yml └── workflows │ ├── assign-pr.yml │ ├── check-md-links.yml │ ├── ci.yml │ ├── codeql.yml │ ├── coverage.yml │ ├── enforce-labels.yml │ ├── quality-monitor-pit.yml │ ├── run-release-drafter.yml │ ├── sync-labels.yml │ └── update-badges.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── PMDPlugin.xml ├── checkstyle-idea.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── runConfigurations │ └── All_in_codingstyle.xml ├── .remarkrc ├── CHANGELOG.md ├── Jenkinsfile ├── LICENSE ├── LIESMICH.md ├── README.md ├── badges ├── branch-coverage.svg ├── bugs.svg ├── line-coverage.svg ├── mutation-coverage.svg └── style.svg ├── bin ├── jenkins.sh ├── release.sh └── run.sh ├── doc ├── Abstract-Test-Pattern.md ├── Arbeiten-mit-GitHub-Classroom.md ├── Arbeiten-mit-GitHub.md ├── Arbeiten-mit-GitLab.md ├── Autograding.md ├── Best-Practice.md ├── Continuous-Integration.md ├── Externe-Tool-Integration.md ├── Fehlerbehandlung.md ├── Formatierung.md ├── Git.md ├── Kommentare.md ├── Namensgebung.md ├── State-Based-Vs-Interaction-Based.md ├── Testen.md ├── Working-with-Github.md ├── dependency-graph.puml ├── images │ ├── actions-annotation.png │ ├── actions-autograding.png │ ├── actions-buildlog.png │ ├── actions-overview.png │ ├── build-result.png │ ├── gitlab-autograding.png │ ├── gitlab-commit.png │ ├── gitlab-console.png │ └── quality-monitor.png └── uml │ ├── activity-diagram.puml │ ├── class-diagram-domain-model.puml │ ├── class-diagram-technical.puml │ ├── component-diagram-details.puml │ ├── component-diagram-overview.puml │ ├── context-diagram.puml │ ├── deployment-diagram.puml │ ├── sequence-diagram.puml │ ├── state-diagram.puml │ └── usecase-diagram.puml ├── docker-compose.yaml ├── docker ├── images │ ├── java-agent │ │ └── Dockerfile │ ├── jenkins-controller │ │ ├── Dockerfile │ │ ├── jenkins.yaml │ │ ├── plugins.txt │ │ ├── preconfigured-jobs │ │ │ ├── freestyle-analysis-model │ │ │ │ ├── config.xml │ │ │ │ └── nextBuildNumber │ │ │ ├── history-coverage-model │ │ │ │ ├── config.xml │ │ │ │ └── nextBuildNumber │ │ │ └── pipeline-codingstyle │ │ │ │ ├── config.xml │ │ │ │ └── nextBuildNumber │ │ └── security.groovy │ └── key-generator │ │ ├── Dockerfile │ │ └── keygen.sh └── volumes │ └── jenkins-home │ ├── .gitignore │ └── JENKINS_HOME_EXPORTED_BY_DOCKER ├── etc ├── Jenkinsfile.autograding ├── Jenkinsfile.coverage ├── Jenkinsfile.reference ├── assertj-templates │ ├── assertion_class_template.txt │ ├── assertions_entry_point_class_template.txt │ ├── has_assertion_template.txt │ └── soft_assertions_entry_point_class_template.txt ├── checkstyle-java-configuration.xml ├── checkstyle-tests-configuration.xml ├── pmd-java-configuration.xml ├── pmd-javascript-configuration.xml ├── pmd-tests-configuration.xml └── spotbugs-exclusion-filter.xml ├── package.json ├── pom.xml └── src ├── main └── java │ ├── edu │ └── hm │ │ └── hafner │ │ └── util │ │ ├── CSharpNamespaceDetector.java │ │ ├── Ensure.java │ │ ├── FilteredLog.java │ │ ├── Generated.java │ │ ├── JavaPackageDetector.java │ │ ├── KotlinPackageDetector.java │ │ ├── LineRange.java │ │ ├── LineRangeList.java │ │ ├── LookaheadStream.java │ │ ├── PackageDetector.java │ │ ├── PackageDetectorFactory.java │ │ ├── PackageDetectorRunner.java │ │ ├── PathUtil.java │ │ ├── PrefixLogger.java │ │ ├── ResourceExtractor.java │ │ ├── SecureXmlParserFactory.java │ │ ├── TreeString.java │ │ ├── TreeStringBuilder.java │ │ ├── VisibleForTesting.java │ │ └── package-info.java │ └── module-info.java └── test ├── java └── edu │ └── hm │ └── hafner │ ├── archunit │ ├── ArchitectureRules.java │ ├── ArchitectureRulesTest.java │ ├── ArchitectureTest.java │ └── PackageArchitectureTest.java │ └── util │ ├── AbstractComparableTest.java │ ├── AbstractEqualsTest.java │ ├── EnsureTest.java │ ├── FilteredLogTest.java │ ├── LineRangeListTest.java │ ├── LineRangeTest.java │ ├── LookaheadStreamTest.java │ ├── PackageDetectorRunnerTest.java │ ├── PathUtilTest.java │ ├── PrefixLoggerTest.java │ ├── ResourceExtractorTest.java │ ├── ResourceTest.java │ ├── SecureXmlParserFactoryTest.java │ ├── SerializableTest.java │ ├── StringComparableTest.java │ ├── StringEqualsTest.java │ └── TreeStringBuilderTest.java └── resources ├── archunit_ignore_patterns.txt ├── design.puml ├── edu └── hm │ └── hafner │ └── util │ ├── ActionBinding.cs │ ├── KotlinTest.txt │ ├── KotlinTest.txt.kt │ ├── MavenJavaTest.txt │ ├── MavenJavaTest.txt.java │ └── relative.txt └── logging.properties /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.java text 7 | *.xml text 8 | *.md text 9 | *.txt text 10 | 11 | # Denote all files that are truly binary and should not be modified. 12 | *.gif binary 13 | *.png binary 14 | *.jpg binary 15 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: false 2 | addAssignees: true 3 | 4 | assignees: 5 | - uhafner 6 | 7 | skipKeywords: 8 | - wip 9 | 10 | numberOfAssignees: 0 11 | -------------------------------------------------------------------------------- /.github/check-md-links.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpHeaders": [ 3 | { 4 | "urls": ["https://github.com/", "https://guides.github.com/", "https://help.github.com/", "https://docs.github.com/", "https://classroom.github.com"], 5 | "headers": { 6 | "Accept-Encoding": "zstd, br, gzip, deflate" 7 | } 8 | } 9 | ], 10 | "aliveStatusCodes": [200, 500, 503, 429] 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "" 10 | ignore: 11 | - dependency-name: org.eclipse.collections:eclipse-collections 12 | versions: 13 | - ">= 10.a" 14 | - dependency-name: org.eclipse.collections:eclipse-collections-api 15 | versions: 16 | - ">= 10.a" 17 | - dependency-name: net.javacrumbs.json-unit:json-unit-assertj 18 | versions: 19 | - ">= 3.0.0" 20 | 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | commit-message: 24 | prefix: "" 25 | schedule: 26 | interval: "daily" 27 | 28 | - package-ecosystem: "npm" 29 | directory: "/" 30 | commit-message: 31 | prefix: "" 32 | schedule: 33 | interval: "daily" 34 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: bug 2 | description: Bugs or performance problems 3 | color: CC0000 4 | - name: feature 5 | color: 1d76db 6 | description: New features 7 | - name: enhancement 8 | description: Enhancement of existing functionality 9 | color: 0366d6 10 | - name: breaking 11 | description: Breaking Changes 12 | color: e4b21d 13 | - name: tests 14 | description: Enhancement of tests 15 | color: 0e8a16 16 | - name: documentation 17 | description: Enhancement of documentation 18 | color: bfafea 19 | - name: internal 20 | description: Internal changes without user or API impact 21 | color: e6e6e6 22 | - name: dependencies 23 | description: Update of dependencies 24 | color: e6e6e6 25 | - name: java 26 | description: Pull requests that update Maven Java dependencies 27 | color: b6b6b6 28 | - name: javascript 29 | description: Pull requests that update Node Javascript dependencies 30 | color: b6b6b6 31 | - name: github_actions 32 | description: Pull requests that update Github Actions workflows 33 | color: 909090 34 | - name: hacktoberfest 35 | description: Pull requests that participate in Hacktoberfest 36 | color: 7057ff 37 | -------------------------------------------------------------------------------- /.github/linkspector.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - README.md 3 | dirs: 4 | - ./ 5 | - doc 6 | useGitIgnore: true 7 | ignorePatterns: 8 | - pattern: '^http://localhost:.*$' 9 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🎁' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | template: | 5 | $CHANGES 6 | 7 | # Emoji reference: https://gitmoji.carloscuesta.me/ 8 | categories: 9 | - title: 💥 Breaking Changes 10 | label: breaking 11 | - title: 🚀 New Features 12 | label: feature 13 | - title: ✨ Improvements 14 | label: enhancement 15 | - title: 🐛 Bug Fixes 16 | label: bug 17 | - title: 📝 Documentation 18 | label: documentation 19 | - title: 📦 Dependency Updates 20 | label: dependencies 21 | - title: 🔧 Internal Changes 22 | label: internal 23 | - title: 🚦 Tests 24 | label: tests 25 | 26 | version-resolver: 27 | major: 28 | labels: 29 | - 'removed' 30 | minor: 31 | labels: 32 | - 'feature' 33 | - 'enhancement' 34 | - 'deprecated' 35 | - 'dependencies' 36 | - 'documentation' 37 | - 'tests' 38 | - 'internal' 39 | patch: 40 | labels: 41 | - 'bug' 42 | default: minor 43 | 44 | replacers: 45 | - search: '/\[*JENKINS-(\d+)\]*\s*-*\s*/g' 46 | replace: '[JENKINS-$1](https://issues.jenkins.io/browse/JENKINS-$1) - ' 47 | -------------------------------------------------------------------------------- /.github/workflows/assign-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign PR' 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | assign-pr: 7 | name: 'Auto Assign PR' 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v2.0.0 11 | with: 12 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 13 | -------------------------------------------------------------------------------- /.github/workflows/check-md-links.yml: -------------------------------------------------------------------------------- 1 | name: 'Link Checker' 2 | 3 | on: push 4 | 5 | jobs: 6 | check-markdown-links: 7 | name: 'Check Markdown links' 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: umbrelladocs/action-linkspector@v1.3.4 12 | with: 13 | github_token: ${{ secrets.github_token }} 14 | reporter: github-pr-check 15 | fail_on_error: true 16 | filter_mode: nofilter 17 | config_file: '.github/linkspector.yml' 18 | level: 'info' 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'GitHub CI' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | strategy: 13 | matrix: 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | jdk: [17, 21] 16 | 17 | runs-on: ${{ matrix.platform }} 18 | name: on ${{ matrix.platform }} with JDK ${{ matrix.jdk }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up JDK ${{ matrix.jdk }} 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: 'temurin' 26 | java-version: '${{ matrix.jdk }}' 27 | check-latest: true 28 | cache: 'maven' 29 | - name: Set up Maven 30 | uses: stCarolas/setup-maven@v5 31 | with: 32 | maven-version: 3.9.9 33 | - name: Build with Maven 34 | env: 35 | BROWSER: chrome-container 36 | run: mvn -V --color always -ntp clean verify '-Djenkins.test.timeout=5000' '-Dgpg.skip' -Pno-ui-tests 37 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: "32 3 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze code 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ java ] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Setup Java 29 | uses: actions/setup-java@v4 30 | with: 31 | distribution: temurin 32 | java-version: 21 33 | cache: maven 34 | 35 | - name: Set up Maven 36 | uses: stCarolas/setup-maven@v5 37 | with: 38 | maven-version: 3.9.9 39 | 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v3 42 | with: 43 | languages: ${{ matrix.language }} 44 | queries: +security-and-quality 45 | 46 | - name: Build with Maven 47 | run: mvn -V --color always -ntp clean verify -Pskip 48 | 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v3 51 | with: 52 | upload: false 53 | output: sarif-results 54 | category: "/language:${{ matrix.language }}" 55 | 56 | - name: Filter SARIF results 57 | uses: advanced-security/filter-sarif@v1 58 | with: 59 | patterns: | 60 | -**/*Assert* 61 | input: sarif-results/${{ matrix.language }}.sarif 62 | output: sarif-results/${{ matrix.language }}.sarif 63 | 64 | - name: Upload SARIF results 65 | uses: github/codeql-action/upload-sarif@v3 66 | with: 67 | sarif_file: sarif-results/${{ matrix.language }}.sarif 68 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeCov' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | coverage: 11 | 12 | runs-on: ubuntu-latest 13 | name: Create and upload coverage report 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 21 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: '21' 22 | check-latest: true 23 | cache: 'maven' 24 | - name: Set up Maven 25 | uses: stCarolas/setup-maven@v5 26 | with: 27 | maven-version: 3.9.9 28 | - name: Generate coverage with JaCoCo 29 | run: mvn -V --color always -ntp clean verify -Pci 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v5.4.3 32 | with: 33 | file: 'target/site/jacoco/jacoco.xml' 34 | disable_search: true 35 | token: ${{secrets.CODECOV_TOKEN}} 36 | -------------------------------------------------------------------------------- /.github/workflows/enforce-labels.yml: -------------------------------------------------------------------------------- 1 | name: Label Checker 2 | on: 3 | pull_request_target: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | jobs: 6 | enforce-labels: 7 | name: 'Enforce PR labels' 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: mheap/github-action-required-labels@v5 14 | with: 15 | mode: minimum 16 | count: 1 17 | labels: "bug,feature,enhancement,breaking,tests,documentation,internal,dependencies" 18 | message: "Maintainer needs to assign at least one label before merge" 19 | -------------------------------------------------------------------------------- /.github/workflows/run-release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Drafter' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | update-release-draft: 11 | name: 'Update Release Draft' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: release-drafter/release-drafter@v6.1.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | paths: 8 | - .github/labels.yml 9 | - .github/workflows/sync-labels.yml 10 | 11 | jobs: 12 | sync-labels: 13 | name: Sync labels 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: micnncim/action-label-syncer@v1.3.0 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | manifest: .github/labels.yml 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node 2 | node_modules 3 | target 4 | *.iml 5 | *.bak 6 | .classpath 7 | .project 8 | .settings 9 | pom.xml.versionsBackup 10 | pom.xml.releaseBackup 11 | release.properties 12 | metrics.env 13 | maven.log 14 | .DS_Store 15 | .idea 16 | /package-lock.json 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | *.xml 2 | !checkstyle-idea.xml 3 | !PMDPlugin.xml 4 | !codeStyles/codeStyleConfig.xml 5 | !codeStyles/Project.xml 6 | !inspectionProfiles/Project_Default.xml 7 | !runConfigurations/*.xml 8 | !misc.xml 9 | !compiler.xml 10 | 11 | libraries 12 | scopes 13 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Ullrich Hafner's Coding Style -------------------------------------------------------------------------------- /.idea/PMDPlugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 18 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.18.0 5 | JavaOnlyWithTests 6 | true 7 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 34 | 43 | 44 | 46 | 47 | 69 | 70 | 72 | 73 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 59 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations/All_in_codingstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "remark-preset-lint-recommended", 4 | ["remark-lint-list-item-indent", false] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes of this coding style will be automatically 4 | logged by release drafter in [GitHub releases](https://github.com/uhafner/codingstyle/releases). 5 | 6 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | node('java-agent') { 2 | stage ('Checkout') { 3 | checkout scm 4 | } 5 | 6 | stage ('Git mining') { 7 | discoverGitReferenceBuild() 8 | mineRepository() 9 | gitDiffStat() 10 | } 11 | 12 | stage ('Build, Test, and Static Analysis') { 13 | withMaven(mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { 14 | sh 'mvn -V -e clean verify -Dgpg.skip -Pci' 15 | } 16 | 17 | recordIssues tools: [java(), javaDoc()], aggregatingResults: 'true', id: 'java', name: 'Java' 18 | recordIssues tool: errorProne(), healthy: 1, unhealthy: 20 19 | 20 | junit testResults: '**/target/*-reports/TEST-*.xml' 21 | 22 | recordCoverage(tools: [[parser: 'JACOCO']], 23 | id: 'jacoco', name: 'JaCoCo Coverage', 24 | sourceCodeRetention: 'EVERY_BUILD', 25 | qualityGates: [ 26 | [threshold: 60.0, metric: 'LINE', baseline: 'PROJECT', unstable: true], 27 | [threshold: 60.0, metric: 'BRANCH', baseline: 'PROJECT', unstable: true]]) 28 | 29 | recordIssues tools: [checkStyle(pattern: 'target/checkstyle-result.xml'), 30 | spotBugs(pattern: 'target/spotbugsXml.xml'), 31 | pmdParser(pattern: 'target/pmd.xml'), 32 | cpd(pattern: 'target/cpd.xml'), 33 | taskScanner(highTags:'FIXME', normalTags:'TODO', includePattern: '**/*.java', excludePattern: 'target/**/*')], 34 | qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]] 35 | } 36 | 37 | stage ('Mutation Coverage') { 38 | withMaven(mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { 39 | sh "mvn org.pitest:pitest-maven:mutationCoverage" 40 | } 41 | recordCoverage(tools: [[parser: 'PIT']], 42 | id: 'pit', name: 'Mutation Coverage', 43 | sourceCodeRetention: 'EVERY_BUILD', 44 | qualityGates: [ 45 | [threshold: 60.0, metric: 'MUTATION', baseline: 'PROJECT', unstable: true]]) 46 | } 47 | 48 | stage ('Collect Maven Warnings') { 49 | recordIssues tool: mavenConsole() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ---------------- Documents -------------- 2 | 3 | http://creativecommons.org/licenses/by/4.0/ 4 | 5 | ---------------- Sourcecode -------------- 6 | 7 | MIT License 8 | 9 | Copyright (c) 2014-2023 Dr. Ullrich Hafner 10 | 11 | Permission is hereby granted, free of charge, to any person 12 | obtaining a copy of this software and associated documentation 13 | files (the "Software"), to deal in the Software without 14 | restriction, including without limitation the rights to use, 15 | copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the 17 | Software is furnished to do so, subject to the following 18 | conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 30 | OTHER DEALINGS IN THE SOFTWARE. 31 | -------------------------------------------------------------------------------- /LIESMICH.md: -------------------------------------------------------------------------------- 1 | [![GitHub Actions](https://github.com/uhafner/codingstyle/workflows/GitHub%20CI/badge.svg)](https://github.com/uhafner/codingstyle/actions) 2 | [![CodeQL](https://github.com/uhafner/codingstyle/workflows/CodeQL/badge.svg)](https://github.com/uhafner/codingstyle/actions/workflows/codeql.yml) 3 | [![Line Coverage](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/line-coverage.svg)](https://app.codecov.io/gh/uhafner/codingstyle) 4 | [![Branch Coverage](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/branch-coverage.svg)](https://app.codecov.io/gh/uhafner/codingstyle) 5 | [![Mutation Coverage](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/mutation-coverage.svg)](https://github.com/uhafner/codingstyle/actions/workflows/quality-monitor-pit.yml) 6 | [![Warnings](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/style.svg)](https://github.com/uhafner/codingstyle/actions/workflows/quality-monitor-pit.yml) 7 | [![Bugs](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/bugs.svg)](https://github.com/uhafner/codingstyle/actions/workflows/quality-monitor-pit.yml) 8 | 9 | In jedem Java Projekt sollte der gesamte Quelltext die gleichen Kriterien bei Stil, Formatierung, etc. 10 | verwenden. In diesem Projekt werden die Kodierungsrichtlinien zu meinen Vorlesungen an der Hochschule 11 | München zusammengefasst. 12 | 13 | Dieses Projekt enthält neben der Dokumentation der wichtigsten Kodierungsrichtlinien auch gleichzeitig eine sinnvolle 14 | Konfiguration aller für Java kostenlos verfügbaren statischen Codeanalyse Tools mittels Maven. Diese dort enthaltenen und automatisch 15 | prüfbaren Richtlinien werden - soweit wie möglich - nicht mehr extra im Text erwähnt. Damit kann diese Projekt gleichzeitig als 16 | Vorlage für neue Projekte genutzt werden. Unterstützt werden aktuell folgende Tools: 17 | - [Checkstyle](https://checkstyle.org) 18 | - [PMD](https://pmd.github.io/) 19 | - [SpotBugs](https://spotbugs.github.io) 20 | - [Error Prone](https://errorprone.info) 21 | 22 | Die automatisch prüfbaren Richtlinien können für CheckStyle und Error Prone auch direkt als Warnungen in der 23 | Entwicklungsumgebung [IntelliJ](https://www.jetbrains.com/idea/) angezeigt werden (nach der Installation des 24 | entsprechenden IntelliJ Plugins). Zusätzlich sind die 25 | [IntelliJ Code Inspections](https://www.jetbrains.com/help/idea/code-inspection.html) gemäß meiner Richtlinien konfiguriert. 26 | Aktuell können diese allerdings noch nicht automatisch im Build überprüft werden 27 | (siehe [#7](https://github.com/uhafner/codingstyle/issues/7)). Insgesamt ist damit sichergestellt, 28 | dass immer die gleichen Warnungen angezeigt werden - egal wie und wo die Java Dateien weiterverarbeitet werden. 29 | Für SpotBugs und PMD ist der Umweg über das Build Management Tool [Maven](http://maven.apache.org/) erforderlich 30 | (die entsprechenden IntelliJ Plugins sind leider aus meiner Sicht noch nicht ausgereift genug bzw. verwenden eine separate Konfiguration). 31 | Die Verwendung von Maven hat zudem den Vorteil, dass die Ergebnisse hinterher leicht in den Continuous Integration Server 32 | [Jenkins](https://jenkins.io/) eingebunden werden können. Eine beispielhafte Integration in GitHub Actions und Jenkins ist auch bereits vorhanden. 33 | Diese ist im eigenen Abschnitt [Continuous Integration](doc/Continuous-Integration.md) 34 | ausführlicher beschrieben. Ebenso sind mehrere externe Tools konfiguriert, die die Qualität der Pull Requests 35 | in diesem Repository bewerten, Details dazu sind im Abschnitt [Integration externer Tools](doc/Externe-Tool-Integration.md) 36 | beschrieben. 37 | 38 | Die Richtlinien sind in den Vorlesungen 2014/2015 entstanden und werden laufend ergänzt. 39 | Aktuell bestehen diese aus den folgenden Abschnitten: 40 | 41 | - [Formatierung](doc/Formatierung.md) 42 | - [Namensgebung](doc/Namensgebung.md) 43 | - [Kommentare](doc/Kommentare.md) 44 | - Testen 45 | - [Allgemeine Tipps zum Testen](doc/Testen.md) 46 | - [State Based vs. Interaction Based Testing](doc/State-Based-Vs-Interaction-Based.md) 47 | - [Testen von Schnittstellen und Basisklassen](doc/Abstract-Test-Pattern.md) 48 | - [Fehlerbehandlung](doc/Fehlerbehandlung.md) 49 | - [Best Practice](doc/Best-Practice.md) 50 | 51 | Zur besseren Verdeutlichung der angesprochenen Themen sind diesem Projekt auch [Java Beispiele](./src/) angefügt, 52 | die sich möglichst genau an diese Richtlinien halten. 53 | 54 | Ideen und Inhalte für diesen Styleguide lieferten verschiedene Bücher, insbesondere aber das Buch 55 | "The Elements of Java Style" [1]. Diese Bücher sind allesamt wegweisend für die Softwareentwicklung und sind 56 | damit Pflichtlektüre für Berufstätige in der Softwareentwicklung: 57 | - [1] "The Elements of Java Style", Vermeulen, Ambler, Bumgardner, Metz, Misfeldt, Shur und Thompson, Cambridge University Press, 2000 58 | - [2] "The Pragmatic Programmer. From Journeyman to Master", Andrew Hunt, David Thomas, Ward Cunningham, Addison Wesley, 1999 59 | - [3] "Code Complete: A Practical Handbook of Software Construction", Steve McConnell, Microsoft Press, 2004 60 | - [4] "Clean Code: A Handbook of Agile Software Craftsmanship", Robert C. Martin, Prentice Hall, 2008 61 | - [5] "Effective Java", Third Edition, Joshua Bloch, Addison Wesley, 2017 62 | - [6] "Refactoring: Improving the Design of Existing Code", Martin Fowler, Addison Wesley, 1999 63 | - [7] "Java by Comparison", Simon Harrer, Jörg Lenhard, Linus Dietz, Pragmatic Programmers, 2018 64 | 65 | Die gesamten Dokumente dieser Kodierungsrichtlinien unterliegen der 66 | [Creative Commons Attribution 4.0 International Lizenz](https://creativecommons.org/licenses/by/4.0/). Der 67 | Quelltext aller Beispiele und Klassen unterliegt der [MIT Lizenz](https://en.wikipedia.org/wiki/MIT_License). 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Actions](https://github.com/uhafner/codingstyle/workflows/GitHub%20CI/badge.svg)](https://github.com/uhafner/codingstyle/actions) 2 | [![CodeQL](https://github.com/uhafner/codingstyle/workflows/CodeQL/badge.svg)](https://github.com/uhafner/codingstyle/actions/workflows/codeql.yml) 3 | [![Line Coverage](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/line-coverage.svg)](https://app.codecov.io/gh/uhafner/codingstyle) 4 | [![Branch Coverage](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/branch-coverage.svg)](https://app.codecov.io/gh/uhafner/codingstyle) 5 | [![Mutation Coverage](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/mutation-coverage.svg)](https://github.com/uhafner/codingstyle/actions/workflows/quality-monitor-pit.yml) 6 | [![Warnings](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/style.svg)](https://github.com/uhafner/codingstyle/actions/workflows/quality-monitor-pit.yml) 7 | [![Bugs](https://raw.githubusercontent.com/uhafner/codingstyle/main/badges/bugs.svg)](https://github.com/uhafner/codingstyle/actions/workflows/quality-monitor-pit.yml) 8 | 9 | Each Java project should follow a given coding style. 10 | I.e., all contributions to the source code should use the same formatting rules, design principles, code patterns, idioms, etc. 11 | This coding style provides the set of rules that I am using in my lectures about software development at Munich University of Applied Sciences. 12 | 13 | This project describes the coding style in detail (currently only available in German) and serves as a template project. 14 | It provides all necessary resources for a Java project to enforce this coding style using the following static analysis tools via Maven (and partly in IntelliJ): 15 | - [Checkstyle](https://checkstyle.org) 16 | - [PMD](https://pmd.github.io/) 17 | - [SpotBugs](https://spotbugs.github.io) 18 | - [Error Prone](https://errorprone.info) 19 | 20 | ❗This project requires a JDK version of 17 or higher.❗ 21 | 22 | Moreover, this project provides some sample classes that already use this style guide. 23 | These classes can be used as such but are not required in this project. 24 | These classes also use some additional libraries that are included using the Maven dependency mechanism. 25 | If the sample classes are deleted, then the dependencies can be safely deleted, too. 26 | 27 | This project and the associated static analysis tools are already running in continuous integration: an example CI pipeline is active for GitHub Actions. 28 | For [Jenkins](https://jenkins.io/) a full CI pipeline has been configured that includes stages to compile, test, run static code analysis, run code coverage analysis, and run mutation coverage analysis, see section [Continuous Integration](doc/Continuous-Integration.md) for details. 29 | Additionally, some development tools are configured in this GitHub project to evaluate the quality of pull requests, see section [integration of external tools](doc/Externe-Tool-Integration.md). 30 | 31 | Content of the style guide (only in German): 32 | - [Formatierung](doc/Formatierung.md) 33 | - [Namensgebung](doc/Namensgebung.md) 34 | - [Kommentare](doc/Kommentare.md) 35 | - Testen 36 | - [Allgemeine Tipps zum Testen](doc/Testen.md) 37 | - [State Based vs. Interaction Based Testing](doc/State-Based-Vs-Interaction-Based.md) 38 | - [Testen von Schnittstellen und Basisklassen](doc/Abstract-Test-Pattern.md) 39 | - [Fehlerbehandlung](doc/Fehlerbehandlung.md) 40 | - [Best Practice](doc/Best-Practice.md) 41 | 42 | A lot of ideas in this style are based on the following path-breaking books about software development: 43 | 44 | - [1] "The Elements of Java Style", Vermeulen, Ambler, Bumgardner, Metz, Misfeldt, Shur und Thompson, Cambridge University Press, 2000 45 | - [2] "The Pragmatic Programmer: journey to mastery", Second Edition, Andrew Hunt, David Thomas, Addison Wesley, 2019 46 | - [3] "Code Complete: A Practical Handbook of Software Construction", Steve McConnell, Microsoft Press, 2004 47 | - [4] "Clean Code: A Handbook of Agile Software Craftsmanship", Robert C. Martin, Prentice Hall, 2008 48 | - [5] "Effective Java", Third Edition, Joshua Bloch, Addison Wesley, 2017 49 | - [6] "Refactoring: Improving the Design of Existing Code", Martin Fowler, Addison Wesley, 1999 50 | - [7] "Java by Comparison", Simon Harrer, Jörg Lenhard, Linus Dietz, Pragmatic Programmers, 2018 51 | 52 | All documents in this project use the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). 53 | Source code (snippets, examples, and classes) are using the [MIT license](https://en.wikipedia.org/wiki/MIT_License). 54 | 55 | [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://en.wikipedia.org/wiki/MIT_License) 56 | [![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/) 57 | ![JDK17](https://img.shields.io/badge/jdk-17-blue.svg) 58 | -------------------------------------------------------------------------------- /badges/branch-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | Branches: 91% 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/bugs.svg: -------------------------------------------------------------------------------- 1 | 2 | Bugs: 0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/line-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | Lines: 92% 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/mutation-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | Mutations: 80% 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /badges/style.svg: -------------------------------------------------------------------------------- 1 | 2 | Warnings: 0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /bin/jenkins.sh: -------------------------------------------------------------------------------- 1 | # Make sure that docker runs with the current UID to avoid permission problems on volume docker/volumes/jenkins-home 2 | CURRENT_UID="$(id -u)" 3 | export CURRENT_UID 4 | 5 | CURRENT_GID="$(id -g)" 6 | export CURRENT_GID 7 | 8 | CURRENT_USER="$(id -u):$(id -g)" 9 | export CURRENT_USER 10 | 11 | echo Running docker compose with user ID $CURRENT_USER 12 | 13 | docker pull jenkins/jenkins:lts-jdk21 14 | docker compose build --pull 15 | docker compose up --always-recreate-deps 16 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git pull 4 | git push 5 | mvn -B clean build-helper:parse-version release:prepare release:perform -DdevelopmentVersion=\${parsedVersion.majorVersion}.\${parsedVersion.nextMinorVersion}.0-SNAPSHOT 6 | mvn -Dproject.version=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.patchVersion} com.github.ferstl:depgraph-maven-plugin:graph scm:add -Dincludes=doc/dependency-graph.puml 7 | -------------------------------------------------------------------------------- /bin/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git pull 4 | git push 5 | mvn build-helper:parse-version 6 | mvn -Dproject.version=1.0.0 com.github.ferstl:depgraph-maven-plugin:graph 7 | -------------------------------------------------------------------------------- /doc/Continuous-Integration.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration des Coding Style 2 | 3 | Gemäß dem Grundsatz **eat your own dogfood** ist dieser Coding Style bereits für die Continuous Integration in [GitHub Actions](https://github.com/features/actions), [GitLab CI](https://docs.gitlab.com/ee/ci/) und [Jenkins](https://jenkins.io) vorbereitet. 4 | 5 | ## Maven Konfiguration 6 | 7 | Sowohl für GitHub Actions als auch für Jenkins erfolgt die Automatisierung des Builds über Maven. Im zugehörigen [POM](../pom.xml) sind alle Versionen der benutzten Maven Plugins und der benötigten Abhängigkeiten über Properties definiert, d.h. eine Aktualisierung lässt sich im entsprechenden Abschnitt leicht selbst durchführen bzw. wird über den [Dependabot](https://dependabot.com) Roboter von GitHub automatisch über einen Pull Request aktualisiert. 8 | U.a. sind die folgenden Plugins vorkonfiguriert: 9 | - maven-compiler-plugin: konfiguriert die Java Version auf Java 11 und legt alle Error Prone Regeln fest. Die Java Version kann beliebig aktualisiert werden. 10 | - maven-javadoc-plugin: aktiviert die strikte Prüfung von JavaDoc Kommentaren 11 | - maven-jar-plugin: legt einen Modulnamen fest. Außerdem wird ein test-jar konfiguriert, sodass alle Tests (und abstrakte Testklassen) auch als Dependencies genutzt werden können. 12 | - maven-pmd-plugin: prüft das Projekt mit [PMD](https://pmd.github.io/), die Regeln liegen in den Dateien [pmd-java-configuration.xml](../etc/pmd-java-configuration.xml), [pmd-tests-configuration.xml](../etc/pmd-tests-configuration.xml) und [pmd-javascript-configuration.xml](../etc/pmd-javascript-configuration.xml). 13 | - maven-checkstyle-plugin: prüft das Projekt mit [CheckStyle](https://checkstyle.sourceforge.io/), die Regeln liegen in den Dateien [checkstyle-java-configuration.xml](../etc/checkstyle-java-configuration.xml) und [checkstyle-tests-configuration.xml](../etc/checkstyle-tests-configuration.xml). 14 | - spotbugs-maven-plugin: prüft das Projekt mit [SpotBugs](https://spotbugs.github.io/), alle Regeln werden verwendet mit den Ausnahmen definiert in der Datei [spotbugs-exclusion-filter.xml](../etc/spotbugs-exclusion-filter.xml). 15 | - revapi-maven-plugin: prüft, ob die aktuelle Versionsnummer die [semantische Versionierung](https://semver.org) berücksichtigt (source and binary). D.h. es gilt: 16 | 1. Eine neue **Major** Version wurde definiert, wenn das API nicht mehr abwärtskompatibel ist. 17 | 2. Eine neue **Minor** Version wurde definiert, wenn eine neue Funktionalität abwärtskompatibel hinzugefügt wurde. 18 | 3. Eine neue **Patch** Version wurde definiert, wenn Fehler abwärtskompatibel behoben wurden. 19 | - maven-surefire-plugin: aktiviert das Erkennen der Annotationen der Architekturtests mit [ArchUnit](https://www.archunit.org) 20 | - jacoco-maven-plugin: misst die Code Coverage der Testfälle mit [JaCoCo](https://www.jacoco.org) 21 | - pitest-maven: misst die Mutation Coverage der Testfälle mit [PITest](http://pitest.org) 22 | 23 | ## GitHub Actions 24 | 25 | [![GitHub Actions](https://github.com/uhafner/codingstyle/workflows/GitHub%20CI/badge.svg)](https://github.com/uhafner/codingstyle/actions) 26 | 27 | Die Konfiguration der Continuous Integration in GitHub Actions is sehr [einfach](../.github/workflows/ci.yml) über eine Pipeline möglich. Da der gesamte Build über Maven automatisiert ist, besteht die Pipeline eigentlich nur aus einem Maven Aufruf, der das Projekt baut, alle Tests (Unit und Integrationstests) ausgeführt, die statische Code Analyse durchführt und schließlich die Coverage misst. GitHub Actions bietet auch die Möglichkeit, Matrix Builds durchzuführen: d.h., der Build wird z.B. auf den Plattformen Linux, Windows und macOS oder mit den Java Versionen 17 und 21 parallel durchgeführt. Ein Beispiel für die Konfiguration eines Matrix Builds ist in der Datei [ci.yml](../.github/workflows/ci.yml) zu finden. 28 | 29 | Wenn gewünscht, können die Ergebnisse der statischen Code Analyse und der Code Coverage Tools auch direkt im Commit oder Pull-Request angezeigt werden. Dazu muss in der Pipeline meine [Quality Monitor Action](https://github.com/uhafner/quality-monitor) aktiviert werden. Eine Beispielkonfiguration ist in der Datei [quality-monitor-pit.yml](../.github/workflows/quality-monitor-pit.yml) zu finden, das Ergebnis in der nachfolgenden Abbildung: 30 | 31 | ![Quality Monitor](images/quality-monitor.png) 32 | 33 | ## Jenkins 34 | 35 | Eine Beispielintegration mit Jenkins ist auch bereits vorhanden. Diese ist im [Jenkinsfile](../Jenkinsfile) hinterlegt und startet die Integration in mehreren Schritten (Stages). Zunächst werden auch hier alle Schritte wie in GitHub Actions aufgerufen. Anschließend erfolgt noch ein Start der Mutation Coverage mit [PIT](http://pitest.org). Insgesamt ist die CI Konfiguration für Jenkins umfangreicher, da nicht nur der eigentliche Build konfiguriert wird, sondern auch die Darstellung der Ergebnisse im Jenkins UI über die entsprechenden Jenkins Plugins konfiguriert wird. 36 | 37 | ### Lokale CI in Jenkins (über Docker Compose) 38 | 39 | Da es für Jenkins keinen öffentlichen Service wie bei GitHub Actions gibt, um eigene Projekte zu bauen, muss die Jenkins Integration lokal auf einem Team-Server durchgeführt werden. Zur Vereinfachung des Jenkins Setup ist in diesem Coding Style eine lauffähige Jenkins Installation enthalten (im Sinne von *Infrastructure as Code*). Diese kann über `bin/jenkins.sh` gestartet werden. Anschließend wird die aktuelle Jenkins LTS Version mit allen benötigten Plugins in einem Docker Container gebaut und gestartet (das dauert beim ersten Aufruf etwas). Dazu wird ebenso ein als Docker Container initialisierter Java Agent verbunden, der die Builds ausführt. 40 | 41 | 42 | Nach einem erfolgreichen Start von Jenkins sind dann unter [http://localhost:8080](http://localhost:8080) mehrere Jenkins Jobs sichtbar. Einer dieser Jobs baut das vorliegende Coding Style Projekt. Der Zugang auf diesen lokalen Rechner erfolgt zur Vereinfachung mit Benutzer `admin` und Passwort `admin`, anschließend hat man volle Jenkins Administrationsrechte. Die jeweiligen Jobs müssen danach manuell gestartet werden, die Ergebnisse der Tests, Code und Mutation Coverage sowie statischen Analyse werden dann automatisch visualisiert. Das Jenkins Home Verzeichnis ist im Docker Container als externes Volume angelegt: d.h. der Zugriff kann auf dem Host direkt im Verzeichnis `docker/volumes/jenkins-home` erfolgen. 43 | 44 | Nach einem ersten Build in Jenkins sollte sich dann in etwa folgendes Bild ergeben: 45 | 46 | ![Jenkins Build Summary](images/build-result.png) 47 | -------------------------------------------------------------------------------- /doc/Externe-Tool-Integration.md: -------------------------------------------------------------------------------- 1 | # Integration externer Tools 2 | 3 | Zur Unterstützung des Entwicklungsworkflows sind bereits verschiedene externe Tools konfiguriert. 4 | 5 | ## Automatische Aktualisierung der Abhängigkeiten 6 | 7 | Die Abhängigkeiten des Projektes (Dependencies) werden über Maven verwaltet und sind im [POM](../pom.xml) konfiguriert. 8 | Um das Projekt immer auf dem laufenden Stand zu halten, ist die GitHub App [Dependabot](https://dependabot.com) 9 | aktiv geschaltet: diese prüft automatisch, on neue Versionen einer Bibliothek (oder eines Maven Plugins) zur Verfügung 10 | stehen. Ist dies der Fall, erstellt der Roboter automatisch einen 11 | [Pull Request](https://github.com/uhafner/codingstyle/pulls), der die Version aktualisiert. 12 | 13 | ## Automatisierte Generierung eines Changelog 14 | 15 | Damit der Nutzer des Projekts immer über alle Änderungen im Projekt im Bilde ist, ist die GitHub App 16 | [Release Drafter](https://github.com/toolmantim/release-drafter) aktiviert. Diese erstellt automatisch neue Changelog 17 | Einträge im [GitHub Releases Bereich](https://github.com/uhafner/codingstyle/releases). Diese Einträge werden 18 | aus den Titeln der Pull Requests generiert, d.h. jede Änderung am Projekt sollte über Pull Requests erfolgen und nicht 19 | über direkte Git Commits - dies entspricht auch dem Vorgehen des [GitHub Flows](https://guides.github.com/introduction/flow/). 20 | 21 | ## Statische Analyse von Pull Requests mit CheckStyle und PMD 22 | 23 | Wie bereits im Abschnitt [Continuous Integration](Continuous-Integration.md) erwähnt, gibt es keinen öffentlichen 24 | Service, ein GitHub Projekt mit Jenkins automatisiert zu bauen. Für GitHub Actions gibt es diesen Service, aber 25 | die Visualisierung der Ergebnisse der statischen Analyse wird leider nicht unterstützt. 26 | 27 | ## Security Analyse von Pull Requests 28 | 29 | Zusätzlich zur statischen Analyse wird ein Pull Request auf Sicherheitslücken untersucht. Dies erfolgt über die GitHub 30 | App [LGTM](https://lgtm.com). 31 | 32 | ## Bewertung der Code Coverage 33 | 34 | Die Code Coverage der Unit-Tests wird ebenfalls nach jeder Änderung (bzw. für jeden Pull Request) an den Service 35 | [Codecov](https://app.codecov.io/gh/uhafner/codingstyle) weitergeleitet, der die Resultate grafisch visualisiert. 36 | -------------------------------------------------------------------------------- /doc/Fehlerbehandlung.md: -------------------------------------------------------------------------------- 1 | # Fehlerbehandlung mit Exceptions 2 | 3 | In einem Java-Programm können zur Laufzeit verschiedene Fehler auftreten: 4 | - logische Fehler (z.B. Programmierfehler) 5 | - Problem im Java-Laufzeitsystem (z.B. Speichermangel) 6 | - Probleme mit der Peripherie (z.B. mit Internet, Datenbank, Dateisystem) 7 | - fehlerhafte Bedienung (z.B. Benutzereingaben) 8 | 9 | Zum Melden eines solchen Fehlers benutzen wir i.A. Exceptions. D.h. bei Auftritt eines dieser Fehler wird das 10 | Programm an der aktuellen Stelle abgebrochen und eine Exception wird geworfen. Dies hat den Vorteil, dass diese 11 | Laufzeitfehler behandelt werden müssen, und somit nicht ignoriert werden können. 12 | 13 | Auf diese Fehler kann anschließend an geeigneter Stelle im Programm reagiert werden. Je nach Fehler kann 14 | - die zum Fehler führende Handlung wiederholt werden (Internet ist wieder verfügbar, Benutzereingabe verbessert) 15 | - der Fehler in einem Dialog angezeigt werden 16 | - der Fehler ignoriert werden 17 | - das Programm abgebrochen werden 18 | 19 | Wird keine Fehlerbehandlung umgesetzt, wird das Programm mit einem Stacktrace beendet. 20 | 21 | ## Validieren von Eingabeparametern 22 | 23 | Sichere und robuste Software vertraut niemals Eingabewerten. Es gilt der Grundsatz: „all input is evil“. D.h. in 24 | öffentlichen Methoden und Konstruktoren müssen Parameter immer validiert werden. 25 | Genügen diese Parameter nicht dem erwarteten Vertrag, muss eine Exception geworfen werfen. I.A. ist dafür die 26 | `IllegalArgumentExeption` geeignet. Ggf. kann davon abgewichen werden, um z.B. mit der `IndexOutOfBoundsException` oder 27 | der `IllegalStateException` eine genauere Fehlerursache aufzuzeigen. Die `NullPointerException` hat einen Sonderstatus, 28 | sie wird i.A. automatisch geworfen bei Zugriff auf `null`. Ein Werfen dieser Exception ist nur nötig, falls ein 29 | Parameter ohne direkte Nutzung in einer Objektvariable gespeichert wird. 30 | 31 | ## Exception-Typen 32 | 33 | Im JDK ist eine Vielzahl von Exception Klassen vordefiniert, diese haben alle die Endung `Exception` im Klassennamen. 34 | Es macht selten Sinn, eigene weitere Exceptions zu definieren. Wird eine Exception benötigt, ist i.A. im JDK 35 | immer eine passende dabei. 36 | 37 | Java bietet als einzige Programmiersprache zwei verschiedene Exception Kategorien an: 38 | - checked Exceptions: müssen deklariert und gefangen werden 39 | - unchecked Exceptions: können deklariert und gefangen werden 40 | 41 | Das Konzept hat sich in der Praxis nicht bewährt (Details gibt es in Artikeln wie [Checked Exceptions are Evil](https://phauer.com/2015/checked-exceptions-are-evil/) 42 | oder [The Trouble with Checked Exceptions](https://www.artima.com/intv/handcuffs.html)), 43 | daher nutzen wir möglichst immer unchecked Exceptions. Werden Bibliotheken genutzt, die mit checked Exceptions arbeiten, 44 | bietet es sich an, diese an der Aufrufstelle zu fangen und in eine äquivalente unchecked Exception umzuwandeln. 45 | 46 | ## Dokumentation von Exceptions 47 | 48 | Wenn Methoden oder Konstruktoren eine Exception werfen können, sollte dies im Methodenkopf mit einer `throws` Klausel 49 | und im JavaDoc mit einem `@throws` Tag dokumentiert werden. Dort sollte auch immer der Grund beschrieben sein. Dies ist 50 | nicht kaskadierend erforderlich, d.h. eine Methode die mehrere Methoden aufruft, die Exceptions werfen muss diese nicht 51 | mehr aufführen, sondern nur die selbst geworfenen Exceptions. 52 | 53 | ## Fehlerursache (Kontext) 54 | 55 | Wird eine Exception geworfen, muss die Fehlerursache (d.h. der Kontext) genau lokalisiert werden, und als Text im 56 | Konstruktor der Exception übergeben werden. D.h. was ist das Problem? Wie konnte das Problem auftreten? 57 | Welche Parameterwerte sind Ursache? Diese Meldung ist i.A. nur sichtbar für das Entwicklungsteam und kann z.B. auch dafür 58 | passend formuliert werden. Wird darüber hinaus eine andere Exception gefangen und umgewandelt, 59 | so ist diese auch im Konstruktor der neuen Exception zu übergeben. Generell gilt: Der Default-Konstruktor einer 60 | Exception darf **nie** verwendet werden. 61 | 62 | ## Testen von Exceptions 63 | 64 | Das korrekte Werfen von Exceptions sollte generell getestet werden, siehe dazu den passenden Abschnitt im Kapitel zum 65 | [Testen](Testen.md#testen-von-exceptions). 66 | 67 | ## Best practice 68 | 69 | Exceptions dürfen nur für außergewöhnliche Ereignisse verwendet werden, d.h. die Programmflusssteuerung darf 70 | niemals über Exceptions durchgeführt werden. Dies lässt sich umso leichter erreichen, wenn es zu jeder Methode **x** eine 71 | zweite Methode **y** gibt, die prüft, ob die Methode **x** mit den gegebenen Eingabeparametern 72 | eine Exception werfen würde. 73 | 74 | Beispiele: Eine `IndexOutOfBoundsException` lässt sich bei `list.get(0)` vermeiden, wenn vorab die Anzahl der Elemente 75 | geprüft wird (mit `isEmpty()` oder `size()`). Analoge Methodenpaare finden sich z.B. bei `get(object)` und `contains(object)`, 76 | oder `new FileInputStream(file)` und `file.exists()`, oder `iterator.next()` und `iterator.hasNext()`. 77 | 78 | Exceptions können mit try/catch/finally Blöcken gefangen werden. Dadurch wird der Code recht schnell 79 | unübersichtlich, da das **Single Responsibility Principle** (siehe [4, S. 138-139]) verletzt wird. 80 | Sinnvoll ist daher das Aufteilen des Programmstücks in die folgenden Teile: 81 | - im try Block: Aufruf einer Untermethode (keine Fehlerbehandlung) 82 | - im catch Block: Fehlerbehandlung 83 | - im finally Block: ggf. Aufräumen 84 | - in der Untermethode: Implementierung der Anforderungen ohne Rücksicht auf Exceptions 85 | 86 | In einem finally Block sollte niemals eine Exception geworfen werden. Außerdem sollte im finally Block niemals die 87 | Methode mit return beendet werden. 88 | 89 | Ebenso sollten Exceptions niemals ignoriert werden. Sollte es erforderlich sein, einen leeren catch Block zu verwenden, so muss 90 | dies mit einem Kommentar versehen werden! 91 | -------------------------------------------------------------------------------- /doc/Git.md: -------------------------------------------------------------------------------- 1 | Alle Dokumente und Sourcen, die im Semester erstellt werden, 2 | müssen in einer Versionsverwaltung abgelegt werden - dies bezieht sich 3 | auf Texte, UML Diagramme und Source Code. Wir benutzen dafür 4 | [Git](https://git-scm.com). Git ist eine verteilte Versionsverwaltung, d.h. 5 | das Repository, das alle Änderungen an unseren Dateien abspeichert, ist 6 | auf mehreren Rechnern verteilt. Zum einen auf den eigenen lokalen Rechnern der 7 | Teammitglieder und zum anderen auf einem zentralen Server, der als 8 | Synchronisierungsknoten für das Team verwendet wird. Dieser Server wird 9 | typischerweise von einem sogenannten Hoster (oder Service Provider) zur Verfügung gestellt. 10 | Aktuell ist GitHub der bekannteste Hoster, aber auch GitLab oder BitBucket sind 11 | in der Industrie weit verbreitet. Die Hochschule bietet selbst auch ein Hosting über 12 | das LRZ an: hier kommt die Software von GitLab zum Einsatz, allerdings auf unserer eigenen 13 | Hardware. 14 | 15 | Die Arbeit mit Git ist schnell gelernt, es gibt dazu eine Vielzahl an Online verfügbaren 16 | Quellen, die im folgenden aufgelistet sind: 17 | - [Learn Version Control with Git](https://www.git-tower.com/learn/git/videos/) Video Tutorials vom 18 | Tower Team (inkl. passendem [Git Cheat Sheet](https://www.git-tower.com/blog/git-cheat-sheet/)) 19 | - [Learn Git with GitKraken](https://www.gitkraken.com/resources/learn-git) Video Tutorials vom GitKraken Team 20 | - [Das Git-Buch](http://gitbu.ch/index.html) von Valentin Haenel und Julius Plenz 21 | - Das [Pro Git](https://git-scm.com/book/de/v2) Buch von Scott Chacon 22 | - [Git and GitHub learning resources](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/git-and-github-learning-resources) vom GitHub Team 23 | (inkl. passendem [Git Cheat Sheet](https://education.github.com/git-cheat-sheet-education.pdf)) 24 | - [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) von Chris Beams 25 | 26 | 27 | -------------------------------------------------------------------------------- /doc/Kommentare.md: -------------------------------------------------------------------------------- 1 | # Kommentare 2 | 3 | Java kennt drei verschiedene Varianten von Kommentaren: 4 | - der einzeilige Kommentar wird mit `//` eingeleitet und geht bis zum Zeilenende 5 | - der mehrzeilige Kommentar wird mit `/*` gestartet und mit `*/` beendet 6 | - der JavaDoc Kommentar wird mit `/**` gestartet und mit `*/` beendet. 7 | 8 | ## Kommentare zum Quelltext 9 | 10 | Wohlüberlegte Kommentare *können* die Qualität des Quelltextes steigern. Sie sind i.A. ein notwendiges Übel und 11 | helfen uns darüber hinweg, dass wir nicht alles durch Code alleine ausdrücken können. Dafür kann eine der beiden 12 | ersten Varianten genutzt werden. Diese erklären das Ziel des Quelltextes und verdeutlichen damit unsere Absicht. 13 | 14 | Wichtig zu beachten sind aber die folgenden Aussagen von Brian W. Kernighan: 15 | - Make sure comments and code agree. 16 | - Don't just echo the code with comments - make every comment count. 17 | - Don't comment bad code - rewrite it. 18 | 19 | (Aus dem Klassiker: B. W. Kernighan and P. J. Plauger, The Elements of Programming Style, McGraw-Hill, New York, 1974) 20 | 21 | Insgesamt gilt: so wenig Kommentare wie nötig verwenden. Besser Bezeichner passend auswählen und komplexen Code vereinfachen oder 22 | z.B. durch zusätzliche Methoden strukturieren. D.h. bevor ein Kommentar für einen komplexen 23 | Programmausschnitt erstellt wird, sollte dieser Ausschnitt in eine Methode ausgelagert werden. Die Methode selbst wird dann 24 | mit dem beabsichtigten Kommentar benannt. Eine ausführlichere Behandlung dieses Themas findet sich in [4], dort 25 | sind viele Beispiele und Negativbeispiele aufgeführt. 26 | 27 | 28 | ## Spezielle Kommentare 29 | 30 | Häufig findet man auch Kommentare zur Kennzeichnung des Copyrights. Auch wenn diese den obigen Regeln widersprechen, 31 | müssen diese aus juristischen Gründen in vielen Dateien vorhanden sein. 32 | 33 | Werden in einem Programmabschnitt kleine Verbesserungsmöglichkeiten entdeckt, die im Moment nicht behoben werden können, 34 | so können diese ebenso mit einem Kommentar beschrieben werden. Hier ist wichtig, die Kommentare mit einer Markierung 35 | wie TODO oder FIXME zu versehen. In den meisten Projekten gilt: mit FIXME werden Stellen markiert, die noch vor 36 | der Veröffentlichung eines Programms zu beheben sind. Lediglich mit TODO markierte Stellen können länger im Programm 37 | verbleiben. Wichtig bleibt: größere Änderungswünsche sollten immer in einem Issue Tracker verwaltet werden, damit diese 38 | auch mit in die Planung einfließen können. 39 | 40 | Auf der anderen Seite sind Kommentare zur Versionshistorie einer Datei nicht sinnvoll, diese werden sowieso in der 41 | Versionsverwaltung abgelegt und sind somit redundant. 42 | 43 | ## JavaDoc 44 | 45 | JavaDoc wird genutzt um die öffentliche Schnittstelle eines Programms zu dokumentieren. Diese Kommentare sind unerlässlich 46 | und müssen für alle Klassen und Methoden verfasst werden, die mindestens die Sichtbarkeit `protected` haben. Wird eine Klasse 47 | serialisiert (z.B. wenn sie die Schnittstelle `Serializable` implementiert), dann müssen auch 48 | private Attribute kommentiert werden, da in diesem Fall auch diese zur öffentlichen Schnittstelle einer Klasse gehören. 49 | 50 | Die [Java Bibliotheken](https://docs.oracle.com/javase/8/docs/api/) selbst bieten schöne Beispiele, wie solche Kommentare 51 | auszusehen haben und wie nützlich diese sind. Eine kleine Einführung zu diesem Thema ist auf den 52 | [Oracle Seiten](https://www.oracle.com/java/technologies/javase/javadoc.html) zu finden. 53 | 54 | JavaDoc Kommentare werden im aktiv geschrieben. Der erste Satz (der mit einem Punkt abgeschlossen wird) muss eine 55 | Zusammenfassung sein. Dieser wird im generierten HTML Dokument als Überschrift dargestellt. Das zu beschreibende Element 56 | wird dabei nicht noch einmal wiederholt, der Kommentar wird dadurch möglichst knapp. 57 | D.h. **"An ordered collection"** statt **"This interface defines an ordered collection"**, oder 58 | **"Returns whether this list contains no elements"** statt **"This method returns whether this list contains no elements"**. 59 | Die folgenden Sätze können dann das Element genauer beschreiben, hilfreich ist hierbei oft die Angabe eines 60 | Anwendungsbeispiels. Werden dabei Codestücke bzw. Variablen in die Beschreibung eingebettet, so müssen diese die Syntax 61 | `{@code ...}` nutzen. Werden Klassen oder Methoden referenziert, so werden diese mit der Syntax `{@link ...}` bzw. 62 | `{@linkplain ...}` eingefügt, nur so kann die IDE die Kommentare mit entsprechenden Hyperlinks anzeigen (*linkplain* verwendet 63 | einen Zeichensatz mit variabler, *link* mit fester Breite). 64 | Ganze Quelltext-Abschnitte, die über mehrere Zeilen gehen, werden mit der Syntax `
{@code ...}
` eingebettet. 65 | 66 | Beispiel: 67 | 68 | ```java 69 | /** 70 | * Checks if a {@link CharSequence} is empty ("") or {@code null}. 71 | * 72 | *
{@code
73 |  * StringUtils.isEmpty(null)      = true
74 |  * StringUtils.isEmpty("")        = true
75 |  * StringUtils.isEmpty(" ")       = false
76 |  * StringUtils.isEmpty("bob")     = false
77 |  * StringUtils.isEmpty("  bob  ") = false
78 |  * }
79 | * 80 | * @param text 81 | * the text to check, may be {@code null} 82 | * @return {@code true} if the text is empty or {@code null} 83 | * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) 84 | */ 85 | public static boolean isEmpty(final CharSequence text) { 86 | return text == null || text.length() == 0; 87 | } 88 | 89 | ``` 90 | -------------------------------------------------------------------------------- /doc/Namensgebung.md: -------------------------------------------------------------------------------- 1 | # Namensgebung 2 | 3 | Bezeichner (*Identifier*) sind in Java beliebig lang und bestehen aus einer Zeichenkette 4 | von großen und kleinen Buchstaben, Ziffern oder dem Underscore `_`. Hierbei werden Groß und Kleinschreibung 5 | unterschieden (klein ist nicht Klein). Folgende Einschränkungen sind dabei zu beachten: 6 | Das erste Zeichen darf keine Ziffer sein und 7 | [ca. 50 Schlüsselwörter](http://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html) 8 | sind vom System reserviert und für eigenen Namen verboten, z.B. class, import, public, etc. 9 | 10 | Neben dieser formalen Syntax haben sich folgende Konventionen eingebürgert. 11 | 12 | ## Allgemeine Konventionen 13 | 14 | Bezeichner in Java verwenden American English und nutzen damit automatisch nur ASCII Zeichen (keine Umlaute). Der 15 | Underscore `_` wird i.A. nicht verwendet. Auch angehängte Zahlen sind untypisch und meistens ein Zeichen für schlechten 16 | Stil (*Code Smell* [6]). 17 | 18 | Bezeichner sind stets aussagekräftig und bestehen damit oft aus mehreren Teilwörtern. Wir verwenden dann die Schreibweise 19 | [CamelCase](https://en.wikipedia.org/wiki/Camel_case). Bezeichner nutzen i.A. keine Abkürzungen, die Autovervollständigung der 20 | Entwicklungsumgebungen ergänzt lange Bezeichner komfortabel. Wenn ein Bezeichner doch einmal eine Abkürzung enthält, 21 | dann wird auch hier nur der erste Buchstabe groß geschrieben, z.B. `loadXmlDocument`, `writeAsJson`. 22 | 23 | Zum Thema Abkürzung noch ein schönes Zitat von Ken Thompson auf die Frage was er ändern würde, wenn er UNIX 24 | nochmals erfinden dürfte: “I‘d spell creat with an e.“ 25 | 26 | Zum Thema Namensgebung finden sich einige schöne Anti-Beispiele im Essay 27 | ["How To Write Unmaintainable Code"](https://github.com/Droogans/unmaintainable-code) von Roedy Green. 28 | 29 | ### Methodennamen 30 | 31 | Methodennamen enthalten ein Verb im Aktiv, z.B. `computeSum`, `moveForward`, `turnRight`, `compareToIgnoreCase`. Sie beginnen 32 | immer mit einem kleinen Buchstaben. Liefert eine Methode einen `boolean` zurück, dann beginnt der Name i.A. mit einem 33 | `is`, z.B. `isEmpty`, `isTreeFront`, `isNotRunning`, etc. Macht das grammatikalisch keinen Sinn, kann statt dessen auch 34 | `can`, `has`, `should` oder ähnliches verwendet werden. Hauptsache ist, dass sich boolesche Methoden wie eine Frage lesen: d.h. `equals`, 35 | `exists`, `contains` etc. sind auch in Ordnung. 36 | 37 | ## Variablennamen 38 | 39 | Variablennamen beginnen mit einem kleinen Buchstaben. Variablen vom Typ `boolean` nutzen meist den Präfix `is`, siehe 40 | Abschnitt zu booleschen Methodennamen. Alle anderen Variablennamen sind im Allgemeinen ein Substantiv, da ein Objekt 41 | gespeichert wird. Werden in einer Variablen mehrere Objekte gespeichert (Array, Listen, etc.), dann wird die Mehrzahl 42 | verwendet. Beispiele: counter, isLeaf, numberOfTrees, months, etc. 43 | 44 | ## Klassennamen 45 | 46 | Klassennamen sind ein Substantiv und beginnen mit einem großen Buchstaben. Vor oder nach dem Substantiv können 47 | ggf. weitere beschreibende Wörter verwendet werden, z.B. `Counter`, `LimitedCounter`, `OpenCounter`, `HashMap`, 48 | `ConcurrentHashMap`. Abstrakte Klassen halten sich i.A. auch an dieses Schema - manchmal macht es aber auch Sinn 49 | diese durch den Präfix `Abstract` als solche zu markieren, z.B. `AbstractList` oder `AbstractDocument`. Testklassen 50 | haben immer den Suffix `Test` nach dem eigentlichen Namen der Klasse, die getestet werden soll, z.B. `CounterTest` 51 | oder `HashMapTest`. 52 | 53 | ## Interfacenamen 54 | 55 | Interfacenamen sind entweder ein Substantiv (siehe Abschnitt Klassennamen) oder ein Adverb 56 | und beginnen mit einem großen Buchstaben. Vor oder nach dem Substantiv bzw. Adverb können 57 | ggf. weitere beschreibende Wörter verwendet werden, z.B. `Counter`, `Observable`, `WeakListener`, 58 | `Set`, `SortedSet`. Manche Projekte (z.B. Eclipse) verwenden das Anti-Pattern der 59 | [Ungarischen Notation](http://msdn.microsoft.com/de-de/library/aa260976(VS.60).aspx) 60 | und stellen jedem Interface den Präfix `I` voraus. Das ist nur in seltenen Fällen sinnvoll und sollte 61 | vermieden werden. 62 | -------------------------------------------------------------------------------- /doc/State-Based-Vs-Interaction-Based.md: -------------------------------------------------------------------------------- 1 | # State Based vs. Interaction Based Testing 2 | 3 | Prinzipiell gibt es zwei Varianten des Testings: das **State Based Testing** und das **Interaction Based Testing**. 4 | 5 | ## State Based Testing 6 | 7 | Beim **State Based Testing** wird das Testobjekt nach Aufruf der zu 8 | testenden Methoden durch Abfrage seines internen Zustands verifiziert. Analog dazu kann natürlich auch der Zustand 9 | der im Test verwendeten Parameter bzw. Rückgabewerte analysiert werden. Die meisten Tests eines Projekts 10 | laufen nach diesem Muster ab und können folgendermaßen formuliert werden: 11 | 12 | ```java 13 | /** [Kurze Beschreibung: was genau macht der Test] */ 14 | @Test 15 | void should[restlicher Methodenname der den Test fachlich beschreibt]() { 16 | // Given 17 | [Test Setup: Erzeugung der Parameter, die das SUT zum Erzeugen bzw. beim Aufruf benötigt] 18 | [Erzeugung des SUT] 19 | // When 20 | [Aufruf der zu testenden Methoden] 21 | // Then 22 | [Verifikation des Zustands des SUT bzw. von Parametern oder Rückgabewerten mittels AssertJ] 23 | } 24 | ``` 25 | 26 | Die folgenden beiden Tests aus diesem Projekt zeigen die zwei unterschiedlichen Varianten des State Based Testing. 27 | 28 | ### Verifizieren der Rückgabewerte 29 | 30 | Im folgenden Test wird der Rückgabewert einer Methode überprüft. 31 | 32 | ```java 33 | @Test 34 | void shouldConvertToAbsolute() { 35 | PathUtil pathUtil = new PathUtil(); 36 | 37 | assertThat(pathUtil.createAbsolutePath(null, FILE_NAME)).isEqualTo(FILE_NAME); 38 | assertThat(pathUtil.createAbsolutePath("", FILE_NAME)).isEqualTo(FILE_NAME); 39 | assertThat(pathUtil.createAbsolutePath("/", FILE_NAME)).isEqualTo("/" + FILE_NAME); 40 | assertThat(pathUtil.createAbsolutePath("/tmp", FILE_NAME)).isEqualTo("/tmp/" + FILE_NAME); 41 | assertThat(pathUtil.createAbsolutePath("/tmp/", FILE_NAME)).isEqualTo("/tmp/" + FILE_NAME); 42 | } 43 | ``` 44 | 45 | ### Verifizieren der Objektzustands 46 | 47 | Im folgenden Test wird der Zustand eines Objekts überprüft. 48 | 49 | ```java 50 | @Test 51 | void shouldCreateSimpleTreeStringsWithBuilder() { 52 | TreeStringBuilder builder = new TreeStringBuilder(); 53 | 54 | TreeString foo = builder.intern("foo"); 55 | 56 | assertThat(foo).hasToString("foo"); 57 | assertThat(foo.getLabel()).isEqualTo("foo"); 58 | } 59 | 60 | ``` 61 | 62 | ## Interaction Based Testing 63 | 64 | Im Gegensatz dazu wird beim **Interaction Based Testing** nicht der Zustand des SUT analysiert. Statt dessen werden die 65 | Aufrufe aller am Test beteiligten Objekte mit einem Mocking Framework wie [Mockito](https://site.mockito.org) überprüft. 66 | D.h. hier steht nicht der Zustand des Testobjekts im Vordergrund, sondern die Interaktion mit beteiligten Objekten. Ein 67 | typischer Testfall nach dem Interaction Based Testing ist folgendermaßen aufgebaut: 68 | 69 | ```java 70 | /** [Kurze Beschreibung: was genau macht der Test] */ 71 | @Test 72 | void should[restlicher Methodenname der den Test fachliche beschreibt]() { 73 | // Given 74 | [Test Setup 1: Erzeugung der Mocks, die zur Verifikation benötigt werden] 75 | [Test Setup 2: Erzeugung der Stubs, die das SUT zum Erzeugen bzw. beim Aufruf benötigt] 76 | [Erzeugung des SUT] 77 | // When 78 | [Aufruf der zu testenden Methoden] 79 | // Then 80 | [Verifikation des Zustands der Mocks] 81 | } 82 | ``` 83 | 84 | Ein typisches Beispiel für solch einen Test ist in der folgenden Klasse zu finden: 85 | 86 | ```java 87 | package edu.hm.hafner.util; 88 | 89 | import java.io.PrintStream; 90 | 91 | import org.junit.jupiter.api.Test; 92 | 93 | import static java.util.Arrays.*; 94 | import static java.util.Collections.*; 95 | import static org.mockito.Mockito.*; 96 | 97 | /** 98 | * Tests the class {@link PrefixLogger}. 99 | */ 100 | class PrefixLoggerTest { 101 | private static final String LOG_MESSAGE = "Hello PrefixLogger!"; 102 | private static final String PREFIX = "test"; 103 | private static final String EXPECTED_PREFIX = "[test]"; 104 | 105 | @Test 106 | void shouldLogSingleAndMultipleLines() { 107 | PrintStream printStream = mock(PrintStream.class); 108 | PrefixLogger logger = new PrefixLogger(printStream, PREFIX); 109 | 110 | logger.log(LOG_MESSAGE); 111 | 112 | verify(printStream).println(EXPECTED_PREFIX + " " + LOG_MESSAGE); 113 | } 114 | } 115 | ``` 116 | -------------------------------------------------------------------------------- /doc/dependency-graph.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam defaultTextAlignment center 3 | skinparam rectangle { 4 | BackgroundColor<> beige 5 | BackgroundColor<> lightGreen 6 | BackgroundColor<> lightBlue 7 | BackgroundColor<> lightGray 8 | } 9 | rectangle "codingstyle\n\n5.15.0-SNAPSHOT" as edu_hm_hafner_codingstyle_jar 10 | rectangle "spotbugs-annotations\n\n4.9.3" as com_github_spotbugs_spotbugs_annotations_jar 11 | rectangle "error_prone_annotations\n\n2.38.0" as com_google_errorprone_error_prone_annotations_jar 12 | rectangle "commons-lang3\n\n3.17.0" as org_apache_commons_commons_lang3_jar 13 | rectangle "commons-io\n\n2.19.0" as commons_io_commons_io_jar 14 | edu_hm_hafner_codingstyle_jar -[#000000]-> com_github_spotbugs_spotbugs_annotations_jar 15 | edu_hm_hafner_codingstyle_jar -[#000000]-> com_google_errorprone_error_prone_annotations_jar 16 | edu_hm_hafner_codingstyle_jar -[#000000]-> org_apache_commons_commons_lang3_jar 17 | edu_hm_hafner_codingstyle_jar -[#000000]-> commons_io_commons_io_jar 18 | @enduml -------------------------------------------------------------------------------- /doc/images/actions-annotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/actions-annotation.png -------------------------------------------------------------------------------- /doc/images/actions-autograding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/actions-autograding.png -------------------------------------------------------------------------------- /doc/images/actions-buildlog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/actions-buildlog.png -------------------------------------------------------------------------------- /doc/images/actions-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/actions-overview.png -------------------------------------------------------------------------------- /doc/images/build-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/build-result.png -------------------------------------------------------------------------------- /doc/images/gitlab-autograding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/gitlab-autograding.png -------------------------------------------------------------------------------- /doc/images/gitlab-commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/gitlab-commit.png -------------------------------------------------------------------------------- /doc/images/gitlab-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/gitlab-console.png -------------------------------------------------------------------------------- /doc/images/quality-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/doc/images/quality-monitor.png -------------------------------------------------------------------------------- /doc/uml/activity-diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam activity { 4 | BackgroundColor #eaeaea 5 | BorderColor #a0a0a0 6 | BorderColor black 7 | ArrowColor black 8 | ArrowThickness 2 9 | FontName Arial Unicode MS 10 | FontSize 20 11 | } 12 | 13 | sprite $rake [16x19/8] { 14 | 0000000000000000 15 | 0000000jj0000000 16 | 0000000jj0000000 17 | 0005555jj5555000 18 | 000jjeejjeejj000 19 | 000jj00jj00jj000 20 | 000jj00jj00jj000 21 | 0000000000000000 22 | } 23 | 24 | skinparam ArrowColor black 25 | skinparam ArrowThickness 2 26 | 27 | skinparam activityDiamondBackgroundColor #f5f5f5 28 | skinparam activityDiamondFontColor #black 29 | skinparam activityDiamondBorderColor black 30 | skinparam activityDiamondFont Arial Unicode MS 31 | skinparam activityArrowFontSize 20 32 | skinparam activityArrowFont Arial Unicode MS 33 | 34 | |Partition 1| 35 | start 36 | repeat 37 | :Erste Aktion; 38 | if( Frage? ) then ([true]\t) 39 | :Alternative Links; 40 | else (\t[false]) 41 | :Alternative Rechts; 42 | endif 43 | |Partition 2| 44 | :Zweite Aktion; 45 | repeat while () is (\t [Bedingung 1]) 46 | -> [Bedingung 2]; 47 | :Weitere Aktion; 48 | :Weitere Aktivität <$rake>; 49 | :Objekt] 50 | |Partition 1| 51 | fork 52 | :Parallele Aktion 1; 53 | fork again 54 | :Parallele Aktion 2; 55 | fork end 56 | stop 57 | @enduml 58 | -------------------------------------------------------------------------------- /doc/uml/class-diagram-domain-model.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam package { 4 | BackgroundColor #eaeaea 5 | BorderColor #a0a0a0 6 | BackgroundColor<
> gold 7 | BorderColor black 8 | ArrowColor black 9 | FontName Arial Unicode MS 10 | FontSize 20 11 | } 12 | 13 | skinparam class { 14 | BackgroundColor #eaeaea 15 | BorderColor #a0a0a0 16 | BackgroundColor<
> gold 17 | BorderColor black 18 | ArrowColor black 19 | FontName Arial Unicode MS 20 | FontSize 20 21 | } 22 | skinparam classFontSize 24 23 | skinparam classAttributeIconSize 0 24 | skinparam defaultFontSize 20 25 | 26 | skinparam ArrowColor black 27 | skinparam ArrowThickness 2 28 | 29 | hide circle 30 | 31 | class Entität { 32 | -attributName1: Datentyp 33 | -attributName2: String 34 | -schlüssel<>: String 35 | } 36 | 37 | class "Abgeleitete Entität" as A 38 | class "Verbundene Entität" as B 39 | 40 | class A { 41 | -attributSubEntität: Datentyp 42 | } 43 | 44 | class B { 45 | -attribut: Datentyp 46 | -schlüssel<>: Datentyp 47 | } 48 | 49 | class Weitere { 50 | -attribut: Datentyp 51 | -schlüssel<>: Datentyp 52 | } 53 | 54 | class Assoziationsentität { 55 | -beziehungsAttribut: Datentyp 56 | } 57 | 58 | Entität "0..1" - "*" B : \t\t 59 | Entität <|-- A 60 | A o- Aggregation : rollenName > 61 | B *- Komposition : > rollenName 62 | 63 | (Komposition, Weitere) . Assoziationsentität 64 | 65 | class " Mehrfach verbundene Entität " as Beziehung 66 | 67 | Weitere -left- Beziehung : Rollenname 1 \t \t\t > 68 | Weitere -left- Beziehung: Rollenname 2 \t \t \t \t > 69 | 70 | @enduml 71 | -------------------------------------------------------------------------------- /doc/uml/class-diagram-technical.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam package { 4 | BackgroundColor #efefef 5 | BorderColor #a0a0a0 6 | BackgroundColor<
> gold 7 | BorderColor black 8 | ArrowColor black 9 | FontName Arial Unicode MS 10 | FontSize 20 11 | } 12 | 13 | skinparam class { 14 | BackgroundColor #white 15 | BorderColor #f4f4f4 16 | BackgroundColor<
> gold 17 | BorderColor black 18 | ArrowColor black 19 | FontName Arial Unicode MS 20 | FontSize 20 21 | } 22 | 23 | skinparam note { 24 | BackgroundColor LightBlue 25 | BorderColor #a0a0a0 26 | FontName Arial 27 | FontSize 14 28 | FontColor black 29 | RoundCorner 15 30 | LineType solid 31 | } 32 | 33 | skinparam classFontSize 24 34 | skinparam classAttributeIconSize 0 35 | skinparam defaultFontSize 20 36 | 37 | skinparam ArrowColor black 38 | skinparam ArrowThickness 2 39 | 40 | hide circle 41 | 42 | package java.util { 43 | class Observable { 44 | - changed: boolean 45 | +registerObserver(o: Observer) 46 | +unregisterObserver(o: Observer) 47 | #notifyObservers() 48 | } 49 | 50 | interface Observer <> { 51 | +update() {abstract} 52 | } 53 | } 54 | 55 | class ConcreteObservable { 56 | -concreteState: State 57 | +getState(): State 58 | +setState(s: State) 59 | } 60 | 61 | Observable <|-- ConcreteObservable 62 | 63 | hide interface fields 64 | 65 | abstract class AbstractObserver < T extends Observer > { 66 | +update() 67 | #handleUpdate(o: Observer) {abstract} 68 | } 69 | 70 | class ConcreteObserver { 71 | #handleUpdate(o: Observer) 72 | } 73 | 74 | class StringUtils { 75 | +isEmpty(value: String) {static} 76 | } 77 | 78 | Observable o- Observer : \t\t 79 | 80 | Observer <|.. AbstractObserver 81 | AbstractObserver <|-- ConcreteObserver : <> \n ConcreteObserver> 82 | ConcreteObservable <-left- ConcreteObserver : \t\t 83 | ConcreteObservable .down.> StringUtils : <> 84 | 85 | ' Notes 86 | 87 | note "Package" as Package 88 | note "Subpackage" as Subpackage 89 | note "Klasse" as Class 90 | note "Interface" as Interface 91 | note "Vererbung" as Vererbung 92 | note "Implementierung" as Implementierung 93 | note "gerichtete Assoziation" as Assoziation 94 | note "generischen Typ binden" as Generics 95 | note "gerichtete Abhängigkeit:\n<> benutzt\n<> erzeugt\n<> Aufruf" as Dependency 96 | note "Aggregation" as Aggregation 97 | 98 | java .right[#lightblue,dotted]. Package 99 | util .down[#lightblue,dotted]. Subpackage 100 | Observable .up. Class 101 | Observable .[#lightblue,dotted]. Vererbung 102 | ConcreteObservable .up[#lightblue,dotted]. Vererbung 103 | Observer .up. Interface 104 | 105 | Observer .up[#lightblue,dotted]. Aggregation 106 | Aggregation .up[#lightblue,dotted]. Observable 107 | 108 | Observer .down[#lightblue,dotted]. Implementierung 109 | AbstractObserver .up[#lightblue,dotted]. Implementierung 110 | 111 | ConcreteObserver .down[#lightblue,dotted]. Assoziation 112 | ConcreteObservable .down[#lightblue,dotted]. Assoziation 113 | 114 | ConcreteObserver .up[#lightblue,dotted]. Generics 115 | AbstractObserver .down[#lightblue,dotted]. Generics 116 | 117 | ConcreteObservable .[#lightblue,dotted]. Dependency 118 | StringUtils .up[#lightblue,dotted]. Dependency 119 | 120 | note right of ConcreteObservable::concreteState 121 | Objektvariable 122 | vom Typ State 123 | end note 124 | 125 | note right of ConcreteObservable::getState 126 | Methode mit 127 | Rückgabetyp State 128 | end note 129 | 130 | note right of ConcreteObservable::setState 131 | Methode mit 132 | Parametertyp State 133 | end note 134 | 135 | note right of StringUtils::isEmpty 136 | statische Methode 137 | end note 138 | 139 | note as floating 140 | Sichtbarkeiten 141 | 142 | + public 143 | # protected 144 | ~ package private 145 | - private 146 | end note 147 | 148 | note as notes 149 | Anmerkungen 150 | 151 | Die blauen Boxen und Linien 152 | sind nur Anmerkungen. In 153 | eigenen Diagrammen werden 154 | sie nicht verwendet. 155 | end note 156 | 157 | @enduml 158 | -------------------------------------------------------------------------------- /doc/uml/component-diagram-details.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam component { 4 | BackgroundColor #f8f8f8 5 | BorderColor #a0a0a0 6 | BackgroundColor<
> #FFFF00 7 | BorderColor black 8 | FontName Arial Unicode MS 9 | FontSize 20 10 | ArrowFontName Arial Unicode MS 11 | ArrowFontSize 18 12 | } 13 | 14 | skinparam package { 15 | BackgroundColor #eaeaea 16 | BorderColor #a0a0a0 17 | BackgroundColor<
> gold 18 | BorderColor black 19 | ArrowColor black 20 | FontName Arial Unicode MS 21 | FontSize 20 22 | } 23 | 24 | skinparam class { 25 | BackgroundColor #f4f4f4 26 | BorderColor #a0a0a0 27 | BackgroundColor<
> gold 28 | BorderColor black 29 | ArrowColor black 30 | FontName Arial Unicode MS 31 | FontSize 20 32 | } 33 | skinparam classFontSize 24 34 | skinparam classAttributeIconSize 0 35 | skinparam defaultFontSize 20 36 | 37 | skinparam ArrowColor black 38 | skinparam ArrowThickness 2 39 | 40 | hide circle 41 | hide interface fields 42 | skinparam componentStyle uml2 43 | 44 | component "Anwendung" <> as anwendung { 45 | class ConcreteSubject { 46 | -state: State 47 | +getState(): State 48 | +setState(s: State) 49 | } 50 | 51 | class ConcreteObserver { 52 | +update() 53 | } 54 | 55 | } 56 | 57 | component "Fachkomponente" <> as fachkomponente { 58 | } 59 | 60 | component " org.apache.commons:commons-lang3 " <> as library { 61 | artifact commons-lang3-3.12.jar <> as lang { 62 | } 63 | } 64 | 65 | component "JDK" <> as Bibliothek { 66 | 67 | package java.util { 68 | 69 | class Subject { 70 | - changed: boolean 71 | - observers: Vector 72 | +registerObserver(o: Observer) 73 | +unregisterObserver(o: Observer) 74 | #notifyObservers() 75 | } 76 | 77 | 78 | interface Observer { 79 | +update() {abstract} 80 | } 81 | 82 | } 83 | } 84 | 85 | anwendung ...> library : <> 86 | Subject <|-- ConcreteSubject 87 | Subject o- Observer : \t\t 88 | 89 | Observer <|.. ConcreteObserver 90 | ConcreteSubject <- ConcreteObserver : \t\t\t 91 | 92 | circle " \t\t\tFassade " 93 | 94 | anwendung ..> " \t\t\tFassade " : "\t\t\t\t" 95 | " \t\t\tFassade " -- fachkomponente 96 | 97 | 98 | 99 | 100 | 101 | @enduml 102 | -------------------------------------------------------------------------------- /doc/uml/component-diagram-overview.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam component { 4 | BackgroundColor #f8f8f8 5 | BorderColor #a0a0a0 6 | BackgroundColor<
> #FFFF00 7 | BorderColor black 8 | FontName Arial Unicode MS 9 | FontSize 20 10 | ArrowFontName Arial Unicode MS 11 | ArrowFontSize 18 12 | } 13 | 14 | skinparam classFontSize 24 15 | skinparam classAttributeIconSize 0 16 | skinparam defaultFontSize 20 17 | 18 | skinparam ArrowColor black 19 | skinparam ArrowThickness 2 20 | 21 | hide circle 22 | 23 | skinparam componentStyle uml2 24 | 25 | component "Anwendung" <> as anwendung { 26 | artifact anwendung-1.0.0.war <> as war { 27 | } 28 | } 29 | 30 | component "Fachkomponente" <> as fachkomponente { 31 | artifact fachlogik-1.0.0.jar <> as jar { 32 | } 33 | } 34 | 35 | component " org.apache.commons:commons-lang3 " <> as library { 36 | artifact commons-lang3-3.12.jar <> as lang { 37 | } 38 | } 39 | 40 | anwendung .up.> library : " <>" 41 | 42 | note "Artefakt" as artefact 43 | note "Komponente" as component 44 | note "Schnittstelle" as schnittstelle 45 | note "Implementierung\neiner Schnittstelle" as Implementierung 46 | note "Aufruf einer\nSchnittstelle" as Use 47 | note "gerichtete Abhängigkeit\nohne Angabe Interface" as Dependency 48 | 49 | circle "\t Fassade " as fassade 50 | 51 | anwendung ..> fassade : " <>" 52 | fassade -up- fachkomponente 53 | 54 | anwendung .down. Use 55 | fassade .up. Use 56 | 57 | fassade .left. schnittstelle 58 | library .up. component 59 | lang .down. artefact 60 | 61 | fassade .down. Implementierung 62 | fachkomponente .right. Implementierung 63 | 64 | anwendung .up. Dependency 65 | library .down. Dependency 66 | 67 | @enduml 68 | -------------------------------------------------------------------------------- /doc/uml/context-diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam componentStyle uml2 4 | hide stereotype 5 | 6 | skinparam actor { 7 | BackgroundColor #eaeaea 8 | BorderColor #a0a0a0 9 | BackgroundColor<
> #FFFF00 10 | BorderColor black 11 | ArrowColor black 12 | FontName Arial Unicode MS 13 | FontSize 20 14 | } 15 | 16 | skinparam class { 17 | BackgroundColor #4AD386 18 | BorderColor #a0a0a0 19 | BackgroundColor<
> gold 20 | BorderColor black 21 | ArrowColor black 22 | FontName Arial Unicode MS 23 | FontSize 20 24 | } 25 | 26 | skinparam component { 27 | BackgroundColor #f8f8f8 28 | BorderColor #a0a0a0 29 | BackgroundColor<
> #FFFF00 30 | BorderColor black 31 | FontName Arial Unicode MS 32 | FontSize 20 33 | ArrowFontName Arial Unicode MS 34 | ArrowFontSize 18 35 | } 36 | 37 | :Akteur: 38 | :Abgeleiteter Akteur: 39 | 40 | :Akteur: <|-- :Abgeleiteter Akteur: :\t\t\t 41 | 42 | [geplantes System] <
> 43 | 44 | [geplantes System] -> :Akteur: : Datenfluss Ausgabe 45 | [geplantes System] <- :Abgeleiteter Akteur: : Datenfluss Eingabe 46 | 47 | [geplantes System] -left-> [Nachbarsystem] : Datenfluss Export 48 | [geplantes System] <-left- [Nachbarsystem] : Datenfluss Import 49 | 50 | @enduml 51 | -------------------------------------------------------------------------------- /doc/uml/deployment-diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam componentStyle uml2 4 | skinparam FontName Arial Black 5 | skinparam classFontName Arial 6 | skinparam classAttributeIconSize 0 7 | skinparam defaultFontSize 20 8 | skinparam classFontSize 24 9 | skinparam componentStyle uml2 10 | skinparam ArrowColor black 11 | 12 | skinparam node { 13 | BackgroundColor #f4f4f4 14 | BorderColor #a0a0a0 15 | BackgroundColor<
> gold 16 | BorderColor black 17 | } 18 | 19 | skinparam artifact { 20 | BackgroundColor #e4e4e4 21 | BorderColor #a0a0a0 22 | ArrowColor black 23 | FontName Arial Unicode MS 24 | FontSize 20 25 | StereotypeFontSize 14 26 | } 27 | 28 | node ":MacBook" <> { 29 | node ":macOS Monterey" <> { 30 | node ":Safari 15.4" <> { 31 | artifact ECharts.JS <> as EI 32 | artifact DataTables.JS <> as DI 33 | artifact ClientModel.JS <> as P 34 | } 35 | } 36 | } 37 | 38 | 39 | P ..> EI 40 | P ..> DI 41 | 42 | node ":Server" <> { 43 | node ":Jetty 9.4.45" <> { 44 | artifact Warnings <> as W 45 | artifact Jenkins.Core <> as C 46 | } 47 | } 48 | 49 | C <. W 50 | 51 | ":Server" -down- ":MacBook" : " <>" 52 | 53 | note "Artefakt" as artifact 54 | note "Kommunikationspfad\nmit Protokoll" as channel 55 | note "Device" as device 56 | note "Execution Environment" as env 57 | 58 | ":Server" .up. device 59 | ":Safari 15.4" .down. env 60 | 61 | ":Server" .. channel 62 | ":MacBook" .. channel 63 | 64 | C .left. artifact 65 | 66 | 67 | @enduml 68 | -------------------------------------------------------------------------------- /doc/uml/sequence-diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skin rose 4 | 5 | skinparam sequence { 6 | BackgroundColor #4AD386 7 | BorderColor #a0a0a0 8 | BackgroundColor<
> gold 9 | BorderColor black 10 | ArrowColor black 11 | FontName Arial Unicode MS 12 | FontSize 20 13 | 14 | LifeLineBorderColor black 15 | LifeLineBackgroundColor AE396 16 | 17 | ParticipantBackgroundColor f4f4f4 18 | ParticipantBorderColor black 19 | } 20 | 21 | skinparam actor { 22 | BackgroundColor #f4f4f4 23 | BorderColor #a0a0a0 24 | BackgroundColor<
> gold 25 | BorderColor black 26 | ArrowColor black 27 | FontName Arial Unicode MS 28 | FontSize 20 29 | 30 | LifeLineBorderColor black 31 | LifeLineBackgroundColor AE396 32 | 33 | } 34 | 35 | skinparam classFontSize 24 36 | skinparam classAttributeIconSize 0 37 | skinparam defaultFontSize 20 38 | 39 | skinparam ArrowColor black 40 | skinparam ArrowThickness 1 41 | 42 | hide circle 43 | hide footbox 44 | 45 | actor Akteur 46 | participant ":Klasse" as unnamed 47 | participant "rot:ZweiteKlasse" as rot 48 | participant "blau:ZweiteKlasse" as blau 49 | 50 | activate Akteur 51 | Akteur -> unnamed: start(1, 3, 2) 52 | activate unnamed 53 | create rot 54 | 55 | unnamed -->> rot : new 56 | unnamed -> rot: synchronerAufruf({1, 3, 2}) 57 | activate rot 58 | opt "Anzahl Elemente > 1.000" 59 | rot ->> blau: asynchronerAufrufFortschrittZeigen("Sortiere...") 60 | activate blau 61 | blau -> blau: fortschrittAnzeigen("Sortiere...") 62 | activate blau 63 | end 64 | alt "Anzahl Elemente > 10.000" 65 | rot -> rot: quicksort() 66 | activate rot 67 | deactivate rot 68 | else 69 | rot -> rot: mergesort() 70 | activate rot 71 | deactivate rot 72 | end 73 | opt "Anzahl Elemente > 1.000" 74 | rot ->> blau: asynchronerAufrufFortschrittBeenden() 75 | deactivate blau 76 | blau -> blau: schließeDialog() 77 | activate blau 78 | end 79 | rot -->> unnamed : "{1, 2, 3}" 80 | unnamed -->> Akteur 81 | deactivate rot 82 | deactivate unnamed 83 | loop noch Elemente vorhanden? 84 | Akteur -> unnamed : gibElementAus(e) 85 | ||| 86 | deactivate blau 87 | activate unnamed 88 | unnamed -->> Akteur 89 | deactivate unnamed 90 | deactivate Akteur 91 | ||| 92 | end 93 | 94 | @enduml 95 | -------------------------------------------------------------------------------- /doc/uml/state-diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam state { 3 | BackgroundColor #eaeaea 4 | BorderColor #a0a0a0 5 | BackgroundColor<
> gold 6 | BorderColor black 7 | ArrowColor black 8 | ArrowThickness 2 9 | FontName Arial Unicode MS 10 | FontSize 20 11 | } 12 | 13 | skinparam ArrowColor black 14 | skinparam ArrowThickness 2 15 | 16 | skinparam activityDiamondBackgroundColor #f4f4f4 17 | skinparam activityDiamondBorderColor black 18 | skinparam activityDiamondFont Arial Unicode MS 19 | skinparam activityArrowFontSize 20 20 | skinparam activityArrowFont Arial Unicode MS 21 | 22 | state "Detaillierter Zustand" as A 23 | state "Zustand" as B 24 | state "Auswahl Unten" as C 25 | state "Auswahl Rechts" as D 26 | 27 | state Choice <> 28 | state AnotherEnd <> 29 | 30 | [*] -> A 31 | A --> A : Trigger für Transition in selben Zustand 32 | A: entry / Eintrittsaktivität 33 | A: do / Andauernde Aktivität 34 | A: exit / Austrittsaktivität 35 | 36 | A --> B : \tTrigger [Bedingung für Zustandswechsel] / Verhalten 37 | B -> [*] : \twhen [Bedingung für Ende] 38 | 39 | B --> Choice : \tTrigger / Verhalten 40 | 41 | Choice --> C : [Bedingung Unten] 42 | Choice -> D : [Bedingung Rechts] 43 | 44 | C -> AnotherEnd : mehrere Endzustände sind erlaubt 45 | @enduml 46 | -------------------------------------------------------------------------------- /doc/uml/usecase-diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam actor { 4 | BackgroundColor #f4f4f4 5 | BorderColor #a0a0a0 6 | BorderColor black 7 | ArrowColor black 8 | FontName Arial Unicode MS 9 | FontSize 20 10 | } 11 | 12 | skinparam usecase { 13 | BackgroundColor #f4f4f4 14 | BorderColor #a0a0a0 15 | BorderColor black 16 | ArrowColor black 17 | FontName Arial Unicode MS 18 | FontSize 20 19 | } 20 | 21 | skinparam ArrowColor black 22 | skinparam ArrowThickness 2 23 | 24 | left to right direction 25 | 26 | :Akteur: 27 | :Abgeleiteter Akteur: 28 | 29 | :Akteur: <|- :Abgeleiteter Akteur: :\t\t\t 30 | 31 | rectangle Gruppierung #e4e4e4 { 32 | (Anwendungsfall) -up- Akteur 33 | (Eingebundener\nAnwendungsfall) <. (Anwendungsfall) : \t\t <> 34 | :Abgeleiteter Akteur: -- (Beschränkter\nAnwendungsfall) 35 | } 36 | 37 | @enduml 38 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: codingstyle 2 | 3 | services: 4 | jenkins: 5 | container_name: codingstyle-jenkins 6 | build: 7 | context: docker/images/jenkins-controller 8 | args: 9 | - CURRENT_UID=${CURRENT_UID} 10 | - CURRENT_GID=${CURRENT_GID} 11 | image: codingstyle/jenkins-controller 12 | volumes: 13 | - ./docker/volumes/jenkins-home:/var/jenkins_home # Mounts the local jenkins_home volume to the /var/jenkins_home path inside the container 14 | - agent-ssh-dir:/ssh-dir # Mounts the shared volume agent-ssh-dir to a path inside the container 15 | ports: 16 | - 8080:8080 # Jenkins UI - HOST:CONTAINER 17 | environment: 18 | - TRY_UPGRADE_IF_NO_MARKER=true 19 | - JAVA_OPTS= -Dstapler.jelly.noCache=true -Dhudson.remoting.ClassFilter=com.google.common.collect.ImmutableListMultimap -DexecutableWar.jetty.disableCustomSessionIdCookieName=true -DexecutableWar.jetty.sessionIdCookieName=codingstyle 20 | user: ${CURRENT_USER} 21 | restart: unless-stopped 22 | depends_on: 23 | key-generator: 24 | condition: service_completed_successfully # Depends on the successful completion of the sidekick_service 25 | healthcheck: 26 | test: ["CMD-SHELL", "[ -f /ssh-dir/conductor_ok ] || exit 1"] 27 | # Checks if the conductor_ok file exists in the /ssh-dir path 28 | interval: 5s 29 | timeout: 10s 30 | retries: 5 31 | 32 | key-generator: 33 | container_name: codingstyle-key-generator 34 | build: 35 | context: docker/images/key-generator 36 | image: codingstyle/key-generator 37 | stdin_open: true 38 | tty: true 39 | # The entrypoint script generates the SSH keys and outputs them to the /ssh-dir directory. 40 | entrypoint: sh -c "/usr/local/bin/keygen.sh /ssh-dir" # Runs the keygen.sh script and specifies the output directory 41 | volumes: 42 | - agent-ssh-dir:/ssh-dir # Mounts the agent-ssh-dir volume to the /ssh-dir path inside the container 43 | # The healthcheck command checks if the conductor_ok file exists in the /ssh-dir directory. 44 | healthcheck: 45 | test: ["CMD-SHELL", "[ -f /ssh-dir/conductor_ok ] || exit 1"] 46 | # Checks if the conductor_ok file exists in the /ssh-dir path 47 | interval: 5s 48 | timeout: 10s 49 | retries: 5 50 | 51 | java-agent: 52 | container_name: codingstyle-java-agent 53 | build: 54 | context: docker/images/java-agent 55 | args: 56 | - CURRENT_UID=${CURRENT_UID} 57 | - CURRENT_GID=${CURRENT_GID} 58 | image: codingstyle/java-agent 59 | depends_on: 60 | key-generator: 61 | condition: service_completed_successfully # Depends on the successful completion of the sidekick_service 62 | jenkins: 63 | condition: service_started 64 | restart: unless-stopped 65 | healthcheck: 66 | test: ["CMD-SHELL", "[ -f /ssh-dir/conductor_ok ] || exit 1"] 67 | # Checks if the conductor_ok file exists in the /ssh-dir path 68 | interval: 5s 69 | timeout: 10s 70 | retries: 5 71 | volumes: 72 | - agent-ssh-dir:/home/jenkins/.ssh:ro # Mounts the agent-ssh-dir volume to the /home/jenkins/.ssh path inside the container as read-only 73 | 74 | volumes: 75 | agent-ssh-dir: 76 | name: codingstyle-agent-ssh-dir # Creates a named volume called agent-ssh-dir 77 | jenkins_home: 78 | name: codingstyle-jenkins-home # Creates a named volume called jenkins-home 79 | external: true 80 | -------------------------------------------------------------------------------- /docker/images/java-agent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/ssh-agent:latest-jdk21 2 | 3 | # Install prerequisites for Java and Maven 4 | RUN apt-get update \ 5 | && apt-get install -y --no-install-recommends ca-certificates curl \ 6 | && apt-get clean \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | # Install Maven 10 | ARG MAVEN_VERSION=3.9.8 11 | 12 | SHELL ["/bin/bash", "-eo", "pipefail", "-c"] 13 | RUN curl -sS -L -O --output-dir /tmp/ --create-dirs https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \ 14 | && printf "%s" "$(sha512sum /tmp/apache-maven-${MAVEN_VERSION}-bin.tar.gz)" | sha512sum -c - \ 15 | && curl -sS -L -O --output-dir /tmp/ --create-dirs https://downloads.apache.org/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz.sha512 \ 16 | && printf "%s /tmp/apache-maven-${MAVEN_VERSION}-bin.tar.gz" "$(cat /tmp/apache-maven-${MAVEN_VERSION}-bin.tar.gz.sha512)" | sha512sum --check --status - \ 17 | && tar xzf "/tmp/apache-maven-${MAVEN_VERSION}-bin.tar.gz" -C /opt/ \ 18 | && rm "/tmp/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \ 19 | && ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven \ 20 | && ln -s /opt/maven/bin/mvn /usr/bin/mvn \ 21 | && mkdir -p /etc/profile.d \ 22 | && echo "export JAVA_HOME=$JAVA_HOME \n \ 23 | export M2_HOME=/opt/maven \n \ 24 | export PATH=${M2_HOME}/bin:${PATH}" > /etc/profile.d/maven.sh 25 | ENV M2_HOME="/opt/maven" 26 | ENV PATH="${M2_HOME}/bin/:${PATH}" 27 | 28 | RUN echo "PATH=${PATH}" >> /etc/environment && \ 29 | mkdir -pv "${JENKINS_AGENT_HOME}/.m2/repository" && \ 30 | chown -R jenkins:jenkins "${JENKINS_AGENT_HOME}" 31 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/Dockerfile: -------------------------------------------------------------------------------- 1 | # Prepare a Debian-based Docker image with several utilities installed to automatically generate SSH keys 2 | FROM jenkins/jenkins:lts-jdk21 3 | 4 | # We switch back to the Jenkins user for the remaining operations. 5 | USER jenkins 6 | 7 | # We copy the jobs directory from our current directory to the Jenkins home directory in the image. 8 | COPY preconfigured-jobs /usr/share/jenkins/ref/jobs 9 | 10 | # We write the Jenkins version to the UpgradeWizard state file. 11 | # This prevents the Upgrade Wizard from showing up when Jenkins starts. 12 | RUN echo "${JENKINS_VERSION}" > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state 13 | 14 | # We copy a list of plugins to install to the Jenkins ref directory in the image. 15 | COPY plugins.txt /usr/share/jenkins/ref/plugins.txt 16 | 17 | # We use the Jenkins plugin CLI to install the plugins listed in the plugins.txt file. 18 | RUN jenkins-plugin-cli --verbose -f /usr/share/jenkins/ref/plugins.txt 19 | 20 | # We copy a pre-configured Jenkins configuration file to the Jenkins ref directory in the image. 21 | # This allows us to pre-configure Jenkins with our desired settings. 22 | COPY jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml 23 | 24 | # Create an admin user and don't start the wizard 25 | RUN echo 2.x > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state 26 | ENV JENKINS_OPTS -Djenkins.install.runSetupWizard=false 27 | COPY security.groovy /usr/share/jenkins/ref/init.groovy.d/basic-security.groovy 28 | 29 | # Configure Jenkins 30 | COPY jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml 31 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/jenkins.yaml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | agentProtocols: 3 | - "JNLP4-connect" 4 | - "Ping" 5 | authorizationStrategy: "loggedInUsersCanDoAnything" 6 | crumbIssuer: 7 | standard: 8 | excludeClientIPFromCrumb: false 9 | disableRememberMe: false 10 | disabledAdministrativeMonitors: 11 | - "hudson.diagnosis.ReverseProxySetupMonitor" 12 | markupFormatter: 13 | rawHtml: 14 | disableSyntaxHighlighting: false 15 | mode: NORMAL 16 | nodes: 17 | - permanent: 18 | labelString: "docker linux agent java jdk21 java21" 19 | launcher: 20 | ssh: 21 | credentialsId: "jenkins-ssh-agent-private-key" 22 | host: "java-agent" 23 | port: 22 24 | sshHostKeyVerificationStrategy: "nonVerifyingKeyVerificationStrategy" 25 | name: "java-agent" 26 | nodeDescription: "Jenkins Docker Agent with Java 21 and Maven" 27 | numExecutors: 1 28 | remoteFS: "/home/jenkins/agent" 29 | retentionStrategy: "always" 30 | numExecutors: 0 31 | primaryView: 32 | list: 33 | columns: 34 | - "status" 35 | - "weather" 36 | - "jobName" 37 | - "gitBranchSpecifierColumn" 38 | - "lastSuccess" 39 | - "lastFailure" 40 | - "lastDuration" 41 | - coverageTotalsColumn: 42 | columnName: "Line Coverage" 43 | metric: LINE 44 | - coverageTotalsColumn: 45 | columnName: "Branch Coverage" 46 | metric: BRANCH 47 | - "issueTotalsColumn" 48 | - "buildButton" 49 | includeRegex: ".*" 50 | name: "List View" 51 | projectNamingStrategy: "standard" 52 | quietPeriod: 5 53 | scmCheckoutRetryCount: 0 54 | securityRealm: 55 | local: 56 | allowsSignup: false 57 | enableCaptcha: false 58 | slaveAgentPort: 50000 59 | systemMessage: "

Java and Maven Showcase

" 60 | updateCenter: 61 | sites: 62 | - id: "default" 63 | url: "https://updates.jenkins.io/update-center.json" 64 | views: 65 | - list: 66 | columns: 67 | - "status" 68 | - "weather" 69 | - "jobName" 70 | - "gitBranchSpecifierColumn" 71 | - "lastSuccess" 72 | - "lastFailure" 73 | - "lastDuration" 74 | - coverageTotalsColumn: 75 | columnName: "Line Coverage" 76 | metric: LINE 77 | - coverageTotalsColumn: 78 | columnName: "Branch Coverage" 79 | metric: BRANCH 80 | - "issueTotalsColumn" 81 | - "buildButton" 82 | includeRegex: ".*" 83 | name: "List View" 84 | - dashboard: 85 | columns: 86 | - "status" 87 | - "weather" 88 | - "jobName" 89 | - "lastSuccess" 90 | - "lastFailure" 91 | - "lastDuration" 92 | - coverageTotalsColumn: 93 | columnName: "Line Coverage" 94 | metric: LINE 95 | - coverageTotalsColumn: 96 | columnName: "Branch Coverage" 97 | metric: BRANCH 98 | - "issueTotalsColumn" 99 | - "buildButton" 100 | includeStdJobList: true 101 | includeRegex: ".*" 102 | leftPortlets: 103 | - issuesChartPortlet: 104 | height: 600 105 | name: "Static analysis issues chart" 106 | name: "Dashboard View" 107 | rightPortlets: 108 | - issuesTablePortlet: 109 | name: "Static analysis issues per tool and job" 110 | showIcons: true 111 | viewsTabBar: "standard" 112 | 113 | appearance: 114 | locale: 115 | ignoreAcceptLanguage: true 116 | systemLocale: "USE_BROWSER_LOCALE" 117 | prism: 118 | theme: PRISM 119 | 120 | security: 121 | apiToken: 122 | creationOfLegacyTokenEnabled: false 123 | tokenGenerationOnCreationEnabled: false 124 | usageStatisticsEnabled: true 125 | globalJobDslSecurityConfiguration: 126 | useScriptSecurity: true 127 | sSHD: 128 | port: -1 129 | 130 | credentials: 131 | system: 132 | domainCredentials: 133 | - credentials: 134 | - basicSSHUserPrivateKey: 135 | description: "Private SSH key for Jenkins agent" 136 | id: "jenkins-ssh-agent-private-key" 137 | privateKeySource: 138 | directEntry: 139 | privateKey: ${readFile:/ssh-dir/jenkins_agent_ed} 140 | scope: SYSTEM 141 | username: "jenkins" 142 | unclassified: 143 | globalDefaultFlowDurabilityLevel: 144 | durabilityHint: PERFORMANCE_OPTIMIZED 145 | location: 146 | adminAddress: "Address not configured " 147 | url: "http://localhost:8080/" 148 | mailer: 149 | charset: "UTF-8" 150 | useSsl: false 151 | pollSCM: 152 | pollingThreadCount: 10 153 | 154 | tool: 155 | git: 156 | installations: 157 | - home: "git" 158 | name: "git-default" 159 | maven: 160 | installations: 161 | - home: "/opt/maven" 162 | name: "mvn-default" 163 | pipelineMaven: 164 | publisherOptions: 165 | - jacocoPublisher: 166 | disabled: true 167 | - junitPublisher: 168 | disabled: true 169 | healthScaleFactor: 1.0 170 | triggerDownstreamUponResultAborted: false 171 | triggerDownstreamUponResultFailure: false 172 | triggerDownstreamUponResultNotBuilt: false 173 | triggerDownstreamUponResultSuccess: true 174 | triggerDownstreamUponResultUnstable: false 175 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/plugins.txt: -------------------------------------------------------------------------------- 1 | analysis-model-api 2 | ansicolor 3 | antisamy-markup-formatter 4 | apache-httpcomponents-client-4-api 5 | authentication-tokens 6 | autograding 7 | bouncycastle-api 8 | branch-api 9 | cloudbees-folder 10 | coverage 11 | command-launcher 12 | configuration-as-code 13 | credentials 14 | credentials-binding 15 | dashboard-view 16 | display-url-api 17 | docker-commons 18 | docker-workflow 19 | durable-task 20 | email-ext 21 | file-operations 22 | form-element-path 23 | git 24 | git-client 25 | git-forensics 26 | git-server 27 | github 28 | github-api 29 | github-checks 30 | github-branch-source 31 | jackson2-api 32 | javadoc 33 | jdk-tool 34 | job-dsl 35 | jsch 36 | junit 37 | locale 38 | lockable-resources 39 | mailer 40 | matrix-auth 41 | matrix-project 42 | maven-plugin 43 | mock-security-realm 44 | pam-auth 45 | pipeline-build-step 46 | pipeline-graph-analysis 47 | pipeline-input-step 48 | pipeline-maven 49 | pipeline-milestone-step 50 | pipeline-model-api 51 | pipeline-model-definition 52 | pipeline-model-extensions 53 | pipeline-rest-api 54 | pipeline-stage-step 55 | pipeline-stage-tags-metadata 56 | plain-credentials 57 | resource-disposer 58 | scm-api 59 | script-security 60 | ssh-credentials 61 | ssh-slaves 62 | structs 63 | timestamper 64 | token-macro 65 | trilead-api 66 | warnings-ng 67 | workflow-api 68 | workflow-basic-steps 69 | workflow-cps 70 | workflow-durable-task-step 71 | workflow-job 72 | workflow-multibranch 73 | workflow-scm-step 74 | workflow-step-api 75 | workflow-support 76 | ws-cleanup 77 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/preconfigured-jobs/freestyle-analysis-model/nextBuildNumber: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/preconfigured-jobs/history-coverage-model/nextBuildNumber: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/preconfigured-jobs/pipeline-codingstyle/nextBuildNumber: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /docker/images/jenkins-controller/security.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | import jenkins.model.* 4 | import hudson.security.* 5 | 6 | def instance = Jenkins.getInstance() 7 | 8 | println "--> creating local user 'admin'" 9 | 10 | def hudsonRealm = new HudsonPrivateSecurityRealm(false) 11 | hudsonRealm.createAccount('admin','admin') 12 | instance.setSecurityRealm(hudsonRealm) 13 | 14 | def strategy = new FullControlOnceLoggedInAuthorizationStrategy() 15 | instance.setAuthorizationStrategy(strategy) 16 | instance.save() 17 | -------------------------------------------------------------------------------- /docker/images/key-generator/Dockerfile: -------------------------------------------------------------------------------- 1 | # Prepare a Debian-based Docker image with several utilities installed to automatically generate SSH keys 2 | FROM debian:bookworm-20240612 3 | 4 | # Copy all shell scripts from the current directory to /usr/local/bin/ in the image. 5 | COPY *sh /usr/local/bin/ 6 | 7 | # Make all shell scripts in /usr/local/bin/ executable. 8 | RUN chmod +x /usr/local/bin/*.sh 9 | 10 | # The RUN command executes a series of commands in the new layer of the image and commits the results. 11 | # The following commands are executed: 12 | 13 | # 1. Update the package list. 14 | # 2. Install necessary dependencies including several utilities and remove the package list to reduce the image size. 15 | RUN apt update \ 16 | && apt install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client procps unzip wget \ 17 | && rm -rf /var/lib/apt/lists/* && rm -fr /tmp/* 18 | 19 | # Run the keygen.sh script with /ssh-dir as an argument. 20 | # This script is expected to generate SSH keys and store them in /ssh-dir. 21 | RUN /usr/local/bin/keygen.sh /ssh-dir 22 | 23 | # The CMD command specifies the default command to execute when the container starts. 24 | # In this case, it prints a message and lists the contents of /ssh-dir. 25 | CMD ["sh", "-c", "echo 'Export stage is ready'; ls -l /ssh-dir/"] 26 | -------------------------------------------------------------------------------- /docker/images/key-generator/keygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script generates a new SSH key pair and updates a Docker Compose file with the public key. 3 | # Create secrets directory if it doesn't exist 4 | set -x 5 | LOC=$1 6 | if [[ -z "$LOC" ]]; then 7 | LOC="./" 8 | fi 9 | 10 | echo "Location is $LOC" 11 | ls -artl "$LOC" 12 | 13 | rm "$LOC/conductor_ok" 14 | 15 | mkdir -p "$LOC/secrets" 16 | 17 | # Remove existing keys 18 | rm -fr "$LOC/jenkins_agent_ed" "$LOC/jenkins_agent_ed.pub" 19 | 20 | # Generate new ed25519 SSH key pair 21 | ssh-keygen -t ed25519 -f "$LOC/jenkins_agent_ed" -N "" 22 | 23 | # Set appropriate permissions for private key 24 | chmod 444 "$LOC/jenkins_agent_ed" 25 | 26 | # Extract public key 27 | pubkey=$(cat "$LOC/jenkins_agent_ed.pub") 28 | 29 | echo "The public key is $pubkey" 30 | 31 | # Update the authorized_keys file with the public key 32 | echo "$pubkey" > "$LOC/authorized_keys" && chown 1000:1000 "$LOC/authorized_keys" 33 | 34 | # Generate a random token for JCasc 35 | openssl rand -hex 24 > "$LOC/secrets/jcasc_token" 36 | cat "$LOC/secrets/jcasc_token" 37 | 38 | # This file will be used by other containers to know we went up to the end of the key/token generation 39 | echo "OK" > "$LOC/conductor_ok" 40 | 41 | # Display success message 42 | echo "SSH key pair generated successfully." 43 | -------------------------------------------------------------------------------- /docker/volumes/jenkins-home/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !JENKINS_HOME_EXPORTED_BY_DOCKER 4 | 5 | -------------------------------------------------------------------------------- /docker/volumes/jenkins-home/JENKINS_HOME_EXPORTED_BY_DOCKER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/docker/volumes/jenkins-home/JENKINS_HOME_EXPORTED_BY_DOCKER -------------------------------------------------------------------------------- /etc/Jenkinsfile.autograding: -------------------------------------------------------------------------------- 1 | node { 2 | def mvnHome = tool 'mvn-default' 3 | 4 | stage ('Checkout') { 5 | checkout scm 6 | } 7 | 8 | stage ('Git mining') { 9 | discoverGitReferenceBuild() 10 | mineRepository() 11 | } 12 | 13 | stage ('Build, Test and Static Analysis') { 14 | withMaven(maven: 'mvn-default', mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { 15 | sh 'mvn -ntp -V -e clean verify -Dmaven.test.failure.ignore -Dgpg.skip' 16 | } 17 | 18 | recordIssues tools: [java(), javaDoc()], aggregatingResults: 'true', id: 'java', name: 'Java' 19 | recordIssues tool: errorProne(), healthy: 1, unhealthy: 20 20 | 21 | junit testResults: '**/target/*-reports/TEST-*.xml' 22 | recordCoverage tools: [[parser: 'JACOCO']], sourceCodeRetention: 'EVERY_BUILD', name: 'Code Coverage' 23 | 24 | recordIssues tools: [checkStyle(pattern: 'target/checkstyle-result.xml'), 25 | spotBugs(pattern: 'target/spotbugsXml.xml'), 26 | pmdParser(pattern: 'target/pmd.xml'), 27 | cpd(pattern: 'target/cpd.xml'), 28 | taskScanner(highTags:'FIXME', normalTags:'TODO', includePattern: '**/*.java', excludePattern: 'target/**/*')], 29 | qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]] 30 | } 31 | 32 | stage ('Mutation Coverage') { 33 | withMaven(maven: 'mvn-default', mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { 34 | sh "mvn -ntp org.pitest:pitest-maven:mutationCoverage" 35 | } 36 | recordCoverage tools: [[parser: 'PIT']], id: 'pit', name: 'Mutation Coverage', sourceCodeRetention: 'EVERY_BUILD' 37 | } 38 | 39 | stage ('Collect Maven Warnings') { 40 | recordIssues tool: mavenConsole() 41 | } 42 | 43 | stage ('Autograding') { 44 | autoGrade(''' 45 | { 46 | "tests": { 47 | "tools": [ 48 | { 49 | "name": "Tests" 50 | } 51 | ], 52 | "name": "JUnit", 53 | "passedImpact": 0, 54 | "skippedImpact": -1, 55 | "failureImpact": -5, 56 | "maxScore": 100 57 | }, 58 | "analysis": [ 59 | { 60 | "name": "Style", 61 | "id": "style", 62 | "tools": [ 63 | { 64 | "id": "checkstyle", 65 | "name": "CheckStyle" 66 | }, 67 | { 68 | "id": "pmd", 69 | "name": "PMD" 70 | } 71 | ], 72 | "errorImpact": -1, 73 | "highImpact": -1, 74 | "normalImpact": -1, 75 | "lowImpact": -1, 76 | "maxScore": 100 77 | }, 78 | { 79 | "name": "Bugs", 80 | "id": "bugs", 81 | "icon": "bug", 82 | "tools": [ 83 | { 84 | "id": "spotbugs", 85 | "name": "SpotBugs" 86 | } 87 | ], 88 | "errorImpact": -3, 89 | "highImpact": -3, 90 | "normalImpact": -3, 91 | "lowImpact": -3, 92 | "maxScore": 100 93 | } 94 | ], 95 | "coverage": [ 96 | { 97 | "tools": [ 98 | { 99 | "id": "coverage", 100 | "name": "Line Coverage", 101 | "metric": "line" 102 | }, 103 | { 104 | "id": "coverage", 105 | "name": "Branch Coverage", 106 | "metric": "branch" 107 | } 108 | ], 109 | "name": "Code Coverage", 110 | "maxScore": 100, 111 | "missedPercentageImpact": -1 112 | }, 113 | { 114 | "tools": [ 115 | { 116 | "id": "pit", 117 | "name": "Mutation Coverage", 118 | "metric": "mutation" 119 | } 120 | ], 121 | "name": "Mutation Coverage", 122 | "maxScore": 100, 123 | "missedPercentageImpact": -1 124 | } 125 | ] 126 | } 127 | ''') 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /etc/Jenkinsfile.coverage: -------------------------------------------------------------------------------- 1 | node('java11-agent') { 2 | stage ('Checkout') { 3 | checkout scm 4 | } 5 | 6 | stage ('Git mining') { 7 | discoverGitReferenceBuild() 8 | mineRepository() 9 | } 10 | 11 | stage ('Build, Test, and Static Analysis') { 12 | withMaven(mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { 13 | sh 'mvn -V -e clean verify -Dmaven.test.failure.ignore -Dgpg.skip' 14 | } 15 | 16 | recordIssues tools: [java(), javaDoc()], aggregatingResults: 'true', id: 'java', name: 'Java' 17 | recordIssues tool: errorProne(), healthy: 1, unhealthy: 20 18 | 19 | junit testResults: '**/target/*-reports/TEST-*.xml' 20 | recordCoverage tools: [[parser: 'JACOCO']], sourceCodeRetention: 'EVERY_BUILD' 21 | 22 | recordIssues tools: [checkStyle(pattern: 'target/checkstyle-result.xml'), 23 | spotBugs(pattern: 'target/spotbugsXml.xml'), 24 | pmdParser(pattern: 'target/pmd.xml'), 25 | cpd(pattern: 'target/cpd.xml'), 26 | taskScanner(highTags:'FIXME', normalTags:'TODO', includePattern: '**/*.java', excludePattern: 'target/**/*')], 27 | qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]] 28 | } 29 | 30 | stage ('Collect Maven Warnings') { 31 | recordIssues tool: mavenConsole() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /etc/Jenkinsfile.reference: -------------------------------------------------------------------------------- 1 | node { 2 | def mvnHome = tool 'mvn-default' 3 | 4 | stage ('Checkout') { 5 | checkout scm 6 | } 7 | 8 | stage ('Git mining') { 9 | discoverGitReferenceBuild() 10 | mineRepository() 11 | } 12 | 13 | stage ('Build and Static Analysis') { 14 | withMaven(maven: 'mvn-default', mavenLocalRepo: '/var/data/m2repository', mavenOpts: '-Xmx768m -Xms512m') { 15 | sh 'mvn -ntp -V -e clean verify -Dmaven.test.failure.ignore -Dgpg.skip' 16 | } 17 | 18 | recordIssues tools: [java(), javaDoc()], aggregatingResults: 'true', id: 'java', name: 'Java' 19 | recordIssues tool: errorProne(), healthy: 1, unhealthy: 20 20 | 21 | junit testResults: '**/target/*-reports/TEST-*.xml' 22 | recordCoverage tools: [[parser: 'JACOCO']], sourceCodeRetention: 'EVERY_BUILD' 23 | 24 | recordIssues tools: [checkStyle(pattern: 'target/checkstyle-result.xml'), 25 | spotBugs(pattern: 'target/spotbugsXml.xml'), 26 | pmdParser(pattern: 'target/pmd.xml'), 27 | cpd(pattern: 'target/cpd.xml'), 28 | taskScanner(highTags:'FIXME', normalTags:'TODO', includePattern: '**/*.java', excludePattern: 'target/**/*')], 29 | qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]] 30 | recordIssues tool: mavenConsole() 31 | } 32 | 33 | stage ('Autograding') { 34 | autoGrade(''' 35 | { 36 | "analysis": { 37 | "maxScore": 100, 38 | "errorImpact": -5, 39 | "highImpact": -2, 40 | "normalImpact": -1, 41 | "lowImpact": -1 42 | }, 43 | "tests": { 44 | "maxScore": 100, 45 | "passedImpact": 0, 46 | "failureImpact": -5, 47 | "skippedImpact": -1 48 | }, 49 | "coverage": { 50 | "maxScore": 100, 51 | "coveredPercentageImpact": 0, 52 | "missedPercentageImpact": -1 53 | } 54 | } 55 | ''') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /etc/assertj-templates/assertion_class_template.txt: -------------------------------------------------------------------------------- 1 | package ${package}; 2 | ${imports} 3 | /** 4 | * {@link ${class_to_assert}} specific assertions - Generated by CustomAssertionGenerator. 5 | */ 6 | @edu.hm.hafner.util.Generated(value="assertj-assertions-generator") 7 | public class ${custom_assertion_class} extends AbstractObjectAssert<${custom_assertion_class}, ${class_to_assert}> { 8 | 9 | /** 10 | * Creates a new {@link ${custom_assertion_class}} to make assertions on actual ${class_to_assert}. 11 | * @param actual the ${class_to_assert} we want to make assertions on. 12 | */ 13 | public ${custom_assertion_class}(${class_to_assert} actual) { 14 | super(actual, ${custom_assertion_class}.class); 15 | } 16 | 17 | /** 18 | * An entry point for ${custom_assertion_class} to follow AssertJ standard assertThat() statements.
19 | * With a static import, one can write directly: assertThat(my${class_to_assert}) and get specific assertion with code completion. 20 | * @param actual the ${class_to_assert} we want to make assertions on. 21 | * @return a new {@link ${custom_assertion_class}} 22 | */ 23 | @org.assertj.core.util.CheckReturnValue 24 | public static ${custom_assertion_class} assertThat(${class_to_assert} actual) { 25 | return new ${custom_assertion_class}(actual); 26 | } 27 | -------------------------------------------------------------------------------- /etc/assertj-templates/assertions_entry_point_class_template.txt: -------------------------------------------------------------------------------- 1 | package ${package}; 2 | 3 | /** 4 | * Entry point for assertions of different data types. Each method in this class is a static factory for the 5 | * type-specific assertion objects. 6 | */ 7 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("NM") 8 | @edu.hm.hafner.util.Generated(value="assertj-assertions-generator") 9 | public class Assertions extends org.assertj.core.api.Assertions { 10 | ${all_assertions_entry_points} 11 | /** 12 | * Creates a new {@link Assertions}. 13 | */ 14 | protected Assertions() { 15 | // empty 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /etc/assertj-templates/has_assertion_template.txt: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Verifies that the actual ${class_to_assert}'s ${property} is equal to the given one. 4 | * @param ${property_safe} the given ${property} to compare the actual ${class_to_assert}'s ${property} to. 5 | * @return this assertion object. 6 | * @throws AssertionError - if the actual ${class_to_assert}'s ${property} is not equal to the given one.${throws_javadoc} 7 | */ 8 | public ${self_type} has${Property}(${propertyType} ${property_safe}) ${throws}{ 9 | // check that actual ${class_to_assert} we want to make assertions on is not null. 10 | isNotNull(); 11 | 12 | // overrides the default error message with a more explicit one 13 | String assertjErrorMessage = "\nExpecting ${property} of:\n <%s>\nto be:\n <%s>\nbut was:\n <%s>"; 14 | 15 | // null safe check 16 | ${propertyType} actual${Property} = actual.${getter}(); 17 | if (!java.util.Objects.deepEquals(actual${Property}, ${property_safe})) { 18 | failWithMessage(assertjErrorMessage, actual, ${property_safe}, actual${Property}); 19 | } 20 | 21 | // return the current assertion for method chaining 22 | return ${myself}; 23 | } 24 | -------------------------------------------------------------------------------- /etc/assertj-templates/soft_assertions_entry_point_class_template.txt: -------------------------------------------------------------------------------- 1 | package ${package}; 2 | 3 | /** 4 | * Entry point for soft assertions of different data types. 5 | */ 6 | @edu.hm.hafner.util.Generated(value="assertj-assertions-generator") 7 | public class SoftAssertions extends org.assertj.core.api.AutoCloseableSoftAssertions { 8 | ${all_assertions_entry_points} 9 | } 10 | -------------------------------------------------------------------------------- /etc/pmd-java-configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Ullrich Hafner's PMD Java Production Code Rules 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /etc/pmd-javascript-configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Ullrich Hafner's PMD JavaScript Rules 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /etc/pmd-tests-configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Ullrich Hafner's PMD Java Test Code Rules 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /etc/spotbugs-exclusion-filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codingstyle", 3 | "version": "1.0.0", 4 | "description": "Java coding style", 5 | "directories": { 6 | "doc": "doc" 7 | }, 8 | "dependencies": { 9 | "remark-cli": "^12.0.0", 10 | "remark-lint": "^10.0.0", 11 | "remark-preset-lint-recommended": "^7.0.0" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "lint-md": "remark ." 16 | }, 17 | "remarkConfig": { 18 | "plugins": [ 19 | "remark-preset-lint-recommended" 20 | ] 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/jenkinsci/analysis-model.git" 25 | }, 26 | "author": "Ullrich Hafner", 27 | "license": "MIT", 28 | "homepage": "https://github.com/jenkinsci/analysis-model#readme" 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import edu.hm.hafner.util.PackageDetectorFactory.FileSystemFacade; 6 | 7 | /** 8 | * Detects the namespace of a C# workspace file. 9 | * 10 | * @author Ullrich Hafner 11 | */ 12 | class CSharpNamespaceDetector extends PackageDetector { 13 | private static final Pattern NAMESPACE_PATTERN = Pattern.compile("^\\s*namespace\\s+([^{]*)\\s*\\{?\\s*$"); 14 | 15 | CSharpNamespaceDetector(final FileSystemFacade fileSystem) { 16 | super(fileSystem); 17 | } 18 | 19 | @Override 20 | public boolean accepts(final String fileName) { 21 | return fileName.endsWith(".cs"); 22 | } 23 | 24 | @Override 25 | Pattern getPattern() { 26 | return NAMESPACE_PATTERN; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/Generated.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.*; 7 | import static java.lang.annotation.RetentionPolicy.*; 8 | 9 | /** 10 | * This annotation is used to mark source code that has been generated or is somehow not relevant for style checking or 11 | * code coverage analysis. It is quite similar to the JSR305 annotation. The 12 | * main difference is that it has class retention, so it is available for tools that work on bytecode (like JaCoCo, 13 | * PIT, or SpotBugs). 14 | */ 15 | @Retention(CLASS) 16 | @Target({PACKAGE, TYPE, ANNOTATION_TYPE, METHOD, CONSTRUCTOR, FIELD, 17 | LOCAL_VARIABLE, PARAMETER}) 18 | public @interface Generated { 19 | /** 20 | * An optional property that identifies the code generator. 21 | * 22 | * @return the name of the generator 23 | */ 24 | String[] value() default ""; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/JavaPackageDetector.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import edu.hm.hafner.util.PackageDetectorFactory.FileSystemFacade; 6 | 7 | /** 8 | * Detects the package name of a Java file. 9 | * 10 | * @author Ullrich Hafner 11 | */ 12 | class JavaPackageDetector extends PackageDetector { 13 | private static final Pattern PACKAGE_PATTERN = Pattern.compile( 14 | "^\\s*package\\s*([a-z]+[.\\w]*)\\s*;.*"); 15 | 16 | JavaPackageDetector(final FileSystemFacade fileSystem) { 17 | super(fileSystem); 18 | } 19 | 20 | @Override 21 | Pattern getPattern() { 22 | return PACKAGE_PATTERN; 23 | } 24 | 25 | @Override 26 | public boolean accepts(final String fileName) { 27 | return fileName.endsWith(".java"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import edu.hm.hafner.util.PackageDetectorFactory.FileSystemFacade; 6 | 7 | /** 8 | * Detects the package name of a Kotlin file. 9 | * 10 | * @author Bastian Kersting 11 | */ 12 | class KotlinPackageDetector extends PackageDetector { 13 | private static final Pattern PACKAGE_PATTERN = Pattern.compile( 14 | "^\\s*package\\s*([a-z]+[.\\w]*)\\s*.*"); 15 | 16 | @VisibleForTesting 17 | KotlinPackageDetector(final FileSystemFacade fileSystem) { 18 | super(fileSystem); 19 | } 20 | 21 | @Override 22 | Pattern getPattern() { 23 | return PACKAGE_PATTERN; 24 | } 25 | 26 | @Override 27 | boolean accepts(final String fileName) { 28 | return fileName.endsWith(".kt"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/LineRange.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | import java.util.Locale; 6 | import java.util.NavigableSet; 7 | import java.util.Objects; 8 | import java.util.TreeSet; 9 | 10 | /** 11 | * A line range in a source file is defined by its first and last line. 12 | * 13 | * @author Ullrich Hafner 14 | */ 15 | public final class LineRange implements Serializable { 16 | @Serial 17 | private static final long serialVersionUID = -4124143085672930110L; 18 | 19 | private final int start; 20 | private final int end; 21 | 22 | /** 23 | * Creates a new instance of {@link LineRange}. 24 | * 25 | * @param line 26 | * the single line of this range 27 | */ 28 | public LineRange(final int line) { 29 | this(line, line); 30 | } 31 | 32 | /** 33 | * Creates a new instance of {@link LineRange}. 34 | * 35 | * @param start 36 | * start of the range 37 | * @param end 38 | * end of the range 39 | */ 40 | public LineRange(final int start, final int end) { 41 | if (start <= 0) { 42 | this.start = 0; 43 | this.end = 0; 44 | } 45 | else if (start < end) { 46 | this.start = start; 47 | this.end = end; 48 | } 49 | else { 50 | this.start = end; 51 | this.end = start; 52 | } 53 | } 54 | 55 | /** 56 | * Returns the first line of this range. 57 | * 58 | * @return the first line of this range 59 | */ 60 | public int getStart() { 61 | return start; 62 | } 63 | 64 | /** 65 | * Returns the last line of this range. 66 | * 67 | * @return the last line of this range 68 | */ 69 | public int getEnd() { 70 | return end; 71 | } 72 | 73 | /** 74 | * Returns the lines of this line lange in a sorted set. 75 | * 76 | * @return the containing lines, one by one 77 | */ 78 | public NavigableSet getLines() { 79 | var lines = new TreeSet(); 80 | for (int line = getStart(); line <= getEnd(); line++) { 81 | lines.add(line); 82 | } 83 | return lines; 84 | } 85 | 86 | /** 87 | * Returns whether the specified line is contained in this range. 88 | * 89 | * @param line the line to check 90 | * @return {@code true} if the line is contained in this range, {@code false} otherwise 91 | */ 92 | public boolean contains(final int line) { 93 | return line >= start && line <= end; 94 | } 95 | 96 | /** 97 | * Returns whether this range is just a single line. 98 | * 99 | * @return {@code true} if this range is just a single line, {@code false} otherwise 100 | */ 101 | public boolean isSingleLine() { 102 | return start == end; 103 | } 104 | 105 | @Override 106 | @Generated 107 | public boolean equals(final Object o) { 108 | if (this == o) { 109 | return true; 110 | } 111 | if (o == null || getClass() != o.getClass()) { 112 | return false; 113 | } 114 | var lineRange = (LineRange) o; 115 | return start == lineRange.start 116 | && end == lineRange.end; 117 | } 118 | 119 | @Override 120 | @Generated 121 | public int hashCode() { 122 | return Objects.hash(start, end); 123 | } 124 | 125 | @Override 126 | public String toString() { 127 | return String.format(Locale.ENGLISH, "[%d-%d]", start, end); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/LookaheadStream.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.NoSuchElementException; 5 | import java.util.regex.Pattern; 6 | import java.util.stream.Stream; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | /** 11 | * A stream of lines with a lookahead of one line. Useful to parse a stream of lines when it is required to check if the 12 | * next line matches a given regular expression. 13 | * 14 | * @author Ullrich Hafner 15 | */ 16 | public class LookaheadStream implements AutoCloseable { 17 | private final Stream stream; 18 | private final Iterator lineIterator; 19 | private final String fileName; 20 | 21 | private boolean isLookaheadFilled; 22 | private String lookaheadLine = StringUtils.EMPTY; 23 | private int line; 24 | 25 | /** 26 | * Wraps the specified stream of lines into a {@link LookaheadStream}. 27 | * 28 | * @param stream 29 | * the lines to wrap 30 | */ 31 | public LookaheadStream(final Stream stream) { 32 | this(stream, StringUtils.EMPTY); 33 | } 34 | 35 | /** 36 | * Wraps the specified stream of lines into a {@link LookaheadStream}. 37 | * 38 | * @param stream 39 | * the lines to wrap 40 | * @param fileName 41 | * the file name of the stream 42 | */ 43 | public LookaheadStream(final Stream stream, final String fileName) { 44 | this.stream = stream; 45 | lineIterator = stream.iterator(); 46 | this.fileName = fileName; 47 | } 48 | 49 | public String getFileName() { 50 | return fileName; 51 | } 52 | 53 | @Override 54 | public void close() { 55 | stream.close(); 56 | } 57 | 58 | /** 59 | * Returns {@code true} if the stream has more elements. (In other words, returns {@code true} if {@link #next} 60 | * would return an element rather than throwing an exception.) 61 | * 62 | * @return {@code true} if the stream has more elements 63 | */ 64 | public boolean hasNext() { 65 | return lineIterator.hasNext() || isLookaheadFilled; 66 | } 67 | 68 | /** 69 | * Returns {@code true} if the stream has at least one more element that matches the given regular expression. 70 | * 71 | * @param regexp 72 | * the regular expression 73 | * 74 | * @return {@code true} if the stream has more elements that match the regexp 75 | */ 76 | public boolean hasNext(final String regexp) { 77 | if (!isLookaheadFilled) { 78 | if (!hasNext()) { 79 | return false; 80 | } 81 | fillLookahead(); 82 | } 83 | 84 | return Pattern.compile(regexp).matcher(lookaheadLine).find(); 85 | } 86 | 87 | /** 88 | * Peeks the next element in the stream. I.e., the next element is returned but not removed from the stream so that 89 | * the next call of {@link #next()} will again return this value. 90 | * 91 | * @return the next element in the stream 92 | * @throws NoSuchElementException 93 | * if the stream has no more elements 94 | */ 95 | public String peekNext() { 96 | if (!isLookaheadFilled) { 97 | fillLookahead(); 98 | } 99 | return lookaheadLine; 100 | } 101 | 102 | private void fillLookahead() { 103 | lookaheadLine = lineIterator.next(); 104 | isLookaheadFilled = true; 105 | } 106 | 107 | /** 108 | * Returns the next element in the stream. 109 | * 110 | * @return the next element in the stream 111 | * @throws NoSuchElementException 112 | * if the stream has no more elements 113 | */ 114 | public String next() { 115 | line++; 116 | 117 | if (isLookaheadFilled) { 118 | isLookaheadFilled = false; 119 | return lookaheadLine; 120 | } 121 | return lineIterator.next(); 122 | } 123 | 124 | /** 125 | * Returns the line number of the line that has been handed out using the {@link #next()} method. 126 | * 127 | * @return the current line, or 0 if no line has been handed out yet 128 | */ 129 | public int getLine() { 130 | return line; 131 | } 132 | 133 | @Override @Generated 134 | public String toString() { 135 | return "[%d] -> '%s'".formatted(line, lookaheadLine); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/PackageDetector.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.nio.charset.Charset; 8 | import java.nio.file.InvalidPathException; 9 | import java.util.Optional; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | import java.util.stream.Stream; 13 | 14 | import org.apache.commons.io.input.BOMInputStream; 15 | 16 | import edu.hm.hafner.util.PackageDetectorFactory.FileSystemFacade; 17 | 18 | /** 19 | * Base class for package detectors. 20 | * 21 | * @author Ullrich Hafner 22 | */ 23 | abstract class PackageDetector { 24 | private final FileSystemFacade fileSystem; 25 | 26 | /** 27 | * Creates a new instance of {@link PackageDetector}. 28 | * 29 | * @param fileSystem 30 | * file system facade 31 | */ 32 | PackageDetector(final FileSystemFacade fileSystem) { 33 | this.fileSystem = fileSystem; 34 | } 35 | 36 | /** 37 | * Detects the package or namespace name of the specified file. 38 | * 39 | * @param fileName 40 | * the file name of the file to scan 41 | * @param charset 42 | * the charset to use when reading the source files 43 | * 44 | * @return the detected package or namespace name 45 | */ 46 | public Optional detectPackageName(final String fileName, final Charset charset) { 47 | try (var stream = fileSystem.openFile(fileName)) { 48 | return detectPackageName(stream, charset); 49 | } 50 | catch (IOException | InvalidPathException ignore) { 51 | // ignore IO errors 52 | } 53 | return Optional.empty(); 54 | } 55 | 56 | private Optional detectPackageName(final InputStream stream, final Charset charset) throws IOException { 57 | try (var buffer = new BufferedReader( 58 | new InputStreamReader(BOMInputStream.builder().setInputStream(stream).get(), charset))) { 59 | return detectPackageName(buffer.lines()); 60 | } 61 | } 62 | 63 | /** 64 | * Detects the package or namespace name of the specified input stream. The stream will be closed automatically by 65 | * the caller of this method. 66 | * 67 | * @param lines 68 | * the content of the file to scan 69 | * 70 | * @return the detected package or namespace name 71 | */ 72 | private Optional detectPackageName(final Stream lines) { 73 | Pattern pattern = getPattern(); 74 | return lines.map(pattern::matcher) 75 | .filter(Matcher::matches) 76 | .findFirst() 77 | .map(matcher -> matcher.group(1)) 78 | .map(String::trim); 79 | } 80 | 81 | /** 82 | * Returns the Pattern for the Package Name in this kind of file. 83 | * 84 | * @return the Pattern. 85 | */ 86 | abstract Pattern getPattern(); 87 | 88 | /** 89 | * Returns whether this classifier accepts the specified file for processing. 90 | * 91 | * @param fileName 92 | * the file name 93 | * 94 | * @return {@code true} if the classifier accepts the specified file for processing. 95 | */ 96 | abstract boolean accepts(String fileName); 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.InvalidPathException; 7 | import java.nio.file.Paths; 8 | 9 | import com.google.errorprone.annotations.MustBeClosed; 10 | 11 | /** 12 | * Factory to create package detectors. 13 | * 14 | * @author Ullrich Hafner 15 | */ 16 | public final class PackageDetectorFactory { 17 | /** 18 | * Creates a new package detector runner that uses the detectors for Java, Kotlin, and C#. 19 | * 20 | * @return the package detector runner 21 | */ 22 | public static PackageDetectorRunner createPackageDetectors() { 23 | return createPackageDetectors(new FileSystemFacade()); 24 | } 25 | 26 | /** 27 | * Creates a new package detector runner that uses the detectors for Java, Kotlin, and C#. 28 | * 29 | * @param facade 30 | * the file system facade to use 31 | * 32 | * @return the package detector runner 33 | */ 34 | @VisibleForTesting 35 | public static PackageDetectorRunner createPackageDetectors(final FileSystemFacade facade) { 36 | return new PackageDetectorRunner( 37 | new JavaPackageDetector(facade), 38 | new KotlinPackageDetector(facade), 39 | new CSharpNamespaceDetector(facade)); 40 | } 41 | 42 | private PackageDetectorFactory() { 43 | // prevents instantiation 44 | } 45 | 46 | /** 47 | * Facade for file system operations. May be replaced by stubs in test cases. 48 | */ 49 | @VisibleForTesting 50 | public static class FileSystemFacade { 51 | /** 52 | * Opens the specified file. 53 | * 54 | * @param fileName 55 | * the name of the file to open 56 | * 57 | * @return the input stream to read the file 58 | * @throws IOException 59 | * if the file could not be opened 60 | * @throws InvalidPathException 61 | * the file name is invalid 62 | */ 63 | @MustBeClosed 64 | public InputStream openFile(final String fileName) throws IOException, InvalidPathException { 65 | return Files.newInputStream(Paths.get(fileName)); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.nio.charset.Charset; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | /** 9 | * Detects package or namespace names of files in the file system. 10 | * 11 | * @author Ullrich Hafner 12 | */ 13 | public class PackageDetectorRunner { 14 | private final List detectors; 15 | 16 | PackageDetectorRunner(final PackageDetector... detectors) { 17 | this.detectors = Arrays.asList(detectors); 18 | } 19 | 20 | /** 21 | * Detects the package name of the specified file based on several detector strategies. 22 | * 23 | * @param fileName 24 | * the filename of the file to scan 25 | * @param charset 26 | * the charset to use when reading the source files 27 | * 28 | * @return the detected package name or {@link Optional#empty()} if no package name could be detected 29 | */ 30 | public Optional detectPackageName(final String fileName, final Charset charset) { 31 | return detectors.stream() 32 | .filter(detector -> detector.accepts(fileName)) 33 | .map(detector -> detector.detectPackageName(fileName, charset)) 34 | .flatMap(Optional::stream) 35 | .findFirst(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/PrefixLogger.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.PrintStream; 4 | import java.util.Collection; 5 | 6 | import com.google.errorprone.annotations.FormatMethod; 7 | 8 | /** 9 | * A simple logger that prefixes each message with a given name. 10 | * 11 | * @author Ullrich Hafner 12 | */ 13 | public class PrefixLogger { 14 | private final String toolName; 15 | private final PrintStream delegate; 16 | 17 | /** 18 | * Creates a new {@link PrefixLogger}. 19 | * 20 | * @param logger 21 | * the logger to create 22 | * @param prefix 23 | * the prefix to print 24 | */ 25 | public PrefixLogger(final PrintStream logger, final String prefix) { 26 | if (prefix.contains("[")) { 27 | this.toolName = prefix + " "; 28 | } 29 | else { 30 | this.toolName = "[%s] ".formatted(prefix); 31 | } 32 | delegate = logger; 33 | } 34 | 35 | /** 36 | * Logs the specified message. 37 | * 38 | * @param format 39 | * A format string 40 | * @param args 41 | * Arguments referenced by the format specifiers in the format string. If there are more arguments than 42 | * format specifiers, the extra arguments are ignored. The number of arguments is variable and may be 43 | * zero. 44 | */ 45 | @FormatMethod 46 | public void log(final String format, final Object... args) { 47 | print(format.formatted(args)); 48 | } 49 | 50 | /** 51 | * Logs the specified messages. 52 | * 53 | * @param lines 54 | * the messages to log 55 | */ 56 | public void logEachLine(final Collection lines) { 57 | lines.forEach(this::print); 58 | } 59 | 60 | private void print(final String line) { 61 | delegate.println(toolName + line); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/TreeString.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import edu.umd.cs.findbugs.annotations.CheckForNull; 6 | 7 | import java.io.Serial; 8 | import java.io.Serializable; 9 | import java.util.Map; 10 | 11 | /** 12 | * {@link TreeString} is an alternative string representation that saves the memory when you have a large number of 13 | * strings that share common prefixes (such as various file names.) 14 | * 15 | *

16 | * {@link TreeString} can be built with {@link TreeStringBuilder}. 17 | *

18 | * 19 | * @author Kohsuke Kawaguchi 20 | */ 21 | public final class TreeString implements Serializable { 22 | @Serial 23 | private static final long serialVersionUID = 3621959682117480904L; 24 | 25 | /** Parent node that represents the prefix. */ 26 | @CheckForNull 27 | private TreeString parent; 28 | 29 | /** {@link #parent} + {@code label} is the string value of this node. */ 30 | private char[] label; 31 | 32 | /** 33 | * Creates a new root {@link TreeString}. 34 | */ 35 | TreeString() { 36 | this(null, ""); 37 | } 38 | 39 | /** 40 | * Creates a new {@link TreeString} with the given parent and suffix. 41 | * 42 | * @param parent 43 | * the parent 44 | * @param label 45 | * the suffix 46 | */ 47 | @SuppressWarnings("NullAway") 48 | TreeString(@CheckForNull final TreeString parent, final String label) { 49 | Ensure.that(parent == null || !label.isEmpty()) 50 | .isTrue("if there's a parent '%s', label '%s' can't be empty", parent, label); 51 | 52 | this.parent = parent; 53 | this.label = label.toCharArray(); // String created as a substring of another string can have a lot of garbage attached to it. 54 | } 55 | 56 | String getLabel() { 57 | return String.valueOf(label); 58 | } 59 | 60 | /** 61 | * Inserts a new node between this node and its parent, and returns the newly inserted node. 62 | * 63 | *

64 | * This operation doesn't change the string representation of this node. 65 | *

66 | * 67 | * @param prefix 68 | * the prefix to remove 69 | * 70 | * @return the new node in the middle 71 | */ 72 | TreeString split(final String prefix) { 73 | Ensure.that(getLabel().startsWith(prefix)).isTrue(); 74 | 75 | var suffix = new char[label.length - prefix.length()]; 76 | System.arraycopy(label, prefix.length(), suffix, 0, suffix.length); 77 | 78 | var middle = new TreeString(parent, prefix); 79 | label = suffix; 80 | parent = middle; 81 | 82 | return middle; 83 | } 84 | 85 | @VisibleForTesting 86 | @CheckForNull 87 | TreeString getParent() { 88 | return parent; 89 | } 90 | 91 | /** 92 | * How many nodes do we have from the root to this node (including 'this' itself?). Thus depth of the root node is 93 | * 1. 94 | * 95 | * @return the depth 96 | */ 97 | private int depth() { 98 | int i = 0; 99 | for (var p = this; p != null; p = p.parent) { 100 | i++; 101 | } 102 | return i; 103 | } 104 | 105 | @Override 106 | public boolean equals(@CheckForNull final Object o) { 107 | if (this == o) { 108 | return true; 109 | } 110 | if (o == null || getClass() != o.getClass()) { 111 | return false; 112 | } 113 | return toString().equals(((TreeString) o).toString()); 114 | } 115 | 116 | @Override 117 | public int hashCode() { 118 | return toString().hashCode(); 119 | } 120 | 121 | /** 122 | * Returns the full string representation. 123 | */ 124 | @Override 125 | public String toString() { 126 | var tokens = new char[depth()][]; 127 | int i = tokens.length; 128 | int sz = 0; 129 | for (var p = this; p != null; p = p.parent) { 130 | tokens[--i] = p.label; 131 | sz += p.label.length; 132 | } 133 | 134 | var buf = new StringBuilder(sz); 135 | for (char[] token : tokens) { 136 | buf.append(token); 137 | } 138 | 139 | return buf.toString(); 140 | } 141 | 142 | /** 143 | * Interns {@link #label}. 144 | * 145 | * @param table 146 | * the table containing the existing strings 147 | */ 148 | void dedup(final Map table) { 149 | var l = getLabel(); 150 | var v = table.get(l); 151 | if (v == null) { 152 | table.put(l, label); 153 | } 154 | else { 155 | label = v; 156 | } 157 | } 158 | 159 | public boolean isBlank() { 160 | return StringUtils.isBlank(toString()); 161 | } 162 | 163 | /** 164 | * Creates a {@link TreeString}. Useful if you need to create one-off {@link TreeString} without {@link 165 | * TreeStringBuilder}. Memory consumption is still about the same to {@code new String(string)}. 166 | * 167 | * @param string 168 | * the tree string 169 | * 170 | * @return the new {@link TreeString} 171 | */ 172 | public static TreeString valueOf(final String string) { 173 | return new TreeString(null, string); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/TreeStringBuilder.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * Builds {@link TreeString}s that share common prefixes. Call {@link #intern(String)} and you get the {@link 9 | * TreeString} that represents the same string, but as you intern more strings that share the same prefixes, those 10 | * {@link TreeString}s that you get back start to share data. 11 | * 12 | *

13 | * Because the internal state of {@link TreeString}s get mutated as new strings are interned (to exploit new-found 14 | * common prefixes), {@link TreeString}s returned from {@link #intern(String)} aren't thread-safe until {@link 15 | * TreeStringBuilder} is disposed. That is, you have to make sure other threads don't see those {@link TreeString}s 16 | * until you are done interning strings. 17 | *

18 | * 19 | * @author Kohsuke Kawaguchi 20 | */ 21 | public class TreeStringBuilder { 22 | private final Child root = new Child(new TreeString()); 23 | 24 | /** 25 | * Interns a string. 26 | * 27 | * @param string 28 | * the string to intern 29 | * 30 | * @return the String as {@link TreeString} instance 31 | */ 32 | public TreeString intern(final String string) { 33 | return getRoot().intern(string).getNode(); 34 | } 35 | 36 | /** 37 | * Interns a {@link TreeString} created elsewhere. 38 | * 39 | * @param treeString 40 | * the {@link TreeString} to intern 41 | * 42 | * @return the String as {@link TreeString} instance 43 | */ 44 | public TreeString intern(final TreeString treeString) { 45 | return getRoot().intern(treeString.toString()).getNode(); 46 | } 47 | 48 | /** 49 | * Further reduces the memory footprint by finding the same labels across multiple {@link TreeString}s. 50 | */ 51 | public void dedup() { 52 | getRoot().dedup(new HashMap<>()); 53 | } 54 | 55 | /** Placeholder that represents no child node, until one is added. */ 56 | private static final Map NO_CHILDREN = Collections.emptyMap(); 57 | 58 | Child getRoot() { 59 | return root; 60 | } 61 | 62 | /** 63 | * Child node that may store other elements. 64 | */ 65 | private static final class Child { 66 | private final TreeString node; 67 | 68 | private Map children = NO_CHILDREN; 69 | 70 | Child(final TreeString node) { 71 | this.node = node; 72 | } 73 | 74 | /** 75 | * Adds one edge and leaf to this tree node, or returns an existing node if any. 76 | * 77 | * @param string 78 | * the string to intern 79 | * 80 | * @return the node 81 | */ 82 | public Child intern(final String string) { 83 | if (string.isEmpty()) { 84 | return this; 85 | } 86 | 87 | makeWritable(); 88 | for (Map.Entry entry : children.entrySet()) { 89 | int plen = commonPrefix(entry.getKey(), string); 90 | if (plen > 0) { 91 | if (plen < entry.getKey().length()) { 92 | // insert a node between this and entry.value 93 | var child = entry.getValue(); 94 | var prefix = string.substring(0, plen); 95 | var middle = child.split(prefix); 96 | 97 | // add 'middle' instead of 'child' 98 | children.remove(entry.getKey()); 99 | children.put(prefix, middle); 100 | 101 | return middle.intern(string.substring(plen)); 102 | } 103 | else { 104 | return entry.getValue().intern(string.substring(plen)); // entire key is suffix 105 | } 106 | } 107 | } 108 | 109 | // no common prefix. an entirely new node. 110 | var t = children.get(string); 111 | if (t == null) { 112 | t = new Child(new TreeString(getNode(), string)); 113 | children.put(string, t); 114 | } 115 | return t; 116 | } 117 | 118 | /** 119 | * Makes sure {@link #children} is writable. 120 | */ 121 | private void makeWritable() { 122 | if (children == NO_CHILDREN) { 123 | children = new HashMap<>(); 124 | } 125 | } 126 | 127 | /** 128 | * Inserts a new node between this node and its parent, and returns that node. Newly inserted 'middle' node will 129 | * have this node as its sole child. 130 | * 131 | * @param prefix 132 | * the prefix 133 | * 134 | * @return the node 135 | */ 136 | private Child split(final String prefix) { 137 | var suffix = getNode().getLabel().substring(prefix.length()); 138 | 139 | var middle = new Child(getNode().split(prefix)); 140 | middle.makeWritable(); 141 | middle.children.put(suffix, this); 142 | 143 | return middle; 144 | } 145 | 146 | /** 147 | * Returns the common prefix between two strings. 148 | * 149 | * @param a 150 | * a string 151 | * @param b 152 | * another string 153 | * 154 | * @return the prefix in characters 155 | */ 156 | private int commonPrefix(final String a, final String b) { 157 | int m = Math.min(a.length(), b.length()); 158 | 159 | for (int i = 0; i < m; i++) { 160 | if (a.charAt(i) != b.charAt(i)) { 161 | return i; 162 | } 163 | } 164 | return m; 165 | } 166 | 167 | /** 168 | * Calls {@link TreeString#dedup(Map)} recursively. 169 | * 170 | * @param table 171 | * the table containing the existing strings 172 | */ 173 | private void dedup(final Map table) { 174 | getNode().dedup(table); 175 | for (Child child : children.values()) { 176 | child.dedup(table); 177 | } 178 | } 179 | 180 | TreeString getNode() { 181 | return node; 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/VisibleForTesting.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | /** 4 | * An annotation that indicates that the visibility of a type or member has 5 | * been relaxed to make the code testable. 6 | * 7 | * @author Johannes Henkel (copied from Google Guava Library) 8 | */ 9 | public @interface VisibleForTesting { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/edu/hm/hafner/util/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides highly reusable utility classes and static methods, chiefly concerned 3 | * with adding value to java.lang, java.util, and other standard core classes. 4 | * 5 | * @author Ullrich Hafner 6 | */ 7 | @DefaultAnnotation(NonNull.class) 8 | package edu.hm.hafner.util; 9 | 10 | import edu.umd.cs.findbugs.annotations.DefaultAnnotation; 11 | import edu.umd.cs.findbugs.annotations.NonNull; 12 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module edu.hm.hafner.codingstyle { 2 | requires java.xml; 3 | requires org.apache.commons.lang3; 4 | requires org.apache.commons.io; 5 | requires com.github.spotbugs.annotations; 6 | requires com.google.errorprone.annotations; 7 | 8 | exports edu.hm.hafner.util; 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/archunit/ArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.archunit; 2 | 3 | import com.tngtech.archunit.core.importer.ImportOption; 4 | import com.tngtech.archunit.core.importer.Location; 5 | import com.tngtech.archunit.junit.AnalyzeClasses; 6 | import com.tngtech.archunit.junit.ArchTest; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | 9 | import edu.hm.hafner.archunit.ArchitectureTest.DoNotIncludeRulesUnderTest; 10 | 11 | /** 12 | * Checks the architecture of this module. 13 | * 14 | * @author Ullrich Hafner 15 | */ 16 | @AnalyzeClasses(packages = "edu.hm.hafner", importOptions = DoNotIncludeRulesUnderTest.class) 17 | class ArchitectureTest { 18 | @ArchTest 19 | static final ArchRule NO_PUBLIC_TEST_CLASSES = ArchitectureRules.NO_PUBLIC_TEST_CLASSES; 20 | 21 | @ArchTest 22 | static final ArchRule ONLY_PACKAGE_PRIVATE_TEST_METHODS = ArchitectureRules.ONLY_PACKAGE_PRIVATE_TEST_METHODS; 23 | 24 | @ArchTest 25 | static final ArchRule ONLY_PACKAGE_PRIVATE_ARCHITECTURE_TESTS = ArchitectureRules.ONLY_PACKAGE_PRIVATE_ARCHITECTURE_TESTS; 26 | 27 | @ArchTest 28 | static final ArchRule NO_FIELDS_IN_TESTS = ArchitectureRules.NO_FIELDS_IN_TESTS; 29 | 30 | @ArchTest 31 | static final ArchRule NO_TEST_API_CALLED = ArchitectureRules.NO_TEST_API_CALLED; 32 | 33 | @ArchTest 34 | static final ArchRule NO_FORBIDDEN_PACKAGE_ACCESSED = ArchitectureRules.NO_FORBIDDEN_PACKAGE_ACCESSED; 35 | 36 | @ArchTest 37 | static final ArchRule NO_FORBIDDEN_CLASSES_CALLED = ArchitectureRules.NO_FORBIDDEN_CLASSES_CALLED; 38 | 39 | @ArchTest 40 | static final ArchRule NO_FORBIDDEN_ANNOTATION_USED = ArchitectureRules.NO_FORBIDDEN_ANNOTATION_USED; 41 | 42 | @ArchTest 43 | static final ArchRule NO_EXCEPTIONS_WITH_NO_ARG_CONSTRUCTOR = ArchitectureRules.NO_EXCEPTIONS_WITH_NO_ARG_CONSTRUCTOR; 44 | 45 | static final class DoNotIncludeRulesUnderTest implements ImportOption { 46 | @Override 47 | public boolean includes(final Location location) { 48 | return !location.contains(ArchitectureRulesTest.class.getSimpleName()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/archunit/PackageArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.archunit; 2 | 3 | import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; 4 | import com.tngtech.archunit.junit.AnalyzeClasses; 5 | import com.tngtech.archunit.junit.ArchTest; 6 | import com.tngtech.archunit.lang.ArchRule; 7 | 8 | import java.net.URL; 9 | import java.util.Objects; 10 | 11 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; 12 | import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.*; 13 | import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.*; 14 | 15 | /** 16 | * Checks the package architecture of this module. 17 | * 18 | * @author Ullrich Hafner 19 | */ 20 | @AnalyzeClasses(packages = "edu.hm.hafner..", importOptions = DoNotIncludeTests.class) 21 | class PackageArchitectureTest { 22 | private static final URL PACKAGE_DESIGN = PackageArchitectureTest.class.getResource("/design.puml"); 23 | 24 | @ArchTest 25 | static final ArchRule ADHERES_TO_PACKAGE_DESIGN 26 | = classes().should(adhereToPlantUmlDiagram(Objects.requireNonNull(PACKAGE_DESIGN), 27 | consideringOnlyDependenciesInAnyPackage("edu.hm.hafner.."))); 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/AbstractComparableTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.*; 6 | 7 | /** 8 | * Verifies that comparable objects comply with the contract in {@link Comparable#compareTo(Object)}. 9 | * 10 | * @author Ullrich Hafner 11 | * @param the type of the subject under test 12 | */ 13 | public abstract class AbstractComparableTest> { 14 | /** 15 | * Verifies that a negative integer, zero, or a positive integer as this object is less than, equal to, or greater 16 | * than the specified object. 17 | */ 18 | @Test 19 | @SuppressWarnings({"SelfComparison", "EqualsWithItself"}) 20 | void shouldBeNegativeIfThisIsSmaller() { 21 | T smaller = createSmallerSut(); 22 | T greater = createGreaterSut(); 23 | 24 | assertThat(smaller.compareTo(greater)).isNegative(); 25 | assertThat(greater.compareTo(smaller)).isPositive(); 26 | 27 | assertThat(smaller.compareTo(smaller)).isZero(); 28 | assertThat(greater.compareTo(greater)).isZero(); 29 | } 30 | 31 | /** 32 | * Verifies that {@code sgn(x.compareTo(y)) == -sgn(y.compareTo(x))} for all {@code x} and {@code y}. 33 | */ 34 | @Test 35 | void shouldBeSymmetric() { 36 | T left = createSmallerSut(); 37 | T right = createGreaterSut(); 38 | 39 | int leftCompareToResult = left.compareTo(right); 40 | int rightCompareToResult = right.compareTo(left); 41 | 42 | assertThat(Integer.signum(leftCompareToResult)).isEqualTo(-Integer.signum(rightCompareToResult)); 43 | } 44 | 45 | /** 46 | * Creates a subject under test. The SUT must be smaller than the SUT of the opposite method {@link 47 | * #createGreaterSut()}. 48 | * 49 | * @return the SUT 50 | */ 51 | protected abstract T createSmallerSut(); 52 | 53 | /** 54 | * Creates a subject under test. The SUT must be greater than the SUT of the opposite method {@link 55 | * #createSmallerSut()}. 56 | * 57 | * @return the SUT 58 | */ 59 | protected abstract T createGreaterSut(); 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/AbstractEqualsTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.*; 6 | 7 | /** 8 | * Verifies that objects of any Java class comply with the contract in {@link Object#equals(Object)}. 9 | * 10 | * @author Ullrich Hafner 11 | */ 12 | public abstract class AbstractEqualsTest { 13 | /** 14 | * Creates the subject under test. 15 | * 16 | * @return the SUT 17 | */ 18 | protected abstract Object createSut(); 19 | 20 | /** 21 | * Verifies that for any non-null reference value {@code x}, {@code x.equals(null)} should return {@code false}. 22 | */ 23 | @Test 24 | @SuppressWarnings({"PMD.EqualsNull", "checkstyle:equalsavoidnull", "ConstantConditions"}) 25 | void shouldReturnFalseOnEqualsNull() { 26 | assertThat(createSut().equals(null)).isFalse(); 27 | } 28 | 29 | /** 30 | * Verifies that equals is reflexive: for any non-null reference value {@code x}, {@code x.equals(x)} should 31 | * return {@code true}. 32 | */ 33 | @SuppressWarnings("EqualsWithItself") 34 | @Test 35 | void shouldReturnTrueOnEqualsThis() { 36 | var sut = createSut(); 37 | 38 | assertThat(sut.equals(sut)).isTrue(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/LineRangeListTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.List; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.Assertions.*; 8 | 9 | /** 10 | * Tests the class {@link LineRangeList}. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | * @author Ullrich Hafner 14 | */ 15 | class LineRangeListTest { 16 | @Test 17 | void shouldStoreBigValues() { 18 | var list = new LineRangeList(); 19 | var range = new LineRange(1350, Integer.MAX_VALUE); 20 | list.add(range); 21 | assertThat(list).containsExactly(range); 22 | } 23 | 24 | @Test 25 | void shouldStoreRangeWithOneLines() { 26 | var list = new LineRangeList(); 27 | var range = new LineRange(0, 0); 28 | list.add(range); 29 | assertThat(list).containsExactly(range); 30 | } 31 | 32 | @Test 33 | void shouldStoreRangeWithTwoLines() { 34 | var list = new LineRangeList(); 35 | var range = new LineRange(128, 129); 36 | list.add(range); 37 | assertThat(list).containsExactly(range); 38 | } 39 | 40 | @Test 41 | void shouldSupportSetOperations() { 42 | var list = new LineRangeList(); 43 | var range = new LineRange(1, 2); 44 | list.add(range); 45 | 46 | assertThat(list.get(0)).isEqualTo(range); 47 | assertThat(list.get(0)).isNotSameAs(range); 48 | assertThat(list).hasSize(1); 49 | 50 | var other = new LineRange(3, 4); 51 | assertThat(list.set(0, other)).isEqualTo(range); 52 | assertThat(list.get(0)).isEqualTo(other); 53 | assertThat(list.get(0)).isNotSameAs(other); 54 | assertThat(list).hasSize(1); 55 | 56 | assertThat(list.remove(0)).isEqualTo(other); 57 | assertThat(list).hasSize(0); 58 | } 59 | 60 | /** Tests the internal buffer resize operation. */ 61 | @Test 62 | void shouldResizeCorrectly() { 63 | var list = new LineRangeList(); 64 | for (int i = 0; i < 100; i++) { 65 | list.add(new LineRange(i * 2, i * 2 + 1)); 66 | } 67 | list.trim(); 68 | assertThat(list).hasSize(100); 69 | 70 | for (int i = 0; i < 100; i++) { 71 | assertThat(list.get(i)).isEqualTo(new LineRange(i * 2, i * 2 + 1)); 72 | assertThat(list.contains(new LineRange(i * 2, i * 2 + 1))).isTrue(); 73 | } 74 | 75 | assertThat(list).hasSize(100); 76 | } 77 | 78 | @Test 79 | void shouldProvideContains() { 80 | var last = createThreeElements(); 81 | last.remove(new LineRange(4, 5)); 82 | assertThat(last).containsExactly(new LineRange(0, 1), new LineRange(2, 3)); 83 | 84 | var middle = createThreeElements(); 85 | middle.remove(new LineRange(2, 3)); 86 | assertThat(middle).containsExactly(new LineRange(0, 1), new LineRange(4, 5)); 87 | 88 | var first = createThreeElements(); 89 | assertThat(first).contains(new LineRange(0, 1)); 90 | assertThat(first).contains(new LineRange(2, 3)); 91 | assertThat(first).contains(new LineRange(4, 5)); 92 | 93 | first.remove(new LineRange(0, 1)); 94 | assertThat(first).containsExactly(new LineRange(2, 3), new LineRange(4, 5)); 95 | 96 | assertThat(first).contains(new LineRange(2, 3)); 97 | assertThat(first).doesNotContain(new LineRange(0, 1)); 98 | } 99 | 100 | private List createThreeElements() { 101 | var range = new LineRangeList(); 102 | range.add(new LineRange(0, 1)); 103 | range.add(new LineRange(2, 3)); 104 | range.add(new LineRange(4, 5)); 105 | assertThat(range).containsExactly(new LineRange(0, 1), new LineRange(2, 3), new LineRange(4, 5)); 106 | return range; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/LineRangeTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import nl.jqno.equalsverifier.EqualsVerifier; 6 | 7 | import static edu.hm.hafner.util.assertions.Assertions.*; 8 | 9 | /** 10 | * Tests the class {@link LineRangeList}. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | * @author Ullrich Hafner 14 | */ 15 | class LineRangeTest { 16 | @Test 17 | void shouldRefuseIllegalValue() { 18 | assertThat(new LineRange(-5, 1)).hasStart(0).hasEnd(0).isSingleLine(); 19 | } 20 | 21 | @Test 22 | void shouldFindLinesInsideAndOutsideOfLineRange() { 23 | var lineRange = new LineRange(1, 2); 24 | 25 | assertThat(lineRange.contains(0)).isFalse(); 26 | assertThat(lineRange.contains(1)).isTrue(); 27 | assertThat(lineRange.contains(2)).isTrue(); 28 | assertThat(lineRange.contains(3)).isFalse(); 29 | assertThat(lineRange).hasStart(1).hasEnd(2) 30 | .hasLines(1, 2).isNotSingleLine().hasToString("[1-2]"); 31 | 32 | var wrongOrder = new LineRange(2, 1); 33 | assertThat(wrongOrder.contains(0)).isFalse(); 34 | assertThat(wrongOrder.contains(1)).isTrue(); 35 | assertThat(wrongOrder.contains(2)).isTrue(); 36 | assertThat(wrongOrder.contains(3)).isFalse(); 37 | assertThat(wrongOrder).hasStart(1).hasEnd(2) 38 | .hasLines(1, 2).isNotSingleLine().hasToString("[1-2]"); 39 | 40 | var point = new LineRange(2); 41 | assertThat(point.contains(1)).isFalse(); 42 | assertThat(point.contains(2)).isTrue(); 43 | assertThat(point.contains(3)).isFalse(); 44 | assertThat(point).hasStart(2).hasEnd(2) 45 | .hasLines(2).isSingleLine().hasToString("[2-2]"); 46 | } 47 | 48 | @Test 49 | void shouldAdhereToEquals() { 50 | EqualsVerifier.forClass(LineRange.class).verify(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.stream.Stream; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static edu.hm.hafner.util.assertions.Assertions.*; 10 | import static org.mockito.Mockito.*; 11 | 12 | /** 13 | * Tests the class {@link LookaheadStream}. 14 | * 15 | * @author Ullrich Hafner 16 | */ 17 | class LookaheadStreamTest extends ResourceTest { 18 | private static final String FIRST_LINE = "First Line"; 19 | private static final String EMPTY = StringUtils.EMPTY; 20 | 21 | @Test 22 | void shouldHandleEmptyLines() { 23 | try (var stream = new LookaheadStream(getTextLinesAsStream(""))) { 24 | assertThat(stream).doesNotHaveNext().hasLine(0).hasFileName(EMPTY); 25 | 26 | assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(stream::peekNext); 27 | assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(stream::next); 28 | } 29 | } 30 | 31 | @Test 32 | void shouldReturnSingleLine() { 33 | try (var stream = new LookaheadStream(getTextLinesAsStream(FIRST_LINE))) { 34 | assertThat(stream).hasNext().hasLine(0); 35 | assertThat(stream.peekNext()).isEqualTo(FIRST_LINE); 36 | // Now reading from the buffer: 37 | assertThat(stream).hasNext(); 38 | assertThat(stream.peekNext()).isEqualTo(FIRST_LINE); 39 | 40 | assertThat(stream.next()).isEqualTo(FIRST_LINE); 41 | assertThat(stream).hasLine(1).doesNotHaveNext(); 42 | } 43 | } 44 | 45 | @Test 46 | void shouldReturnMultipleLines() { 47 | try (var stream = new LookaheadStream(getTextLinesAsStream("First Line\nSecond Line"))) { 48 | assertThat(stream.hasNext()).isTrue(); 49 | assertThat(stream.next()).isEqualTo(FIRST_LINE); 50 | assertThat(stream.getLine()).isEqualTo(1); 51 | assertThat(stream.hasNext()).isTrue(); 52 | assertThat(stream.next()).isEqualTo("Second Line"); 53 | assertThat(stream.getLine()).isEqualTo(2); 54 | 55 | assertThat(stream.hasNext()).isFalse(); 56 | } 57 | } 58 | 59 | @Test 60 | void shouldReturnLookAheadLines() { 61 | try (var stream = new LookaheadStream(getTextLinesAsStream("First Line\nSecond Line"))) { 62 | assertThat(stream.hasNext()).isTrue(); 63 | assertThat(stream.hasNext("Line$")).isTrue(); 64 | assertThat(stream.hasNext("Second.*")).isFalse(); 65 | assertThat(stream.next()).isEqualTo(FIRST_LINE); 66 | assertThat(stream.getLine()).isEqualTo(1); 67 | 68 | assertThat(stream.hasNext()).isTrue(); 69 | assertThat(stream.hasNext("Line$")).isTrue(); 70 | assertThat(stream.hasNext("First.*")).isFalse(); 71 | assertThat(stream.next()).isEqualTo("Second Line"); 72 | assertThat(stream.getLine()).isEqualTo(2); 73 | 74 | assertThat(stream.hasNext()).isFalse(); 75 | assertThat(stream.hasNext(".*")).isFalse(); 76 | } 77 | } 78 | 79 | @Test 80 | @SuppressWarnings("unchecked") 81 | void shouldCloseStream() { 82 | try (Stream lines = mock(Stream.class)) { 83 | try (var stream = new LookaheadStream(lines)) { 84 | assertThat(stream.getLine()).isZero(); 85 | } 86 | 87 | verify(lines).close(); // lines will be closed by stream 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Optional; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.CsvSource; 11 | import org.junit.jupiter.params.provider.ValueSource; 12 | 13 | import edu.hm.hafner.util.PackageDetectorFactory.FileSystemFacade; 14 | 15 | import static org.assertj.core.api.Assertions.*; 16 | import static org.mockito.Mockito.*; 17 | 18 | /** 19 | * Tests the class {@link PackageDetectorRunner}. 20 | * 21 | * @author Ullrich Hafner 22 | */ 23 | class PackageDetectorRunnerTest extends ResourceTest { 24 | @ParameterizedTest(name = "{index} => file={0}, expected package={1}") 25 | @CsvSource({ 26 | "MavenJavaTest.txt.java, hudson.plugins.tasks.util", 27 | "ActionBinding.cs, Avaloq.SmartClient.Utilities", 28 | "KotlinTest.txt.kt, edu.hm.kersting"}) 29 | void shouldExtractPackageNames(final String fileName, final String expectedPackage) throws IOException { 30 | assertThat(detect(fileName)).contains(expectedPackage); 31 | } 32 | 33 | @ParameterizedTest(name = "{index} => file={0}, no package found") 34 | @ValueSource(strings = {"MavenJavaTest.txt", "relative.txt", "KotlinTest.txt"}) 35 | void shouldNotAcceptFile(final String fileName) throws IOException { 36 | assertThat(detect(fileName)).isEmpty(); 37 | } 38 | 39 | private Optional detect(final String fileName) throws IOException { 40 | try (InputStream stream = asInputStream(fileName)) { 41 | var fileSystem = mock(FileSystemFacade.class); 42 | when(fileSystem.openFile(fileName)).thenReturn(stream); 43 | 44 | var detectors = PackageDetectorFactory.createPackageDetectors(fileSystem); 45 | 46 | return detectors.detectPackageName(fileName, StandardCharsets.UTF_8); 47 | } 48 | } 49 | 50 | @Test 51 | void shouldHandleException() throws IOException { 52 | var fileSystem = mock(FileSystemFacade.class); 53 | when(fileSystem.openFile(anyString())).thenThrow(new IOException("Simulated")); 54 | 55 | assertThat(PackageDetectorFactory.createPackageDetectors(fileSystem) 56 | .detectPackageName("file.java", StandardCharsets.UTF_8)).isEmpty(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/PrefixLoggerTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.PrintStream; 4 | import java.util.List; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static java.util.Arrays.*; 9 | import static java.util.Collections.emptyList; 10 | import static org.mockito.Mockito.*; 11 | 12 | /** 13 | * Tests the class {@link PrefixLogger}. 14 | * 15 | * @author Ullrich Hafner 16 | */ 17 | class PrefixLoggerTest { 18 | private static final String LOG_MESSAGE = "Hello PrefixLogger!"; 19 | private static final String TOOL_NAME = "test"; 20 | private static final String EXPECTED_TOOL_PREFIX = "[test]"; 21 | private static final String FIRST_MESSAGE = "One"; 22 | private static final String SECOND_MESSAGE = "Two"; 23 | 24 | @Test @SuppressWarnings("PMD.CloseResource") 25 | void shouldLogSingleAndMultipleLines() { 26 | var printStream = mock(PrintStream.class); 27 | var logger = new PrefixLogger(printStream, TOOL_NAME); 28 | 29 | logger.log(LOG_MESSAGE); 30 | 31 | verify(printStream).println(EXPECTED_TOOL_PREFIX + " " + LOG_MESSAGE); 32 | 33 | var loggerWithBraces = new PrefixLogger(printStream, EXPECTED_TOOL_PREFIX); 34 | 35 | loggerWithBraces.log(LOG_MESSAGE); 36 | 37 | verify(printStream, times(2)).println(EXPECTED_TOOL_PREFIX + " " + LOG_MESSAGE); 38 | 39 | logger.logEachLine(emptyList()); 40 | 41 | verifyNoMoreInteractions(printStream); 42 | 43 | logger.logEachLine(List.of(FIRST_MESSAGE)); 44 | 45 | verify(printStream).println(EXPECTED_TOOL_PREFIX + " " + FIRST_MESSAGE); 46 | 47 | logger.logEachLine(asList(FIRST_MESSAGE, SECOND_MESSAGE)); 48 | verify(printStream, times(2)).println(EXPECTED_TOOL_PREFIX + " " + FIRST_MESSAGE); 49 | verify(printStream).println(EXPECTED_TOOL_PREFIX + " " + SECOND_MESSAGE); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/ResourceExtractorTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import java.io.IOException; 4 | import java.io.UncheckedIOException; 5 | import java.net.URL; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.security.CodeSource; 10 | import java.security.ProtectionDomain; 11 | import java.util.NoSuchElementException; 12 | 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.io.TempDir; 16 | 17 | import static org.assertj.core.api.Assertions.*; 18 | import static org.mockito.Mockito.*; 19 | 20 | /** 21 | * Tests the class {@link ResourceExtractor}. 22 | * 23 | * @author Ullrich Hafner 24 | */ 25 | class ResourceExtractorTest { 26 | private static final String ASSERTJ_TEMPLATES = "assertj-templates/has_assertion_template.txt"; 27 | private static final String JENKINS_FILE = "Jenkinsfile.reference"; 28 | private static final String MANIFEST_MF = "META-INF/MANIFEST.MF"; 29 | 30 | @Test 31 | void shouldLocateResourcesInFolder() { 32 | var folderExtractor = new ResourceExtractor(ResourceExtractor.class); 33 | assertThat(folderExtractor.isReadingFromJarFile()).isFalse(); 34 | assertThat(normalizePath(folderExtractor.getResourcePath())).endsWith("target/classes"); 35 | } 36 | 37 | private String normalizePath(final String resourcePath) { 38 | return new PathUtil().getAbsolutePath(resourcePath); 39 | } 40 | 41 | @Test 42 | void shouldLocateResourcesInJarFile() { 43 | var jarExtractor = new ResourceExtractor(StringUtils.class); 44 | assertThat(jarExtractor.isReadingFromJarFile()).isTrue(); 45 | assertThat(jarExtractor.getResourcePath()).matches(".*commons-lang3-\\d+\\.\\d+\\.\\d+.jar"); 46 | } 47 | 48 | @Test 49 | void shouldExtractFromFolder(@TempDir final Path targetFolder) { 50 | var proxy = new ResourceExtractor(ResourceExtractor.class); 51 | 52 | proxy.extract(targetFolder, ASSERTJ_TEMPLATES, JENKINS_FILE, 53 | "edu/hm/hafner/util/ResourceExtractor.class"); 54 | 55 | assertThat(readToString(targetFolder.resolve(ASSERTJ_TEMPLATES))) 56 | .contains("has${Property}(${propertyType} ${property_safe})"); 57 | assertThat(readToString(targetFolder.resolve(JENKINS_FILE))) 58 | .contains("node", "stage ('Build and Static Analysis')"); 59 | } 60 | 61 | @Test 62 | void shouldThrowExceptionIfTargetIsFileInFolder() throws IOException { 63 | var proxy = new ResourceExtractor(ResourceExtractor.class); 64 | 65 | var tempFile = Files.createTempFile("tmp", "tmp"); 66 | assertThatIllegalArgumentException().isThrownBy(() -> proxy.extract(tempFile, MANIFEST_MF)); 67 | } 68 | 69 | @Test 70 | void shouldThrowExceptionIfFileDoesNotExistInFolder(@TempDir final Path targetFolder) { 71 | var proxy = new ResourceExtractor(ResourceExtractor.class); 72 | 73 | assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(() -> 74 | proxy.extract(targetFolder, "does-not-exist")); 75 | } 76 | 77 | @Test 78 | void shouldExtractFromJar(@TempDir final Path targetFolder) { 79 | var proxy = new ResourceExtractor(StringUtils.class); 80 | 81 | proxy.extract(targetFolder, MANIFEST_MF, 82 | "org/apache/commons/lang3/StringUtils.class"); 83 | 84 | assertThat(readToString(targetFolder.resolve(MANIFEST_MF))).contains("Manifest-Version: 1.0", 85 | "Bundle-SymbolicName: org.apache.commons.lang3"); 86 | } 87 | 88 | @Test 89 | void shouldThrowExceptionIfTargetIsFileInJar() throws IOException { 90 | var proxy = new ResourceExtractor(StringUtils.class); 91 | 92 | var tempFile = Files.createTempFile("tmp", "tmp"); 93 | assertThatIllegalArgumentException().isThrownBy(() -> proxy.extract(tempFile, MANIFEST_MF)); 94 | } 95 | 96 | @Test 97 | void shouldThrowExceptionIfFileDoesNotExistInJar(@TempDir final Path targetFolder) { 98 | var proxy = new ResourceExtractor(StringUtils.class); 99 | 100 | assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> 101 | proxy.extract(targetFolder, "does-not-exist")); 102 | } 103 | 104 | @Test 105 | void shouldHandleClassloaderProblems() { 106 | var protectionDomain = mock(ProtectionDomain.class); 107 | 108 | assertThatIllegalArgumentException() 109 | .isThrownBy(() -> new ResourceExtractor(ResourceExtractor.class, protectionDomain)) 110 | .withMessageContainingAll("CodeSource for", "ResourceExtractor"); 111 | 112 | var codeSource = mock(CodeSource.class); 113 | when(protectionDomain.getCodeSource()).thenReturn(codeSource); 114 | 115 | assertThatIllegalArgumentException() 116 | .isThrownBy(() -> new ResourceExtractor(ResourceExtractor.class, protectionDomain)) 117 | .withMessageContaining("CodeSource location for", "ResourceExtractor"); 118 | 119 | var url = mock(URL.class); 120 | when(codeSource.getLocation()).thenReturn(url); 121 | assertThatIllegalArgumentException() 122 | .isThrownBy(() -> new ResourceExtractor(ResourceExtractor.class, protectionDomain)) 123 | .withMessageContaining("CodeSource location path", "ResourceExtractor"); 124 | 125 | when(url.getPath()).thenReturn("file.jar"); 126 | var extractor = new ResourceExtractor(ResourceExtractor.class, protectionDomain); 127 | 128 | assertThat(extractor.getResourcePath()).isEqualTo("file.jar"); 129 | } 130 | 131 | private String readToString(final Path output) { 132 | try { 133 | return new String(Files.readAllBytes(output), StandardCharsets.UTF_8); 134 | } 135 | catch (IOException exception) { 136 | throw new UncheckedIOException(exception); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/SerializableTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import org.assertj.core.api.ObjectAssert; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.ObjectInputStream; 11 | import java.io.ObjectOutputStream; 12 | import java.io.Serializable; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.StandardOpenOption; 16 | 17 | import static org.assertj.core.api.Assertions.*; 18 | 19 | /** 20 | * Base class to test the serialization of instances of {@link Serializable}. Note that the instances under test must 21 | * override equals so that the test case can check the serialized instances for equality. 22 | * 23 | * @param 24 | * concrete type of the {@link Serializable} under test 25 | * 26 | * @author Ullrich Hafner 27 | */ 28 | public abstract class SerializableTest extends ResourceTest { 29 | /** 30 | * Factory method to create an instance of the {@link Serializable} under test. The instance returned by this method 31 | * will be serialized to a byte stream, deserialized into an object again, and finally compared to another instance 32 | * using {@link Object#equals(Object)}. 33 | * 34 | * @return the subject under test 35 | */ 36 | protected abstract T createSerializable(); 37 | 38 | @Test 39 | @DisplayName("should be serializable: instance -> byte array -> instance") 40 | void shouldBeSerializable() { 41 | var serializableInstance = createSerializable(); 42 | 43 | var bytes = toByteArray(serializableInstance); 44 | 45 | assertThatSerializableCanBeRestoredFrom(bytes); 46 | } 47 | 48 | /** 49 | * Resolves the subject under test from an array of bytes and compares the created instance with the original 50 | * subject under test. 51 | * 52 | * @param serializedInstance 53 | * the byte stream of the serializable 54 | */ 55 | protected void assertThatSerializableCanBeRestoredFrom(final byte... serializedInstance) { 56 | assertThatRestoredInstanceEqualsOriginalInstance(createSerializable(), restore(serializedInstance)); 57 | } 58 | 59 | /** 60 | * Asserts that the instance restored from the serialization is equal to the original instance before the 61 | * serialization. By default, the {@link ObjectAssert#usingRecursiveComparison() recursive comparison strategy} of 62 | * AssertJ is used to compare these instances. If your subject under test overrides 63 | * {@link Object#equals(Object) equals}, then you should override this method with {@code original.equals(restored)} 64 | * so the customized equals method will be used. 65 | * 66 | * @param original 67 | * the instance before the serialization 68 | * @param restored 69 | * the instance restored by the deserialization 70 | */ 71 | protected void assertThatRestoredInstanceEqualsOriginalInstance(final T original, final T restored) { 72 | assertThat(restored).usingRecursiveComparison().isEqualTo(original); 73 | } 74 | 75 | /** 76 | * Deserializes the subject under test from an array of bytes. 77 | * 78 | * @param serializedInstance 79 | * the byte stream of the serializable 80 | * 81 | * @return the deserialized instance 82 | */ 83 | @SuppressWarnings({"unchecked", "BanSerializableRead"}) 84 | protected T restore(final byte[] serializedInstance) { 85 | try (var inputStream = new ObjectInputStream(new ByteArrayInputStream(serializedInstance))) { 86 | var object = inputStream.readObject(); 87 | return (T) object; 88 | } 89 | catch (IOException | ClassNotFoundException e) { 90 | throw new AssertionError("Can't resolve instance from byte array", e); 91 | } 92 | } 93 | 94 | /** 95 | * Write the specified object to a byte array using an {@link ObjectOutputStream}. The class of the object, the 96 | * signature of the class, and the values of the non-transient and non-static fields of the class and all of its 97 | * supertypes are written. Objects referenced by this object are written transitively so that a complete equivalent 98 | * graph of objects can be reconstructed by an ObjectInputStream. 99 | * 100 | * @param object 101 | * the object to serialize 102 | * 103 | * @return the object serialization 104 | */ 105 | protected byte[] toByteArray(final Serializable object) { 106 | var out = new ByteArrayOutputStream(); 107 | try (var stream = new ObjectOutputStream(out)) { 108 | stream.writeObject(object); 109 | } 110 | catch (IOException exception) { 111 | throw new IllegalStateException("Can't serialize object " + object, exception); 112 | } 113 | return out.toByteArray(); 114 | } 115 | 116 | /** 117 | * Serializes an issue using an {@link ObjectOutputStream } to the file /tmp/serializable.ser. 118 | * 119 | * @throws IOException 120 | * if the file could not be created 121 | */ 122 | protected void createSerializationFile() throws IOException { 123 | Files.write(Path.of("/tmp/serializable.ser"), toByteArray(createSerializable()), 124 | StandardOpenOption.CREATE_NEW); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/StringComparableTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | /** 4 | * Example class that shows on how to verify that String instances comply with the contract in {@link 5 | * Comparable#compareTo(Object)}. 6 | * 7 | * @author Ullrich Hafner 8 | */ 9 | class StringComparableTest extends AbstractComparableTest { 10 | @Override 11 | protected String createSmallerSut() { 12 | return "a bc"; 13 | } 14 | 15 | @Override 16 | protected String createGreaterSut() { 17 | return "z yx"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/StringEqualsTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | /** 4 | * Example class that shows on how to verify that String instances comply with the contract in {@link 5 | * Object#equals(Object)}. 6 | * 7 | * @author Ullrich Hafner 8 | */ 9 | class StringEqualsTest extends AbstractEqualsTest { 10 | @Override 11 | protected Object createSut() { 12 | return "Hello World"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/edu/hm/hafner/util/TreeStringBuilderTest.java: -------------------------------------------------------------------------------- 1 | package edu.hm.hafner.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.Random; 9 | import nl.jqno.equalsverifier.EqualsVerifier; 10 | import nl.jqno.equalsverifier.Warning; 11 | 12 | import static org.assertj.core.api.Assertions.*; 13 | 14 | /** 15 | * Tests the class {@link TreeStringBuilder}. 16 | * 17 | * @author Kohsuke Kawaguchi 18 | * @author Ullrich Hafner 19 | */ 20 | class TreeStringBuilderTest { 21 | /** Creates several tree strings and checks that new tree strings will use the prefix of a previous one. */ 22 | @Test 23 | @SuppressWarnings({"ConstantConditions", "NullAway"}) 24 | void shouldCreateSimpleTreeStringsWithBuilder() { 25 | var builder = new TreeStringBuilder(); 26 | var foo = builder.intern("foo"); 27 | assertThat(foo).hasToString("foo"); 28 | assertThat(foo.getLabel()).isEqualTo("foo"); 29 | 30 | var treeString = builder.intern("foo/bar/zot"); 31 | assertThat(treeString).hasToString("foo/bar/zot"); 32 | assertThat(treeString.getLabel()).isEqualTo("/bar/zot"); 33 | assertThat(treeString.getParent()).isSameAs(foo); 34 | 35 | var interned = builder.intern(treeString); 36 | assertThat(interned).hasToString("foo/bar/zot"); 37 | assertThat(interned.getLabel()).isEqualTo("/bar/zot"); 38 | 39 | assertThat(treeString).isSameAs(interned); 40 | 41 | assertThat(builder.intern("")).hasToString(""); 42 | 43 | var otherMiddleChild = builder.intern("foo/bar/xxx"); 44 | assertThat(otherMiddleChild).hasToString("foo/bar/xxx"); 45 | assertThat(otherMiddleChild.getLabel()).isEqualTo("xxx"); 46 | 47 | var parent = otherMiddleChild.getParent(); 48 | assertThat(parent).isSameAs(treeString.getParent()); 49 | assertThat(Objects.requireNonNull(parent.getParent())).isSameAs(foo); 50 | 51 | // middle node changed label but not toString 52 | assertThat(treeString.getLabel()).isEqualTo("zot"); 53 | assertThat(treeString).hasToString("foo/bar/zot"); 54 | 55 | assertThat(builder.intern("").isBlank()).isTrue(); 56 | assertThat(TreeString.valueOf("foo/bar/zot")).hasToString("foo/bar/zot"); 57 | } 58 | 59 | @Test 60 | void shouldProvideProperEqualsAndHashCode() { 61 | var builder = new TreeStringBuilder(); 62 | 63 | var foo = builder.intern("foo"); 64 | var bar = builder.intern("foo/bar"); 65 | var zot = builder.intern("foo/bar/zot"); 66 | 67 | assertThat(new TreeStringBuilder().intern("foo")).isEqualTo(foo); 68 | assertThat(new TreeStringBuilder().intern("foo/bar")).isEqualTo(bar); 69 | assertThat(new TreeStringBuilder().intern("foo/bar/zot")).isEqualTo(zot); 70 | 71 | assertThat(new TreeStringBuilder().intern("foo")).hasSameHashCodeAs(foo); 72 | assertThat(new TreeStringBuilder().intern("foo/bar")).hasSameHashCodeAs(bar); 73 | assertThat(new TreeStringBuilder().intern("foo/bar/zot")).hasSameHashCodeAs(zot); 74 | } 75 | 76 | @Test 77 | void shouldThrowAssertionErrorIfLabelIsEmpty() { 78 | assertThatThrownBy(() -> new TreeString(new TreeString(), "")).isInstanceOf(AssertionError.class); 79 | } 80 | 81 | /** 82 | * Pseudo random (but deterministic) test. 83 | */ 84 | @Test 85 | void shouldCreateRandomTreeStrings() { 86 | String[] dictionary = {"aa", "b", "aba", "ba"}; 87 | var builder = new TreeStringBuilder(); 88 | 89 | var random = new Random(0); 90 | 91 | List a = new ArrayList<>(); 92 | List o = new ArrayList<>(); 93 | 94 | for (int i = 0; i < 1000; i++) { 95 | var b = new StringBuilder(); 96 | for (int j = 0; j < random.nextInt(10) + 3; j++) { 97 | b.append(dictionary[random.nextInt(4)]); 98 | } 99 | var s = b.toString(); 100 | 101 | a.add(s); 102 | 103 | var p = builder.intern(s); 104 | assertThat(p).hasToString(s); 105 | o.add(p); 106 | } 107 | 108 | // make sure values are still all intact 109 | for (int i = 0; i < a.size(); i++) { 110 | assertThat(o.get(i)).hasToString(a.get(i)); 111 | } 112 | 113 | builder.dedup(); 114 | 115 | // verify one more time 116 | for (int i = 0; i < a.size(); i++) { 117 | assertThat(o.get(i)).hasToString(a.get(i)); 118 | } 119 | } 120 | 121 | @Test 122 | void shouldObeyEqualsContract() { 123 | EqualsVerifier.forClass(TreeString.class) 124 | .suppress(Warning.NONFINAL_FIELDS) 125 | .withPrefabValues(TreeString.class, TreeString.valueOf("Red"), TreeString.valueOf("Blue")) 126 | .verify(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/resources/archunit_ignore_patterns.txt: -------------------------------------------------------------------------------- 1 | # see https://www.archunit.org/userguide/html/000_Index.html#_ignoring_violations 2 | 3 | # ParserConfiguration.doCheckConsoleLogScanningPermitted does not require @POST or a permission check. 4 | # As-implemented, it does not leak data, even if called illegally. 5 | .*edu.hm.hafner.util.ResourceTest.* 6 | -------------------------------------------------------------------------------- /src/test/resources/design.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam componentStyle uml2 4 | skinparam component { 5 | BorderColor #a0a0a0 6 | BackgroundColor #f8f8f8 7 | } 8 | 9 | [Utilities] <<..util>> 10 | 11 | @enduml 12 | -------------------------------------------------------------------------------- /src/test/resources/edu/hm/hafner/util/ActionBinding.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Hafner Ullrich 3 | */ 4 | 5 | using System; 6 | using System.ComponentModel; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Windows.Forms; 9 | using Avaloq.Utilities; 10 | using log4net; 11 | 12 | namespace Avaloq.SmartClient.Utilities { 13 | /// 14 | /// Acts as mediator between an and a clickable Windows Forms control. 15 | /// 16 | public class ActionBinding : IDisposable 17 | // Rest of the file is omitted 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/edu/hm/hafner/util/KotlinTest.txt: -------------------------------------------------------------------------------- 1 | package edu.hm.kersting 2 | 3 | class HelloWorld { 4 | fun main() = println("Hello World") 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/edu/hm/hafner/util/KotlinTest.txt.kt: -------------------------------------------------------------------------------- 1 | package edu.hm.kersting 2 | 3 | class HelloWorld { 4 | fun main() = println("Hello World") 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/edu/hm/hafner/util/MavenJavaTest.txt: -------------------------------------------------------------------------------- 1 | package hudson.plugins.tasks.util; 2 | 3 | /** 4 | * Indicates an orderly abortion of the processing. 5 | * 6 | // Rest of the file is omitted 7 | -------------------------------------------------------------------------------- /src/test/resources/edu/hm/hafner/util/MavenJavaTest.txt.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.tasks.util; 2 | 3 | /** 4 | * Indicates an orderly abortion of the processing. 5 | */ 6 | // Rest of the file is omitted 7 | -------------------------------------------------------------------------------- /src/test/resources/edu/hm/hafner/util/relative.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhafner/codingstyle/d3fffc46e4368574d8432d6c3595e9df09a59822/src/test/resources/edu/hm/hafner/util/relative.txt -------------------------------------------------------------------------------- /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | # Turn off any java.util.Loggers (or use SEVERE to see problems) 2 | .level=OFF 3 | --------------------------------------------------------------------------------