diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..be6d63e5d2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: mapstruct +open_collective: mapstruct diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md new file mode 100644 index 0000000000..139ee6cdb3 --- /dev/null +++ b/.github/INCIDENT_RESPONSE.md @@ -0,0 +1,134 @@ +# MapStruct Incident Response Plan + +This document is the playbook MapStruct maintainers follow when an incident is active. It is not a replacement for ordinary bug triage — regular contributions and bug reports still flow through the process described in [`CONTRIBUTING.md`](../CONTRIBUTING.md). For instructions on how to *report* a security vulnerability, see [`SECURITY.md`](../SECURITY.md). + +MapStruct is a compile-time Java annotation processor distributed via Maven Central. The incidents this plan covers reflect that reality: immutable published artifacts, a manual release workflow, and a small maintainer team. There is no running service to page someone about at 3am. + +## What counts as an incident + +Four categories, with a concrete MapStruct-flavored example of each: + +* **Security vulnerability / CVE** — e.g. a crafted `@Mapper` input triggers arbitrary code execution in the annotation processor during `javac`, or the generated code contains an injection vulnerability. +* **Critical bug breaking user builds** — e.g. a released `1.x.y` generates non-compiling code for all users of `@Mapping`, with no workaround short of pinning to the previous version. +* **Release / supply-chain issue** — e.g. a bad artifact reaches Maven Central, a release secret is leaked, or `release.yml` fails mid-publish after already pushing version-bump commits to `main`. +* **Infra / CI incident** — e.g. a GitHub Actions workflow blocks all PRs, CodeQL flags a real finding, or the mapstruct.org website is out of sync after a release. + +Ordinary user bugs filed as issues are *not* incidents — they follow the normal contribution flow. + +## Severity levels + +| Severity | Definition | MapStruct example | +|---|---|---| +| **SEV-0** | Active exploitation or supply-chain compromise of a published artifact; users must stop using a version immediately. | Compromised signed JAR on Maven Central; leaked GPG or Sonatype credentials with evidence of misuse; RCE in the annotation processor triggered during `javac`. | +| **SEV-1** | Released version is broken for a majority of users or contains an undisclosed security flaw with no workaround. | A `1.x.y` release generates non-compiling code for all users of `@Mapping`; confirmed CVE with CVSS ≥ 7 but not yet exploited. | +| **SEV-2** | Released version broken for a specific but significant cohort; workaround exists; or lower-severity CVE; or the release pipeline is blocked. | Regression affecting only Java 21 + records; CVE with CVSS < 7; JReleaser fails staging so a release is blocked but nothing published; CI red on `main`. | +| **SEV-3** | Degraded but not blocking. | CodeQL false-positive noise; a single OS matrix leg flaky; mapstruct.org docs out of sync post-release. | + +## General response flow + +Every incident follows the same six-step spine, regardless of category: + +1. **Acknowledge** — confirm the report has been received and is being looked at. +2. **Assess severity** — assign SEV-0 / SEV-1 / SEV-2 / SEV-3 using the table above. +3. **Declare** — open the tracking artifact (GitHub Security Advisory for security issues, pinned issue for regressions, Discussion thread for CI incidents). +4. **Mitigate** — give users a workaround, yank a bad release, or disable a broken workflow — whatever stops the immediate bleeding. +5. **Fix and verify** — land the real fix, add a regression test, cut a patch release if needed, verify the fix on the published artifact. +6. **Close out** — announce resolution on the public channels, credit reporters, update this document if the process needs changing. + +The playbooks below are specializations of this spine. + +## Playbooks + +### A. Security vulnerability / CVE + +1. Acknowledge receipt within 48 hours via GitHub Private Vulnerability Reporting. +2. Confirm reproducibility against the latest release and `main`; assign a CVSS score. +3. Open a **draft GitHub Security Advisory (GHSA)** — this is the tracking artifact. Do **not** open a public issue. +4. Request a CVE via the GHSA (GitHub auto-requests from MITRE). +5. Develop the fix on the private fork the advisory creates; maintainers review before merge. +6. Merge the private-fork fix into `main` using the "Merge" button on the advisory page; confirm CI is green on `main` before proceeding. +7. Coordinate a disclosure date with the reporter — target ≤ 90 days from report, sooner for SEV-0. +8. Add a `### Security` entry to `NEXT_RELEASE_CHANGELOG.md`, then cut a patch release from `main`; verify the GPG signature and checksums on the published artifact before publishing the advisory. +9. Publish the GHSA, announce on Discussions + mapstruct.org blog, and credit the reporter unless they decline. + +### B. Critical bug breaking user builds + +1. Reproduce the bug against the reported version using a minimal `@Mapper` (ideally in `integrationtest/`). +2. Use `git bisect` across tagged releases to find the introducing commit. +3. Open a pinned GitHub issue titled `[REGRESSION ] ...`, labeled `regression` and `priority:critical`. +4. Post a user-facing workaround (e.g. pin `` to the prior release) in the issue within 24 hours. +5. Decide between a patch release or a revert-only fix. For SEV-1, target a patch release within 72 hours. +6. Add a regression test in `processor/src/test/` that locks the behavior. +7. Update `NEXT_RELEASE_CHANGELOG.md` under `### Bugs`, run the `release.yml` workflow dispatch, verify the artifact lands on Central before closing the issue. +8. Announce on Discussions; link from the pinned issue. + +### C. Release / supply-chain issue + +1. **Stop the bleeding** — if `release.yml` is still running, cancel the run from the Actions UI immediately. +2. Assess blast radius: did artifacts reach Maven Central staging only, Central itself, or also the mapstruct.org website? +3. If any secret is suspected compromised, rotate **before** taking any other recovery action: `GPG_PASSPHRASE`, `GPG_PUBLIC_KEY`, `GPG_PRIVATE_KEY`, `SONATYPE_USERNAME`, `SONATYPE_PASSWORD`, `SONATYPE_CENTRAL_USERNAME`, `SONATYPE_CENTRAL_TOKEN`, `GIT_WEBSITE_ACCESS_TOKEN`. Revoke the GPG key via a public keyserver. Rotate in this order: **Sonatype first** (stops further publishes), **GPG second** (revoke publicly), **website PAT last** — see the rotation order guidance in the **Secrets and rotation** section below. +4. If a bad artifact reached Maven Central: **you cannot delete it.** Publish a superseding version immediately (e.g. `1.6.4` to replace a bad `1.6.3`), and mark the bad version yanked via the **Yanked-release procedure** section below. +5. If `main` was corrupted (stray version bump, bad tag): revert commits, recreate the tag, coordinate any force-push with maintainers' explicit agreement. +6. If the website push succeeded but the release failed: manually revert the corresponding commit on the `mapstruct/mapstruct.org` repo. +7. Open a SEV-0 or SEV-1 tracking issue; freeze further releases until the root cause is known. +8. Replay the release from a clean state, verifying signatures at each step, then announce the yank on Discussions and mapstruct.org. + +### D. Infra / CI incident + +1. Identify scope: single workflow (e.g. `windows.yml`), all workflows, or a GitHub Actions-wide outage. +2. Check [githubstatus.com](https://www.githubstatus.com) — if the cause is upstream, park and monitor. +3. If it is our bug: open an issue labeled `ci`, and disable the affected workflow (comment the `on:` block or gate with `if: false`) to unblock contributors. +4. If contributor PRs are blocked for more than 4 hours, post a status update in Discussions. +5. Root-cause. Common suspects: action version drift, JDK EA breakage, Maven cache corruption, CodeQL rule updates. +6. Fix in a focused PR; require a green run on a throwaway branch before merging. +7. Re-enable the workflow. +8. Escalate to SEV-1 only if the incident is blocking a pending release. + +## Yanked-release procedure + +Maven Central is **immutable**: once an artifact is published, it cannot be deleted. When a release must be withdrawn, this is the procedure: + +1. Publish a superseding version (e.g. `1.6.4` to replace a bad `1.6.3`) as soon as a fix or clean rebuild is ready. +2. If the yank is security-related, open or update the corresponding GitHub Security Advisory (GHSA) for the bad version — the GHSA is the machine-readable record of the security flaw. For non-security yanks (e.g. a broken build regression), skip this step; the `readme.md` banner and blog post (steps 3–4) are the durable record. +3. Add a short banner to `readme.md` listing the yanked version, the reason, and the replacement — e.g. *"⚠ MapStruct 1.6.3 is yanked due to [reason]. Please upgrade to 1.6.4."* +4. Post a notice on the mapstruct.org blog and link it from Discussions. +5. Remove the banner from `readme.md` at the next normal release cycle (the GHSA, if opened, is the durable record for security yanks; the blog post is the durable record for non-security yanks). + +## Secrets and rotation + +The IRP itself does not store any secret. This section is the **index** of which secrets drive releases and CI, so that a rotation during an incident hits everything in one pass. The live rotation credentials and step-by-step procedures live outside the repo in the maintainers' password manager. + +Secrets used by `.github/workflows/release.yml`: + +| Secret | Purpose | Rotation path | +|---|---|---| +| `GPG_PASSPHRASE` | Passphrase for the release signing key | Change on a local keyring, update the repo secret, import new passphrase | +| `GPG_PUBLIC_KEY` | ASCII-armored public key for JReleaser | Re-export from local keyring after rotation | +| `GPG_PRIVATE_KEY` | ASCII-armored private key for JReleaser | Generate new key pair, re-export, revoke old key via keyserver | +| `SONATYPE_CENTRAL_USERNAME` | New Sonatype Central Portal username | Rotate token in Sonatype Central Portal | +| `SONATYPE_CENTRAL_TOKEN` | New Sonatype Central Portal token | Rotate token in Sonatype Central Portal | +| `SONATYPE_USERNAME` | Legacy OSSRH / Nexus2 username | Rotate in Sonatype JIRA / legacy portal | +| `SONATYPE_PASSWORD` | Legacy OSSRH / Nexus2 password | Rotate in Sonatype JIRA / legacy portal | +| `GIT_WEBSITE_ACCESS_TOKEN` | PAT for pushing to `mapstruct/mapstruct.org` | Regenerate PAT on the owning account, update repo secret | + +Secrets used by `.github/workflows/main.yml`: + +| Secret | Purpose | Rotation path | +|---|---|---| +| `SONATYPE_USERNAME` / `SONATYPE_PASSWORD` | Snapshot deploys to OSSRH from `main` | Same as above | +| `CODECOV_TOKEN` | Upload coverage to Codecov | Regenerate on codecov.io, update repo secret | + +Rotation order during a suspected compromise: **Sonatype first** (to stop any further publish), **GPG second** (revoke key publicly), **website PAT last** (lowest blast radius). + +## Communication channels map + +| Incident type | Private tracking | Public acknowledgement | Resolution announcement | +|---|---|---|---| +| Security / CVE | GitHub Private Advisory | Only after the fix is released | GHSA publish + Discussion post + mapstruct.org blog | +| Critical bug | None (public) | Pinned GitHub issue within 24 hours | Issue closed + Discussion post | +| Release / supply chain | Direct maintainer DM (Signal) | Pinned issue + mapstruct.org banner | Discussion post + mapstruct.org blog | +| Infra / CI | None | Discussion thread if contributor-blocking | Close the discussion | + +## Maintenance of this document + +This document is reviewed annually and after every SEV-0 or SEV-1 incident. Changes are made via normal PRs. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..b6703e1800 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,45 @@ +name: Bug report +description: Create a report and help us improve +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please fill in all required fields with as many details as possible. + - type: textarea + id: expected + attributes: + label: Expected behavior + description: | + Describe what you were expecting MapStruct to do + placeholder: | + Here you can also add the generated code that you would like MapStruct to generate + - type: textarea + id: actual + attributes: + label: Actual behavior + description: | + Describe what you observed MapStruct did instead + placeholder: | + Here you can also add the generated code that MapStruct generated + - type: textarea + id: steps + attributes: + label: Steps to reproduce the problem + description: | + - Share your mapping configuration + - An [MCVE (Minimal Complete Verifiable Example)](https://stackoverflow.com/help/minimal-reproducible-example) can be helpful to provide a complete reproduction case + placeholder: | + Share your MapStruct configuration + validations: + required: true + - type: input + id: mapstruct-version + attributes: + label: MapStruct Version + description: | + Which MapStruct version did you use? + Note: While you can obviously continue using older versions of MapStruct, it may well be that your bug is already fixed. If you're using an older version, please also try to reproduce the bug in the latest version of MapStruct before reporting it. + placeholder: ex. MapStruct 1.5.2 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..5b7ced86a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,16 @@ +contact_links: + - name: MapStruct Discussions + url: https://github.com/mapstruct/mapstruct/discussions + about: Please use the MapStruct GitHub Discussions for open ended discussions and to reach out to the community. + - name: Stack Overflow + url: https://stackoverflow.com/questions/tagged/mapstruct + about: For questions about how to use MapStruct, consider asking your question on Stack Overflow, tagged [mapstruct]. + - name: Documentation + url: https://mapstruct.org/documentation/stable/reference/html/ + about: The MapStruct reference documentation. + - name: Gitter Chat + url: https://gitter.im/mapstruct/mapstruct-users + about: For realtime communication with the MapStruct community, consider writing in our Gitter chat room. + - name: MapStruct Examples + url: https://github.com/mapstruct/mapstruct-examples + about: Some examples of what can be achieved with MapStruct. (contributions are always welcome) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..191bba8345 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Feature Request +description: Suggest an idea +body: + - type: markdown + attributes: + value: | + Please describe the use-case you have. This will better help us understand the context in which you're looking for a new feature. + - type: textarea + id: use-case + attributes: + label: Use case + description: | + Please describe the use-case you have. This will better help us understand the context in which you're looking for a new feature. + placeholder: Describe the use-case here + validations: + required: true + - type: textarea + id: solution + attributes: + label: Generated Code + description: | + Please describe the possible generated code you'd like to see in MapStruct generate. + Please note, it's not always easy to describe a good solution. Describing the use-case above is much more important to us. + placeholder: Describe the possible solution here. + validations: + required: false + - type: textarea + id: workarounds + attributes: + label: Possible workarounds + description: | + Please describe the possible workarounds you've implemented to work around the lacking functionality. + placeholder: Describe the possible workarounds here. + validations: + required: false + - type: input + id: mapstruct-version + attributes: + label: MapStruct Version + description: What MapStruct version and edition did you try? + placeholder: ex. MapStruct 1.5.2 + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..f6faee6938 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/scripts/update-website.sh b/.github/scripts/update-website.sh new file mode 100644 index 0000000000..7c92b8b43b --- /dev/null +++ b/.github/scripts/update-website.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# +# Copyright MapStruct Authors. +# +# Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 +# + +# env vars: +# VERSION +# GH_BOT_EMAIL + +# This script has been inspired by the JReleaser update-website.sh (https://github.com/jreleaser/jreleaser/blob/main/.github/scripts/update-website.sh) +set -e + +function computePlainVersion() { + echo $1 | sed 's/\([[:digit:]]*\)\.\([[:digit:]]*\)\.\([[:digit:]]*\).*/\1.\2.\3/' +} + +function computeMajorMinorVersion() { + echo $1 | sed 's/\([[:digit:]]*\)\.\([[:digit:]]*\).*/\1.\2/' +} + +function isStable() { + local PLAIN_VERSION=$(computePlainVersion $1) + if [ "${PLAIN_VERSION}" == "$1" ]; then + echo "yes" + else + echo "no" + fi +} + +STABLE=$(isStable $VERSION) +MAJOR_MINOR_VERSION=$(computeMajorMinorVersion $VERSION) + +DEV_VERSION=`grep devVersion config.toml | sed 's/.*"\(.*\)"/\1/'` +MAJOR_MINOR_DEV_VERSION=$(computeMajorMinorVersion $DEV_VERSION) +STABLE_VERSION=`grep stableVersion config.toml | sed 's/.*"\(.*\)"/\1/'` +MAJOR_MINOR_STABLE_VERSION=$(computeMajorMinorVersion $STABLE_VERSION) + +echo "📝 Updating versions" + +SEDOPTION="-i" +if [[ "$OSTYPE" == "darwin"* ]]; then + SEDOPTION="-i ''" +fi + +sed $SEDOPTION -e "s/^devVersion = \"\(.*\)\"/devVersion = \"${VERSION}\"/g" config.toml + +if [ "${STABLE}" == "yes" ]; then + sed $SEDOPTION -e "s/^stableVersion = \"\(.*\)\"/stableVersion = \"${VERSION}\"/g" config.toml + if [ "${MAJOR_MINOR_STABLE_VERSION}" != ${MAJOR_MINOR_VERSION} ]; then + echo "📝 Updating new stable version" + # This means that we have a new stable version and we need to change the order of the releases. + sed $SEDOPTION -e "s/^order = \(.*\)/order = 500/g" data/releases/${MAJOR_MINOR_VERSION}.toml + NEXT_STABLE_ORDER=$((`ls -1 data/releases | wc -l` - 2)) + sed $SEDOPTION -e "s/^order = \(.*\)/order = ${NEXT_STABLE_ORDER}/g" data/releases/${MAJOR_MINOR_STABLE_VERSION}.toml + git add data/releases/${MAJOR_MINOR_STABLE_VERSION}.toml + fi +elif [ "${MAJOR_MINOR_DEV_VERSION}" != "${MAJOR_MINOR_VERSION}" ]; then + echo "📝 Updating new dev version" + # This means that we are updating for a new dev version, but the last dev version is not the one that we are doing. + # Therefore, we need to update add the new data configuration + cp data/releases/${MAJOR_MINOR_DEV_VERSION}.toml data/releases/${MAJOR_MINOR_VERSION}.toml + sed $SEDOPTION -e "s/^order = \(.*\)/order = 1000/g" data/releases/${MAJOR_MINOR_VERSION}.toml +fi + +sed $SEDOPTION -e "s/^name = \"\(.*\)\"/name = \"${VERSION}\"/g" data/releases/${MAJOR_MINOR_VERSION}.toml +sed $SEDOPTION -e "s/^releaseDate = \(.*\)/releaseDate = $(date +%F)/g" data/releases/${MAJOR_MINOR_VERSION}.toml +git add data/releases/${MAJOR_MINOR_VERSION}.toml +git add config.toml + +echo "📝 Updating distribution resources" +tar -xf tmp/mapstruct-${VERSION}-dist.tar.gz --directory tmp +rm -rf static/documentation/${MAJOR_MINOR_VERSION} +cp -R tmp/mapstruct-${VERSION}/docs static/documentation/${MAJOR_MINOR_VERSION} +mv static/documentation/${MAJOR_MINOR_VERSION}/reference/html/mapstruct-reference-guide.html static/documentation/${MAJOR_MINOR_VERSION}/reference/html/index.html +git add static/documentation/${MAJOR_MINOR_VERSION} + +git config --global user.email "${GH_BOT_EMAIL}" +git config --global user.name "GitHub Action" +git commit -a -m "Releasing version ${VERSION}" +git push diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..0c6333a8ff --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,45 @@ +name: CodeQL + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '37 23 * * 6' + +permissions: {} + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: java-kotlin + build-mode: autobuild + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Initialize CodeQL + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Set up Java + if: matrix.language == 'java-kotlin' + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: 'zulu' + java-version: '21' + cache: 'maven' + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/java-ea.yml b/.github/workflows/java-ea.yml index 598a8979a6..04897b2156 100644 --- a/.github/workflows/java-ea.yml +++ b/.github/workflows/java-ea.yml @@ -2,23 +2,23 @@ name: Java EA on: [push] +permissions: + contents: read + env: MAVEN_ARGS: -V -B --no-transfer-progress -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 jobs: test_jdk_ea: - strategy: - fail-fast: false - matrix: - java: [17-ea] - name: 'Linux JDK ${{ matrix.java }}' + name: 'Linux JDK EA' runs-on: ubuntu-latest steps: - name: 'Checkout' - uses: actions/checkout@v2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 'Set up JDK' - uses: actions/setup-java@v1 + uses: oracle-actions/setup-java@fff43251af9936a0e6a4d5d0946e14f1680e9b6b # v1.5.0 with: - java-version: ${{ matrix.java }} + website: jdk.java.net + release: EA - name: 'Test' - run: ./mvnw ${MAVEN_ARGS} install -DskipDistribution=true + run: ./mvnw ${MAVEN_ARGS} -Djacoco.skip=true install -DskipDistribution=true diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000000..865820f6ab --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,23 @@ +name: Mac OS CI + +on: push + +permissions: + contents: read + +env: + MAVEN_ARGS: -V -B --no-transfer-progress -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 + +jobs: + mac: + name: 'Mac OS' + runs-on: macos-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: 'Set up JDK 21' + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: 'zulu' + java-version: 21 + - name: 'Test' + run: ./mvnw ${MAVEN_ARGS} install diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 741f8bce61..d1ebef4642 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,9 @@ name: CI on: [push, pull_request] +permissions: + contents: read + env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} @@ -12,56 +15,52 @@ jobs: strategy: fail-fast: false matrix: - java: [11, 13, 16] + java: [21, 25, 26] name: 'Linux JDK ${{ matrix.java }}' runs-on: ubuntu-latest steps: - name: 'Checkout' - uses: actions/checkout@v2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 'Set up JDK' - uses: actions/setup-java@v1 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: + distribution: 'zulu' java-version: ${{ matrix.java }} - name: 'Test' - run: ./mvnw ${MAVEN_ARGS} install -DskipDistribution=true - linux: - name: 'Linux JDK 8' - runs-on: ubuntu-latest - steps: - - name: 'Checkout' - uses: actions/checkout@v2 - - name: 'Set up JDK 8' - uses: actions/setup-java@v1 - with: - java-version: 8 - - name: 'Test' - run: ./mvnw ${MAVEN_ARGS} install + run: ./mvnw ${MAVEN_ARGS} -Djacoco.skip=${{ matrix.java != 21 }} install -DskipDistribution=${{ matrix.java != 21 }} - name: 'Generate coverage report' + if: matrix.java == 21 run: ./mvnw jacoco:report - name: 'Upload coverage to Codecov' - uses: codecov/codecov-action@v1 + if: matrix.java == 21 && github.repository == 'mapstruct/mapstruct' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} - name: 'Publish Snapshots' - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'mapstruct/mapstruct' + if: matrix.java == 21 && github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'mapstruct/mapstruct' run: ./mvnw -s etc/ci-settings.xml -DskipTests=true -DskipDistribution=true deploy - windows: - name: 'Windows' - runs-on: windows-latest + integration_test_jdk: + strategy: + fail-fast: false + matrix: + java: [ 8, 17 ] + name: 'Linux JDK ${{ matrix.java }}' + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: 'Set up JDK 8' - uses: actions/setup-java@v1 + - name: 'Checkout' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: 'Set up JDK 21 for building everything' + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 8 - - name: 'Test' - run: ./mvnw ${MAVEN_ARGS} install - mac: - name: 'Mac OS' - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - name: 'Set up JDK 8' - uses: actions/setup-java@v1 + distribution: 'zulu' + java-version: 21 + - name: 'Install Processor' + run: ./mvnw ${MAVEN_ARGS} -DskipTests install -pl processor -am + - name: 'Set up JDK ${{ matrix.java }} for running integration tests' + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 8 - - name: 'Test' - run: ./mvnw ${MAVEN_ARGS} install + distribution: 'zulu' + java-version: ${{ matrix.java }} + - name: 'Run integration tests' + run: ./mvnw ${MAVEN_ARGS} verify -pl integrationtest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..58440dcebd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,124 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version' + required: true + next: + description: 'Next version' + required: false + +jobs: + release: + # This job has been inspired by the moditect release (https://github.com/moditect/moditect/blob/main/.github/workflows/release.yml) + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Setup Java + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + java-version: 21 + distribution: 'zulu' + cache: maven + + - name: Set release version + id: version + run: | + RELEASE_VERSION=${{ github.event.inputs.version }} + NEXT_VERSION=${{ github.event.inputs.next }} + PLAIN_VERSION=`echo ${RELEASE_VERSION} | awk 'match($0, /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)/) { print substr($0, RSTART, RLENGTH); }'` + COMPUTED_NEXT_VERSION="${PLAIN_VERSION}-SNAPSHOT" + if [ -z $NEXT_VERSION ] + then + NEXT_VERSION=$COMPUTED_NEXT_VERSION + fi + ./mvnw -ntp -B versions:set versions:commit -DnewVersion=$RELEASE_VERSION -pl :mapstruct-parent -DgenerateBackupPoms=false + git config --global user.email "${{ vars.GH_BOT_EMAIL }}" + git config --global user.name "GitHub Action" + git commit -a -m "Releasing version $RELEASE_VERSION" + git push + echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV + echo "NEXT_VERSION=$NEXT_VERSION" >> $GITHUB_ENV + echo "PLAIN_VERSION=$PLAIN_VERSION" >> $GITHUB_ENV + + - name: Stage + run: | + export GPG_TTY=$(tty) + ./mvnw -ntp -B --file pom.xml \ + -Dmaven.site.skip=true -Drelease=true -Ppublication,stage + + - name: Release + env: + JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} + JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} + JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN: ${{ secrets.SONATYPE_CENTRAL_TOKEN }} + JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: | + ./mvnw -ntp -B --file pom.xml -pl :mapstruct-parent -Pjreleaser jreleaser:release + + - name: JReleaser output + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: jreleaser-release + path: | + parent/target/jreleaser/trace.log + parent/target/jreleaser/output.properties + + - name: Reset NEXT_RELEASE_CHANGELOG.md + run: | + echo -e "### Features\n\n### Enhancements\n\n### Bugs\n\n### Documentation\n\n### Build\n" > NEXT_RELEASE_CHANGELOG.md + + - name: Set next version + run: | + ./mvnw -ntp -B versions:set versions:commit -DnewVersion=${{ env.NEXT_VERSION }} -pl :mapstruct-parent -DgenerateBackupPoms=false + sed -i -e "s@project.build.outputTimestamp>.*\${git.commit.author.time} org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/build-config/src/main/resources/build-config/checkstyle.xml b/build-config/src/main/resources/build-config/checkstyle.xml index a4591c39de..b15e108c5c 100644 --- a/build-config/src/main/resources/build-config/checkstyle.xml +++ b/build-config/src/main/resources/build-config/checkstyle.xml @@ -29,7 +29,9 @@ - + + + @@ -118,6 +120,14 @@ + + + + + + + @@ -139,12 +149,14 @@ - + - + + + diff --git a/copyright.txt b/copyright.txt index 01b6526c70..74f18d89b1 100644 --- a/copyright.txt +++ b/copyright.txt @@ -1,18 +1,24 @@ Contributors ============ +Aleksey Ivashin - https://github.com/xumk Alexandr Shalugin - https://github.com/shalugin Amine Touzani - https://github.com/ttzn Andreas Gudian - https://github.com/agudian Andrei Arlou - https://github.com/Captain1653 Andres Jose Sebastian Rincon Gonzalez - https://github.com/stianrincon Arne Seime - https://github.com/seime +Burak Yildirim - https://github.com/bydrim +Cause Chung - https://github.com/cuzfrog Christian Bandowski - https://github.com/chris922 +Chris DeLashmutt - https://github.com/cdelashmutt-pivotal +Christian Kosmowski - https://github.com/ckosmowski Christian Schuster - https://github.com/chschu Christophe Labouisse - https://github.com/ggtools Ciaran Liedeman - https://github.com/cliedeman Cindy Wang - https://github.com/birdfriend Cornelius Dirmeier - https://github.com/cornzy +Dennis Melzer - https://github.com/ David Feinblum - https://github.com/dvfeinblum Darren Rambaud - https://github.com/xyzst Dekel Pilli - https://github.com/dekelpilli @@ -26,40 +32,64 @@ Florian Tavares - https://github.com/neoXfire Gervais Blaise - https://github.com/gervaisb Gibou Damien - https://github.com/dmngb Gunnar Morling - https://github.com/gunnarmorling +hduelme - https://github.com/hduelme Ivo Smid - https://github.com/bedla Jason Bodnar - https://github.com/Blackbaud-JasonBodnar Jeroen van Wilgenburg - https://github.com/jvwilge Jeff Smyth - https://github.com/smythie86 +jmwbRyDWLeNsvtzrihGoY - https://github.com/jmwbRyDWLeNsvtzrihGoY João Paulo Bassinello - https://github.com/jpbassinello Jonathan Kraska - https://github.com/jakraska Joshua Spoerri - https://github.com/spoerri Jude Niroshan - https://github.com/JudeNiroshan +Justyna Kubica-Ledzion - https://github.com/JKLedzion Kemal Özcan - https://github.com/yekeoe +Ken Wang - https://github.com/ro0sterjam Kevin Grüneberg - https://github.com/kevcodez Lukas Lazar - https://github.com/LukeLaz Nikolas Charalambidis - https://github.com/Nikolas-Charalambidis +Maciej Kucharczyk - https://github.com/mk868 Michael Pardo - https://github.com/pardom +Muhammad Usama - https://github.com/the-mgi Mustafa Caylak - https://github.com/luxmeter +Neale Upstone - https://github.com/nealeu Oliver Ehrenmüller - https://github.com/greuelpirat +Oliver Erhart - https://github.com/thunderhook Paul Strugnell - https://github.com/ps-powa Pascal Grün - https://github.com/pascalgn Pavel Makhov - https://github.com/streetturtle Peter Larson - https://github.com/pjlarson +Raimund Klein - https://github.com/Chessray Remko Plantenga - https://github.com/sonata82 Remo Meier - https://github.com/remmeier +Roel Mangelschots - https://github.com/rmschots +Ritesh Chopade - https://github.com/codeswithritesh Richard Lea - https://github.com/chigix +Roman Obolonskyi - https://github.com/Obolrom +Samil Can - https://github.com/SamilCan Saheb Preet Singh - https://github.com/sahebpreet Samuel Wright - https://github.com/samwright Sebastian Haberey - https://github.com/sebastianhaberey Sebastian Hasait - https://github.com/shasait Sean Huang - https://github.com/seanjob +seonwoojung - https://github.com/seonwooj0810 Sjaak Derksen - https://github.com/sjaakd Stefan May - https://github.com/osthus-sm +Takch02 - https://github.com/Takch02 Taras Mychaskiw - https://github.com/twentylemon Thibault Duperron - https://github.com/Zomzog Tomáš Poledný - https://github.com/Saljack Tobias Meggendorfer - https://github.com/incaseoftrouble +Tran Ngoc Nhan - https://github.com/ Tillmann Gaida - https://github.com/Tillerino Timo Eckhardt - https://github.com/timoe Tomek Gubala - https://github.com/vgtworld +Valentin Kulesh - https://github.com/unshare Vincent Alexander Beelte - https://github.com/grandmasterpixel +Winter Andreas - https://github.dev/wandi34 +Xiu Hong Kooi - https://github.com/kooixh +Yang Tang - https://github.com/tangyang9464 +Yevhen Vasyliev - https://github.com/yvasyliev +Zegveld - https://github.com/Zegveld +znight1020 - https://github.com/znight1020 +zral - https://github.com/zyberzebra diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index 293043e6f9..c73676192b 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index 10c95cf3b5..e62d5e4682 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core/src/main/java/org/mapstruct/AnnotateWith.java b/core/src/main/java/org/mapstruct/AnnotateWith.java new file mode 100644 index 0000000000..79b7cec98c --- /dev/null +++ b/core/src/main/java/org/mapstruct/AnnotateWith.java @@ -0,0 +1,183 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * This can be used to have mapstruct generate additional annotations on classes/methods. + *

+ * Examples based on the spring framework annotations. + *

+ * Marking a class as `Lazy`: + * + *

+ * @AnnotateWith( value = Lazy.class )
+ * @Mapper
+ * public interface FooMapper {
+ *     // mapper code
+ * }
+ * 
+ * + * The following code would be generated: + * + *

+ * @Lazy
+ * public class FooMapperImpl implements FooMapper {
+ *     // mapper code
+ * }
+ * 
+ * Setting the profile on the generated implementation: + * + *

+ * @AnnotateWith( value = Profile.class, elements = @AnnotateWith.Element( strings = "prod" ) )
+ * @Mapper
+ * public interface FooMapper {
+ *     // mapper code
+ * }
+ * 
+ * + * The following code would be generated: + * + *

+ * @Profile( value = "prod" )
+ * public class FooMapperImpl implements FooMapper {
+ *     // mapper code
+ * }
+ * 
+ * + * @author Ben Zegveld + * @since 1.6 + */ +@Repeatable( AnnotateWiths.class ) +@Retention( CLASS ) +@Target( { TYPE, METHOD, ANNOTATION_TYPE } ) +public @interface AnnotateWith { + + /** + * The annotation class that needs to be added. + * + * @return the annotation class that needs to be added. + */ + Class value(); + + /** + * The annotation elements that are to be applied to the annotation that should be added. + * + * @return the annotation elements that are to be applied to this annotation. + */ + Element[] elements() default {}; + + /** + * Used in combination with {@link AnnotateWith} to configure the annotation elements. Only 1 value type may be used + * within the same annotation at a time. For example mixing shorts and ints is not allowed. + * + * @author Ben Zegveld + * @since 1.6 + */ + @interface Element { + /** + * The name of the annotation element. + * + * @return name of the annotation element. + */ + String name() default "value"; + + /** + * cannot be used in conjunction with other value fields. + * + * @return short value(s) for the annotation element. + */ + short[] shorts() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return byte value(s) for the annotation element. + */ + byte[] bytes() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return int value(s) for the annotation element. + */ + int[] ints() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return long value(s) for the annotation element. + */ + long[] longs() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return float value(s) for the annotation element. + */ + float[] floats() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return double value(s) for the annotation element. + */ + double[] doubles() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return char value(s) for the annotation element. + */ + char[] chars() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return boolean value(s) for the annotation element. + */ + boolean[] booleans() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return string value(s) for the annotation element. + */ + String[] strings() default {}; + + /** + * cannot be used in conjunction with other value fields. + * + * @return class value(s) for the annotation element. + */ + Class[] classes() default {}; + + /** + * only used in conjunction with the {@link #enums()} annotation element. + * + * @return the class of the enum. + */ + Class> enumClass() default NullEnum.class; + + /** + * cannot be used in conjunction with other value fields. {@link #enumClass()} is also required when using + * {@link #enums()} + * + * @return enum value(s) for the annotation element. + */ + String[] enums() default {}; + + } +} diff --git a/core/src/main/java/org/mapstruct/AnnotateWiths.java b/core/src/main/java/org/mapstruct/AnnotateWiths.java new file mode 100644 index 0000000000..5e86ad9a19 --- /dev/null +++ b/core/src/main/java/org/mapstruct/AnnotateWiths.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * This can be used to have mapstruct generate additional annotations on classes/methods. + * + * @author Ben Zegveld + * @since 1.6 + */ +@Retention( CLASS ) +@Target( { TYPE, METHOD } ) +public @interface AnnotateWiths { + + /** + * The configuration of the additional annotations. + * + * @return The configuration of the additional annotations. + */ + AnnotateWith[] value(); +} diff --git a/core/src/main/java/org/mapstruct/BeanMapping.java b/core/src/main/java/org/mapstruct/BeanMapping.java index f8c749fbde..309458f861 100644 --- a/core/src/main/java/org/mapstruct/BeanMapping.java +++ b/core/src/main/java/org/mapstruct/BeanMapping.java @@ -14,10 +14,13 @@ import org.mapstruct.control.MappingControl; import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; +import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR; /** * Configures the mapping between two bean types. *

+ * Unless otherwise specified these properties are inherited to the generated bean mapping methods. + *

* Either {@link #resultType()}, {@link #qualifiedBy()} or {@link #nullValueMappingStrategy()} must be specified. *

*

Example: Determining the result type

@@ -57,6 +60,8 @@ /** * Specifies the result type of the factory method to be used in case several factory methods qualify. + *

+ * NOTE: This property is not inherited to generated mapping methods * * @return the resultType to select */ @@ -116,9 +121,32 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() */ NullValueCheckStrategy nullValueCheckStrategy() default ON_IMPLICIT_CONVERSION; + /** + * Determines how to handle missing implementation for super classes when using the {@link SubclassMapping}. + * + * Overrides the setting on {@link MapperConfig} and {@link Mapper}. + * + * @return strategy to handle missing implementation combined with {@link SubclassMappings}. + * + * @since 1.5 + */ + SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; + + /** + * Specifies the exception type to be thrown when a missing subclass implementation is detected + * in combination with {@link SubclassMappings}, based on the {@link #subclassExhaustiveStrategy()}. + *

+ * This exception will only be thrown when the {@code subclassExhaustiveStrategy} is set to + * {@link SubclassExhaustiveStrategy#RUNTIME_EXCEPTION}. + * + * @return the exception class to throw when missing implementations are found. + * Defaults to {@link IllegalArgumentException}. + */ + Class subclassExhaustiveException() default IllegalArgumentException.class; + /** * Default ignore all mappings. All mappings have to be defined manually. No automatic mapping will take place. No - * warning will be issued on missing target properties. + * warning will be issued on missing source or target properties. * * @return The ignore strategy (default false). * @@ -133,6 +161,8 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() * source properties report. *

* NOTE: This does not support ignoring nested source properties + *

+ * NOTE: This property is not inherited to generated mapping methods * * @return The source properties that should be ignored when performing a report * @@ -140,6 +170,17 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() */ String[] ignoreUnmappedSourceProperties() default {}; + /** + * How unmapped properties of the source type of a mapping should be reported. + * If no policy is configured, the policy given via {@link MapperConfig#unmappedSourcePolicy()} or + * {@link Mapper#unmappedSourcePolicy()} will be applied, using {@link ReportingPolicy#IGNORE} by default. + * + * @return The reporting policy for unmapped source properties. + * + * @since 1.6 + */ + ReportingPolicy unmappedSourcePolicy() default ReportingPolicy.IGNORE; + /** * How unmapped properties of the target type of a mapping should be reported. * If no policy is configured, the policy given via {@link MapperConfig#unmappedTargetPolicy()} or diff --git a/core/src/main/java/org/mapstruct/ClassAccessibility.java b/core/src/main/java/org/mapstruct/ClassAccessibility.java new file mode 100644 index 0000000000..c533b1ea32 --- /dev/null +++ b/core/src/main/java/org/mapstruct/ClassAccessibility.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +/** + * Determines whether a generated Mapper implementation class will be declared {@code public} or not. + * + * @author Raimund Klein + * + * @since 1.7.0 + */ +public enum ClassAccessibility { + /** + * The generated Mapper will have the same modifier ({@code public} or none) as the annotated class or interface. + */ + DEFAULT, + /** + * The generated Mapper will be declared {@code public}. + */ + PUBLIC, + /** + * The generated Mapper will have no visibility modifier ("package-private"). + */ + PACKAGE_PRIVATE +} diff --git a/core/src/main/java/org/mapstruct/CollectionMappingStrategy.java b/core/src/main/java/org/mapstruct/CollectionMappingStrategy.java index afaba97370..0ea3ee7df5 100644 --- a/core/src/main/java/org/mapstruct/CollectionMappingStrategy.java +++ b/core/src/main/java/org/mapstruct/CollectionMappingStrategy.java @@ -7,6 +7,54 @@ /** * Strategy for propagating the value of collection-typed properties from source to target. + *

+ * In the table below, the dash {@code -} indicates a property name. + * Next, the trailing {@code s} indicates the plural form. + * The table explains the options and how they are applied to the presence / absence of a + * {@code set-s}, {@code add-} and / or {@code get-s} method on the target object. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Collection mapping strategy options
OptionOnly target set-s AvailableOnly target add- AvailableBoth set-s/add- AvailableNo set-s/add- AvailableExisting Target ({@code @TargetType})
{@link #ACCESSOR_ONLY}set-sget-sset-sget-sget-s
{@link #SETTER_PREFERRED}set-sadd-set-sget-sget-s
{@link #ADDER_PREFERRED}set-sadd-add-get-sget-s
{@link #TARGET_IMMUTABLE}set-sexceptionset-sexceptionset-s
* * @author Sjaak Derksen */ diff --git a/core/src/main/java/org/mapstruct/Condition.java b/core/src/main/java/org/mapstruct/Condition.java index ec1bf06a88..8abe2f817c 100644 --- a/core/src/main/java/org/mapstruct/Condition.java +++ b/core/src/main/java/org/mapstruct/Condition.java @@ -11,24 +11,35 @@ import java.lang.annotation.Target; /** - * This annotation marks a method as a presence check method to check check for presence in beans. + * This annotation marks a method as a presence check method to check for presence in beans + * or it can be used to define additional check methods for something like source parameters. *

- * By default bean properties are checked against {@code null} or using a presence check method in the source bean. + * By default, bean properties are checked against {@code null} or using a presence check method in the source bean. * If a presence check method is available then it will be used instead. *

* Presence check methods have to return {@code boolean}. * The following parameters are accepted for the presence check methods: *

    *
  • The parameter with the value of the source property. - * e.g. the value given by calling {@code getName()} for the name property of the source bean
  • + * e.g. the value given by calling {@code getName()} for the name property of the source bean + * - only possible when using the {@link ConditionStrategy#PROPERTIES} + * *
  • The mapping source parameter
  • *
  • {@code @}{@link Context} parameter
  • + *
  • + * {@code @}{@link TargetPropertyName} parameter - + * only possible when using the {@link ConditionStrategy#PROPERTIES} + *
  • + *
  • + * {@code @}{@link SourcePropertyName} parameter - + * only possible when using the {@link ConditionStrategy#PROPERTIES} + *
  • *
* * Note: The usage of this annotation is mandatory * for a method to be considered as a presence check method. * - *

+ * 

  * public class PresenceCheckUtils {
  *
  *   @Condition
@@ -43,11 +54,10 @@
  *     MovieDto map(Movie movie);
  * }
  * 
- * + *

* The following implementation of {@code MovieMapper} will be generated: * - *

- * 
+ * 

  * public class MovieMapperImpl implements MovieMapper {
  *
  *     @Override
@@ -65,14 +75,25 @@
  *         return movieDto;
  *     }
  * }
- * 
- * 
+ *
+ *

+ * This annotation can also be used as a meta-annotation to define the condition strategy. * * @author Filip Hrisafov + * @see SourceParameterCondition * @since 1.5 */ -@Target({ ElementType.METHOD }) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.CLASS) public @interface Condition { + /** + * The condition strategy for the condition. + * This determines whether the condition is applied to properties, parameters, or both. + * + * @return the places where the condition should apply to + * @since 1.6 + */ + ConditionStrategy[] appliesTo() default ConditionStrategy.PROPERTIES; + } diff --git a/core/src/main/java/org/mapstruct/ConditionStrategy.java b/core/src/main/java/org/mapstruct/ConditionStrategy.java new file mode 100644 index 0000000000..6b042017c2 --- /dev/null +++ b/core/src/main/java/org/mapstruct/ConditionStrategy.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +/** + * Strategy for defining what to what a condition (check) method is applied to + * + * @author Filip Hrisafov + * @since 1.6 + */ +public enum ConditionStrategy { + /** + * The condition method should be applied whether a property should be mapped. + */ + PROPERTIES, + /** + * The condition method should be applied to check if a source parameters should be mapped. + */ + SOURCE_PARAMETERS, +} diff --git a/core/src/main/java/org/mapstruct/DecoratedWith.java b/core/src/main/java/org/mapstruct/DecoratedWith.java index 78b2c580ff..3db27b2a0a 100644 --- a/core/src/main/java/org/mapstruct/DecoratedWith.java +++ b/core/src/main/java/org/mapstruct/DecoratedWith.java @@ -103,12 +103,12 @@ * private PersonMapper personMapper; // injects the decorator, with the injected original mapper *

* - *

3. Component model 'jsr330'

+ *

3. Component model 'jsr330' or 'jakarta'

*

Referencing the original mapper

*

- * JSR 330 doesn't specify qualifiers and only allows to specifically name the beans. Hence, the generated - * implementation of the original mapper is annotated with - * {@code @javax.inject.Named("fully-qualified-name-of-generated-impl")} and {@code @Singleton} (please note that when + * JSR 330 / Jakarta Inject doesn't specify qualifiers and only allows to specifically name the beans. Hence, + * the generated implementation of the original mapper is annotated with + * {@code @Named("fully-qualified-name-of-generated-impl")} and {@code @Singleton} (please note that when * using a decorator, the class name of the mapper implementation ends with an underscore). To inject that bean in your * decorator, add the same annotation to the delegate field (e.g. by copy/pasting it from the generated class): * @@ -140,12 +140,11 @@ * @javax.inject.Named * private PersonMapper personMapper; // injects the decorator, with the injected original mapper * - *

* * @author Gunnar Morling */ @Target(ElementType.TYPE) -@Retention(RetentionPolicy.SOURCE) +@Retention(RetentionPolicy.CLASS) public @interface DecoratedWith { /** diff --git a/core/src/main/java/org/mapstruct/EnumMapping.java b/core/src/main/java/org/mapstruct/EnumMapping.java index ba3a5e0cb0..375f969b01 100644 --- a/core/src/main/java/org/mapstruct/EnumMapping.java +++ b/core/src/main/java/org/mapstruct/EnumMapping.java @@ -98,7 +98,7 @@ *

    *
  • {@link MappingConstants#SUFFIX_TRANSFORMATION} - applies the given {@link #configuration()} as a * suffix to the source enum
  • - *
  • {@link MappingConstants#STRIP_SUFFIX_TRANSFORMATION} - strips the the given {@link #configuration()} + *
  • {@link MappingConstants#STRIP_SUFFIX_TRANSFORMATION} - strips the given {@link #configuration()} * from the end of the source enum
  • *
  • {@link MappingConstants#PREFIX_TRANSFORMATION} - applies the given {@link #configuration()} as a * prefix to the source enum
  • diff --git a/core/src/main/java/org/mapstruct/Ignored.java b/core/src/main/java/org/mapstruct/Ignored.java new file mode 100644 index 0000000000..47e4961f43 --- /dev/null +++ b/core/src/main/java/org/mapstruct/Ignored.java @@ -0,0 +1,67 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configures the ignored of one bean attribute. + * + *

    + * The name all attributes of for ignored is to be specified via {@link #targets()}. + *

    + * + *

    + * Example 1: Implicitly mapping fields with the same name: + *

    + * + *
    
    + * // We need ignored Human.name and Human.lastName
    + * // we can use @Ignored with parameters "name", "lastName" {@link #targets()}
    + * @Mapper
    + * public interface HumanMapper {
    + *    @Ignored( targets = { "name", "lastName" } )
    + *    HumanDto toHumanDto(Human human)
    + * }
    + * 
    + *
    
    + * // generates:
    + * @Override
    + * public HumanDto toHumanDto(Human human) {
    + *    humanDto.setFullName( human.getFullName() );
    + *    // ...
    + * }
    + * 
    + * + * @author Ivashin Aleksey + */ +@Repeatable(IgnoredList.class) +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface Ignored { + + /** + * Whether the specified properties should be ignored by the generated mapping method. + * This can be useful when certain attributes should not be propagated from source to target or when properties in + * the target object are populated using a decorator and thus would be reported as unmapped target property by + * default. + * + * @return The target names of the configured properties that should be ignored + */ + String[] targets(); + + /** + * The prefix that should be applied to all the properties specified via {@link #targets()}. + * + * @return The target prefix to be applied to the defined properties + */ + String prefix() default ""; + +} diff --git a/core/src/main/java/org/mapstruct/IgnoredList.java b/core/src/main/java/org/mapstruct/IgnoredList.java new file mode 100644 index 0000000000..e47db6bac7 --- /dev/null +++ b/core/src/main/java/org/mapstruct/IgnoredList.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configures the ignored list for several bean attributes. + *

    + * TIP: When using Java 8 or later, you can omit the {@code @IgnoredList} + * wrapper annotation and directly specify several {@code @Ignored} annotations on one method. + * + *

    These two examples are equal. + *

    + *
    
    + * // before Java 8
    + * @Mapper
    + * public interface MyMapper {
    + *     @IgnoredList({
    + *         @Ignored(targets = { "firstProperty" } ),
    + *         @Ignored(targets = { "secondProperty" } )
    + *     })
    + *     HumanDto toHumanDto(Human human);
    + * }
    + * 
    + *
    
    + * // Java 8 and later
    + * @Mapper
    + * public interface MyMapper {
    + *     @Ignored(targets = { "firstProperty" } ),
    + *     @Ignored(targets = { "secondProperty" } )
    + *     HumanDto toHumanDto(Human human);
    + * }
    + * 
    + * + * @author Ivashin Aleksey + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface IgnoredList { + + /** + * The configuration of the bean attributes. + * + * @return The configuration of the bean attributes. + */ + Ignored[] value(); +} diff --git a/core/src/main/java/org/mapstruct/InheritInverseConfiguration.java b/core/src/main/java/org/mapstruct/InheritInverseConfiguration.java index ac582a5dfa..b659b7f37a 100644 --- a/core/src/main/java/org/mapstruct/InheritInverseConfiguration.java +++ b/core/src/main/java/org/mapstruct/InheritInverseConfiguration.java @@ -81,8 +81,8 @@ public @interface InheritInverseConfiguration { /** - * The name of the inverse mapping method to inherit the mappings from. Needs only to be specified in case more than - * one inverse method with matching source and target type exists. + * The name of the inverse mapping method to inherit the mappings from. Needs to be specified only in case more than + * one inverse method exists with a matching source and target type exists. * * @return The name of the inverse mapping method to inherit the mappings from. */ diff --git a/core/src/main/java/org/mapstruct/InjectionStrategy.java b/core/src/main/java/org/mapstruct/InjectionStrategy.java index 84baa8afa9..f5029e2246 100644 --- a/core/src/main/java/org/mapstruct/InjectionStrategy.java +++ b/core/src/main/java/org/mapstruct/InjectionStrategy.java @@ -7,9 +7,10 @@ /** * Strategy for handling injection. This is only used on annotated based component models such as CDI, Spring and - * JSR330. + * JSR330 / Jakarta. * * @author Kevin Grüneberg + * @author Lucas Resch */ public enum InjectionStrategy { @@ -17,5 +18,8 @@ public enum InjectionStrategy { FIELD, /** Annotations are written on the constructor **/ - CONSTRUCTOR + CONSTRUCTOR, + + /** A dedicated setter method is created */ + SETTER } diff --git a/core/src/main/java/org/mapstruct/IterableMapping.java b/core/src/main/java/org/mapstruct/IterableMapping.java index 76153127bd..d644dfe03b 100644 --- a/core/src/main/java/org/mapstruct/IterableMapping.java +++ b/core/src/main/java/org/mapstruct/IterableMapping.java @@ -66,19 +66,38 @@ /** * A format string as processable by {@link SimpleDateFormat} if the annotated method maps from an iterable of * {@code String} to an iterable {@link Date} or vice-versa. Will be ignored for all other element types. + *

    + * If the {@link #locale()} is also specified, the format will consider the specified locale when processing + * the date. Otherwise, the system's default locale will be used. * * @return A date format string as processable by {@link SimpleDateFormat}. + * @see #locale() */ String dateFormat() default ""; /** * A format string as processable by {@link DecimalFormat} if the annotated method maps from a * {@link Number} to a {@link String} or vice-versa. Will be ignored for all other element types. + *

    + * If the {@link #locale()} is also specified, the number format will be applied in the context of the given locale. + * Otherwise, the system's default locale will be used to process the number format. * * @return A decimal format string as processable by {@link DecimalFormat}. + * @see #locale() */ String numberFormat() default ""; + /** + * Specifies the locale to be used when processing {@link #dateFormat()} or {@link #numberFormat()}. + *

    + * The locale should be a plain tag representing the language, such as "en" for English, "de" for German, etc. + *

    + * If no locale is specified, the system's default locale will be used. + * + * @return A string representing the locale to be used when formatting dates or numbers. + */ + String locale() default ""; + /** * A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple * mappers (hand written of internal) qualify and result in an 'Ambiguous mapping methods found' error. diff --git a/core/src/main/java/org/mapstruct/Javadoc.java b/core/src/main/java/org/mapstruct/Javadoc.java new file mode 100644 index 0000000000..4b5d2fb839 --- /dev/null +++ b/core/src/main/java/org/mapstruct/Javadoc.java @@ -0,0 +1,115 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Allows the definition of Javadoc comments in the MapStruct Mapper generated class. + * + *

    The annotation provides support for the usual Javadoc comments elements by defining analogous attributes.

    + * + * + *

    Please, note that at least one of these attributes must be specified.

    + * + *

    + * For instance, the following definition; + *

    + *
    
    + * @Javadoc(
    + *     value = "This is the description",
    + *     authors = { "author1", "author2" },
    + *     deprecated = "Use {@link OtherMapper} instead",
    + *     since = "0.1"
    + * )
    + * 
    + * + *

    + * will generate: + *

    + * + *
    
    + * /**
    + * * This is the description
    + * *
    + * * @author author1
    + * * @author author2
    + * *
    + * * @deprecated Use {@link OtherMapper} instead
    + * * @since 0.1
    + * */
    + * 
    + * + *

    + * The entire Javadoc comment block can be passed directly: + *

    + *
    
    + * @Javadoc("This is the description\n"
    + *            + "\n"
    + *            + "@author author1\n"
    + *            + "@author author2\n"
    + *            + "\n"
    + *            + "@deprecated Use {@link OtherMapper} instead\n"
    + *            + "@since 0.1\n"
    + * )
    + * 
    + * + *
    
    + * // or using Text Blocks
    + * @Javadoc(
    + *     """
    + *     This is the description
    + *
    + *     @author author1
    + *     @author author2
    + *
    + *     @deprecated Use {@link OtherMapper} instead
    + *     @since 0.1
    + *     """
    + * )
    + * 
    + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface Javadoc { + /** + * Main Javadoc comment text block. + * + * @return Main Javadoc comment text block. + */ + String value() default ""; + + /** + * List of authors of the code that it is being documented. + *

    + * It will generate a list of the Javadoc tool comment element @author + * with the different values and in the order provided. + * + * @return array of javadoc authors. + */ + String[] authors() default { }; + + /** + * Specifies that the functionality that is being documented is deprecated. + *

    + * Corresponds to the @deprecated Javadoc tool comment element. + * + * @return Deprecation message about the documented functionality + */ + String deprecated() default ""; + + /** + * Specifies the version since the functionality that is being documented is available. + *

    + * Corresponds to the @since Javadoc tool comment element. + * + * @return Version since the functionality is available + */ + String since() default ""; +} diff --git a/core/src/main/java/org/mapstruct/MapMapping.java b/core/src/main/java/org/mapstruct/MapMapping.java index 271272bb45..093099cf5a 100644 --- a/core/src/main/java/org/mapstruct/MapMapping.java +++ b/core/src/main/java/org/mapstruct/MapMapping.java @@ -56,8 +56,12 @@ /** * A format string as processable by {@link SimpleDateFormat} if the annotated method maps from a map with key type * {@code String} to an map with key type {@link Date} or vice-versa. Will be ignored for all other key types. + *

    + * If the {@link #locale()} is specified, the format will consider the specified locale when processing the date. + * Otherwise, the system's default locale will be used. * * @return A date format string as processable by {@link SimpleDateFormat}. + * @see #locale() */ String keyDateFormat() default ""; @@ -65,27 +69,50 @@ * A format string as processable by {@link SimpleDateFormat} if the annotated method maps from a map with value * type {@code String} to an map with value type {@link Date} or vice-versa. Will be ignored for all other value * types. + *

    + * If the {@link #locale()} is specified, the format will consider the specified locale when processing the date. + * Otherwise, the system's default locale will be used. * * @return A date format string as processable by {@link SimpleDateFormat}. + * @see #locale() */ String valueDateFormat() default ""; /** * A format string as processable by {@link DecimalFormat} if the annotated method maps from a * {@link Number} to a {@link String} or vice-versa. Will be ignored for all other key types. + *

    + * If the {@link #locale()} is specified, the number format will be applied in the context of the given locale. + * Otherwise, the system's default locale will be used. * * @return A decimal format string as processable by {@link DecimalFormat}. + * @see #locale() */ String keyNumberFormat() default ""; /** * A format string as processable by {@link DecimalFormat} if the annotated method maps from a * {@link Number} to a {@link String} or vice-versa. Will be ignored for all other value types. + *

    + * If the {@link #locale()} is specified, the number format will be applied in the context of the given locale. + * Otherwise, the system's default locale will be used. * * @return A decimal format string as processable by {@link DecimalFormat}. + * @see #locale() */ String valueNumberFormat() default ""; + /** + * Specifies the locale to be used when processing {@link SimpleDateFormat} or {@link DecimalFormat} for key or + * value mappings in maps. The locale should be a plain tag representing the language, such as "en" for English, + * "de" for German, etc. + *

    + * If no locale is specified, the system's default locale will be used. + * + * @return A string representing the locale to be used when formatting dates or numbers in maps. + */ + String locale() default ""; + /** * A key value qualifier can be specified to aid the selection process of a suitable mapper. This is useful in * case multiple mappers (hand written of internal) qualify and result in an 'Ambiguous mapping methods found' diff --git a/core/src/main/java/org/mapstruct/Mapper.java b/core/src/main/java/org/mapstruct/Mapper.java index 98022f1a6c..c502dfe8a9 100644 --- a/core/src/main/java/org/mapstruct/Mapper.java +++ b/core/src/main/java/org/mapstruct/Mapper.java @@ -14,7 +14,9 @@ import org.mapstruct.control.MappingControl; import org.mapstruct.factory.Mappers; +import static org.mapstruct.ClassAccessibility.DEFAULT; import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; +import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR; /** * Marks an interface or abstract class as a mapper and activates the generation of a implementation of that type via @@ -73,6 +75,7 @@ * * * @author Gunnar Morling + * @see Javadoc */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) @@ -141,7 +144,12 @@ * can be retrieved via {@code @Autowired} *

  • * {@code jsr330}: the generated mapper is annotated with {@code @javax.inject.Named} and - * {@code @Singleton}, and can be retrieved via {@code @Inject}
  • + * {@code @Singleton}, and can be retrieved via {@code @Inject}. + * The annotations will either be from javax.inject or jakarta.inject, + * depending on which one is available, with javax.inject having precedence. + *
  • + * {@code jakarta}: the generated mapper is annotated with {@code @jakarta.inject.Named} and + * {@code @Singleton}, and can be retrieved via {@code @Inject}.
  • *
* The method overrides a componentModel set in a central configuration set * by {@link #config() } @@ -202,6 +210,32 @@ */ NullValueMappingStrategy nullValueMappingStrategy() default NullValueMappingStrategy.RETURN_NULL; + /** + * The strategy to be applied when {@code null} is passed as source argument value to an {@link IterableMapping} of + * this mapper. If unset, the strategy set with {@link #nullValueMappingStrategy()} will be applied. If neither + * strategy is configured, the strategy given via {@link MapperConfig#nullValueIterableMappingStrategy()} will be + * applied, using {@link NullValueMappingStrategy#RETURN_NULL} by default. + * + * @since 1.5 + * + * @return The strategy to be applied when {@code null} is passed as source value to an {@link IterableMapping} of + * this mapper. + */ + NullValueMappingStrategy nullValueIterableMappingStrategy() default NullValueMappingStrategy.RETURN_NULL; + + /** + * The strategy to be applied when {@code null} is passed as source argument value to a {@link MapMapping} of this + * mapper. If unset, the strategy set with {@link #nullValueMappingStrategy()} will be applied. If neither strategy + * is configured, the strategy given via {@link MapperConfig#nullValueMapMappingStrategy()} will be applied, using + * {@link NullValueMappingStrategy#RETURN_NULL} by default. + * + * @since 1.5 + * + * @return The strategy to be applied when {@code null} is passed as source value to a {@link MapMapping} of this + * mapper. + */ + NullValueMappingStrategy nullValueMapMappingStrategy() default NullValueMappingStrategy.RETURN_NULL; + /** * The strategy to be applied when a source bean property is {@code null} or not present. If no strategy is * configured, the strategy given via {@link MapperConfig#nullValuePropertyMappingStrategy()} will be applied, @@ -237,6 +271,29 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default */ NullValueCheckStrategy nullValueCheckStrategy() default ON_IMPLICIT_CONVERSION; + /** + * Determines how to handle missing implementation for super classes when using the {@link SubclassMapping}. + * + * Can be overridden by the one on {@link BeanMapping}, but overrides {@link MapperConfig}. + * + * @return strategy to handle missing implementation combined with {@link SubclassMappings}. + * + * @since 1.5 + */ + SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; + + /** + * Specifies the exception type to be thrown when a missing subclass implementation is detected + * in combination with {@link SubclassMappings}, based on the {@link #subclassExhaustiveStrategy()}. + *

+ * This exception will only be thrown when the {@code subclassExhaustiveStrategy} is set to + * {@link SubclassExhaustiveStrategy#RUNTIME_EXCEPTION}. + * + * @return the exception class to throw when missing implementations are found. + * Defaults to {@link IllegalArgumentException}. + */ + Class subclassExhaustiveException() default IllegalArgumentException.class; + /** * Determines whether to use field or constructor injection. This is only used on annotated based component models * such as CDI, Spring and JSR 330. @@ -255,7 +312,7 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default * Can be configured by the {@link MapperConfig#disableSubMappingMethodsGeneration()} as well. *

* Note: If you need to use {@code disableSubMappingMethodsGeneration} please contact the MapStruct team at - * mapstruct.org or + * mapstruct.org or * github.com/mapstruct/mapstruct to share what problem you * are facing with the automatic sub-mapping generation. * @@ -319,4 +376,26 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default * @since 1.4 */ Class unexpectedValueMappingException() default IllegalArgumentException.class; + + /** + * Flag indicating whether the addition of a time stamp in the {@code @Generated} annotation should be suppressed. + * i.e. not be added. + * + * The method overrides the flag set in a central configuration set by {@link #config()} + * or through an annotation processor option. + * + * @return whether the addition of a timestamp should be suppressed + * + * @since 1.5 + */ + boolean suppressTimestampInGenerated() default false; + + /** + * Determines the {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation. Default is to mirror the interface or abstract class annotated by this {@code Mapper}. + * + * @return The {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation + */ + ClassAccessibility accessibility() default DEFAULT; } diff --git a/core/src/main/java/org/mapstruct/MapperConfig.java b/core/src/main/java/org/mapstruct/MapperConfig.java index ecfd888ca2..a81e3e8d4f 100644 --- a/core/src/main/java/org/mapstruct/MapperConfig.java +++ b/core/src/main/java/org/mapstruct/MapperConfig.java @@ -14,7 +14,9 @@ import org.mapstruct.control.MappingControl; import org.mapstruct.factory.Mappers; +import static org.mapstruct.ClassAccessibility.DEFAULT; import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; +import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR; /** * Marks a class or interface as configuration source for generated mappers. This allows to share common configurations @@ -130,7 +132,12 @@ * can be retrieved via {@code @Autowired} *

  • * {@code jsr330}: the generated mapper is annotated with {@code @javax.inject.Named} and - * {@code @Singleton}, and can be retrieved via {@code @Inject}
  • + * {@code @Singleton}, and can be retrieved via {@code @Inject}. + * The annotations will either be from javax.inject or jakarta.inject, + * depending on which one is available, with javax.inject having precedence. + *
  • + * {@code jakarta}: the generated mapper is annotated with {@code @jakarta.inject.Named} and + * {@code @Singleton}, and can be retrieved via {@code @Inject}.
  • * * * @return The component model for the generated mapper. @@ -176,6 +183,28 @@ */ NullValueMappingStrategy nullValueMappingStrategy() default NullValueMappingStrategy.RETURN_NULL; + /** + * The strategy to be applied when {@code null} is passed as source argument value to an {@link IterableMapping}. + * If no strategy is configured, the strategy given via {@link #nullValueMappingStrategy()} will be applied, using + * {@link NullValueMappingStrategy#RETURN_NULL} by default. + * + * @since 1.5 + * + * @return The strategy to be applied when {@code null} is passed as source value to an {@link IterableMapping}. + */ + NullValueMappingStrategy nullValueIterableMappingStrategy() default NullValueMappingStrategy.RETURN_NULL; + + /** + * The strategy to be applied when {@code null} is passed as source argument value to a {@link MapMapping}. + * If no strategy is configured, the strategy given via {@link #nullValueMappingStrategy()} will be applied, using + * {@link NullValueMappingStrategy#RETURN_NULL} by default. + * + * @since 1.5 + * + * @return The strategy to be applied when {@code null} is passed as source value to a {@link MapMapping}. + */ + NullValueMappingStrategy nullValueMapMappingStrategy() default NullValueMappingStrategy.RETURN_NULL; + /** * The strategy to be applied when a source bean property is {@code null} or not present. If no strategy is * configured, {@link NullValuePropertyMappingStrategy#SET_TO_NULL} will be used by default. @@ -210,6 +239,29 @@ MappingInheritanceStrategy mappingInheritanceStrategy() */ NullValueCheckStrategy nullValueCheckStrategy() default ON_IMPLICIT_CONVERSION; + /** + * Determines how to handle missing implementation for super classes when using the {@link SubclassMapping}. + * + * Can be overridden by the one on {@link BeanMapping} or {@link Mapper}. + * + * @return strategy to handle missing implementation combined with {@link SubclassMappings}. + * + * @since 1.5 + */ + SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; + + /** + * Specifies the exception type to be thrown when a missing subclass implementation is detected + * in combination with {@link SubclassMappings}, based on the {@link #subclassExhaustiveStrategy()}. + *

    + * This exception will only be thrown when the {@code subclassExhaustiveStrategy} is set to + * {@link SubclassExhaustiveStrategy#RUNTIME_EXCEPTION}. + * + * @return the exception class to throw when missing implementations are found. + * Defaults to {@link IllegalArgumentException}. + */ + Class subclassExhaustiveException() default IllegalArgumentException.class; + /** * Determines whether to use field or constructor injection. This is only used on annotated based component models * such as CDI, Spring and JSR 330. @@ -230,7 +282,7 @@ MappingInheritanceStrategy mappingInheritanceStrategy() * Can be overridden by {@link Mapper#disableSubMappingMethodsGeneration()} *

    * Note: If you need to use {@code disableSubMappingMethodsGeneration} please contact the MapStruct team at - * mapstruct.org or + * mapstruct.org or * github.com/mapstruct/mapstruct to share what problem you * are facing with the automatic sub-mapping generation. * @@ -295,5 +347,25 @@ MappingInheritanceStrategy mappingInheritanceStrategy() */ Class unexpectedValueMappingException() default IllegalArgumentException.class; + /** + * Flag indicating whether the addition of a time stamp in the {@code @Generated} annotation should be suppressed. + * i.e. not be added. + * + * The method overrides the flag set through an annotation processor option. + * + * @return whether the addition of a timestamp should be suppressed + * + * @since 1.5 + */ + boolean suppressTimestampInGenerated() default false; + + /** + * Determines the {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation. Default is to mirror the interface or abstract class annotated by {@code Mapper}. + * + * @return The {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation + */ + ClassAccessibility accessibility() default DEFAULT; } diff --git a/core/src/main/java/org/mapstruct/Mapping.java b/core/src/main/java/org/mapstruct/Mapping.java index 0af73f3687..c2a6172e67 100644 --- a/core/src/main/java/org/mapstruct/Mapping.java +++ b/core/src/main/java/org/mapstruct/Mapping.java @@ -20,7 +20,7 @@ import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; /** - * Configures the mapping of one bean attribute or enum constant. + * Configures the mapping of one bean attribute. *

    * The name of the mapped attribute or constant is to be specified via {@link #target()}. For mapped bean attributes it * is assumed by default that the attribute has the same name in the source bean. Alternatively, one of @@ -54,7 +54,7 @@ *

    Example 2: Mapping properties with different names

    *
    
      * // We need map Human.companyName to HumanDto.company
    - * // we can use @Mapping with parameters {@link #source()} and {@link #source()}
    + * // we can use @Mapping with parameters {@link #source()} and {@link #target()}
      * @Mapper
      * public interface HumanMapper {
      *    @Mapping(source="companyName", target="company")
    @@ -118,7 +118,7 @@
      * // we can use {@link #defaultValue()} or {@link #defaultExpression()} for it
      * @Mapper
      * public interface HumanMapper {
    - *    @Mapping(source="name", target="name", defaultValue="Somebody")
    + *    @Mapping(source="name", target="fullName", defaultValue="Somebody")
      *    HumanDto toHumanDto(Human human)
      * }
      * 
    @@ -136,9 +136,6 @@ * } * * - * IMPORTANT NOTE: the enum mapping capability is deprecated and replaced by {@link ValueMapping} it - * will be removed in subsequent versions. - * * @author Gunnar Morling */ @@ -178,19 +175,38 @@ /** * A format string as processable by {@link SimpleDateFormat} if the attribute is mapped from {@code String} to * {@link Date} or vice-versa. Will be ignored for all other attribute types and when mapping enum constants. + *

    + * If the {@link #locale()} is also specified, the format will consider the specified locale when processing + * the date. Otherwise, the system's default locale will be used. * * @return A date format string as processable by {@link SimpleDateFormat}. + * @see #locale() */ String dateFormat() default ""; /** * A format string as processable by {@link DecimalFormat} if the annotated method maps from a * {@link Number} to a {@link String} or vice-versa. Will be ignored for all other element types. + *

    + * If the {@link #locale()} is also specified, the number format will be applied in the context of the given locale. + * Otherwise, the system's default locale will be used to process the number format. * * @return A decimal format string as processable by {@link DecimalFormat}. + * @see #locale() */ String numberFormat() default ""; + /** + * Specifies the locale to be used when processing {@link #dateFormat()} or {@link #numberFormat()}. + *

    + * The locale should be a plain tag representing the language, such as "en" for English, "de" for German, etc. + *

    + * If no locale is specified, the system's default locale will be used. + * + * @return A string representing the locale to be used when formatting dates or numbers. + */ + String locale() default ""; + /** * A constant {@link String} based on which the specified target property is to be set. *

    @@ -214,10 +230,13 @@ *

    * MapStruct handles the constant as {@code String}. The value will be converted by applying a matching method, * type conversion method or built-in conversion. - *

    * * *

    + * You can use {@link #qualifiedBy()} or {@link #qualifiedByName()} to force the use of a conversion method + * even when one would not apply. (e.g. {@code String} to {@code String}) + *

    + *

    * This attribute can not be used together with {@link #source()}, {@link #defaultValue()}, * {@link #defaultExpression()} or {@link #expression()}. * @@ -283,9 +302,12 @@ /** * Whether the property specified via {@link #target()} should be ignored by the generated mapping method or not. - * This can be useful when certain attributes should not be propagated from source or target or when properties in + * This can be useful when certain attributes should not be propagated from source to target or when properties in * the target object are populated using a decorator and thus would be reported as unmapped target property by * default. + *

    + * If you have multiple properties to ignore, + * you can use the {@link Ignored} annotation instead and group them all at once. * * @return {@code true} if the given property should be ignored, {@code false} otherwise */ @@ -295,6 +317,8 @@ * A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple * mapping methods (hand written or generated) qualify and thus would result in an 'Ambiguous mapping methods found' * error. A qualifier is a custom annotation and can be placed on a hand written mapper class or a method. + *

    + * Note that {@link #defaultValue()} usage will also be converted using this qualifier. * * @return the qualifiers * @see Qualifier @@ -309,6 +333,8 @@ * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large * number of qualifiers as no custom annotation types are needed. + *

    + * Note that {@link #defaultValue()} usage will also be converted using this qualifier. * * @return One or more qualifier name(s) * @see #qualifiedBy() @@ -419,13 +445,11 @@ * If not possible, MapStruct will try to apply a user defined mapping method. * * - *

    * *

  • other *

    * MapStruct handles the constant as {@code String}. The value will be converted by applying a matching method, * type conversion method or built-in conversion. - *

    *

  • * *

    @@ -476,6 +500,4 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() */ Class mappingControl() default MappingControl.class; - - } diff --git a/core/src/main/java/org/mapstruct/MappingConstants.java b/core/src/main/java/org/mapstruct/MappingConstants.java index 002f005299..3d3d8a4c77 100644 --- a/core/src/main/java/org/mapstruct/MappingConstants.java +++ b/core/src/main/java/org/mapstruct/MappingConstants.java @@ -109,7 +109,12 @@ private ComponentModel() { public static final String DEFAULT = "default"; /** - * The generated mapper is an application-scoped CDI bean and can be retrieved via @Inject + * The generated mapper is an application-scoped CDI bean and can be retrieved via @Inject. + * The annotations are either from {@code javax} or {@code jakarta}. + * Priority have the {@code javax} annotations. + * In case you want to only use Jakarta then use {@link #JAKARTA_CDI}. + * + * @see #JAKARTA_CDI */ public static final String CDI = "cdi"; @@ -120,11 +125,30 @@ private ComponentModel() { public static final String SPRING = "spring"; /** - * The generated mapper is annotated with @javax.inject.Named and @Singleton, and can be retrieved via @Inject + * The generated mapper is annotated with @Named and @Singleton, and can be retrieved via @Inject. + * The annotations are either from {@code javax.inject} or {@code jakarta.inject}. + * Priority have the {@code javax.inject} annotations. + * In case you want to only use Jakarta then use {@link #JAKARTA}. * + * @see #JAKARTA */ public static final String JSR330 = "jsr330"; + /** + * The generated mapper is annotated with @Named and @Singleton, and can be retrieved via @Inject. + * The annotations are from {@code jakarta.inject}. + * In case you want to use {@code javax.inject} then use {@link #JSR330}. + * + * @see #JSR330 + */ + public static final String JAKARTA = "jakarta"; + + /** + * The generated mapper is an application-scoped Jakarta CDI bean and can be retrieved via @Inject. + * @see #CDI + */ + public static final String JAKARTA_CDI = "jakarta-cdi"; + } } diff --git a/core/src/main/java/org/mapstruct/NullEnum.java b/core/src/main/java/org/mapstruct/NullEnum.java new file mode 100644 index 0000000000..ac39b3485d --- /dev/null +++ b/core/src/main/java/org/mapstruct/NullEnum.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +/** + * To be used as a default value for enum class annotation elements. + * + * @author Ben Zegveld + * @since 1.6 + */ +enum NullEnum { +} diff --git a/core/src/main/java/org/mapstruct/NullValueCheckStrategy.java b/core/src/main/java/org/mapstruct/NullValueCheckStrategy.java index 446b879d92..22dba7c58b 100644 --- a/core/src/main/java/org/mapstruct/NullValueCheckStrategy.java +++ b/core/src/main/java/org/mapstruct/NullValueCheckStrategy.java @@ -8,7 +8,7 @@ /** * Strategy for dealing with null source values. * - * Note: This strategy is not in effect when the a specific source presence check method is defined + * Note: This strategy is not in effect when a specific source presence check method is defined * in the service provider interface (SPI). *

    * Note: some types of mappings (collections, maps), in which MapStruct is instructed to use a getter or adder diff --git a/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java b/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java index 519a28bd81..4cece83031 100644 --- a/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java +++ b/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java @@ -13,8 +13,9 @@ public enum NullValueMappingStrategy { /** - * If {@code null} is passed to a mapping method, {@code null} will be returned. That's the default behavior if no - * alternative strategy is configured globally, for given mapper or method. + * If {@code null} is passed to a mapping method, {@code null} will be returned, unless the return type is + * {@link java.util.Optional Optional}, in which case an empty optional will be returned. + * That's the default behavior if no alternative strategy is configured globally, for given mapper or method. */ RETURN_NULL, @@ -28,6 +29,7 @@ public enum NullValueMappingStrategy { * case. *

  • For iterable mapping methods an empty collection will be returned.
  • *
  • For map mapping methods an empty map will be returned.
  • + *
  • For optional mapping methods an empty optional will be returned.
  • * */ RETURN_DEFAULT; diff --git a/core/src/main/java/org/mapstruct/NullValuePropertyMappingStrategy.java b/core/src/main/java/org/mapstruct/NullValuePropertyMappingStrategy.java index 9e06e723b6..0ab30d5271 100644 --- a/core/src/main/java/org/mapstruct/NullValuePropertyMappingStrategy.java +++ b/core/src/main/java/org/mapstruct/NullValuePropertyMappingStrategy.java @@ -10,7 +10,7 @@ * {@link NullValuePropertyMappingStrategy} can be defined on {@link MapperConfig}, {@link Mapper}, {@link BeanMapping} * and {@link Mapping}. * Precedence is arranged in the reverse order. So {@link Mapping} will override {@link BeanMapping}, will - * overide {@link Mapper} + * override {@link Mapper} * * The enum only applies to update methods: methods that update a pre-existing target (annotated with * {@code @}{@link MappingTarget}). @@ -27,7 +27,9 @@ public enum NullValuePropertyMappingStrategy { /** - * If a source bean property equals {@code null} the target bean property will be set explicitly to {@code null}. + * If a source bean property equals {@code null} the target bean property will be set explicitly to {@code null} + * or {@link java.util.Optional#empty() Optional.empty()} in case the target type is an + * {@link java.util.Optional Optional}. */ SET_TO_NULL, @@ -36,6 +38,7 @@ public enum NullValuePropertyMappingStrategy { *

    * This means: *

      + *
    1. For {@code Optional} MapStruct generates an {@code Optional.empty()}
    2. *
    3. For {@code List} MapStruct generates an {@code ArrayList}
    4. *
    5. For {@code Map} a {@code HashMap}
    6. *
    7. For arrays an empty array
    8. @@ -53,5 +56,12 @@ public enum NullValuePropertyMappingStrategy { * If a source bean property equals {@code null} the target bean property will be ignored and retain its * existing value. */ - IGNORE; + IGNORE, + + /** + * If a source bean property equals {@code null} the target bean property will be cleared. + * This strategy is only applicable to target properties that are of type {@link java.util.Collection Collection} + * or {@link java.util.Map Map}. + */ + CLEAR; } diff --git a/core/src/main/java/org/mapstruct/Qualifier.java b/core/src/main/java/org/mapstruct/Qualifier.java index be51f49e04..9e18c3e420 100644 --- a/core/src/main/java/org/mapstruct/Qualifier.java +++ b/core/src/main/java/org/mapstruct/Qualifier.java @@ -21,6 +21,7 @@ *
    9. {@link IterableMapping#qualifiedBy() }
    10. *
    11. {@link MapMapping#keyQualifiedBy() }
    12. *
    13. {@link MapMapping#valueQualifiedBy() }
    14. + *
    15. {@link SubclassMapping#qualifiedBy() }
    16. * *

      Example:

      *
      
      diff --git a/core/src/main/java/org/mapstruct/SourceParameterCondition.java b/core/src/main/java/org/mapstruct/SourceParameterCondition.java
      new file mode 100644
      index 0000000000..8bff97abc4
      --- /dev/null
      +++ b/core/src/main/java/org/mapstruct/SourceParameterCondition.java
      @@ -0,0 +1,74 @@
      +/*
      + * Copyright MapStruct Authors.
      + *
      + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
      + */
      +package org.mapstruct;
      +
      +import java.lang.annotation.ElementType;
      +import java.lang.annotation.Retention;
      +import java.lang.annotation.RetentionPolicy;
      +import java.lang.annotation.Target;
      +
      +/**
      + * This annotation marks a method as a check method to check if a source parameter needs to be mapped.
      + * 

      + * By default, source parameters are checked against {@code null}, unless they are primitives. + *

      + * Check methods have to return {@code boolean}. + * The following parameters are accepted for the presence check methods: + *

        + *
      • The mapping source parameter
      • + *
      • {@code @}{@link Context} parameter
      • + *
      + * + * Note: The usage of this annotation is mandatory + * for a method to be considered as a source check method. + * + *
      
      + * public class PresenceCheckUtils {
      + *
      + *   @SourceParameterCondition
      + *   public static boolean isDefined(Car car) {
      + *      return car != null && car.getId() != null;
      + *   }
      + * }
      + *
      + * @Mapper(uses = PresenceCheckUtils.class)
      + * public interface CarMapper {
      + *
      + *     CarDto map(Car car);
      + * }
      + * 
      + * + * The following implementation of {@code CarMapper} will be generated: + * + *
      
      + * public class CarMapperImpl implements CarMapper {
      + *
      + *     @Override
      + *     public CarDto map(Car car) {
      + *         if ( !PresenceCheckUtils.isDefined( car ) ) {
      + *             return null;
      + *         }
      + *
      + *         CarDto carDto = new CarDto();
      + *
      + *         carDto.setId( car.getId() );
      + *         // ...
      + *
      + *         return carDto;
      + *     }
      + * }
      + * 
      + * + * @author Filip Hrisafov + * @since 1.6 + * @see Condition @Condition + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.CLASS) +@Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS) +public @interface SourceParameterCondition { + +} diff --git a/core/src/main/java/org/mapstruct/SourcePropertyName.java b/core/src/main/java/org/mapstruct/SourcePropertyName.java new file mode 100644 index 0000000000..a9d036d5d8 --- /dev/null +++ b/core/src/main/java/org/mapstruct/SourcePropertyName.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation marks a presence check method parameter as a source property name parameter. + *

      + * This parameter enables conditional filtering based on source property name at run-time. + * Parameter must be of type {@link String} and can be present only in {@link Condition} method. + *

      + * + * @author Oliver Erhart + * @since 1.6 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.CLASS) +public @interface SourcePropertyName { +} diff --git a/core/src/main/java/org/mapstruct/SubclassExhaustiveStrategy.java b/core/src/main/java/org/mapstruct/SubclassExhaustiveStrategy.java new file mode 100644 index 0000000000..a60d067faa --- /dev/null +++ b/core/src/main/java/org/mapstruct/SubclassExhaustiveStrategy.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +/** + * Strategy for dealing with subclassMapping annotated methods. + * + * @since 1.5 + * @author Ben Zegveld + */ +public enum SubclassExhaustiveStrategy { + + /** + * If there is no valid constructor or known method to create the return value of a with `@SubclassMapping` + * annotated mapping then a compilation error will be thrown. + */ + COMPILE_ERROR, + + /** + * If there is no valid constructor or known method to create the return value of a with `@SubclassMapping` + * annotated mapping then an {@link IllegalArgumentException} will be thrown if a call is made with a type for which + * there is no {@link SubclassMapping} available. + */ + RUNTIME_EXCEPTION; +} diff --git a/core/src/main/java/org/mapstruct/SubclassMapping.java b/core/src/main/java/org/mapstruct/SubclassMapping.java new file mode 100644 index 0000000000..4d635d8aa3 --- /dev/null +++ b/core/src/main/java/org/mapstruct/SubclassMapping.java @@ -0,0 +1,115 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mapstruct.util.Experimental; + +/** + * Configures the mapping to handle hierarchy of the source type. + *

      + * The subclass to be mapped is to be specified via {@link #source()}. + * The subclass to map to is to be specified via {@link #target()}. + *

      + *

      + * This annotation can be combined with @Mapping annotations. + *

      + * + *
      
      + * @Mapper
      + * public interface MyMapper {
      + *    @SubclassMapping (target = TargetSubclass.class, source = SourceSubclass.class)
      + *    TargetParent mapParent(SourceParent parent);
      + *
      + *    TargetSubclass mapSubclass(SourceSubclass subInstant);
      + * }
      + * 
      + * Below follow examples of the implementation for the mapParent method. + * Example 1: For parents that cannot be created. (e.g. abstract classes or interfaces) + *
      
      + * // generates
      + * @Override
      + * public TargetParent mapParent(SourceParent parent) {
      + *     if (parent instanceof SourceSubclass) {
      + *         return mapSubclass( (SourceSubclass) parent );
      + *     }
      + *     else {
      + *         throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for "
      + *                    + parent.getClass());
      + *     }
      + * }
      + * 
      + * Example 2: For parents that can be created. (e.g. normal classes or interfaces with + * @Mapper( uses = ObjectFactory.class ) ) + *
      
      + * // generates
      + * @Override
      + * public TargetParent mapParent(SourceParent parent) {
      + *     TargetParent targetParent1;
      + *     if (parent instanceof SourceSubclass) {
      + *         targetParent1 = mapSubclass( (SourceSubclass) parent );
      + *     }
      + *     else {
      + *         targetParent1 = new TargetParent();
      + *         // ...
      + *     }
      + * }
      + * 
      + * + * @author Ben Zegveld + * @since 1.5 + */ +@Repeatable(value = SubclassMappings.class) +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Experimental +public @interface SubclassMapping { + + /** + * The source subclass to check for before using the default mapping as fallback. + * + * @return the source subclass to check for before using the default mapping as fallback. + */ + Class source(); + + /** + * The target subclass to map the source to. + * + * @return the target subclass to map the source to. + */ + Class target(); + + /** + * A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple + * mapping methods (hand written or generated) qualify and thus would result in an 'Ambiguous mapping methods found' + * error. A qualifier is a custom annotation and can be placed on a hand written mapper class or a method. + * + * @return the qualifiers + * @see Qualifier + */ + Class[] qualifiedBy() default {}; + + /** + * String-based form of qualifiers; When looking for a suitable mapping method for a given property, MapStruct will + * only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation + * for each of the specified qualifier names. + *

      + * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and + * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large + * number of qualifiers as no custom annotation types are needed. + * + * @return One or more qualifier name(s) + * @see #qualifiedBy() + * @see Named + */ + String[] qualifiedByName() default {}; +} diff --git a/core/src/main/java/org/mapstruct/SubclassMappings.java b/core/src/main/java/org/mapstruct/SubclassMappings.java new file mode 100644 index 0000000000..d6aac264d4 --- /dev/null +++ b/core/src/main/java/org/mapstruct/SubclassMappings.java @@ -0,0 +1,60 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mapstruct.util.Experimental; + +/** + * Configures the SubclassMappings of several subclasses. + *

      + * TIP: When using java 8 or later, you can omit the @SubclassMappings + * Wrapper annotation and directly specify several @SubclassMapping annotations + * on one method. + *

      + *

      These two examples are equal. + *

      + *
      
      + * // before java 8
      + * @Mapper
      + * public interface MyMapper {
      + *     @SubclassMappings({
      + *         @SubclassMapping(source = FirstSub.class, target = FirstTargetSub.class),
      + *         @SubclassMapping(source = SecondSub.class, target = SecondTargetSub.class)
      + *     })
      + *     ParentTarget toParentTarget(Parent parent);
      + * }
      + * 
      + *
      
      + * // java 8 and later
      + * @Mapper
      + * public interface MyMapper {
      + *     @SubclassMapping(source = First.class, target = FirstTargetSub.class),
      + *     @SubclassMapping(source = SecondSub.class, target = SecondTargetSub.class)
      + *     ParentTarget toParentTarget(Parent parent);
      + * }
      + * 
      + * + * @author Ben Zegveld + * @since 1.5 + */ +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.CLASS) +@Experimental +public @interface SubclassMappings { + + /** + * The subclassMappings that should be applied. + * + * @return the subclassMappings to apply. + */ + SubclassMapping[] value(); + +} diff --git a/core/src/main/java/org/mapstruct/TargetPropertyName.java b/core/src/main/java/org/mapstruct/TargetPropertyName.java new file mode 100644 index 0000000000..c7ab8d957d --- /dev/null +++ b/core/src/main/java/org/mapstruct/TargetPropertyName.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation marks a presence check method parameter as a target property name parameter. + *

      + * This parameter enables conditional filtering based on target property name at run-time. + * Parameter must be of type {@link String} and can be present only in {@link Condition} method. + *

      + * @author Nikola Ivačič + * @since 1.6 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.CLASS) +public @interface TargetPropertyName { +} diff --git a/core/src/main/java/org/mapstruct/ValueMapping.java b/core/src/main/java/org/mapstruct/ValueMapping.java index 21ea5f4a83..7ad3726e48 100644 --- a/core/src/main/java/org/mapstruct/ValueMapping.java +++ b/core/src/main/java/org/mapstruct/ValueMapping.java @@ -84,7 +84,7 @@ */ @Repeatable(ValueMappings.class) @Retention(RetentionPolicy.CLASS) -@Target(ElementType.METHOD) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface ValueMapping { /** * The source value constant to use for this mapping. diff --git a/core/src/main/java/org/mapstruct/ValueMappings.java b/core/src/main/java/org/mapstruct/ValueMappings.java index faefbf7105..95d4c7d5ce 100644 --- a/core/src/main/java/org/mapstruct/ValueMappings.java +++ b/core/src/main/java/org/mapstruct/ValueMappings.java @@ -40,10 +40,15 @@ * * @author Sjaak Derksen */ -@Target(ElementType.METHOD) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.CLASS) public @interface ValueMappings { + /** + * The value mappings that should be applied. + * + * @return the value mappings + */ ValueMapping[] value(); } diff --git a/core/src/main/java/org/mapstruct/control/MappingControl.java b/core/src/main/java/org/mapstruct/control/MappingControl.java index c44797602a..12c95c4494 100644 --- a/core/src/main/java/org/mapstruct/control/MappingControl.java +++ b/core/src/main/java/org/mapstruct/control/MappingControl.java @@ -11,7 +11,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - /** * Controls which means of mapping are considered between the source and the target in mappings. * @@ -99,8 +98,16 @@ @MappingControl( MappingControl.Use.COMPLEX_MAPPING ) public @interface MappingControl { + /** + * The type of mapping control that should be used. + * + * @return What should be used for the mapping control + */ Use value(); + /** + * Defines the options that can be used for the mapping control. + */ enum Use { /** diff --git a/core/src/main/java/org/mapstruct/control/MappingControls.java b/core/src/main/java/org/mapstruct/control/MappingControls.java index a3efef661f..c1663e551e 100644 --- a/core/src/main/java/org/mapstruct/control/MappingControls.java +++ b/core/src/main/java/org/mapstruct/control/MappingControls.java @@ -21,5 +21,10 @@ @Target(ElementType.ANNOTATION_TYPE) public @interface MappingControls { + /** + * The mapping controls that should be applied to the annotated class. + * + * @return The mapping controls that should be applied to the annotated class. + */ MappingControl[] value(); } diff --git a/core/src/main/java/org/mapstruct/factory/Mappers.java b/core/src/main/java/org/mapstruct/factory/Mappers.java index 05b616fe9a..d20bd59fa3 100644 --- a/core/src/main/java/org/mapstruct/factory/Mappers.java +++ b/core/src/main/java/org/mapstruct/factory/Mappers.java @@ -84,10 +84,10 @@ private static T doGetMapper(Class clazz, ClassLoader classLoader) throws return constructor.newInstance(); } - catch (ClassNotFoundException e) { + catch ( ClassNotFoundException e ) { return getMapperFromServiceLoader( clazz, classLoader ); } - catch ( InstantiationException | InvocationTargetException | IllegalAccessException e) { + catch ( InstantiationException | InvocationTargetException | IllegalAccessException e ) { throw new RuntimeException( e ); } } diff --git a/core/src/main/java/org/mapstruct/package-info.java b/core/src/main/java/org/mapstruct/package-info.java index 02f2b31a42..5a53b5a6d3 100644 --- a/core/src/main/java/org/mapstruct/package-info.java +++ b/core/src/main/java/org/mapstruct/package-info.java @@ -13,6 +13,6 @@ * This package contains several annotations which allow to configure how mapper interfaces are generated. *

      * - * @see MapStruct reference documentation + * @see MapStruct reference documentation */ package org.mapstruct; diff --git a/core/src/main/java/org/mapstruct/util/Experimental.java b/core/src/main/java/org/mapstruct/util/Experimental.java index a945a87d7c..5fd5eb3b5c 100644 --- a/core/src/main/java/org/mapstruct/util/Experimental.java +++ b/core/src/main/java/org/mapstruct/util/Experimental.java @@ -17,5 +17,11 @@ @Documented @Retention(RetentionPolicy.SOURCE) public @interface Experimental { + + /** + * The reason why the feature is considered experimental. + * + * @return the reason why the feature is considered experimental. + */ String value() default ""; } diff --git a/core/src/test/java/org/mapstruct/factory/MappersTest.java b/core/src/test/java/org/mapstruct/factory/MappersTest.java index 6118ba09cb..cde4b5be3b 100644 --- a/core/src/test/java/org/mapstruct/factory/MappersTest.java +++ b/core/src/test/java/org/mapstruct/factory/MappersTest.java @@ -5,12 +5,12 @@ */ package org.mapstruct.factory; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.mapstruct.test.model.Foo; import org.mapstruct.test.model.SomeClass; +import static org.assertj.core.api.Assertions.assertThat; + /** * Unit test for {@link Mappers}. * diff --git a/distribution/pom.xml b/distribution/pom.xml index 337ad35e51..b480e24a06 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml @@ -39,6 +39,18 @@ org.mapstruct.tools.gem gem-api + + + org.jetbrains.kotlin + kotlin-metadata-jvm + + + + jakarta.xml.bind + jakarta.xml.bind-api + provided + true + @@ -59,7 +71,7 @@ org.freemarker freemarker ${project.build.directory}/freemarker-unpacked - META-INF/LICENSE.txt,META-INF/NOTICE.txt + META-INF/LICENSE @@ -93,7 +105,7 @@ MapStruct ${project.version} MapStruct ${project.version} - MapStruct Authors; All rights reserved. Released under the Apache Software License 2.0.]]> + MapStruct Authors; All rights reserved. Released under the Apache Software License 2.0.]]> @@ -123,7 +135,7 @@ ${basedir}/src/main/assembly/dist.xml mapstruct-${project.version} - gnu + posix @@ -187,7 +199,6 @@ javax.xml.bind jaxb-api - 2.3.1 provided true diff --git a/distribution/src/main/assembly/dist.xml b/distribution/src/main/assembly/dist.xml index f519e5fc9c..e0bc00496b 100644 --- a/distribution/src/main/assembly/dist.xml +++ b/distribution/src/main/assembly/dist.xml @@ -42,11 +42,7 @@ / - target/freemarker-unpacked/META-INF/NOTICE.txt - / - - - target/freemarker-unpacked/META-INF/LICENSE.txt + target/freemarker-unpacked/META-INF/LICENSE etc/freemarker @@ -80,7 +76,7 @@ - target/site/apidocs + target/reports/apidocs docs/api diff --git a/documentation/pom.xml b/documentation/pom.xml index 8850da641c..21a2b151bd 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc index d3ab2424b1..306169193d 100644 --- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc +++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc @@ -88,7 +88,7 @@ Default expressions are a combination of default values and expressions. They wi The same warnings and restrictions apply to default expressions that apply to expressions. Only Java is supported, and MapStruct will not validate the expression at generation-time. -The example below demonstrates how two source properties can be mapped to one target: +The example below demonstrates how a default expression can be used to set a value when the source attribute is not present (e.g. is `null`): .Mapping method using a default expression ==== @@ -110,6 +110,51 @@ public interface SourceTargetMapper { The example demonstrates how to use defaultExpression to set an `ID` field if the source field is null, this could be used to take the existing `sourceId` from the source object if it is set, or create a new `Id` if it isn't. Please note that the fully qualified package name is specified because MapStruct does not take care of the import of the `UUID` class (unless it’s used otherwise explicitly in the `SourceTargetMapper`). This can be resolved by defining imports on the @Mapper annotation (see <>). +[[sub-class-mappings]] +=== Subclass Mapping + +When both input and result types have an inheritance relation, you would want the correct specialization be mapped to the matching specialization. +Suppose an `Apple` and a `Banana`, which are both specializations of `Fruit`. + +.Specifying the sub class mappings of a fruit mapping +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface FruitMapper { + + @SubclassMapping( source = AppleDto.class, target = Apple.class ) + @SubclassMapping( source = BananaDto.class, target = Banana.class ) + Fruit map( FruitDto source ); + +} +---- +==== + +If you would just use a normal mapping both the `AppleDto` and the `BananaDto` would be made into a `Fruit` object, instead of an `Apple` and a `Banana` object. +By using the subclass mapping an `AppleDtoToApple` mapping will be used for `AppleDto` objects, and an `BananaDtoToBanana` mapping will be used for `BananaDto` objects. +If you try to map a `GrapeDto` it would still turn it into a `Fruit`. + +In the case that the `Fruit` is an abstract class or an interface, you would get a compile error. + +To allow mappings for abstract classes or interfaces you need to set the `subclassExhaustiveStrategy` to `RUNTIME_EXCEPTION`, you can do this at the `@MapperConfig`, `@Mapper` or `@BeanMapping` annotations. If you then pass a `GrapeDto` an `IllegalArgumentException` will be thrown because it is unknown how to map a `GrapeDto`. +Adding the missing (`@SubclassMapping`) for it will fix that. + +<> can be used to further control which methods may be chosen to map a specific subclass. For that, you will need to use one of `SubclassMapping#qualifiedByName` or `SubclassMapping#qualifiedBy`. + +[TIP] +==== +If the mapping method for the subclasses does not exist it will be created and any other annotations on the fruit mapping method will be inherited by the newly generated mappings. +==== + +[NOTE] +==== +Combining `@SubclassMapping` with update methods is not supported. +If you try to use subclass mappings there will be a compile error. +The same issue exists for the `@Context` and `@TargetType` parameters. +==== + [[determining-result-type]] === Determining the result type @@ -159,7 +204,8 @@ The mechanism is also present on iterable mapping and map mapping. `@IterableMap [[mapping-result-for-null-arguments]] === Controlling mapping result for 'null' arguments -MapStruct offers control over the object to create when the source argument of the mapping method equals `null`. By default `null` will be returned. +MapStruct offers control over the object to create when the source argument of the mapping method equals `null`. +By default `null` will be returned, or `Optional.empty()` if the return type is `Optional`. However, by specifying `nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT` on `@BeanMapping`, `@IterableMapping`, `@MapMapping`, or globally on `@Mapper` or `@MapperConfig`, the mapping result can be altered to return empty *default* values. This means for: @@ -169,13 +215,36 @@ However, by specifying `nullValueMappingStrategy = NullValueMappingStrategy.RETU The strategy works in a hierarchical fashion. Setting `nullValueMappingStrategy` on mapping method level will override `@Mapper#nullValueMappingStrategy`, and `@Mapper#nullValueMappingStrategy` will override `@MapperConfig#nullValueMappingStrategy`. +[[mapping-result-for-null-collection-or-map-arguments]] +=== Controlling mapping result for 'null' collection or map arguments + +With <> it is possible to control how the return type should be constructed when the source argument of the mapping method is `null`. +That is applied for all mapping methods (bean, iterable or map mapping methods). + +However, MapStruct also offers a more dedicated way to control how collections / maps should be mapped. +e.g. return default (empty) collections / maps, but return `null` for beans. + +For collections (iterables) this can be controlled through: + +* `MapperConfig#nullValueIterableMappingStrategy` +* `Mapper#nullValueIterableMappingStrategy` +* `IterableMapping#nullValueMappingStrategy` + +For maps this can be controlled through: + +* `MapperConfig#nullValueMapMappingStrategy` +* `Mapper#nullValueMapMappingStrategy` +* `MapMapping#nullValueMappingStrategy` + +How the value of the `NullValueMappingStrategy` is applied is the same as in <> + [[mapping-result-for-null-properties]] === Controlling mapping result for 'null' properties in bean mappings (update mapping methods only). MapStruct offers control over the property to set in an `@MappingTarget` annotated target bean when the source property equals `null` or the presence check method results in 'absent'. -By default the target property will be set to null. +By default the target property will be set to `null`, or `Optional.empty()` if the target property is `Optional`. However: @@ -185,6 +254,9 @@ For all other objects an new instance is created. Please note that a default con 2. By specifying `nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE` on `@Mapping`, `@BeanMapping`, `@Mapper` or `@MapperConfig`, the mapping result will be equal to the original value of the `@MappingTarget` annotated target. +3. By specifying `nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR` on `@Mapping`, `@BeanMapping`, `@Mapper` or `@MapperConfig`, the target collection or map will be cleared when the source property is `null`. +This strategy only applies to `Collection` and `Map` target properties. + The strategy works in a hierarchical fashion. Setting `nullValuePropertyMappingStrategy` on mapping method level will override `@Mapper#nullValuePropertyMappingStrategy`, and `@Mapper#nullValuePropertyMappingStrategy` will override `@MapperConfig#nullValuePropertyMappingStrategy`. [NOTE] @@ -198,6 +270,18 @@ null check, regardless of the value of the `NullValuePropertyMappingStrategy`, t `NullValuePropertyMappingStrategy` also applies when the presence checker returns `not present`. ==== +[NOTE] +==== +When working with `java.util.Optional` types, `NullValuePropertyMappingStrategy` applies to empty Optionals in the same way it applies to null values. +An empty `Optional` (i.e., `Optional.isEmpty()` returns `true`) is treated similarly to a `null` value for the purposes of this strategy. For example: + +* With `IGNORE` strategy: an empty `Optional` source property will not update the target property +* With `SET_TO_NULL` strategy: an empty `Optional` source will set the target to `null` (for non-`Optional` targets) or `Optional.empty()` (for Optional targets) +* With `SET_TO_DEFAULT` strategy: an empty `Optional` source will set the target to its default value + +See <> for detailed examples and behavior with Optional types. +==== + [[checking-source-property-for-null-arguments]] === Controlling checking result for 'null' properties in bean mapping @@ -213,7 +297,50 @@ First calling a mapping method on the source property is not protected by a null The option `nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS` will always include a null check when source is non primitive, unless a source presence checker is defined on the source bean. -The strategy works in a hierarchical fashion. `@Mapping#nullValueCheckStrategy` will override `@BeanMapping#nullValueCheckStrategy`, `@BeanMapping#nullValueCheckStrategy` will override `@Mapper#nullValueCheckStrategy` and `@Mapper#nullValueCheckStrategy` will override `@MaperConfig#nullValueCheckStrategy`. +The strategy works in a hierarchical fashion. `@Mapping#nullValueCheckStrategy` will override `@BeanMapping#nullValueCheckStrategy`, `@BeanMapping#nullValueCheckStrategy` will override `@Mapper#nullValueCheckStrategy` and `@Mapper#nullValueCheckStrategy` will override `@MapperConfig#nullValueCheckStrategy`. + +[[jspecify-nullness-annotations]] +=== JSpecify nullness annotations + +MapStruct recognises https://jspecify.dev/[JSpecify] nullness annotations on source and target types and uses them to refine when null checks are generated. Only the annotations from `org.jspecify.annotations` are interpreted; MapStruct does not introduce a compile-time dependency on JSpecify, so consumers opt in by adding the dependency themselves. + +The following annotations participate: + +* `@NonNull`: the annotated value is guaranteed not to be `null`. +* `@Nullable`: the annotated value may be `null`. +* `@NullMarked`: within this scope (package, class, or method), unannotated reference types are treated as `@NonNull`. +* `@NullUnmarked`: reverts to unknown nullability within its scope (overrides a `@NullMarked` outer scope). + +==== Property-level rules + +For a single property mapping: + +* If the source is `@NonNull`, the null check is skipped, since the value is treated as guaranteed non-null. +* If the target is `@NonNull` (and the source is not `@NonNull`), a null check is always added so the target's contract is not violated. +* In all other cases, the existing `NullValueCheckStrategy` decides. + +The existing safety guards (`defaultValue` / `defaultExpression`, primitive unboxing, and the relevant `NullValuePropertyMappingStrategy` modes) continue to apply. + +==== Scope: `@NullMarked` and `@NullUnmarked` + +`@NullMarked` and `@NullUnmarked` are resolved by walking from the annotated element outward: method, then declaring class, then outer classes, then package. The closest annotation wins: a `@NullUnmarked` method inside a `@NullMarked` class reverts only that method, while a `@Nullable` annotation on an individual type always overrides the surrounding scope. + +==== Method-level source parameter + +When the source parameter of a mapping method is annotated `@NonNull` (directly or via a `@NullMarked` scope), MapStruct skips the method-level null guard, since the caller is contractually obliged to pass a non-null value. +This rule applies uniformly to bean, iterable, map, and stream mapping methods. + +==== Method-level return type + +When the return type of a mapping method is `@NonNull` (directly or via a `@NullMarked` scope), MapStruct forces `NullValueMappingStrategy.RETURN_DEFAULT` semantics. The generated method returns a default-constructed target (bean methods), an empty collection (`Iterable` / array mappings), an empty map (`Map` mappings), or `Stream.empty()` (stream mappings) rather than `null`, so the return contract is never violated. This rule applies regardless of the explicit `NullValueMappingStrategy` setting. + +==== Constructor parameter constraint + +If a property mapping would assign a potentially `null` source value to a `@NonNull` constructor parameter, MapStruct raises a *compilation error*. Neither inserting a null check (which would leave the variable at `null` and violate the contract) nor passing the value through is safe. Provide a `defaultValue` or `defaultExpression` on the `@Mapping` to satisfy the parameter when the source is absent. + +==== Disabling JSpecify support + +All JSpecify-driven inference can be turned off by setting the processor option `-Amapstruct.disableJSpecify=true`. With the option enabled, MapStruct ignores every JSpecify annotation (`@NonNull`, `@Nullable`, `@NullMarked`, `@NullUnmarked`) and the compilation error for nullable sources mapped to `@NonNull` constructor parameters is not raised. Null-check generation falls back to the pre-JSpecify behavior driven by `NullValueCheckStrategy`. Use this flag when you need to freeze MapStruct's null-handling behavior while JSpecify annotations exist elsewhere on the classpath. [[source-presence-check]] === Source presence checking @@ -226,7 +353,7 @@ The source presence checker name can be changed in the MapStruct service provide [NOTE] ==== -Some types of mappings (collections, maps), in which MapStruct is instructed to use a getter or adder as target accessor see `CollectionMappingStrategy`, MapStruct will always generate a source property +Some types of mappings (collections, maps), in which MapStruct is instructed to use a getter or adder as target accessor (see `CollectionMappingStrategy`), MapStruct will always generate a source property null check, regardless the value of the `NullValueCheckStrategy` to avoid addition of `null` to the target collection or map. ==== @@ -235,10 +362,12 @@ null check, regardless the value of the `NullValueCheckStrategy` to avoid additi Conditional Mapping is a type of <>. The difference is that it allows users to write custom condition methods that will be invoked to check if a property needs to be mapped or not. +Conditional mapping can also be used to check if a source parameter should be mapped or not. -A custom condition method is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`. +A custom condition method for properties is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`. +A custom condition method for source parameters is annotated with `org.mapstruct.SourceParameterCondition`, `org.mapstruct.Condition(appliesTo = org.mapstruct.ConditionStrategy#SOURCE_PARAMETERS)` or meta-annotated with `Condition(appliesTo = ConditionStrategy#SOURCE_PARAMETERS)` -e.g. if you only want to map a String property when it is not `null, and it is not empty then you can do something like: +e.g. if you only want to map a String property when it is not `null` and not empty then you can do something like: .Mapper using custom condition check method ==== @@ -260,7 +389,7 @@ public interface CarMapper { The generated mapper will look like: -.try-catch block in generated implementation +.Custom condition check in generated implementation ==== [source, java, linenums] [subs="verbatim,attributes"] @@ -288,6 +417,119 @@ public class CarMapperImpl implements CarMapper { ---- ==== +When using this in combination with an update mapping method it will replace the `null-check` there, for example: + +.Update mapper using custom condition check method +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CarMapper { + + CarDto carToCarDto(Car car, @MappingTarget CarDto carDto); + + @Condition + default boolean isNotEmpty(String value) { + return value != null && !value.isEmpty(); + } +} +---- +==== + +The generated update mapper will look like: + +.Custom condition check in generated implementation +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class CarMapperImpl implements CarMapper { + + @Override + public CarDto carToCarDto(Car car, CarDto carDto) { + if ( car == null ) { + return carDto; + } + + if ( isNotEmpty( car.getOwner() ) ) { + carDto.setOwner( car.getOwner() ); + } else { + carDto.setOwner( null ); + } + + // Mapping of other properties + + return carDto; + } +} +---- +==== + +Additionally `@TargetPropertyName` or `@SourcePropertyName` of type `java.lang.String` can be used in custom condition check method: + +.Mapper using custom condition check method with `@TargetPropertyName` and `@SourcePropertyName` +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CarMapper { + + @Mapping(target = "owner", source = "ownerName") + CarDto carToCarDto(Car car, @MappingTarget CarDto carDto); + + @Condition + default boolean isNotEmpty( + String value, + @TargetPropertyName String targetPropertyName, + @SourcePropertyName String sourcePropertyName + ) { + + if ( targetPropertyName.equals( "owner" ) + && sourcePropertyName.equals( "ownerName" ) ) { + + return value != null + && !value.isEmpty() + && !value.equals( value.toLowerCase() ); + } + return value != null && !value.isEmpty(); + } +} +---- +==== + +The generated mapper with `@TargetPropertyName` and `@SourcePropertyName` will look like: + +.Custom condition check in generated implementation +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class CarMapperImpl implements CarMapper { + + @Override + public CarDto carToCarDto(Car car, CarDto carDto) { + if ( car == null ) { + return carDto; + } + + if ( isNotEmpty( car.getOwner(), "owner", "ownerName" ) ) { + carDto.setOwner( car.getOwner() ); + } else { + carDto.setOwner( null ); + } + + // Mapping of other properties + + return carDto; + } +} +---- +==== + [IMPORTANT] ==== If there is a custom `@Condition` method applicable for the property it will have a precedence over a presence check method in the bean itself. @@ -296,11 +538,62 @@ If there is a custom `@Condition` method applicable for the property it will hav [NOTE] ==== Methods annotated with `@Condition` in addition to the value of the source property can also have the source parameter as an input. + +`@TargetPropertyName` and `@SourcePropertyName` parameters can only be used in `@Condition` methods. ==== <> is also valid for `@Condition` methods. In order to use a more specific condition method you will need to use one of `Mapping#conditionQualifiedByName` or `Mapping#conditionQualifiedBy`. +If we want to only map cars that have an id provided then we can do something like: + + +.Mapper using custom condition source parameter check method +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CarMapper { + + CarDto carToCarDto(Car car); + + @SourceParameterCondition + default boolean hasCar(Car car) { + return car != null && car.getId() != null; + } +} +---- +==== + +The generated mapper will look like: + +.Custom condition source parameter check generated implementation +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class CarMapperImpl implements CarMapper { + + @Override + public CarDto carToCarDto(Car car) { + if ( !hasCar( car ) ) { + return null; + } + + CarDto carDto = new CarDto(); + + carDto.setOwner( car.getOwner() ); + + // Mapping of other properties + + return carDto; + } +} +---- +==== + [[exceptions]] === Exceptions diff --git a/documentation/src/main/asciidoc/chapter-11-reusing-mapping-configurations.asciidoc b/documentation/src/main/asciidoc/chapter-11-reusing-mapping-configurations.asciidoc index fb8c71a622..efacaedbe5 100644 --- a/documentation/src/main/asciidoc/chapter-11-reusing-mapping-configurations.asciidoc +++ b/documentation/src/main/asciidoc/chapter-11-reusing-mapping-configurations.asciidoc @@ -46,6 +46,8 @@ In case of bi-directional mappings, e.g. from entity to DTO and from DTO to enti Use the annotation `@InheritInverseConfiguration` to indicate that a method shall inherit the inverse configuration of the corresponding reverse method. +In the example below, there is no need to write the inverse mapping manually. Think of a case where there are several mappings, so writing the inverse ones can be cumbersome and error prone. + .Inverse mapping method inheriting its configuration and ignoring some of them ==== [source, java, linenums] @@ -162,4 +164,4 @@ The attributes `@Mapper#mappingInheritanceStrategy()` / `@MapperConfig#mappingIn * `EXPLICIT` (default): the configuration will only be inherited, if the target mapping method is annotated with `@InheritConfiguration` and the source and target types are assignable to the corresponding types of the prototype method, all as described in <>. * `AUTO_INHERIT_FROM_CONFIG`: the configuration will be inherited automatically, if the source and target types of the target mapping method are assignable to the corresponding types of the prototype method. If multiple prototype methods match, the ambiguity must be resolved using `@InheritConfiguration(name = ...)` which will cause `AUTO_INHERIT_FROM_CONFIG` to be ignored. * `AUTO_INHERIT_REVERSE_FROM_CONFIG`: the inverse configuration will be inherited automatically, if the source and target types of the target mapping method are assignable to the corresponding types of the prototype method. If multiple prototype methods match, the ambiguity must be resolved using `@InheritInverseConfiguration(name = ...)` which will cause ``AUTO_INHERIT_REVERSE_FROM_CONFIG` to be ignored. -* `AUTO_INHERIT_ALL_FROM_CONFIG`: both the configuration and the inverse configuration will be inherited automatically. The same rules apply as for `AUTO_INHERIT_FROM_CONFIG` or `AUTO_INHERIT_REVERSE_FROM_CONFIG`. \ No newline at end of file +* `AUTO_INHERIT_ALL_FROM_CONFIG`: both the configuration and the inverse configuration will be inherited automatically. The same rules apply as for `AUTO_INHERIT_FROM_CONFIG` or `AUTO_INHERIT_REVERSE_FROM_CONFIG`. diff --git a/documentation/src/main/asciidoc/chapter-12-customizing-mapping.asciidoc b/documentation/src/main/asciidoc/chapter-12-customizing-mapping.asciidoc index 0c873eac09..dc07b30e62 100644 --- a/documentation/src/main/asciidoc/chapter-12-customizing-mapping.asciidoc +++ b/documentation/src/main/asciidoc/chapter-12-customizing-mapping.asciidoc @@ -248,9 +248,8 @@ All before/after-mapping methods that *can* be applied to a mapping method *will The order of the method invocation is determined primarily by their variant: -1. `@BeforeMapping` methods without an `@MappingTarget` parameter are called before any null-checks on source -parameters and constructing a new target bean. -2. `@BeforeMapping` methods with an `@MappingTarget` parameter are called after constructing a new target bean. +1. `@BeforeMapping` methods without parameters, a `@MappingTarget` parameter or a `@TargetType` parameter are called before any null-checks on source parameters and constructing a new target bean. +2. `@BeforeMapping` methods with a `@MappingTarget` parameter are called after constructing a new target bean. 3. `@AfterMapping` methods are called at the end of the mapping method before the last `return` statement. Within those groups, the method invocations are ordered by their location of definition: @@ -262,4 +261,11 @@ Within those groups, the method invocations are ordered by their location of def *Important:* the order of methods declared within one type can not be guaranteed, as it depends on the compiler and the processing environment implementation. -*Important:* when using a builder, the `@AfterMapping` annotated method must have the builder as `@MappingTarget` annotated parameter so that the method is able to modify the object going to be build. The `build` method is called when the `@AfterMapping` annotated method scope finishes. MapStruct will not call the `@AfterMapping` annotated method if the real target is used as `@MappingTarget` annotated parameter. \ No newline at end of file +[NOTE] +==== +Before/After-mapping methods can also be used with builders: + +* `@BeforeMapping` methods with a `@MappingTarget` parameter of the real target will not be invoked because it is only available after the mapping was already performed. +* To be able to modify the object that is going to be built, the `@AfterMapping` annotated method must have the builder as `@MappingTarget` annotated parameter. The `build` method is called when the `@AfterMapping` annotated method scope finishes. +* The `@AfterMapping` annotated method can also have the real target as `@TargetType` or `@MappingTarget`. It will be invoked after the real target was built (first the methods annotated with `@TargetType`, then the methods annotated with `@MappingTarget`) +==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc b/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc index 51fec968bf..51b2bbff3c 100644 --- a/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc +++ b/documentation/src/main/asciidoc/chapter-13-using-mapstruct-spi.asciidoc @@ -1,7 +1,18 @@ [[using-spi]] == Using the MapStruct SPI + +To use a custom SPI implementation, it must be located in a separate JAR file together with a file named after the SPI (e.g. `org.mapstruct.ap.spi.AccessorNamingStrategy`) in `META-INF/services/` with the fully qualified name of your custom implementation as content (e.g. `org.mapstruct.example.CustomAccessorNamingStrategy`). This JAR file needs to be added to the annotation processor classpath (i.e. add it next to the place where you added the mapstruct-processor jar). + + +[NOTE] +==== +It might also be necessary to add the jar to your IDE's annotation processor factory path. Otherwise you might get an error stating that it cannot be found, while a run using your build tool does succeed. +==== + === Custom Accessor Naming Strategy +SPI name: `org.mapstruct.ap.spi.AccessorNamingStrategy` + MapStruct offers the possibility to override the `AccessorNamingStrategy` via the Service Provider Interface (SPI). A nice example is the use of the fluent API on the source object `GolfPlayer` and `GolfPlayerDto` below. .Source object GolfPlayer with fluent API. @@ -60,7 +71,7 @@ public class GolfPlayerDto { public GolfPlayerDto withName(String name) { this.name = name; - return this + return this; } } ---- @@ -121,14 +132,14 @@ public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy ==== The `CustomAccessorNamingStrategy` makes use of the `DefaultAccessorNamingStrategy` (also available in mapstruct-processor) and relies on that class to leave most of the default behaviour unchanged. -To use a custom SPI implementation, it must be located in a separate JAR file together with the file `META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy` with the fully qualified name of your custom implementation as content (e.g. `org.mapstruct.example.CustomAccessorNamingStrategy`). This JAR file needs to be added to the annotation processor classpath (i.e. add it next to the place where you added the mapstruct-processor jar). - [TIP] Fore more details: The example above is present in our examples repository (https://github.com/mapstruct/mapstruct-examples). -[mapping-exclusion-provider] +[[mapping-exclusion-provider]] === Mapping Exclusion Provider +SPI name: `org.mapstruct.ap.spi.MappingExclusionProvider` + MapStruct offers the possibility to override the `MappingExclusionProvider` via the Service Provider Interface (SPI). A nice example is to not allow MapStruct to create an automatic sub-mapping for a certain type, i.e. MapStruct will not try to generate an automatic sub-mapping method for an excluded type. @@ -177,16 +188,12 @@ include::{processor-ap-test}/nestedbeans/exclusions/custom/CustomMappingExclusio ---- ==== -To use a custom SPI implementation, it must be located in a separate JAR file -together with the file `META-INF/services/org.mapstruct.ap.spi.MappingExclusionProvider` with the fully qualified name of your custom implementation as content -(e.g. `org.mapstruct.example.CustomMappingExclusionProvider`). -This JAR file needs to be added to the annotation processor classpath -(i.e. add it next to the place where you added the mapstruct-processor jar). - [[custom-builder-provider]] === Custom Builder Provider +SPI name: org.mapstruct.ap.spi.BuilderProvider + MapStruct offers the possibility to override the `DefaultProvider` via the Service Provider Interface (SPI). A nice example is to provide support for a custom builder strategy. @@ -202,6 +209,8 @@ include::{processor-ap-main}/spi/NoOpBuilderProvider.java[tag=documentation] [[custom-enum-naming-strategy]] === Custom Enum Naming Strategy +SPI name: `org.mapstruct.ap.spi.EnumMappingStrategy` + MapStruct offers the possibility to override the `EnumMappingStrategy` via the Service Provider Interface (SPI). This can be used when you have certain enums that follow some conventions within your organization. For example all enums which implement an interface named `CustomEnumMarker` are prefixed with `CUSTOM_` @@ -349,6 +358,8 @@ public class CheeseTypeMapperImpl implements CheeseTypeMapper { [[custom-enum-transformation-strategy]] === Custom Enum Transformation Strategy +SPI name: `org.mapstruct.ap.spi.EnumTransformationStrategy` + MapStruct offers the possibility to other transformations strategies by implementing `EnumTransformationStrategy` via the Service Provider Interface (SPI). A nice example is to provide support for a custom transformation strategy. @@ -359,4 +370,55 @@ A nice example is to provide support for a custom transformation strategy. ---- include::{processor-ap-test}/value/nametransformation/CustomEnumTransformationStrategy.java[tag=documentation] ---- +==== + +[[additional-supported-options-provider]] +=== Additional Supported Options Provider + +SPI name: `org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider` + +MapStruct offers the ability to pass through declared compiler args (or "options") provided to the MappingProcessor +to the individual SPIs, by implementing `AdditionalSupportedOptionsProvider` via the Service Provider Interface (SPI). + +.Custom Additional Supported Options Provider that declares `myorg.custom.defaultNullEnumConstant` as an option to pass through +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +include::{processor-ap-test}/additionalsupportedoptions/CustomAdditionalSupportedOptionsProvider.java[tag=documentation] +---- +==== + +The value of this option is provided by including an `arg` to the `compilerArgs` tag when defining your custom SPI +implementation. + +.Example maven configuration with additional options +==== +[source, maven, linenums] +[subs="verbatim,attributes"] +---- + + + + org.myorg + custom-spi-impl + ${project.version} + + + + -Amyorg.custom.defaultNullEnumConstant=MISSING + + +---- +==== + +Your custom SPI implementations can then access this configured value via `MapStructProcessingEnvironment#getOptions()`. + +.Accessing your custom options +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +include::{processor-ap-test}/additionalsupportedoptions/UnknownEnumMappingStrategy.java[tag=documentation] +---- ==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/chapter-14-third-party-api-integration.asciidoc b/documentation/src/main/asciidoc/chapter-14-third-party-api-integration.asciidoc index c31424a0de..ef2a58f82e 100644 --- a/documentation/src/main/asciidoc/chapter-14-third-party-api-integration.asciidoc +++ b/documentation/src/main/asciidoc/chapter-14-third-party-api-integration.asciidoc @@ -46,8 +46,10 @@ public @interface Default { MapStruct works together with https://projectlombok.org/[Project Lombok] as of MapStruct 1.2.0.Beta1 and Lombok 1.16.14. MapStruct takes advantage of generated getters, setters, and constructors and uses them to generate the mapper implementations. +Be reminded that the generated code by Lombok might not always be compatible with the expectations from the individual mappings. +In such a case, either Mapstruct mapping must be changed or Lombok must be configured accordingly using https://projectlombok.org/features/configuration[`lombok.config`] for mutual synergy. -[NOTE] +[WARNING] ==== Lombok 1.18.16 introduces a breaking change (https://projectlombok.org/changelog[changelog]). The additional annotation processor `lombok-mapstruct-binding` (https://mvnrepository.com/artifact/org.projectlombok/lombok-mapstruct-binding[Maven]) must be added otherwise MapStruct stops working with Lombok. @@ -58,7 +60,7 @@ This resolves the compilation issues of Lombok and MapStruct modules. org.projectlombok lombok-mapstruct-binding - 0.1.0 + 0.2.0 ---- ==== @@ -121,7 +123,7 @@ The set up using Maven or Gradle does not differ from what is described in < org.projectlombok lombok-mapstruct-binding - 0.1.0 + 0.2.0 @@ -140,8 +142,8 @@ The set up using Maven or Gradle does not differ from what is described in < org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 - 1.8 - 1.8 + ${java.version} + ${java.version} org.mapstruct @@ -130,7 +130,7 @@ You can find a complete example in the https://github.com/mapstruct/mapstruct-ex The MapStruct code generator can be configured using _annotation processor options_. -When invoking javac directly, these options are passed to the compiler in the form _-Akey=value_. When using MapStruct via Maven, any processor options can be passed using an `options` element within the configuration of the Maven processor plug-in like this: +When invoking javac directly, these options are passed to the compiler in the form _-Akey=value_. When using MapStruct via Maven, any processor options can be passed using `compilerArgs` within the configuration of the Maven processor plug-in like this: .Maven configuration ==== @@ -141,10 +141,10 @@ When invoking javac directly, these options are passed to the compiler in the fo org.apache.maven.plugins maven-compiler-plugin - 3.5.1 + 3.14.1 - 1.8 - 1.8 + ${java.version} + ${java.version} org.mapstruct @@ -215,9 +215,11 @@ suppressGeneratorVersionInfoComment` Supported values are: * `default`: the mapper uses no component model, instances are typically retrieved via `Mappers#getMapper(Class)` -* `cdi`: the generated mapper is an application-scoped CDI bean and can be retrieved via `@Inject` +* `cdi`: the generated mapper is an application-scoped (from javax.enterprise.context or jakarta.enterprise.context, depending on which one is available with javax.inject having priority) CDI bean and can be retrieved via `@Inject` * `spring`: the generated mapper is a singleton-scoped Spring bean and can be retrieved via `@Autowired` -* `jsr330`: the generated mapper is annotated with {@code @Named} and can be retrieved via `@Inject`, e.g. using Spring +* `jsr330`: the generated mapper is annotated with {@code @Named} and can be retrieved via `@Inject` (from javax.inject or jakarta.inject, depending which one is available with javax.inject having priority), e.g. using Spring +* `jakarta`: the generated mapper is annotated with {@code @Named} and can be retrieved via `@Inject` (from jakarta.inject), e.g. using Spring +* `jakarta-cdi`: the generated mapper is an application-scoped (from jakarta.enterprise.context) CDI bean and can be retrieved via `@Inject` If a component model is given for a specific mapper via `@Mapper#componentModel()`, the value from the annotation takes precedence. |`default` @@ -247,6 +249,53 @@ Supported values are: If a policy is given for a specific mapper via `@Mapper#unmappedTargetPolicy()`, the value from the annotation takes precedence. If a policy is given for a specific bean mapping via `@BeanMapping#unmappedTargetPolicy()`, it takes precedence over both `@Mapper#unmappedTargetPolicy()` and the option. |`WARN` + +|`mapstruct.unmappedSourcePolicy` +|The default reporting policy to be applied in case an attribute of the source object of a mapping method is not populated with a target value. + +Supported values are: + +* `ERROR`: any unmapped source property will cause the mapping code generation to fail +* `WARN`: any unmapped source property will cause a warning at build time +* `IGNORE`: unmapped source properties are ignored + +If a policy is given for a specific mapper via `@Mapper#unmappedSourcePolicy()`, the value from the annotation takes precedence. +If a policy is given for a specific bean mapping via `@BeanMapping#ignoreUnmappedSourceProperties()`, it takes precedence over both `@Mapper#unmappedSourcePolicy()` and the option. +|`IGNORE` + +|`mapstruct. +disableBuilders` +|If set to `true`, then MapStruct will not use builder patterns when doing the mapping. This is equivalent to doing `@Mapper( builder = @Builder( disableBuilder = true ) )` for all of your mappers. +|`false` + +|`mapstruct. +disableJSpecify` +|If set to `true`, MapStruct ignores all JSpecify nullness annotations (`@NonNull`, `@Nullable`, `@NullMarked`, `@NullUnmarked`) when deciding whether to generate null checks and does not raise the compilation error for a nullable source mapped to a `@NonNull` constructor parameter. Null-check generation falls back to the `NullValueCheckStrategy`-driven behavior. +|`false` + +|`mapstruct.nullValueIterableMappingStrategy` +|The strategy to be applied when `null` is passed as a source value to an iterable mapping. + +Supported values are: + +* `RETURN_NULL`: if `null` is passed as a source value, then `null` will be returned +* `RETURN_DEFAULT`: if `null` is passed then a default value (empty collection) will be returned + +If a strategy is given for a specific mapper via `@Mapper#nullValueIterableMappingStrategy()`, the value from the annotation takes precedence. +If a strategy is given for a specific iterable mapping via `@IterableMapping#nullValueMappingStrategy()`, it takes precedence over both `@Mapper#nullValueIterableMappingStrategy()` and the option. +|`RETURN_NULL` + +|`mapstruct.nullValueMapMappingStrategy` +|The strategy to be applied when `null` is passed as a source value to a map mapping. + +Supported values are: + +* `RETURN_NULL`: if `null` is passed as a source value, then `null` will be returned +* `RETURN_DEFAULT`: if `null` is passed then a default value (empty map) will be returned + +If a strategy is given for a specific mapper via `@Mapper#nullValueMapMappingStrategy()`, the value from the annotation takes precedence. +If a strategy is given for a specific map mapping via `@MapMapping#nullValueMappingStrategy()`, it takes precedence over both `@Mapper#nullValueMapMappingStrategy()` and the option. +|`RETURN_NULL` |=== === Using MapStruct with the Java Module System @@ -254,3 +303,87 @@ If a policy is given for a specific bean mapping via `@BeanMapping#unmappedTarge MapStruct can be used with Java 9 and higher versions. To allow usage of the `@Generated` annotation `java.annotation.processing.Generated` (part of the `java.compiler` module) can be enabled. + +=== IDE Integration + +There are optional MapStruct plugins for IntelliJ and Eclipse that allow you to have additional completion support (and more) in the annotations. + +==== IntelliJ + +The https://plugins.jetbrains.com/plugin/10036-mapstruct-support[MapStruct IntelliJ] plugin offers assistance in projects that use MapStruct. + +Some features include: + +* Code completion in `target`, `source`, `expression` +* Go To Declaration for properties in `target` and `source` +* Find Usages of properties in `target` and `source` +* Refactoring support +* Errors and Quick Fixes + +==== Eclipse + +The https://marketplace.eclipse.org/content/mapstruct-eclipse-plugin[MapStruct Eclipse] Plugin offers assistance in projects that use MapStruct. + +Some features include: + +* Code completion in `target` and `source` +* Quick Fixes + +[[kotlin-setup]] +=== Kotlin Support + +MapStruct provides support for Kotlin interoperability with Java. + +When using MapStruct with Kotlin, it's recommended to add the `org.jetbrains.kotlin:kotlin-metadata-jvm` library to enable proper introspection of Kotlin metadata: + +.Maven configuration for Kotlin support +==== +[source, xml, linenums] +[subs="verbatim,attributes"] +---- +... + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.jetbrains.kotlin + kotlin-metadata-jvm + ${kotlin.version} + + + + + + +---- +==== + +.Gradle configuration for Kotlin support +==== +[source, groovy, linenums] +[subs="verbatim,attributes"] +---- +dependencies { + implementation "org.mapstruct:mapstruct:${mapstructVersion}" + + annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" + annotationProcessor "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}" +} +---- +==== + +[NOTE] +==== +The `org.jetbrains.kotlin:kotlin-metadata-jvm` dependency is optional but highly recommended when working with Kotlin and Java. +Without it, MapStruct will still work but may not handle complex Kotlin scenarios optimally. +==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc b/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc index 2d2b930114..96c76fc20d 100644 --- a/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc +++ b/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc @@ -39,7 +39,8 @@ The property name as defined in the http://www.oracle.com/technetwork/java/javas ==== [TIP] ==== -By means of the `@BeanMapping(ignoreByDefault = true)` the default behavior will be *explicit mapping*, meaning that all mappings have to be specified by means of the `@Mapping` and no warnings will be issued on missing target properties. +By means of the `@BeanMapping(ignoreByDefault = true)` the default behavior will be *explicit mapping*, meaning that all mappings (including nested ones) have to be specified by means of the `@Mapping` and no warnings will be issued on missing target properties. +This allows to ignore all fields, except the ones that are explicitly defined through `@Mapping`. ==== [TIP] ==== @@ -125,7 +126,7 @@ Collection-typed attributes with the same element type will be copied by creatin MapStruct takes all public properties of the source and target types into account. This includes properties declared on super-types. [[mapping-composition]] -=== Mapping Composition (experimental) +=== Mapping Composition MapStruct supports the use of meta annotations. The `@Mapping` annotation supports now `@Target` with `ElementType#ANNOTATION_TYPE` in addition to `ElementType#METHOD`. This allows `@Mapping` to be used on other (user defined) annotations for re-use purposes. For example: ==== @@ -163,7 +164,7 @@ public interface StorageMapper { Still, they do have some properties in common. The `@ToEntity` assumes both target beans `ShelveEntity` and `BoxEntity` have properties: `"id"`, `"creationDate"` and `"name"`. It furthermore assumes that the source beans `ShelveDto` and `BoxDto` always have a property `"groupName"`. This concept is also known as "duck-typing". In other words, if it quacks like duck, walks like a duck its probably a duck. -This feature is still experimental. Error messages are not mature yet: the method on which the problem occurs is displayed, as well as the concerned values in the `@Mapping` annotation. However, the composition aspect is not visible. The messages are "as if" the `@Mapping` would be present on the concerned method directly. +Error messages are not mature yet: the method on which the problem occurs is displayed, as well as the concerned values in the `@Mapping` annotation. However, the composition aspect is not visible. The messages are "as if" the `@Mapping` would be present on the concerned method directly. Therefore, the user should use this feature with care, especially when uncertain when a property is always present. A more typesafe (but also more verbose) way would be to define base classes / interfaces on the target bean and the source bean and use `@InheritConfiguration` to achieve the same result (see <>). @@ -301,7 +302,7 @@ If you don't want explicitly name all properties from nested source bean, you ca The generated code will map every property from `CustomerDto.record` to `Customer` directly, without need to manually name any of them. The same goes for `Customer.account`. -When there are conflicts, these can be resolved by explicitely defining the mapping. For instance in the example above. `name` occurs in `CustomerDto.record` and in `CustomerDto.account`. The mapping `@Mapping( target = "name", source = "record.name" )` resolves this conflict. +When there are conflicts, these can be resolved by explicitly defining the mapping. For instance in the example above. `name` occurs in `CustomerDto.record` and in `CustomerDto.account`. The mapping `@Mapping( target = "name", source = "record.name" )` resolves this conflict. This "target this" notation can be very useful when mapping hierarchical objects to flat objects and vice versa (`@InheritInverseConfiguration`). @@ -420,8 +421,11 @@ If a Builder exists for a certain type, then that builder will be used for the m The default implementation of the `BuilderProvider` assumes the following: -* The type has a parameterless public static builder creation method that returns a builder. -So for example `Person` has a public static method that returns `PersonBuilder`. +* The type has either +** A parameterless public static builder creation method that returns a builder. +e.g. `Person` has a public static method that returns `PersonBuilder`. +** A public static inner class with the name having the suffix "Builder", and a public no-args constructor +e.g. `Person` has an inner class `PersonBuilder` with a public no-args constructor. * The builder type has a parameterless public method (build method) that returns the type being built. In our example `PersonBuilder` has a method returning `Person`. * In case there are multiple build methods, MapStruct will look for a method called `build`, if such method exists @@ -445,6 +449,11 @@ The <> are also considered for the builder type. E.g. If an object factory exists for our `PersonBuilder` then this factory would be used instead of the builder creation method. ====== +[NOTE] +====== +Detected builders influence `@BeforeMapping` and `@AfterMapping` behavior. See <> for more information. +====== + .Person with Builder example ==== [source, java, linenums] @@ -527,7 +536,7 @@ Otherwise, you would need to write a custom `BuilderProvider` [TIP] ==== -In case you want to disable using builders then you can use the `NoOpBuilderProvider` by creating a `org.mapstruct.ap.spi.BuilderProvider` file in the `META-INF/services` directory with `org.mapstruct.ap.spi.NoOpBuilderProvider` as it's content. +In case you want to disable using builders then you can pass the MapStruct processor option `mapstruct.disableBuilders` to the compiler. e.g. `-Amapstruct.disableBuilders=true`. ==== [[mapping-with-constructors]] @@ -651,6 +660,421 @@ public class PersonMapperImpl implements PersonMapper { ---- ==== +[[mapping-with-optional]] +=== Using Optional + +MapStruct provides comprehensive support for Java's `Optional` type, allowing you to use `Optional` as both source and target types in your mappings. +MapStruct handles all common `Optional` mapping scenarios, including conversions, nested properties, update mappings, and integration with builders and constructors. + +[[optional-basic-behavior]] +==== Basic Optional Behavior + +When using `Optional` as a source type, MapStruct performs presence checks using `Optional.isEmpty()` / `!Optional.isPresent()` instead of `null` checks. +When using `Optional` as a target type, MapStruct returns `Optional.empty()` instead of `null` when the source is absent. + +[[optional-mapping-scenarios]] +==== Optional Mapping Scenarios + +MapStruct supports three primary mapping scenarios with `Optional`: + +1. `Optional` to `Optional` - Both source and target are wrapped in `Optional` +2. `Optional` to Non-`Optional` - Source is `Optional`, target is a regular type +3. Non-`Optional` to `Optional` - Source is a regular type, target is `Optional` + +.Example mapper showing all Optional scenarios +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface ProductMapper { + + Optional map(Optional product); + + ProductDto unwrap(Optional product); + + Optional wrap(Product product); +} +---- +==== + +.Generated code for `Optional` to `Optional` +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +@Override +public Optional map(Optional product) { + if ( product.isEmpty() ) { + return Optional.empty(); + } + + Product productValue = product.get(); + + ProductDto productDto = new ProductDto(); + + productDto.setName( productValue.getName() ); + productDto.setPrice( productValue.getPrice() ); + + return Optional.of( productDto ); +} +---- +==== + +.Generated code for `Optional` to Non-`Optional` +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +@Override +public ProductDto unwrap(Optional product) { + if ( product.isEmpty() ) { + return null; + } + + Product productValue = product.get(); + + ProductDto productDto = new ProductDto(); + + productDto.setName( productValue.getName() ); + productDto.setPrice( productValue.getPrice() ); + + return productDto; +} +---- +==== + +.Generated code for Non-`Optional` to `Optional` +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +@Override +public Optional wrap(Product product) { + if ( product == null ) { + return Optional.empty(); + } + + ProductDto productDto = new ProductDto(); + + productDto.setName( product.getName() ); + productDto.setPrice( product.getPrice() ); + + return Optional.of( productDto ); +} +---- +==== + +[[optional-properties]] +==== Mapping Optional Properties + +MapStruct handles `Optional` properties within your beans seamlessly. +When mapping `Optional` properties, MapStruct generates appropriate presence checks before accessing the values. + +.Example with `Optional` properties +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class Car { + private Optional make; + private Optional numberOfSeats; + private Person driver; + + // getters omitted for brevity +} + +public class CarDto { + private String manufacturer; + private int seatCount; + private PersonDto driver; + + // getters and setters omitted for brevity +} + +@Mapper +public interface CarMapper { + + @Mapping(target = "manufacturer", source = "make") + @Mapping(target = "seatCount", source = "numberOfSeats") + CarDto carToCarDto(Car car); +} +---- +==== + +.Code generated by MapStruct for `Optional` properties +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class CarMapperImpl implements CarMapper { + + @Override + public CarDto carToCarDto(Car car) { + if ( car == null ) { + return null; + } + + CarDto carDto = new CarDto(); + + if ( car.getMake().isPresent() ) { + carDto.setManufacturer( car.getMake().get() ); + } + if ( car.getNumberOfSeats().isPresent() ) { + carDto.setSeatCount( car.getNumberOfSeats().get() ); + } + carDto.setDriver( personToPersonDto( car.getDriver() ) ); + + return carDto; + } + + protected PersonDto personToPersonDto(Person person) { + //... + } +} +---- +==== + +[[optional-update-mappings]] +==== Update Mappings with Optional + +`Optional` works seamlessly with update mappings using `@MappingTarget`. +When the source `Optional` is present, the target object is updated with the unwrapped value. +When the source `Optional` is empty, the behavior depends on the null value property mapping strategy. + +.Update mapping with an `Optional` source +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface ProductUpdateMapper { + + void updateProduct(ProductUpdateDto dto, @MappingTarget Product product); +} + +public class ProductUpdateDto { + private Optional name; + private Optional price; + + // getters omitted +} + +public class Product { + private String name; + private BigDecimal price; + + // getters and setters omitted +} +---- +==== + +.Generated update method +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +@Override +public void updateProduct(ProductUpdateDto dto, Product product) { + if ( dto == null ) { + return; + } + + if ( dto.getName().isPresent() ) { + product.setName( dto.getName().get() ); + } + if ( dto.getPrice().isPresent() ) { + product.setPrice( dto.getPrice().get() ); + } +} +---- +==== + +[[optional-null-value-strategies]] +==== Null Value Mapping Strategies + +When working with `Optional` types, MapStruct's null value mapping strategies behave as follows: + +* For `Optional` return types: `NullValueMappingStrategy.RETURN_DEFAULT` will return `Optional.empty()` instead of `null` +* For `Optional` properties: When a source property `Optional` is empty, `NullValuePropertyMappingStrategy` determines whether to skip the property, set it to null/empty, or use a default value +* `IGNORE` strategy: When a source `Optional` is empty, the target property is not updated +* `SET_TO_NULL` strategy: When a source `Optional` is empty, the target property is set to `null` (for non-`Optional` targets) or `Optional.empty()` (for `Optional` targets) +* `SET_TO_DEFAULT` strategy: When a source `Optional` is empty, the target property is set to its default value + +.Using null value strategies with `Optional` +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface ProductMapper { + + void updateProduct(ProductUpdateDto dto, @MappingTarget Product product); +} +---- +==== + +With the `IGNORE` strategy, when Optional properties are empty, they won't overwrite existing target values. + +[[optional-builders-constructors]] +==== Optional with Builders and Constructors + +MapStruct's Optional support works seamlessly with both builder patterns and constructor-based mappings. + +.Optional with builders +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface PersonMapper { + Optional map(Optional person); +} + +public class PersonDto { + private final String name; + private final Integer age; + + private PersonDto(Builder builder) { + this.name = builder.name; + this.age = builder.age; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private Integer age; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder age(Integer age) { + this.age = age; + return this; + } + + public PersonDto build() { + return new PersonDto(this); + } + } + + // getters omitted +} +---- +==== + +.Generated code using builder +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +@Override +public Optional map(Optional person) { + if ( person.isEmpty() ) { + return Optional.empty(); + } + + Person personValue = person.get(); + + PersonDto.Builder builder = PersonDto.builder(); + + builder.name( personValue.getName() ); + builder.age( personValue.getAge() ); + + return Optional.of( builder.build() ); +} +---- +==== + +.Optional with constructors +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class PersonDto { + private final String name; + private final Integer age; + + public PersonDto(String name, Integer age) { + this.name = name; + this.age = age; + } + + // getters omitted +} +---- +==== + +When the target type uses constructors and has `Optional` properties, MapStruct will properly handle the `Optional` parameters. + +[[optional-complex-mappings]] +==== Complex Optional Mappings + +MapStruct can handle complex scenarios where `Optional` is combined with nested object mappings, collections, and custom mapping methods. + +.Complex nested Optional mappings +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface OrderMapper { + + OrderDto map(Order order); + + // MapStruct will automatically use this for nested mappings + CustomerDto map(Customer customer); +} + +public class Order { + private Optional customer; + private Optional
      shippingAddress; + + // getters omitted +} + +public class OrderDto { + private Optional customer; + private AddressDto shippingAddress; + + // getters and setters omitted +} +---- +==== + +When mapping nested objects that are wrapped in Optional, MapStruct will: + +1. Check if the source `Optional` is present +2. Extract the value from the `Optional` +3. Map the extracted value to the target type +4. Wrap the result in an `Optional` if the target is also Optional + +[[optional-best-practices]] +==== Best Practices and Limitations + +[NOTE] +==== +While MapStruct fully supports `Optional` for target properties (public fields or setter parameters), the Java community generally recommends using `Optional` only for return types (source properties / getters). +Consider your team's coding standards when deciding whether to use `Optional` for target properties. +==== + +[WARNING] +==== +Avoid using `null` when returning `Optional` values to prevent `NullPointerException`. +MapStruct assumes that if you're using `Optional`, the `Optional` itself is not null. +==== + [[mapping-map-to-bean]] === Mapping Map to Bean @@ -696,7 +1120,7 @@ public class CustomerMapperImpl implements CustomerMapper { customer.setId( Integer.parseInt( map.get( "id" ) ) ); } if ( map.containsKey( "customerName" ) ) { - customer.setName( source.get( "customerName" ) ); + customer.setName( map.get( "customerName" ) ); } // ... } @@ -715,3 +1139,143 @@ i.e. You can map from `Map` where for each property a conversio When a raw map or a map that does not have a String as a key is used, then a warning will be generated. The warning is not generated if the map itself is mapped into some other target property directly as is. ==== + +[[adding-annotations]] +=== Adding annotations + +Other frameworks sometimes requires you to add annotations to certain classes so that they can easily detect the mappers. +Using the `@AnnotateWith` annotation you can generate an annotation at the specified location. + +For example Apache Camel has a `@Converter` annotation which you can apply to generated mappers using the `@AnnotateWith` annotation. + +.AnnotateWith source example +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +@AnnotateWith( + value = Converter.class, + elements = @AnnotateWith.Element( name = "generateBulkLoader", booleans = true ) +) +public interface MyConverter { + @AnnotateWith( Converter.class ) + DomainObject map( DtoObject dto ); +} +---- +==== + +.AnnotateWith generated mapper +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Converter( generateBulkLoader = true ) +public class MyConverterImpl implements MyConverter { + @Converter + public DomainObject map( DtoObject dto ) { + // default mapping behaviour + } +} +---- +==== + + +[[javadoc]] +=== Adding Javadoc comments + +MapStruct provides support for defining Javadoc comments in the generated mapper implementation using the +`org.mapstruct.Javadoc` annotation. + +This functionality could be relevant especially in situations where certain Javadoc standards need to be met or +to deal with Javadoc validation constraints. + +The `@Javadoc` annotation defines attributes for the different Javadoc elements. + +Consider the following example: + +.Javadoc annotation example +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +@Javadoc( + value = "This is the description", + authors = { "author1", "author2" }, + deprecated = "Use {@link OtherMapper} instead", + since = "0.1" +) +public interface MyAnnotatedWithJavadocMapper { + //... +} +---- +==== + +.Javadoc annotated generated mapper +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +/** +* This is the description +* +* @author author1 +* @author author2 +* +* @deprecated Use {@link OtherMapper} instead +* @since 0.1 +*/ +public class MyAnnotatedWithJavadocMapperImpl implements MyAnnotatedWithJavadocMapper { + //... +} +---- +==== + +The entire Javadoc comment block can be provided directly as well. + +.Javadoc annotation example with the entire Javadoc comment block provided directly +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +@Javadoc( + "This is the description\n" + + "\n" + + "@author author1\n" + + "@author author2\n" + + "\n" + + "@deprecated Use {@link OtherMapper} instead\n" + + "@since 0.1\n" +) +public interface MyAnnotatedWithJavadocMapper { + //... +} +---- +==== + +Or using Text blocks: + +.Javadoc annotation example with the entire Javadoc comment block provided directly using Text blocks +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +@Javadoc( + """ + This is the description + + @author author1 + @author author2 + + @deprecated Use {@link OtherMapper} instead + @since 0.1 + """ +) +public interface MyAnnotatedWithJavadocMapper { + //... +} +---- +==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc b/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc index 724b42fbb7..ec817e0cdf 100644 --- a/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc +++ b/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc @@ -102,7 +102,7 @@ A mapper which uses other mapper classes (see <>) will o [[injection-strategy]] === Injection strategy -When using <>, you can choose between field and constructor injection. +When using <>, you can choose between constructor, field, or setter injection. This can be done by either providing the injection strategy via `@Mapper` or `@MapperConfig` annotation. .Using constructor injection @@ -117,13 +117,37 @@ public interface CarMapper { ---- ==== -The generated mapper will inject all classes defined in the **uses** attribute. +The generated mapper will inject classes defined in the **uses** attribute if MapStruct has detected that it needs to use an instance of it for a mapping. When `InjectionStrategy#CONSTRUCTOR` is used, the constructor will have the appropriate annotation and the fields won't. When `InjectionStrategy#FIELD` is used, the annotation is on the field itself. +When `InjectionStrategy#SETTER` is used the annotation is on a generated setter method. For now, the default injection strategy is field injection, but it can be configured with <>. It is recommended to use constructor injection to simplify testing. +When you define mappers in Spring with circular dependencies compilation may fail. +In that case utilize the `InjectionStrategy#SETTER` strategy. + [TIP] ==== For abstract classes or decorators setter injection should be used. -==== \ No newline at end of file +==== + +[[class-accessibility]] +=== Class accessibility + +By default, the generated Mapper implementation will have the same accessibility (`public` or none) as the interface or abstract class carrying the `@Mapper` annotation. Especially in conjunction with a DI framework, this might not be desired. If you wish to reduce the visibility of the generated class, you can make use of the `accessibility` attribute like this: + +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, accessibility = ClassAccessibility.PACKAGE_PRIVATE) +public interface CarMapper { + + CarDto carToCarDto(Car car); +} + +---- +==== + +In this case, the generated Mapper will have no modifier. Possible values are `ClassAccessibility.DEFAULT` (the behavior described above), `ClassAccessibility.PUBLIC`, and `ClassAccessibility.PACKAGE_PRIVATE` (no modifier). diff --git a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc index 24f019fa63..10c41506de 100644 --- a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc +++ b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc @@ -43,6 +43,9 @@ public interface CarMapper { ==== * Between `enum` types and `String`. +* Between `enum` types and `Integer`, according to `enum.ordinal()`. +** When converting from an `Integer`, the value needs to be less than the number of values of the enum, otherwise an `ArrayOutOfBoundsException` is thrown. + * Between big number types (`java.math.BigInteger`, `java.math.BigDecimal`) and Java primitive types (including their wrappers) as well as String. A format string as understood by `java.text.DecimalFormat` can be specified. .Conversion from BigDecimal to String @@ -104,6 +107,16 @@ public interface CarMapper { * Between `java.time.Instant` from Java 8 Date-Time package and `java.util.Date`. +* Between `java.time.LocalDateTime` from Java 8 Date-Time package and `java.time.LocalDate` from the same package. + +* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.time.LocalDateTime` from the same package. + +* Between `java.time.OffsetDateTime` from Java 8 Date-Time package and `java.time.LocalDateTime` from the same package. + +* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.time.Instant` from the same package. + +* Between `java.time.OffsetDateTime` from Java 8 Date-Time package and `java.time.Instant` from the same package. + * Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.util.Calendar`. * Between `java.sql.Date` and `java.util.Date` @@ -122,6 +135,23 @@ public interface CarMapper { * Between `String` and `StringBuilder` +* Between `java.net.URI` and `String`. +** When converting from a `String`, the value needs to be a valid https://datatracker.ietf.org/doc/html/rfc2396[RFC 2396 URI] otherwise an `IllegalArgumentException` is thrown. + +* Between `java.net.URL` and `String`. +** When converting from a `String`, the value needs to be a valid https://en.wikipedia.org/wiki/URL[URL] otherwise a `MalformedURLException` is thrown. + +* Between `java.util.Locale` and `String`. +** When converting from a `Locale`, the resulting `String` will be a well-formed IETF BCP 47 language tag representing the locale. When converting from a `String`, the locale that best represents the language tag will be returned. See https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#forLanguageTag-java.lang.String-[Locale.forLanguageTag()] and https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#toLanguageTag--[Locale.toLanguageTag()] for more information. + +[NOTE] +==== +All the above conversions also work when the source or target type is wrapped in `java.util.Optional`, `java.util.OptionalDouble`, `java.util.OptionalLong` or `java.util.OptionalInt`. +For example, `Optional` can be converted to `String`, `int` can be converted to `Optional`, +or `Optional` can be converted to `Integer`. +The same conversion rules apply, with MapStruct automatically handling the wrapping and unwrapping of `Optional` values. +==== + [[mapping-object-references]] === Mapping object references @@ -269,6 +299,13 @@ This puts the configuration of the nested mapping into one place (method) where instead of re-configuring the same things on all of those upper methods. ==== +[TIP] +==== +When ignoring multiple properties instead of defining multiple `@Mapping` annotations, you can use the `@Ignored` annotation to group them together. +e.g. for the `FishTankMapperWithDocument` example above, you could write: +`@Ignored(targets = { "plant", "ornament", "material" })` +==== + [NOTE] ==== In some cases the `ReportingPolicy` that is going to be used for the generated nested method would be `IGNORE`. @@ -390,6 +427,39 @@ When having a custom mapper hooked into the generated mapper with `@Mapper#uses( For instance, the `CarDto` could have a property `owner` of type `Reference` that contains the primary key of a `Person` entity. You could now create a generic custom mapper that resolves any `Reference` objects to their corresponding managed JPA entity instances. +e.g. + +.Example classes for the passing target type example +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class Car { + + private Person owner; + // ... +} + +public class Person extends BaseEntity { + + // ... +} + +public class Reference { + + private String pk; + // ... +} + +public class CarDto { + + private Reference owner; + // ... +} +---- +==== + + .Mapping method expecting mapping target type as parameter ==== [source, java, linenums] @@ -700,3 +770,73 @@ public interface MovieMapper { ==== Although the used mechanism is the same, the user has to be a bit more careful. Refactoring the name of a defined qualifier in an IDE will neatly refactor all other occurrences as well. This is obviously not the case for changing a name. ==== + +=== Combining qualifiers with defaults +Please note that the `Mapping#defaultValue` is in essence a `String`, which needs to be converted to the `Mapping#target`. Providing a `Mapping#qualifiedByName` or `Mapping#qualifiedBy` will force MapStruct to use that method. If you want different behavior for the `Mapping#defaultValue`, then please provide an appropriate mapping method. This mapping method needs to transforms a `String` into the desired type of `Mapping#target` and also be annotated so that it can be found by the `Mapping#qualifiedByName` or `Mapping#qualifiedBy`. + +.Mapper using defaultValue +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface MovieMapper { + + @Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "DEFAULT" ) + GermanRelease toGerman( OriginalRelease movies ); + + @Named("CategoryToString") + default String defaultValueForQualifier(Category cat) { + // some mapping logic + } +} +---- +==== + +In the above example in case that category is null, the method `CategoryToString( Enum.valueOf( Category.class, "DEFAULT" ) )` will be called and the result will be set to the category field. + +.Mapper using defaultValue and default method. +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface MovieMapper { + + @Mapping( target = "category", qualifiedByName = "CategoryToString", defaultValue = "Unknown" ) + GermanRelease toGerman( OriginalRelease movies ); + + @Named("CategoryToString") + default String defaultValueForQualifier(Category cat) { + // some mapping logic + } + + @Named("CategoryToString") + default String defaultValueForQualifier(String value) { + return value; + } +} +---- +==== +In the above example in case that category is null, the method `defaultValueForQualifier( "Unknown" )` will be called and the result will be set to the category field. + +If the above mentioned methods do not work there is the option to use `defaultExpression` to set the default value. + +.Mapper using defaultExpression +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface MovieMapper { + + @Mapping( target = "category", qualifiedByName = "CategoryToString", defaultExpression = "java(\"Unknown\")" ) + GermanRelease toGerman( OriginalRelease movies ); + + @Named("CategoryToString") + default String defaultValueForQualifier(Category cat) { + // some mapping logic + } +} +---- +==== diff --git a/documentation/src/main/asciidoc/chapter-6-mapping-collections.asciidoc b/documentation/src/main/asciidoc/chapter-6-mapping-collections.asciidoc index 17025da4de..4510c82cc0 100644 --- a/documentation/src/main/asciidoc/chapter-6-mapping-collections.asciidoc +++ b/documentation/src/main/asciidoc/chapter-6-mapping-collections.asciidoc @@ -192,7 +192,7 @@ The option `DEFAULT` should not be used explicitly. It is used to distinguish be [TIP] ==== -When working with an `adder` method and JPA entities, Mapstruct assumes that the target collections are initialized with a collection implementation (e.g. an `ArrayList`). You can use factories to create a new target entity with intialized collections instead of Mapstruct creating the target entity by its constructor. +When working with an `adder` method and JPA entities, Mapstruct assumes that the target collections are initialized with a collection implementation (e.g. an `ArrayList`). You can use factories to create a new target entity with initialized collections instead of Mapstruct creating the target entity by its constructor. ==== [[implementation-types-for-collection-mappings]] @@ -212,16 +212,20 @@ When an iterable or map mapping method declares an interface type as return type |`Set`|`LinkedHashSet` +|`SequencedSet`|`LinkedHashSet` + |`SortedSet`|`TreeSet` |`NavigableSet`|`TreeSet` |`Map`|`LinkedHashMap` +|`SequencedMap`|`LinkedHashMap` + |`SortedMap`|`TreeMap` |`NavigableMap`|`TreeMap` |`ConcurrentMap`|`ConcurrentHashMap` |`ConcurrentNavigableMap`|`ConcurrentSkipListMap` -|=== +|=== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc b/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc index 24c7324f1d..fcb353010d 100644 --- a/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc +++ b/documentation/src/main/asciidoc/chapter-8-mapping-values.asciidoc @@ -65,7 +65,7 @@ public class OrderMapperImpl implements OrderMapper { ---- ==== By default an error will be raised by MapStruct in case a constant of the source enum type does not have a corresponding constant with the same name in the target type and also is not mapped to another constant via `@ValueMapping`. This ensures that all constants are mapped in a safe and predictable manner. The generated -mapping method will throw an IllegalStateException if for some reason an unrecognized source value occurs. +mapping method will throw an `IllegalStateException` if for some reason an unrecognized source value occurs. MapStruct also has a mechanism for mapping any remaining (unspecified) mappings to a default. This can be used only once in a set of value mappings and only applies to the source. It comes in two flavors: `` and ``. They cannot be used at the same time. @@ -75,14 +75,18 @@ MapStruct will *not* attempt such name based mapping for `` and di MapStruct is able to handle `null` sources and `null` targets by means of the `` keyword. +In addition, the constant value `` can be used for throwing an exception for particular value mappings. This value is only applicable to `ValueMapping#target()` and not `ValueMapping#source()` since MapStruct can't map from exceptions. + [TIP] ==== -Constants for ``, `` and `` are available in the `MappingConstants` class. +Constants for ``, ``, `` and `` are available in the `MappingConstants` class. ==== Finally `@InheritInverseConfiguration` and `@InheritConfiguration` can be used in combination with `@ValueMappings`. `` and `` will be ignored in that case. -.Enum mapping method, and +The following code snippets exemplify the use of the aforementioned constants. + +.Enum mapping method, `` and `` ==== [source, java, linenums] [subs="verbatim,attributes"] @@ -102,7 +106,7 @@ public interface SpecialOrderMapper { ---- ==== -.Enum mapping method result, and +.Enum mapping method result, `` and `` ==== [source, java, linenums] [subs="verbatim,attributes"] @@ -137,9 +141,52 @@ public class SpecialOrderMapperImpl implements SpecialOrderMapper { *Note:* MapStruct would have refrained from mapping the `RETAIL` and `B2B` when `` was used instead of ``. -[WARNING] +.Enum mapping method with `` ==== -The mapping of enum to enum via the `@Mapping` annotation is *DEPRECATED*. It will be removed from future versions of MapStruct. Please adapt existing enum mapping methods to make use of `@ValueMapping` instead. +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface SpecialOrderMapper { + + SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class ); + + @ValueMappings({ + @ValueMapping( source = "STANDARD", target = "DEFAULT" ), + @ValueMapping( source = "C2C", target = MappingConstants.THROW_EXCEPTION ) + }) + ExternalOrderType orderTypeToExternalOrderType(OrderType orderType); +} +---- +==== + +.Enum mapping method with `` result +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class SpecialOrderMapperImpl implements SpecialOrderMapper { + + @Override + public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType; + + switch ( orderType ) { + case STANDARD: externalOrderType = ExternalOrderType.DEFAULT; + break; + case C2C: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType ); + default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType ); + } + + return externalOrderType; + } +} +---- ==== === Mapping enum-to-String or String-to-enum @@ -152,6 +199,7 @@ MapStruct supports enum to a String mapping along the same lines as is described 2. Similarity: ` stops after handling defined mapping and proceeds to the switch/default clause value. 3. Difference: `` will result in an error. It acts on the premise that there is name similarity between enum constants in source and target which does not make sense for a String type. 4. Difference: Given 1. and 3. there will never be unmapped values. +5. Similarity: `` can be used for throwing an exception for particular enum values. *`String` to enum* @@ -159,6 +207,7 @@ MapStruct supports enum to a String mapping along the same lines as is described 2. Similarity: ` stops after handling defined mapping and proceeds to the switch/default clause value. 3. Similarity: `` will create a mapping for each target enum constant and proceed to the switch/default clause value. 4. Difference: A switch/default value needs to be provided to have a determined outcome (enum has a limited set of values, `String` has unlimited options). Failing to specify `` or ` will result in a warning. +5. Similarity: `` can be used for throwing an exception for any arbitrary `String` value. === Custom name transformation @@ -266,3 +315,42 @@ MapStruct provides the following out of the box enum name transformation strateg It is also possible to register custom strategies. For more information on how to do that have a look at <> + +[[value-mapping-composition]] +=== ValueMapping Composition + +The `@ValueMapping` annotation supports now `@Target` with `ElementType#ANNOTATION_TYPE` in addition to `ElementType#METHOD`. +This allows `@ValueMapping` to be used on other (user defined) annotations for re-use purposes. +For example: + +.Custom value mapping annotations +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Retention( RetentionPolicy.CLASS ) +@ValueMapping(source = "EXTRA", target = "SPECIAL") +@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "DEFAULT") +public @interface CustomValueAnnotation { +} +---- +==== +It can be used to describe some common value mapping relationships to avoid duplicate declarations, as in the following example: + +.Using custom combination annotations +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface ValueMappingCompositionMapper { + + @CustomValueAnnotation + ExternalOrderType orderTypeToExternalOrderType(OrderType orderType); + + @CustomValueAnnotation + @ValueMapping(source = "STANDARD", target = "SPECIAL") + ExternalOrderType duplicateAnnotation(OrderType orderType); +} +---- +==== diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 4b48c19496..71a61fc2fe 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml @@ -107,19 +107,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - - - check-style - verify - - checkstyle - - - - @@ -133,10 +120,33 @@ javax.xml.bind jaxb-api - 2.3.1 provided + true + + jdk-21-or-newer + + [21 + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + check-style + verify + + checkstyle + + + + + + + diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationExclusionCliEnhancer.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationExclusionCliEnhancer.java index c8bac6208c..7d397c9499 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationExclusionCliEnhancer.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationExclusionCliEnhancer.java @@ -26,16 +26,42 @@ public Collection getAdditionalCommandLineArguments(ProcessorTest.Proces // SPI not working correctly here.. (not picked up) additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1596/*.java" ); additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1801/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/bugs/_3089/*.java" ); switch ( currentJreVersion ) { + case JAVA_8: + additionalExcludes.add( "org/mapstruct/ap/test/**/spring/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/injectionstrategy/cdi/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/annotatewith/deprecated/jdk11/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk21/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk17/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk17/**/*.java" ); + if ( processorType == ProcessorTest.ProcessorType.ECLIPSE_JDT ) { + additionalExcludes.add( + "org/mapstruct/ap/test/selection/methodgenerics/wildcards/LifecycleIntersectionMapper.java" ); + } + break; case JAVA_9: // TODO find out why this fails: additionalExcludes.add( "org/mapstruct/ap/test/collection/wildcard/BeanMapper.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk17/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk17/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk21/*.java" ); + break; + case JAVA_11: + additionalExcludes.add( "org/mapstruct/ap/test/**/spring/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk17/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk17/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk21/*.java" ); + break; + case JAVA_17: + additionalExcludes.add( "org/mapstruct/ap/test/**/jdk21/*.java" ); break; default: } - Collection result = new ArrayList( additionalExcludes.size() ); + Collection result = new ArrayList<>(additionalExcludes.size()); for ( int i = 0; i < additionalExcludes.size(); i++ ) { result.add( "-DadditionalExclude" + i + "=" + additionalExcludes.get( i ) ); } diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java index 0162582468..a12af7690f 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/GradleIncrementalCompilationTest.java @@ -13,47 +13,49 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.condition.DisabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.runners.Parameterized.Parameters; +import static org.assertj.core.api.Assertions.assertThat; import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE; -import static org.hamcrest.core.StringContains.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** *

      This is supposed to be run from the mapstruct root project folder. * Otherwise, use -Dmapstruct_root=path_to_project. */ @DisabledForJreRange(min = JRE.JAVA_11) -public class GradleIncrementalCompilationTest { +class GradleIncrementalCompilationTest { private static Path rootPath; - private static String projectDir = "integrationtest/src/test/resources/gradleIncrementalCompilationTest"; - private static String compileTaskName = "compileJava"; + private static final String PROJECT_DIR = "integrationtest/src/test/resources/gradleIncrementalCompilationTest"; + private static final String COMPILE_TASK_NAME = "compileJava"; @TempDir - File testBuildDir; + private File testBuildDir; @TempDir - File testProjectDir; + private File testProjectDir; private GradleRunner runner; private File sourceDirectory; private List compileArgs; // Gradle compile task arguments - @Parameters(name = "Gradle {0}") - public static List gradleVersions() { - return Arrays.asList( "5.0", "6.0" ); + static Stream gradleVersions() { + return Stream.of( + Arguments.of( Named.of( "Gradle 5.0", "5.0" ) ), + Arguments.of( Named.of( "Gradle 6.0", "6.0" ) ) ); } private void replaceInFile(File file, CharSequence target, CharSequence replacement) throws IOException { @@ -62,21 +64,19 @@ private void replaceInFile(File file, CharSequence target, CharSequence replacem } private GradleRunner getRunner(String... additionalArguments) { - List fullArguments = new ArrayList( compileArgs ); + List fullArguments = new ArrayList<>(compileArgs); fullArguments.addAll( Arrays.asList( additionalArguments ) ); return runner.withArguments( fullArguments ); } private void assertCompileOutcome(BuildResult result, TaskOutcome outcome) { - assertEquals( outcome, result.task( ":" + compileTaskName ).getOutcome() ); + assertEquals( outcome, result.task( ":" + COMPILE_TASK_NAME ).getOutcome() ); } private void assertRecompiled(BuildResult result, int recompiledCount) { assertCompileOutcome( result, recompiledCount > 0 ? SUCCESS : UP_TO_DATE ); - assertThat( - result.getOutput(), - containsString( String.format( "Incremental compilation of %d classes completed", recompiledCount ) ) - ); + assertThat( result.getOutput() ) + .contains( String.format( "Incremental compilation of %d classes completed", recompiledCount ) ); } private List buildCompileArgs() { @@ -85,11 +85,11 @@ private List buildCompileArgs() { // Inject the path to the folder containing the mapstruct-processor JAR String jarDirectoryArg = "-PmapstructRootPath=" + rootPath.toString(); - return Arrays.asList( compileTaskName, buildDirPropertyArg, jarDirectoryArg ); + return Arrays.asList( COMPILE_TASK_NAME, buildDirPropertyArg, jarDirectoryArg ); } @BeforeAll - public static void setupClass() throws Exception { + static void setupClass() { rootPath = Paths.get( System.getProperty( "mapstruct_root", "." ) ).toAbsolutePath(); } @@ -102,7 +102,7 @@ public void setup(String gradleVersion) throws IOException { testProjectDir.mkdirs(); } // Copy test project files to the temp dir - Path gradleProjectPath = rootPath.resolve( projectDir ); + Path gradleProjectPath = rootPath.resolve( PROJECT_DIR ); FileUtils.copyDirectory( gradleProjectPath.toFile(), testProjectDir ); compileArgs = buildCompileArgs(); sourceDirectory = new File( testProjectDir, "src/main/java" ); @@ -111,7 +111,7 @@ public void setup(String gradleVersion) throws IOException { @ParameterizedTest @MethodSource("gradleVersions") - public void testBuildSucceeds(String gradleVersion) throws IOException { + void testBuildSucceeds(String gradleVersion) throws IOException { setup( gradleVersion ); // Make sure the test build setup actually compiles BuildResult buildResult = getRunner().build(); @@ -120,7 +120,7 @@ public void testBuildSucceeds(String gradleVersion) throws IOException { @ParameterizedTest @MethodSource("gradleVersions") - public void testUpToDate(String gradleVersion) throws IOException { + void testUpToDate(String gradleVersion) throws IOException { setup( gradleVersion ); getRunner().build(); BuildResult secondBuildResult = getRunner().build(); @@ -129,7 +129,7 @@ public void testUpToDate(String gradleVersion) throws IOException { @ParameterizedTest @MethodSource("gradleVersions") - public void testChangeConstant(String gradleVersion) throws IOException { + void testChangeConstant(String gradleVersion) throws IOException { setup( gradleVersion ); getRunner().build(); // Change return value in class Target @@ -143,7 +143,7 @@ public void testChangeConstant(String gradleVersion) throws IOException { @ParameterizedTest @MethodSource("gradleVersions") - public void testChangeTargetField(String gradleVersion) throws IOException { + void testChangeTargetField(String gradleVersion) throws IOException { setup( gradleVersion ); getRunner().build(); // Change target field in mapper interface @@ -157,7 +157,7 @@ public void testChangeTargetField(String gradleVersion) throws IOException { @ParameterizedTest @MethodSource("gradleVersions") - public void testChangeUnrelatedFile(String gradleVersion) throws IOException { + void testChangeUnrelatedFile(String gradleVersion) throws IOException { setup( gradleVersion ); getRunner().build(); File unrelatedFile = new File( sourceDirectory, "org/mapstruct/itest/gradle/lib/UnrelatedComponent.java" ); diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/KotlinFullFeatureCompilationExclusionCliEnhancer.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/KotlinFullFeatureCompilationExclusionCliEnhancer.java new file mode 100644 index 0000000000..b4dddfb308 --- /dev/null +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/KotlinFullFeatureCompilationExclusionCliEnhancer.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.tests; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.condition.JRE; +import org.mapstruct.itest.testutil.extension.ProcessorTest; + +/** + * Adds explicit exclusions of test mappers that are known or expected to not work with specific compilers. + * + * @author Filip Hrisafov + */ +public final class KotlinFullFeatureCompilationExclusionCliEnhancer implements ProcessorTest.CommandLineEnhancer { + @Override + public Collection getAdditionalCommandLineArguments(ProcessorTest.ProcessorType processorType, + JRE currentJreVersion) { + List additionalExcludes = new ArrayList<>(); + + + switch ( currentJreVersion ) { + case JAVA_8: + addJdkExclude( additionalExcludes, "jdk17" ); + addJdkExclude( additionalExcludes, "jdk21" ); + break; + case JAVA_11: + addJdkExclude( additionalExcludes, "jdk17" ); + addJdkExclude( additionalExcludes, "jdk21" ); + break; + case JAVA_17: + addJdkExclude( additionalExcludes, "jdk21" ); + break; + default: + } + + Collection result = new ArrayList<>( additionalExcludes.size() ); + for ( int i = 0; i < additionalExcludes.size(); i++ ) { + result.add( "-DadditionalExclude" + i + "=" + additionalExcludes.get( i ) ); + } + + return result; + } + + private static void addJdkExclude(Collection additionalExcludes, String jdk) { + additionalExcludes.add( "org/mapstruct/ap/test/**/kotlin/**/" + jdk + "/**/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/**/kotlin/**/" + jdk + "/**/*.kt" ); + } +} diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java index 2ec0ba9aa9..042d6ec1da 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java @@ -6,6 +6,7 @@ package org.mapstruct.itest.tests; import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.parallel.Execution; @@ -26,7 +27,6 @@ void autoValueBuilderTest() { } @ProcessorTest(baseDir = "cdiTest") - @DisabledForJreRange(min = JRE.JAVA_16) void cdiTest() { } @@ -77,16 +77,31 @@ void java8Test() { void jaxbTest() { } + @ProcessorTest(baseDir = "jakartaJaxbTest") + void jakartaJaxbTest() { + } + @ProcessorTest(baseDir = "jsr330Test") + @EnabledForJreRange(min = JRE.JAVA_17) void jsr330Test() { } @ProcessorTest(baseDir = "lombokBuilderTest", processorTypes = { ProcessorTest.ProcessorType.JAVAC }) + @DisabledOnJre(versions = { 27, 28 }) void lombokBuilderTest() { } + @ProcessorTest(baseDir = "lombokModuleTest", processorTypes = { + ProcessorTest.ProcessorType.JAVAC, + ProcessorTest.ProcessorType.JAVAC_WITH_PATHS + }) + @EnabledForJreRange(min = JRE.JAVA_11) + @DisabledOnJre(versions = { 27, 28 }) + void lombokModuleTest() { + } + @ProcessorTest(baseDir = "namingStrategyTest", processorTypes = { ProcessorTest.ProcessorType.JAVAC }) @@ -102,6 +117,11 @@ void namingStrategyTest() { void protobufBuilderTest() { } + @ProcessorTest(baseDir = "sealedSubclassTest") + @EnabledForJreRange(min = JRE.JAVA_17) + void sealedSubclassTest() { + } + @ProcessorTest(baseDir = "recordsTest", processorTypes = { ProcessorTest.ProcessorType.JAVAC }) @@ -116,19 +136,47 @@ void recordsTest() { void recordsCrossModuleTest() { } + @ProcessorTest(baseDir = "recordsCrossModuleInterfaceTest", processorTypes = { + ProcessorTest.ProcessorType.JAVAC + }) + @EnabledForJreRange(min = JRE.JAVA_17) + void recordsCrossModuleInterfaceTest() { + } + + @ProcessorTest(baseDir = "expressionTextBlocksTest", processorTypes = { + ProcessorTest.ProcessorType.JAVAC + }) + @EnabledForJreRange(min = JRE.JAVA_17) + void expressionTextBlocksTest() { + } + @ProcessorTest(baseDir = "kotlinDataTest", processorTypes = { ProcessorTest.ProcessorType.JAVAC }, forkJvm = true) // We have to fork the jvm because there is an NPE in com.intellij.openapi.util.SystemInfo.getRtVersion // and the kotlin-maven-plugin uses that. See also https://youtrack.jetbrains.com/issue/IDEA-238907 + @DisabledOnJre(versions = 27) void kotlinDataTest() { } + @ProcessorTest(baseDir = "kotlinFullFeatureTest", processorTypes = { + ProcessorTest.ProcessorType.JAVAC_WITH_PATHS + }, commandLineEnhancer = KotlinFullFeatureCompilationExclusionCliEnhancer.class) + @DisabledForJreRange(min = JRE.JAVA_26) + void kotlinFullFeatureTest() { + } + @ProcessorTest(baseDir = "simpleTest") void simpleTest() { } + // for issue #2593 + @ProcessorTest(baseDir = "defaultPackage") + void defaultPackageTest() { + } + @ProcessorTest(baseDir = "springTest") + @EnabledForJreRange(min = JRE.JAVA_17) void springTest() { } diff --git a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java index 89009aaffe..39cd5fdae6 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java @@ -134,17 +134,18 @@ private void addAdditionalCliArguments(Verifier verifier) } private void configureProcessor(Verifier verifier) { - String compilerId = processorTestContext.getProcessor().getCompilerId(); - if ( compilerId != null ) { - String profile = processorTestContext.getProcessor().getProfile(); - if ( profile == null ) { - profile = "generate-via-compiler-plugin"; + ProcessorTest.ProcessorType processor = processorTestContext.getProcessor(); + String compilerId = processor.getCompilerId(); + String profile = processor.getProfile(); + if ( profile == null ) { + profile = "generate-via-compiler-plugin"; + } + verifier.addCliOption( "-P" + profile ); + verifier.addCliOption( "-Dcompiler-id=" + compilerId ); + if ( processor == ProcessorTest.ProcessorType.JAVAC ) { + if ( CURRENT_VERSION.ordinal() >= JRE.JAVA_21.ordinal() ) { + verifier.addCliOption( "-Dmaven.compiler.proc=full" ); } - verifier.addCliOption( "-P" + profile ); - verifier.addCliOption( "-Dcompiler-id=" + compilerId ); - } - else { - verifier.addCliOption( "-Pgenerate-via-processor-plugin" ); } } diff --git a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java index 95480eee9d..d5b4860d5b 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java @@ -46,9 +46,7 @@ enum ProcessorType { JAVAC( "javac" ), JAVAC_WITH_PATHS( "javac", JRE.OTHER, "generate-via-compiler-plugin-with-annotation-processor-paths" ), - ECLIPSE_JDT( "jdt", JRE.JAVA_8 ), - - PROCESSOR_PLUGIN( null, JRE.JAVA_8 ); + ECLIPSE_JDT( "jdt", JRE.JAVA_8 ); private final String compilerId; private final JRE max; @@ -111,7 +109,6 @@ ProcessorType[] processorTypes() default { ProcessorType.JAVAC, ProcessorType.JAVAC_WITH_PATHS, ProcessorType.ECLIPSE_JDT, - ProcessorType.PROCESSOR_PLUGIN }; /** diff --git a/integrationtest/src/test/resources/autoValueBuilderTest/src/test/java/org/mapstruct/itest/auto/value/AutoValueMapperTest.java b/integrationtest/src/test/resources/autoValueBuilderTest/src/test/java/org/mapstruct/itest/auto/value/AutoValueMapperTest.java index 8f640780f3..4ce5530a5d 100644 --- a/integrationtest/src/test/resources/autoValueBuilderTest/src/test/java/org/mapstruct/itest/auto/value/AutoValueMapperTest.java +++ b/integrationtest/src/test/resources/autoValueBuilderTest/src/test/java/org/mapstruct/itest/auto/value/AutoValueMapperTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.auto.value; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,10 +14,10 @@ * * @author Eric Martineau */ -public class AutoValueMapperTest { +class AutoValueMapperTest { @Test - public void testSimpleImmutableBuilderHappyPath() { + void testSimpleImmutableBuilderHappyPath() { PersonDto personDto = PersonMapper.INSTANCE.toDto( Person.builder() .age( 33 ) .name( "Bob" ) @@ -32,7 +32,7 @@ public void testSimpleImmutableBuilderHappyPath() { } @Test - public void testLombokToImmutable() { + void testLombokToImmutable() { Person person = PersonMapper.INSTANCE.fromDto( new PersonDto( "Bob", 33, new AddressDto( "Wild Drive" ) ) ); assertThat( person.getAge() ).isEqualTo( 33 ); assertThat( person.getName() ).isEqualTo( "Bob" ); diff --git a/integrationtest/src/test/resources/cdiTest/pom.xml b/integrationtest/src/test/resources/cdiTest/pom.xml index 30d35a421e..cb00fbaa01 100644 --- a/integrationtest/src/test/resources/cdiTest/pom.xml +++ b/integrationtest/src/test/resources/cdiTest/pom.xml @@ -30,8 +30,8 @@ javax.inject - org.jboss.arquillian.junit - arquillian-junit-container + org.jboss.arquillian.junit5 + arquillian-junit5-container test @@ -45,8 +45,8 @@ test - org.jboss.arquillian.junit - arquillian-junit-core + org.jboss.arquillian.junit5 + arquillian-junit5-core test @@ -56,7 +56,7 @@ org.jboss.weld - weld-core + weld-core-impl test diff --git a/integrationtest/src/test/resources/cdiTest/src/main/java/org/mapstruct/itest/cdi/DecoratedSourceTargetMapper.java b/integrationtest/src/test/resources/cdiTest/src/main/java/org/mapstruct/itest/cdi/DecoratedSourceTargetMapper.java index 18a57ce25e..23bb51a6c9 100644 --- a/integrationtest/src/test/resources/cdiTest/src/main/java/org/mapstruct/itest/cdi/DecoratedSourceTargetMapper.java +++ b/integrationtest/src/test/resources/cdiTest/src/main/java/org/mapstruct/itest/cdi/DecoratedSourceTargetMapper.java @@ -5,9 +5,9 @@ */ package org.mapstruct.itest.cdi; +import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; -import org.mapstruct.DecoratedWith; import org.mapstruct.itest.cdi.other.DateMapper; @Mapper( componentModel = MappingConstants.ComponentModel.CDI, uses = DateMapper.class ) diff --git a/integrationtest/src/test/resources/cdiTest/src/test/java/org/mapstruct/itest/cdi/CdiBasedMapperTest.java b/integrationtest/src/test/resources/cdiTest/src/test/java/org/mapstruct/itest/cdi/CdiBasedMapperTest.java index 702ab6d255..95d2064938 100644 --- a/integrationtest/src/test/resources/cdiTest/src/test/java/org/mapstruct/itest/cdi/CdiBasedMapperTest.java +++ b/integrationtest/src/test/resources/cdiTest/src/test/java/org/mapstruct/itest/cdi/CdiBasedMapperTest.java @@ -5,26 +5,26 @@ */ package org.mapstruct.itest.cdi; -import static org.assertj.core.api.Assertions.assertThat; - import javax.inject.Inject; import org.jboss.arquillian.container.test.api.Deployment; -import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit5.ArquillianExtension; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.itest.cdi.other.DateMapper; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test for generation of CDI-based mapper implementations. * * @author Gunnar Morling */ -@RunWith( Arquillian.class ) -public class CdiBasedMapperTest { +@ExtendWith( ArquillianExtension.class ) +class CdiBasedMapperTest { @Inject private SourceTargetMapper mapper; @@ -38,12 +38,14 @@ public static JavaArchive createDeployment() { .addPackage( SourceTargetMapper.class.getPackage() ) .addPackage( DateMapper.class.getPackage() ) .addAsManifestResource( - new StringAsset("org.mapstruct.itest.cdi.SourceTargetMapperDecorator"), + new StringAsset("" + + "org.mapstruct.itest.cdi.SourceTargetMapperDecorator" + + ""), "beans.xml" ); } @Test - public void shouldCreateCdiBasedMapper() { + void shouldCreateCdiBasedMapper() { Source source = new Source(); Target target = mapper.sourceToTarget( source ); @@ -54,7 +56,7 @@ public void shouldCreateCdiBasedMapper() { } @Test - public void shouldInjectDecorator() { + void shouldInjectDecorator() { Source source = new Source(); Target target = decoratedMapper.sourceToTarget( source ); diff --git a/integrationtest/src/test/resources/defaultPackage/main/java/DefaultPackageObject.java b/integrationtest/src/test/resources/defaultPackage/main/java/DefaultPackageObject.java new file mode 100644 index 0000000000..ba684850b4 --- /dev/null +++ b/integrationtest/src/test/resources/defaultPackage/main/java/DefaultPackageObject.java @@ -0,0 +1,97 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +public class DefaultPackageObject { + public enum CarType { + SEDAN, CAMPER, X4, TRUCK; + } + + static public class Car { + + private String make; + private int numberOfSeats; + private CarType type; + + public Car(String string, int numberOfSeats, CarType sedan) { + this.make = string; + this.numberOfSeats = numberOfSeats; + this.type = sedan; + } + + + public String getMake() { + return make; + } + + public void setMake(String make) { + this.make = make; + } + + public int getNumberOfSeats() { + return numberOfSeats; + } + + public void setNumberOfSeats(int numberOfSeats) { + this.numberOfSeats = numberOfSeats; + } + + public CarType getType() { + return type; + } + + public void setType(CarType type) { + this.type = type; + } + } + + static public class CarDto { + + private String make; + private int seatCount; + private String type; + + public String getMake() { + return make; + } + + public void setMake(String make) { + this.make = make; + } + + public int getSeatCount() { + return seatCount; + } + + public void setSeatCount(int seatCount) { + this.seatCount = seatCount; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } + + + @Mapper + public interface CarMapper { + + CarMapper INSTANCE = Mappers.getMapper( CarMapper.class ); + + @Mapping(source = "numberOfSeats", target = "seatCount") + CarDto carToCarDto(Car car); + } +} diff --git a/integrationtest/src/test/resources/defaultPackage/pom.xml b/integrationtest/src/test/resources/defaultPackage/pom.xml new file mode 100644 index 0000000000..d72300f618 --- /dev/null +++ b/integrationtest/src/test/resources/defaultPackage/pom.xml @@ -0,0 +1,21 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + defaultPackage + jar + diff --git a/integrationtest/src/test/resources/defaultPackage/test/java/DefaultPackageTest.java b/integrationtest/src/test/resources/defaultPackage/test/java/DefaultPackageTest.java new file mode 100644 index 0000000000..c3f84b6ab6 --- /dev/null +++ b/integrationtest/src/test/resources/defaultPackage/test/java/DefaultPackageTest.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ + +class DefaultPackageTest { + + @Test + void shouldWorkCorrectlyInDefaultPackage() { + DefaultPackageObject.CarDto carDto = DefaultPackageObject.CarMapper.INSTANCE.carToCarDto( + new DefaultPackageObject.Car( + "Morris", + 5, + DefaultPackageObject.CarType.SEDAN + ) ); + + assertThat( carDto ).isNotNull(); + assertThat( carDto.getMake() ).isEqualTo( "Morris" ); + assertThat( carDto.getSeatCount() ).isEqualTo( 5 ); + assertThat( carDto.getType() ).isEqualTo( "SEDAN" ); + } +} diff --git a/integrationtest/src/test/resources/expressionTextBlocksTest/pom.xml b/integrationtest/src/test/resources/expressionTextBlocksTest/pom.xml new file mode 100644 index 0000000000..b70c48f9d0 --- /dev/null +++ b/integrationtest/src/test/resources/expressionTextBlocksTest/pom.xml @@ -0,0 +1,22 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + expressionTextBlocksTest + jar + + diff --git a/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/Car.java b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/Car.java new file mode 100644 index 0000000000..28d4fd3cd5 --- /dev/null +++ b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/Car.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.textBlocks; + +/** + * @author Filip Hrisafov + */ +public class Car { + + private WheelPosition wheelPosition; + + public WheelPosition getWheelPosition() { + return wheelPosition; + } + + public void setWheelPosition(WheelPosition wheelPosition) { + this.wheelPosition = wheelPosition; + } +} diff --git a/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/CarAndWheelMapper.java b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/CarAndWheelMapper.java new file mode 100644 index 0000000000..23d665c6ff --- /dev/null +++ b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/CarAndWheelMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.textBlocks; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface CarAndWheelMapper { + + CarAndWheelMapper INSTANCE = Mappers.getMapper( CarAndWheelMapper.class ); + + @Mapping(target = "wheelPosition", + expression = + """ + java( + source.getWheelPosition() == null ? + null : + source.getWheelPosition().getPosition() + ) + """) + CarDto carDtoFromCar(Car source); + + @Mapping(target = "wheelPosition", + expression = """ + java( + source.wheelPosition() == null ? + null : + new WheelPosition(source.wheelPosition()) + ) + """) + Car carFromCarDto(CarDto source); +} diff --git a/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/CarDto.java b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/CarDto.java new file mode 100644 index 0000000000..f4baa4d044 --- /dev/null +++ b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/CarDto.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.textBlocks; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public record CarDto(String wheelPosition) { + +} diff --git a/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/WheelPosition.java b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/WheelPosition.java new file mode 100644 index 0000000000..6effd485a3 --- /dev/null +++ b/integrationtest/src/test/resources/expressionTextBlocksTest/src/main/java/org/mapstruct/itest/textBlocks/WheelPosition.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.textBlocks; + +/** + * @author Filip Hrisafov + */ +public class WheelPosition { + + private final String position; + + public WheelPosition(String position) { + this.position = position; + } + + public String getPosition() { + return position; + } +} diff --git a/integrationtest/src/test/resources/expressionTextBlocksTest/src/test/java/org/mapstruct/itest/textBlocks/TextBlocksTest.java b/integrationtest/src/test/resources/expressionTextBlocksTest/src/test/java/org/mapstruct/itest/textBlocks/TextBlocksTest.java new file mode 100644 index 0000000000..245cf8f304 --- /dev/null +++ b/integrationtest/src/test/resources/expressionTextBlocksTest/src/test/java/org/mapstruct/itest/textBlocks/TextBlocksTest.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.textBlocks; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TextBlocksTest { + + @Test + void textBlockExpressionShouldWork() { + Car car = new Car(); + car.setWheelPosition( new WheelPosition( "left" ) ); + + CarDto carDto = CarAndWheelMapper.INSTANCE.carDtoFromCar(car); + + assertThat( carDto ).isNotNull(); + assertThat( carDto.wheelPosition() ) + .isEqualTo( "left" ); + } +} diff --git a/integrationtest/src/test/resources/externalbeanjar/mapper/src/main/java/org/mapstruct/itest/externalbeanjar/Issue1121Mapper.java b/integrationtest/src/test/resources/externalbeanjar/mapper/src/main/java/org/mapstruct/itest/externalbeanjar/Issue1121Mapper.java index 028941e24d..35cfacdb89 100644 --- a/integrationtest/src/test/resources/externalbeanjar/mapper/src/main/java/org/mapstruct/itest/externalbeanjar/Issue1121Mapper.java +++ b/integrationtest/src/test/resources/externalbeanjar/mapper/src/main/java/org/mapstruct/itest/externalbeanjar/Issue1121Mapper.java @@ -10,11 +10,11 @@ */ package org.mapstruct.itest.externalbeanjar; -import org.mapstruct.itest.externalbeanjar.Source; -import org.mapstruct.itest.externalbeanjar.Target; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; +import org.mapstruct.itest.externalbeanjar.Source; +import org.mapstruct.itest.externalbeanjar.Target; /** * diff --git a/integrationtest/src/test/resources/externalbeanjar/mapper/src/test/java/org/mapstruct/itest/externalbeanjar/ConversionTest.java b/integrationtest/src/test/resources/externalbeanjar/mapper/src/test/java/org/mapstruct/itest/externalbeanjar/ConversionTest.java index 8a18e9e4fc..5d12c1b0f2 100644 --- a/integrationtest/src/test/resources/externalbeanjar/mapper/src/test/java/org/mapstruct/itest/externalbeanjar/ConversionTest.java +++ b/integrationtest/src/test/resources/externalbeanjar/mapper/src/test/java/org/mapstruct/itest/externalbeanjar/ConversionTest.java @@ -6,17 +6,18 @@ package org.mapstruct.itest.simple; import java.math.BigDecimal; -import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; -import org.mapstruct.itest.externalbeanjar.Source; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.externalbeanjar.Issue1121Mapper; +import org.mapstruct.itest.externalbeanjar.Source; import org.mapstruct.itest.externalbeanjar.Target; -public class ConversionTest { +import static org.assertj.core.api.Assertions.assertThat; + +class ConversionTest { @Test - public void shouldApplyConversions() { + void shouldApplyConversions() { Source source = new Source(); source.setBigDecimal( new BigDecimal( "42" ) ); diff --git a/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/generator/pom.xml b/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/generator/pom.xml index 08390085dd..60b73b856c 100644 --- a/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/generator/pom.xml +++ b/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/generator/pom.xml @@ -27,8 +27,8 @@ provided - junit - junit + org.junit.jupiter + junit-jupiter test diff --git a/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/pom.xml b/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/pom.xml index 90c95aa83b..976664f88d 100644 --- a/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/pom.xml +++ b/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test diff --git a/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/src/test/java/org/mapstruct/itest/faultyAstModifyingProcessor/usage/FaultyAstModifyingTestTest.java b/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/src/test/java/org/mapstruct/itest/faultyAstModifyingProcessor/usage/FaultyAstModifyingTestTest.java index 06bfa3c533..ed6a5c0d89 100644 --- a/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/src/test/java/org/mapstruct/itest/faultyAstModifyingProcessor/usage/FaultyAstModifyingTestTest.java +++ b/integrationtest/src/test/resources/faultyAstModifyingAnnotationProcessorTest/usage/src/test/java/org/mapstruct/itest/faultyAstModifyingProcessor/usage/FaultyAstModifyingTestTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.faultyAstModifyingProcessor.usage; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,10 +14,10 @@ * * @author Filip Hrisafov */ -public class FaultyAstModifyingTestTest { +class FaultyAstModifyingTestTest { @Test - public void testMapping() { + void testMapping() { Order order = new Order(); order.setItem( "my item" ); diff --git a/integrationtest/src/test/resources/freeBuilderBuilderTest/src/test/java/org/mapstruct/itest/freebuilder/FreeBuilderMapperTest.java b/integrationtest/src/test/resources/freeBuilderBuilderTest/src/test/java/org/mapstruct/itest/freebuilder/FreeBuilderMapperTest.java index 5047451798..3b1caebf9c 100644 --- a/integrationtest/src/test/resources/freeBuilderBuilderTest/src/test/java/org/mapstruct/itest/freebuilder/FreeBuilderMapperTest.java +++ b/integrationtest/src/test/resources/freeBuilderBuilderTest/src/test/java/org/mapstruct/itest/freebuilder/FreeBuilderMapperTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.freebuilder; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,10 +14,10 @@ * * @author Eric Martineau */ -public class FreeBuilderMapperTest { +class FreeBuilderMapperTest { @Test - public void testSimpleImmutableBuilderHappyPath() { + void testSimpleImmutableBuilderHappyPath() { PersonDto personDto = PersonMapper.INSTANCE.toDto( Person.builder() .setAge( 33 ) .setName( "Bob" ) @@ -32,7 +32,7 @@ public void testSimpleImmutableBuilderHappyPath() { } @Test - public void testLombokToImmutable() { + void testLombokToImmutable() { Person person = PersonMapper.INSTANCE.fromDto( new PersonDto( "Bob", 33, new AddressDto( "Wild Drive" ) ) ); assertThat( person.getAge() ).isEqualTo( 33 ); assertThat( person.getName() ).isEqualTo( "Bob" ); diff --git a/integrationtest/src/test/resources/fullFeatureTest/pom.xml b/integrationtest/src/test/resources/fullFeatureTest/pom.xml index ebf4590d30..670d5cff92 100644 --- a/integrationtest/src/test/resources/fullFeatureTest/pom.xml +++ b/integrationtest/src/test/resources/fullFeatureTest/pom.xml @@ -25,6 +25,13 @@ x x x + x + x + x + x + x + x + x @@ -40,11 +47,21 @@ **/*Test.java **/testutil/**/*.java **/spi/**/*.java + **/kotlin/**/*.java ${additionalExclude0} ${additionalExclude1} ${additionalExclude2} ${additionalExclude3} ${additionalExclude4} + ${additionalExclude5} + ${additionalExclude6} + ${additionalExclude7} + ${additionalExclude8} + ${additionalExclude9} + ${additionalExclude10} + ${additionalExclude11} + ${additionalExclude12} + ${additionalExclude13} @@ -60,6 +77,14 @@ javax.inject javax.inject + + jakarta.inject + jakarta.inject-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + @@ -75,6 +100,13 @@ joda-time joda-time + + + jakarta.xml.bind + jakarta.xml.bind-api + provided + true + @@ -85,14 +117,16 @@ - jakarta.xml.bind - jakarta.xml.bind-api - 2.3.2 + javax.xml.bind + jaxb-api + provided + true org.glassfish.jaxb jaxb-runtime - 2.3.2 + provided + true diff --git a/integrationtest/src/test/resources/fullFeatureTest/src/test/java/org/mapstruct/itest/simple/AnimalTest.java b/integrationtest/src/test/resources/fullFeatureTest/src/test/java/org/mapstruct/itest/simple/AnimalTest.java index a180b4df52..4f2d7bd9ed 100644 --- a/integrationtest/src/test/resources/fullFeatureTest/src/test/java/org/mapstruct/itest/simple/AnimalTest.java +++ b/integrationtest/src/test/resources/fullFeatureTest/src/test/java/org/mapstruct/itest/simple/AnimalTest.java @@ -5,23 +5,22 @@ */ package org.mapstruct.ap.test.ignore; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; - -import org.mapstruct.ap.test.ignore.AnimalMapper; +import org.junit.jupiter.api.Test; import org.mapstruct.ap.test.ignore.Animal; import org.mapstruct.ap.test.ignore.AnimalDto; +import org.mapstruct.ap.test.ignore.AnimalMapper; + +import static org.assertj.core.api.Assertions.assertThat; /** * Test for ignoring properties during the mapping. * * @author Gunnar Morling */ -public class AnimalTest { +class AnimalTest { @Test - public void shouldNotPropagateIgnoredPropertyGivenViaTargetAttribute() { + void shouldNotPropagateIgnoredPropertyGivenViaTargetAttribute() { Animal animal = new Animal( "Bruno", 100, 23, "black" ); AnimalDto animalDto = AnimalMapper.INSTANCE.animalToDto( animal ); @@ -34,7 +33,7 @@ public void shouldNotPropagateIgnoredPropertyGivenViaTargetAttribute() { } @Test - public void shouldNotPropagateIgnoredPropertyInReverseMappingWhenSourceAndTargetAreSpecified() { + void shouldNotPropagateIgnoredPropertyInReverseMappingWhenSourceAndTargetAreSpecified() { AnimalDto animalDto = new AnimalDto( "Bruno", 100, 23, "black" ); Animal animal = AnimalMapper.INSTANCE.animalDtoToAnimal( animalDto ); diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle index e62a8d087f..0df032f0f2 100644 --- a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle +++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/build.gradle @@ -1,3 +1,8 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ plugins { id 'java' } diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle index f62a77ab7b..b6ca918e91 100644 --- a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle +++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/settings.gradle @@ -1 +1,6 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ rootProject.name = 'gradle-incremental-compilation-test' \ No newline at end of file diff --git a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java index 6137ff0eca..52c64051f1 100644 --- a/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java +++ b/integrationtest/src/test/resources/gradleIncrementalCompilationTest/src/main/java/org/mapstruct/itest/gradle/lib/TestMapper.java @@ -8,9 +8,8 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.ReportingPolicy; - -import org.mapstruct.itest.gradle.model.Target; import org.mapstruct.itest.gradle.model.Source; +import org.mapstruct.itest.gradle.model.Target; @Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) public interface TestMapper { diff --git a/integrationtest/src/test/resources/immutablesBuilderTest/mapper/src/test/java/org/mapstruct/itest/immutables/ImmutablesMapperTest.java b/integrationtest/src/test/resources/immutablesBuilderTest/mapper/src/test/java/org/mapstruct/itest/immutables/ImmutablesMapperTest.java index 421954fc90..9990f4278c 100644 --- a/integrationtest/src/test/resources/immutablesBuilderTest/mapper/src/test/java/org/mapstruct/itest/immutables/ImmutablesMapperTest.java +++ b/integrationtest/src/test/resources/immutablesBuilderTest/mapper/src/test/java/org/mapstruct/itest/immutables/ImmutablesMapperTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.immutables; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,10 +14,10 @@ * * @author Eric Martineau */ -public class ImmutablesMapperTest { +class ImmutablesMapperTest { @Test - public void testSimpleImmutableBuilderHappyPath() { + void testSimpleImmutableBuilderHappyPath() { PersonDto personDto = PersonMapper.INSTANCE.toDto( ImmutablePerson.builder() .age( 33 ) .name( "Bob" ) @@ -32,7 +32,7 @@ public void testSimpleImmutableBuilderHappyPath() { } @Test - public void testLombokToImmutable() { + void testLombokToImmutable() { Person person = PersonMapper.INSTANCE.fromDto( new PersonDto( "Bob", 33, new AddressDto( "Wild Drive" ) ) ); assertThat( person.getAge() ).isEqualTo( 33 ); assertThat( person.getName() ).isEqualTo( "Bob" ); diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/pom.xml b/integrationtest/src/test/resources/jakartaJaxbTest/pom.xml new file mode 100644 index 0000000000..3a2d0351ce --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/pom.xml @@ -0,0 +1,64 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + jakartaJaxbTest + jar + + + + jakarta.xml.bind + jakarta.xml.bind-api + provided + true + + + com.sun.xml.bind + jaxb-impl + provided + true + + + + + + + org.codehaus.mojo + jaxb2-maven-plugin + 3.1.0 + + + xjc + initialize + + xjc + + + + + + \${project.build.resources[0].directory}/binding + + + \${project.build.resources[0].directory}/schema + + + + + + diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderDetailsDto.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderDetailsDto.java new file mode 100644 index 0000000000..e8500633e4 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderDetailsDto.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +import java.util.List; + +/** + * @author Sjaak Derksen + */ +public class OrderDetailsDto { + + private String name; + private List description; + private OrderStatusDto status; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getDescription() { + return description; + } + + public void setDescription(List description) { + this.description = description; + } + + public OrderStatusDto getStatus() { + return status; + } + + public void setStatus(OrderStatusDto status) { + this.status = status; + } +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderDto.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderDto.java new file mode 100644 index 0000000000..f94d5362ef --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderDto.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +import java.util.Date; + +/** + * @author Sjaak Derksen + */ +public class OrderDto { + + private Long orderNumber; + private Date orderDate; + private OrderDetailsDto orderDetails; + private ShippingAddressDto shippingAddress; + + public Long getOrderNumber() { + return orderNumber; + } + + public void setOrderNumber(Long orderNumber) { + this.orderNumber = orderNumber; + } + + public Date getOrderDate() { + return orderDate; + } + + public void setOrderDate(Date orderDate) { + this.orderDate = orderDate; + } + + public OrderDetailsDto getOrderDetails() { + return orderDetails; + } + + public void setOrderDetails(OrderDetailsDto orderDetails) { + this.orderDetails = orderDetails; + } + + public ShippingAddressDto getShippingAddress() { + return shippingAddress; + } + + public void setShippingAddress(ShippingAddressDto shippingAddress) { + this.shippingAddress = shippingAddress; + } + +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderStatusDto.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderStatusDto.java new file mode 100644 index 0000000000..5da5d45c99 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/OrderStatusDto.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +/** + * @author Sjaak Derksen + */ +public enum OrderStatusDto { + + ORDERED( "small" ), + PROCESSED( "medium" ), + DELIVERED( "large" ); + private final String value; + + OrderStatusDto(String v) { + value = v; + } + + public String value() { + return value; + } + + public static OrderStatusDto fromValue(String v) { + for ( OrderStatusDto c : OrderStatusDto.values() ) { + if ( c.value.equals( v ) ) { + return c; + } + } + throw new IllegalArgumentException( v ); + } + +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/ShippingAddressDto.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/ShippingAddressDto.java new file mode 100644 index 0000000000..6bc40a19b2 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/ShippingAddressDto.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +/** + * @author Sjaak Derksen + */ +public class ShippingAddressDto { + + private String street; + private String houseNumber; + private String city; + private String country; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getHouseNumber() { + return houseNumber; + } + + public void setHouseNumber(String houseNumber) { + this.houseNumber = houseNumber; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SourceTargetMapper.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SourceTargetMapper.java new file mode 100644 index 0000000000..8356f76192 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SourceTargetMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.mapstruct.itest.jakarta.jaxb.xsd.test1.OrderDetailsType; +import org.mapstruct.itest.jakarta.jaxb.xsd.test1.OrderType; +import org.mapstruct.itest.jakarta.jaxb.xsd.test2.OrderStatusType; +import org.mapstruct.itest.jakarta.jaxb.xsd.test2.ShippingAddressType; +import org.mapstruct.itest.jakarta.jaxb.xsd.underscores.SubType; + +/** + * @author Sjaak Derksen + */ +@Mapper(uses = { + org.mapstruct.itest.jakarta.jaxb.xsd.test1.ObjectFactory.class, + org.mapstruct.itest.jakarta.jaxb.xsd.test2.ObjectFactory.class, + org.mapstruct.itest.jakarta.jaxb.xsd.underscores.ObjectFactory.class +}) +public interface SourceTargetMapper { + + SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); + + // source 2 target methods + OrderDto sourceToTarget(OrderType source); + + OrderDetailsDto detailsToDto(OrderDetailsType source); + + OrderStatusDto statusToDto(OrderStatusType source); + + ShippingAddressDto shippingAddressToDto(ShippingAddressType source); + + SubTypeDto subTypeToDto(SubType source); + + // target 2 source methods + OrderType targetToSource(OrderDto target); + + OrderDetailsType dtoToDetails(OrderDetailsDto target); + + OrderStatusType dtoToStatus(OrderStatusDto target); + + ShippingAddressType dtoToShippingAddress(ShippingAddressDto source); + + SubType dtoToSubType(SubTypeDto source); +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SubTypeDto.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SubTypeDto.java new file mode 100644 index 0000000000..88218c2771 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SubTypeDto.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +public class SubTypeDto extends SuperTypeDto { + private String declaredCamelCase; + private String declaredUnderscore; + + public String getDeclaredCamelCase() { + return declaredCamelCase; + } + + public void setDeclaredCamelCase(String declaredCamelCase) { + this.declaredCamelCase = declaredCamelCase; + } + + public String getDeclaredUnderscore() { + return declaredUnderscore; + } + + public void setDeclaredUnderscore(String declaredUnderscore) { + this.declaredUnderscore = declaredUnderscore; + } +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SuperTypeDto.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SuperTypeDto.java new file mode 100644 index 0000000000..cd0c6e22e7 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/java/org/mapstruct/itest/jakarta/jaxb/SuperTypeDto.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +public class SuperTypeDto { + private String inheritedCamelCase; + private String inheritedUnderscore; + + public String getInheritedCamelCase() { + return inheritedCamelCase; + } + + public void setInheritedCamelCase(String inheritedCamelCase) { + this.inheritedCamelCase = inheritedCamelCase; + } + + public String getInheritedUnderscore() { + return inheritedUnderscore; + } + + public void setInheritedUnderscore(String inheritedUnderscore) { + this.inheritedUnderscore = inheritedUnderscore; + } +} diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/binding/binding.xjb b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/binding/binding.xjb new file mode 100644 index 0000000000..8f26b1a1ea --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/binding/binding.xjb @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/test1.xsd b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/test1.xsd new file mode 100644 index 0000000000..3433b01465 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/test1.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/test2.xsd b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/test2.xsd new file mode 100644 index 0000000000..f3b564a48e --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/test2.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/underscores.xsd b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/underscores.xsd new file mode 100644 index 0000000000..b7f5904656 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/main/resources/schema/underscores.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrationtest/src/test/resources/jakartaJaxbTest/src/test/java/org/mapstruct/itest/jakarta/jaxb/JakartaJaxbBasedMapperTest.java b/integrationtest/src/test/resources/jakartaJaxbTest/src/test/java/org/mapstruct/itest/jakarta/jaxb/JakartaJaxbBasedMapperTest.java new file mode 100644 index 0000000000..78e051a4e9 --- /dev/null +++ b/integrationtest/src/test/resources/jakartaJaxbTest/src/test/java/org/mapstruct/itest/jakarta/jaxb/JakartaJaxbBasedMapperTest.java @@ -0,0 +1,117 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.jakarta.jaxb; + +import java.io.ByteArrayOutputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBElement; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import org.junit.jupiter.api.Test; +import org.mapstruct.itest.jakarta.jaxb.xsd.test1.ObjectFactory; +import org.mapstruct.itest.jakarta.jaxb.xsd.test1.OrderType; +import org.mapstruct.itest.jakarta.jaxb.xsd.underscores.SubType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for generation of Jakarta JAXB based mapper implementations. + * + * @author Iaroslav Bogdanchikov + */ +class JakartaJaxbBasedMapperTest { + + @Test + void shouldMapJakartaJaxb() throws ParseException, JAXBException { + + SourceTargetMapper mapper = SourceTargetMapper.INSTANCE; + + OrderDto source1 = new OrderDto(); + source1.setOrderDetails( new OrderDetailsDto() ); + source1.setOrderNumber( 11L ); + source1.setOrderDate( createDate( "31-08-1982 10:20:56" ) ); + source1.setShippingAddress( new ShippingAddressDto() ); + source1.getShippingAddress().setCity( "SmallTown" ); + source1.getShippingAddress().setHouseNumber( "11a" ); + source1.getShippingAddress().setStreet( "Awesome rd" ); + source1.getShippingAddress().setCountry( "USA" ); + source1.getOrderDetails().setDescription( new ArrayList() ); + source1.getOrderDetails().setName( "Shopping list for a Mapper" ); + source1.getOrderDetails().getDescription().add( "1 MapStruct" ); + source1.getOrderDetails().getDescription().add( "3 Lines of Code" ); + source1.getOrderDetails().getDescription().add( "1 Dose of Luck" ); + source1.getOrderDetails().setStatus( OrderStatusDto.ORDERED ); + + // map to JAXB + OrderType target = mapper.targetToSource( source1 ); + + // do a pretty print + ObjectFactory of = new ObjectFactory(); + System.out.println( toXml( of.createOrder( target ) ) ); + + // map back from JAXB + OrderDto source2 = mapper.sourceToTarget( target ); + + // verify that source1 and source 2 are equal + assertThat( source2.getOrderNumber() ).isEqualTo( source1.getOrderNumber() ); + assertThat( source2.getOrderDate() ).isEqualTo( source1.getOrderDate() ); + assertThat( source2.getOrderDetails().getDescription().size() ).isEqualTo( + source1.getOrderDetails().getDescription().size() + ); + assertThat( source2.getOrderDetails().getDescription().get( 0 ) ).isEqualTo( + source1.getOrderDetails().getDescription().get( 0 ) + ); + assertThat( source2.getOrderDetails().getDescription().get( 1 ) ).isEqualTo( + source1.getOrderDetails().getDescription().get( 1 ) + ); + assertThat( source2.getOrderDetails().getDescription().get( 2 ) ).isEqualTo( + source1.getOrderDetails().getDescription().get( 2 ) + ); + assertThat( source2.getOrderDetails().getName() ).isEqualTo( source1.getOrderDetails().getName() ); + assertThat( source2.getOrderDetails().getStatus() ).isEqualTo( source1.getOrderDetails().getStatus() ); + } + + @Test + void underscores() throws ParseException, JAXBException { + + SourceTargetMapper mapper = SourceTargetMapper.INSTANCE; + + SubTypeDto source1 = new SubTypeDto(); + source1.setInheritedCamelCase("InheritedCamelCase"); + source1.setInheritedUnderscore("InheritedUnderscore"); + source1.setDeclaredCamelCase("DeclaredCamelCase"); + source1.setDeclaredUnderscore("DeclaredUnderscore"); + + SubType target = mapper.dtoToSubType( source1 ); + + SubTypeDto source2 = mapper.subTypeToDto( target ); + + assertThat( source2.getInheritedCamelCase() ).isEqualTo( source1.getInheritedCamelCase() ); + assertThat( source2.getInheritedUnderscore() ).isEqualTo( source1.getInheritedUnderscore() ); + assertThat( source2.getDeclaredCamelCase() ).isEqualTo( source1.getDeclaredCamelCase() ); + assertThat( source2.getDeclaredUnderscore() ).isEqualTo( source1.getDeclaredUnderscore() ); + } + + private Date createDate(String date) throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat( "dd-M-yyyy hh:mm:ss" ); + return sdf.parse( date ); + } + + private String toXml(JAXBElement element) throws JAXBException { + JAXBContext jc = JAXBContext.newInstance( element.getValue().getClass() ); + Marshaller marshaller = jc.createMarshaller(); + marshaller.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE ); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + marshaller.marshal( element, baos ); + return baos.toString(); + } +} diff --git a/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_603/Issue603Test.java b/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_603/Issue603Test.java index c732b96f22..efe68a48ee 100644 --- a/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_603/Issue603Test.java +++ b/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_603/Issue603Test.java @@ -5,15 +5,14 @@ */ package org.mapstruct.ap.test.bugs._603; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -public class Issue603Test { +class Issue603Test { @Test - public void shouldMapDataFromJava8Interface() { + void shouldMapDataFromJava8Interface() { final Source source = new Source(); diff --git a/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_636/Issue636Test.java b/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_636/Issue636Test.java index 99b7a515eb..837eb0b53a 100644 --- a/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_636/Issue636Test.java +++ b/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/ap/test/bugs/_636/Issue636Test.java @@ -5,14 +5,14 @@ */ package org.mapstruct.ap.test.bugs._636; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -public class Issue636Test { +class Issue636Test { @Test - public void shouldMapDataFromJava8Interface() { + void shouldMapDataFromJava8Interface() { final long idFoo = 123; final String idBar = "Bar456"; diff --git a/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/itest/java8/Java8MapperTest.java b/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/itest/java8/Java8MapperTest.java index 556dd2f1e4..4a390628af 100644 --- a/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/itest/java8/Java8MapperTest.java +++ b/integrationtest/src/test/resources/java8Test/src/test/java/org/mapstruct/itest/java8/Java8MapperTest.java @@ -5,19 +5,19 @@ */ package org.mapstruct.itest.java8; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; /** * Test for generation of Java8 based mapper implementations. * * @author Christian Schuster */ -public class Java8MapperTest { +class Java8MapperTest { @Test - public void shouldMapWithRepeatedMappingAnnotation() { + void shouldMapWithRepeatedMappingAnnotation() { Java8Mapper mapper = Java8Mapper.INSTANCE; Source source = new Source(); diff --git a/integrationtest/src/test/resources/jaxbTest/pom.xml b/integrationtest/src/test/resources/jaxbTest/pom.xml index 000e7cf598..0e69e23e01 100644 --- a/integrationtest/src/test/resources/jaxbTest/pom.xml +++ b/integrationtest/src/test/resources/jaxbTest/pom.xml @@ -51,7 +51,7 @@ org.glassfish.jaxb jaxb-runtime - 2.3.2 + ${jaxb-runtime.version} @@ -66,14 +66,16 @@ - jakarta.xml.bind - jakarta.xml.bind-api - 2.3.2 + javax.xml.bind + jaxb-api + provided + true org.glassfish.jaxb jaxb-runtime - 2.3.2 + provided + true diff --git a/integrationtest/src/test/resources/jaxbTest/src/main/java/org/mapstruct/itest/jaxb/SourceTargetMapper.java b/integrationtest/src/test/resources/jaxbTest/src/main/java/org/mapstruct/itest/jaxb/SourceTargetMapper.java index c0a56f8169..49dab0092f 100644 --- a/integrationtest/src/test/resources/jaxbTest/src/main/java/org/mapstruct/itest/jaxb/SourceTargetMapper.java +++ b/integrationtest/src/test/resources/jaxbTest/src/main/java/org/mapstruct/itest/jaxb/SourceTargetMapper.java @@ -13,7 +13,6 @@ import org.mapstruct.itest.jaxb.xsd.test2.ShippingAddressType; import org.mapstruct.itest.jaxb.xsd.underscores.SubType; - /** * @author Sjaak Derksen */ diff --git a/integrationtest/src/test/resources/jaxbTest/src/test/java/org/mapstruct/itest/jaxb/JaxbBasedMapperTest.java b/integrationtest/src/test/resources/jaxbTest/src/test/java/org/mapstruct/itest/jaxb/JaxbBasedMapperTest.java index f54658ba06..d54484334b 100644 --- a/integrationtest/src/test/resources/jaxbTest/src/test/java/org/mapstruct/itest/jaxb/JaxbBasedMapperTest.java +++ b/integrationtest/src/test/resources/jaxbTest/src/test/java/org/mapstruct/itest/jaxb/JaxbBasedMapperTest.java @@ -5,32 +5,31 @@ */ package org.mapstruct.itest.jaxb; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.ByteArrayOutputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; - import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.jaxb.xsd.test1.ObjectFactory; import org.mapstruct.itest.jaxb.xsd.test1.OrderType; import org.mapstruct.itest.jaxb.xsd.underscores.SubType; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test for generation of JAXB based mapper implementations. * * @author Sjaak Derksen */ -public class JaxbBasedMapperTest { +class JaxbBasedMapperTest { @Test - public void shouldMapJaxb() throws ParseException, JAXBException { + void shouldMapJaxb() throws ParseException, JAXBException { SourceTargetMapper mapper = SourceTargetMapper.INSTANCE; @@ -80,7 +79,7 @@ public void shouldMapJaxb() throws ParseException, JAXBException { } @Test - public void underscores() throws ParseException, JAXBException { + void underscores() throws ParseException, JAXBException { SourceTargetMapper mapper = SourceTargetMapper.INSTANCE; diff --git a/integrationtest/src/test/resources/jsr330Test/pom.xml b/integrationtest/src/test/resources/jsr330Test/pom.xml index c0372831f1..e1cf959a90 100644 --- a/integrationtest/src/test/resources/jsr330Test/pom.xml +++ b/integrationtest/src/test/resources/jsr330Test/pom.xml @@ -18,6 +18,10 @@ jsr330Test jar + + + ${org.junit6.jupiter.version} + @@ -34,8 +38,8 @@ spring-context - javax.inject - javax.inject + jakarta.inject + jakarta.inject-api diff --git a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/DecoratedSourceTargetMapper.java b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/DecoratedSourceTargetMapper.java index 77f5ec558a..3f04abd7df 100644 --- a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/DecoratedSourceTargetMapper.java +++ b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/DecoratedSourceTargetMapper.java @@ -5,9 +5,9 @@ */ package org.mapstruct.itest.jsr330; +import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; -import org.mapstruct.DecoratedWith; import org.mapstruct.itest.jsr330.other.DateMapper; @Mapper(componentModel = MappingConstants.ComponentModel.JSR330, uses = DateMapper.class) diff --git a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondDecoratedSourceTargetMapper.java b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondDecoratedSourceTargetMapper.java index f088e21de4..edfaeb3171 100644 --- a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondDecoratedSourceTargetMapper.java +++ b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondDecoratedSourceTargetMapper.java @@ -5,9 +5,9 @@ */ package org.mapstruct.itest.jsr330; +import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; -import org.mapstruct.DecoratedWith; import org.mapstruct.itest.jsr330.other.DateMapper; @Mapper(componentModel = MappingConstants.ComponentModel.JSR330, uses = DateMapper.class) diff --git a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondSourceTargetMapperDecorator.java b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondSourceTargetMapperDecorator.java index d41db89dd5..0fb513c99b 100644 --- a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondSourceTargetMapperDecorator.java +++ b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SecondSourceTargetMapperDecorator.java @@ -5,8 +5,8 @@ */ package org.mapstruct.itest.jsr330; -import javax.inject.Inject; -import javax.inject.Named; +import jakarta.inject.Inject; +import jakarta.inject.Named; public abstract class SecondSourceTargetMapperDecorator implements SecondDecoratedSourceTargetMapper { diff --git a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SourceTargetMapperDecorator.java b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SourceTargetMapperDecorator.java index e7f3423e4b..7f4b968227 100644 --- a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SourceTargetMapperDecorator.java +++ b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/SourceTargetMapperDecorator.java @@ -5,8 +5,8 @@ */ package org.mapstruct.itest.jsr330; -import javax.inject.Inject; -import javax.inject.Named; +import jakarta.inject.Inject; +import jakarta.inject.Named; public abstract class SourceTargetMapperDecorator implements DecoratedSourceTargetMapper { diff --git a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/other/DateMapper.java b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/other/DateMapper.java index 817ab2aa53..d97b168776 100644 --- a/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/other/DateMapper.java +++ b/integrationtest/src/test/resources/jsr330Test/src/main/java/org/mapstruct/itest/jsr330/other/DateMapper.java @@ -9,8 +9,8 @@ import java.text.SimpleDateFormat; import java.util.Date; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Named; +import jakarta.inject.Singleton; @Singleton @Named diff --git a/integrationtest/src/test/resources/jsr330Test/src/test/java/org/mapstruct/itest/jsr330/Jsr330BasedMapperTest.java b/integrationtest/src/test/resources/jsr330Test/src/test/java/org/mapstruct/itest/jsr330/Jsr330BasedMapperTest.java index 26a9863576..7196051ff8 100644 --- a/integrationtest/src/test/resources/jsr330Test/src/test/java/org/mapstruct/itest/jsr330/Jsr330BasedMapperTest.java +++ b/integrationtest/src/test/resources/jsr330Test/src/test/java/org/mapstruct/itest/jsr330/Jsr330BasedMapperTest.java @@ -5,16 +5,15 @@ */ package org.mapstruct.itest.jsr330; -import javax.inject.Inject; -import javax.inject.Named; - -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.itest.jsr330.Jsr330BasedMapperTest.SpringTestConfig; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -24,8 +23,8 @@ * @author Andreas Gudian */ @ContextConfiguration(classes = SpringTestConfig.class) -@RunWith(SpringJUnit4ClassRunner.class) -public class Jsr330BasedMapperTest { +@ExtendWith(SpringExtension.class) +class Jsr330BasedMapperTest { @Configuration @ComponentScan(basePackageClasses = Jsr330BasedMapperTest.class) public static class SpringTestConfig { @@ -43,7 +42,7 @@ public static class SpringTestConfig { private SecondDecoratedSourceTargetMapper secondDecoratedMapper; @Test - public void shouldInjectJsr330BasedMapper() { + void shouldInjectJsr330BasedMapper() { Source source = new Source(); Target target = mapper.sourceToTarget( source ); @@ -54,7 +53,7 @@ public void shouldInjectJsr330BasedMapper() { } @Test - public void shouldInjectDecorator() { + void shouldInjectDecorator() { Source source = new Source(); Target target = decoratedMapper.sourceToTarget( source ); @@ -71,7 +70,7 @@ public void shouldInjectDecorator() { } @Test - public void shouldInjectSecondDecorator() { + void shouldInjectSecondDecorator() { Source source = new Source(); Target target = secondDecoratedMapper.sourceToTarget( source ); diff --git a/integrationtest/src/test/resources/kotlinDataTest/pom.xml b/integrationtest/src/test/resources/kotlinDataTest/pom.xml index 8a43a04194..f86f92b9c4 100644 --- a/integrationtest/src/test/resources/kotlinDataTest/pom.xml +++ b/integrationtest/src/test/resources/kotlinDataTest/pom.xml @@ -22,7 +22,7 @@ jar - 1.3.70 + 1.6.0 diff --git a/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java b/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java index 513f080a8b..39bb909746 100644 --- a/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java +++ b/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java @@ -5,17 +5,17 @@ */ package org.mapstruct.itest.kotlin.data; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.kotlin.data.CustomerDto; import org.mapstruct.itest.kotlin.data.CustomerEntity; import org.mapstruct.itest.kotlin.data.CustomerMapper; -public class KotlinDataTest { +import static org.assertj.core.api.Assertions.assertThat; + +class KotlinDataTest { @Test - public void shouldMapData() { + void shouldMapData() { CustomerEntity customer = CustomerMapper.INSTANCE.fromRecord( new CustomerDto( "Kermit", "kermit@test.com" ) ); assertThat( customer ).isNotNull(); @@ -24,7 +24,7 @@ public void shouldMapData() { } @Test - public void shouldMapIntoData() { + void shouldMapIntoData() { CustomerEntity entity = new CustomerEntity(); entity.setName( "Kermit" ); entity.setMail( "kermit@test.com" ); diff --git a/integrationtest/src/test/resources/kotlinFullFeatureTest/pom.xml b/integrationtest/src/test/resources/kotlinFullFeatureTest/pom.xml new file mode 100644 index 0000000000..f481fd6963 --- /dev/null +++ b/integrationtest/src/test/resources/kotlinFullFeatureTest/pom.xml @@ -0,0 +1,186 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + kotlinFullFeatureTest + jar + + + 2.3.0 + x + x + x + x + x + x + x + x + x + x + x + x + + + + + generate-via-compiler-plugin-with-annotation-processor-paths + + false + + + + + maven-resources-plugin + + + filter-kotlin-patterns + generate-sources + + copy-resources + + + \${project.build.directory}/kotlin-sources + + + ../../../../../processor/src/test/java + + **/kotlin/**/*.kt + + + ${additionalExclude0} + ${additionalExclude1} + ${additionalExclude2} + ${additionalExclude3} + ${additionalExclude4} + ${additionalExclude5} + ${additionalExclude6} + ${additionalExclude7} + ${additionalExclude8} + ${additionalExclude9} + ${additionalExclude10} + ${additionalExclude11} + ${additionalExclude12} + ${additionalExclude13} + + + + true + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + kotlin-compile + compile + + compile + + + \${compiler-source-target-version} + + \${project.build.directory}/kotlin-sources + + + + ${project.groupId} + mapstruct-processor + ${mapstruct.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + + default-compile + none + + + default-testCompile + none + + + + + java-compile + compile + + compile + + + + ../../../../../processor/src/test/java + + + **/kotlin/**/*.java + + + **/erroneous/**/*.java + **/*Erroneous*.java + **/*Test.java + **/testutil/**/*.java + ${additionalExclude0} + ${additionalExclude1} + ${additionalExclude2} + ${additionalExclude3} + ${additionalExclude4} + ${additionalExclude5} + ${additionalExclude6} + ${additionalExclude7} + ${additionalExclude8} + ${additionalExclude9} + ${additionalExclude10} + ${additionalExclude11} + ${additionalExclude12} + ${additionalExclude13} + + + + + java-test-compile + test-compile + + testCompile + + + + + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib + + + + diff --git a/integrationtest/src/test/resources/lombokBuilderTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java b/integrationtest/src/test/resources/lombokBuilderTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java index 6053f294c3..060fb5a990 100644 --- a/integrationtest/src/test/resources/lombokBuilderTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java +++ b/integrationtest/src/test/resources/lombokBuilderTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.lombok; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; import static org.assertj.core.api.Assertions.assertThat; @@ -15,10 +15,10 @@ * * @author Eric Martineau */ -public class LombokMapperTest { +class LombokMapperTest { @Test - public void testSimpleImmutableBuilderHappyPath() { + void testSimpleImmutableBuilderHappyPath() { PersonDto personDto = PersonMapper.INSTANCE.toDto( Person.foo() .age( 33 ) .name( "Bob" ) @@ -33,7 +33,7 @@ public void testSimpleImmutableBuilderHappyPath() { } @Test - public void testLombokToImmutable() { + void testLombokToImmutable() { Person person = PersonMapper.INSTANCE.fromDto( new PersonDto( "Bob", 33, new AddressDto( "Wild Drive" ) ) ); assertThat( person.getAge() ).isEqualTo( 33 ); assertThat( person.getName() ).isEqualTo( "Bob" ); diff --git a/integrationtest/src/test/resources/lombokModuleTest/pom.xml b/integrationtest/src/test/resources/lombokModuleTest/pom.xml new file mode 100644 index 0000000000..4280b38417 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + lombokModuleIntegrationTest + jar + + + 11 + 11 + + + + + org.projectlombok + lombok + compile + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + compile + + + + + + generate-via-compiler-plugin-with-annotation-processor-paths + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + \${compiler-id} + + + ${project.groupId} + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + 1.18.38 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.eclipse.tycho + tycho-compiler-jdt + ${org.eclipse.tycho.compiler-jdt.version} + + + + + + + + diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/main/java/module-info.java b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/module-info.java new file mode 100644 index 0000000000..9bc85bea86 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/module-info.java @@ -0,0 +1,9 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +module org.example { + requires org.mapstruct; + requires lombok; +} diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/Address.java b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/Address.java new file mode 100644 index 0000000000..f83c6881ce --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/Address.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.lombok; + +public class Address { + private final String addressLine; + + public Address(String addressLine) { + this.addressLine = addressLine; + } + + public String getAddressLine() { + return addressLine; + } +} diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/AddressDto.java b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/AddressDto.java new file mode 100644 index 0000000000..d6b4f30f88 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/AddressDto.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.lombok; + +public class AddressDto { + + private final String addressLine; + + public AddressDto(String addressLine) { + this.addressLine = addressLine; + } + + public String getAddressLine() { + return addressLine; + } +} diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/Person.java b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/Person.java new file mode 100644 index 0000000000..b6ca47fca1 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/Person.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.lombok; + +public class Person { + private final String name; + private final int age; + private final Address address; + + public Person(String name, int age, Address address) { + this.name = name; + this.age = age; + this.address = address; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public Address getAddress() { + return address; + } +} diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/PersonDto.java b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/PersonDto.java new file mode 100644 index 0000000000..3223778663 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/PersonDto.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.lombok; + +public class PersonDto { + private final String name; + private final int age; + private final AddressDto address; + + public PersonDto(String name, int age, AddressDto address) { + this.name = name; + this.age = age; + this.address = address; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public AddressDto getAddress() { + return address; + } +} diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/PersonMapper.java b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/PersonMapper.java new file mode 100644 index 0000000000..27184bad27 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/main/java/org/mapstruct/itest/lombok/PersonMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.lombok; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface PersonMapper { + + PersonMapper INSTANCE = Mappers.getMapper( PersonMapper.class ); + + Person fromDto(PersonDto personDto); + PersonDto toDto(Person personDto); +} diff --git a/integrationtest/src/test/resources/lombokModuleTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java b/integrationtest/src/test/resources/lombokModuleTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java new file mode 100644 index 0000000000..bdf8f46a57 --- /dev/null +++ b/integrationtest/src/test/resources/lombokModuleTest/src/test/java/org/mapstruct/itest/lombok/LombokMapperTest.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.lombok; + +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for generation of Lombok Builder Mapper implementations + * + * @author Eric Martineau + */ +class LombokMapperTest { + + @Test + void testSimpleImmutableBuilderHappyPath() { + PersonDto personDto = PersonMapper.INSTANCE.toDto( new Person( "Bob", 33, new Address( "Wild Drive" ) ) ); + assertThat( personDto.getAge() ).isEqualTo( 33 ); + assertThat( personDto.getName() ).isEqualTo( "Bob" ); + assertThat( personDto.getAddress() ).isNotNull(); + assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "Wild Drive" ); + } + + @Test + void testLombokToImmutable() { + Person person = PersonMapper.INSTANCE.fromDto( new PersonDto( "Bob", 33, new AddressDto( "Wild Drive" ) ) ); + assertThat( person.getAge() ).isEqualTo( 33 ); + assertThat( person.getName() ).isEqualTo( "Bob" ); + assertThat( person.getAddress() ).isNotNull(); + assertThat( person.getAddress().getAddressLine() ).isEqualTo( "Wild Drive" ); + } +} diff --git a/integrationtest/src/test/resources/moduleInfoTest/src/test/java/org/mapstruct/itest/modules/ModulesTest.java b/integrationtest/src/test/resources/moduleInfoTest/src/test/java/org/mapstruct/itest/modules/ModulesTest.java index 805662c806..d54c7e6572 100644 --- a/integrationtest/src/test/resources/moduleInfoTest/src/test/java/org/mapstruct/itest/modules/ModulesTest.java +++ b/integrationtest/src/test/resources/moduleInfoTest/src/test/java/org/mapstruct/itest/modules/ModulesTest.java @@ -5,17 +5,17 @@ */ package org.mapstruct.itest.modules; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.modules.CustomerDto; import org.mapstruct.itest.modules.CustomerEntity; import org.mapstruct.itest.modules.CustomerMapper; -public class ModulesTest { +import static org.assertj.core.api.Assertions.assertThat; + +class ModulesTest { @Test - public void shouldMapRecord() { + void shouldMapRecord() { CustomerDto dto = new CustomerDto(); dto.setName( "Kermit" ); dto.setEmail( "kermit@test.com" ); diff --git a/integrationtest/src/test/resources/namingStrategyTest/usage/src/test/java/org/mapstruct/itest/naming/NamingTest.java b/integrationtest/src/test/resources/namingStrategyTest/usage/src/test/java/org/mapstruct/itest/naming/NamingTest.java index d3a45db26b..42cd5084ab 100644 --- a/integrationtest/src/test/resources/namingStrategyTest/usage/src/test/java/org/mapstruct/itest/naming/NamingTest.java +++ b/integrationtest/src/test/resources/namingStrategyTest/usage/src/test/java/org/mapstruct/itest/naming/NamingTest.java @@ -5,22 +5,22 @@ */ package org.mapstruct.itest.naming; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.naming.GolfPlayer; import org.mapstruct.itest.naming.GolfPlayerDto; import org.mapstruct.itest.naming.GolfPlayerMapper; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test for using a custom naming strategy. * * @author Gunnar Morling */ -public class NamingTest { +class NamingTest { @Test - public void shouldApplyCustomNamingStrategy() { + void shouldApplyCustomNamingStrategy() { GolfPlayer player = new GolfPlayer() .withName( "Jared" ) .withHandicap( 9.2D ); diff --git a/integrationtest/src/test/resources/pom.xml b/integrationtest/src/test/resources/pom.xml index c8a19228f0..f4cd0b51de 100644 --- a/integrationtest/src/test/resources/pom.xml +++ b/integrationtest/src/test/resources/pom.xml @@ -26,8 +26,7 @@ ${mapstruct.version} - - 1.7.1 + 6.0.3 @@ -97,43 +96,6 @@ - - generate-via-processor-plugin - - false - - - - - org.bsc.maven - maven-processor-plugin - 3.3.3 - - - process - generate-sources - - process - - - - - \${project.build.directory}/generated-sources/mapstruct - - org.mapstruct.ap.MappingProcessor - - - - - ${project.groupId} - mapstruct-processor - ${mapstruct.version} - - - - - - debug-forked-javac @@ -174,8 +136,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test diff --git a/integrationtest/src/test/resources/protobufBuilderTest/pom.xml b/integrationtest/src/test/resources/protobufBuilderTest/pom.xml index ecbbf1d6d8..c0d8e2a81b 100644 --- a/integrationtest/src/test/resources/protobufBuilderTest/pom.xml +++ b/integrationtest/src/test/resources/protobufBuilderTest/pom.xml @@ -23,7 +23,7 @@ 1.6.0 - 0.5.1 + 0.6.1 @@ -55,10 +55,10 @@ compile-custom - com.google.protobuf:protoc:3.2.0:exe:${os.detected.classifier} + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.2.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.47.0:exe:${os.detected.classifier} diff --git a/integrationtest/src/test/resources/protobufBuilderTest/src/test/java/org/mapstruct/itest/protobuf/ProtobufMapperTest.java b/integrationtest/src/test/resources/protobufBuilderTest/src/test/java/org/mapstruct/itest/protobuf/ProtobufMapperTest.java index ae3740848f..ac97eba8b4 100644 --- a/integrationtest/src/test/resources/protobufBuilderTest/src/test/java/org/mapstruct/itest/protobuf/ProtobufMapperTest.java +++ b/integrationtest/src/test/resources/protobufBuilderTest/src/test/java/org/mapstruct/itest/protobuf/ProtobufMapperTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.protobuf; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,10 +14,10 @@ * * @author Christian Bandowski */ -public class ProtobufMapperTest { +class ProtobufMapperTest { @Test - public void testSimpleImmutableBuilderHappyPath() { + void testSimpleImmutableBuilderHappyPath() { PersonDto personDto = PersonMapper.INSTANCE.toDto( PersonProtos.Person.newBuilder() .setAge( 33 ) .setName( "Bob" ) @@ -33,7 +33,7 @@ public void testSimpleImmutableBuilderHappyPath() { } @Test - public void testLombokToImmutable() { + void testLombokToImmutable() { PersonProtos.Person person = PersonMapper.INSTANCE.fromDto( new PersonDto( "Bob", 33, new AddressDto( "Wild Drive" ) ) ); assertThat( person.getAge() ).isEqualTo( 33 ); diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/pom.xml b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/pom.xml new file mode 100644 index 0000000000..72df10f62c --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/pom.xml @@ -0,0 +1,22 @@ + + + + 4.0.0 + + + recordsCrossModuleInterfaceTest + org.mapstruct + 1.0.0 + + + records-cross-module-1 + + diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/NestedInterface.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/NestedInterface.java new file mode 100644 index 0000000000..ffa53f88b1 --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/NestedInterface.java @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module1; + +public interface NestedInterface { + String field(); +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/RootInterface.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/RootInterface.java new file mode 100644 index 0000000000..fb23ffe157 --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/RootInterface.java @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module1; + +public interface RootInterface { + NestedInterface nested(); +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/SourceNestedRecord.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/SourceNestedRecord.java new file mode 100644 index 0000000000..6a0ddb86af --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/SourceNestedRecord.java @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module1; + +public record SourceNestedRecord( + String field +) implements NestedInterface { +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/SourceRootRecord.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/SourceRootRecord.java new file mode 100644 index 0000000000..151ad5208d --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-1/src/main/java/org/mapstruct/itest/records/module1/SourceRootRecord.java @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module1; + +public record SourceRootRecord( + SourceNestedRecord nested +) implements RootInterface { +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/pom.xml b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/pom.xml new file mode 100644 index 0000000000..5f42efd18e --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + + recordsCrossModuleInterfaceTest + org.mapstruct + 1.0.0 + + + records-cross-module-2 + + + + + org.mapstruct + records-cross-module-1 + 1.0.0 + + + diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/RecordInterfaceIssueMapper.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/RecordInterfaceIssueMapper.java new file mode 100644 index 0000000000..3efba3ec99 --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/RecordInterfaceIssueMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module2; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.mapstruct.itest.records.module1.SourceRootRecord; + +@Mapper +public interface RecordInterfaceIssueMapper { + + RecordInterfaceIssueMapper INSTANCE = Mappers.getMapper(RecordInterfaceIssueMapper.class); + + TargetRootRecord map(SourceRootRecord source); +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/TargetNestedRecord.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/TargetNestedRecord.java new file mode 100644 index 0000000000..d02a4b58e0 --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/TargetNestedRecord.java @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module2; + +public record TargetNestedRecord( + String field +) { +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/TargetRootRecord.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/TargetRootRecord.java new file mode 100644 index 0000000000..09a69f1bf1 --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/main/java/org/mapstruct/itest/records/module2/TargetRootRecord.java @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module2; + +public record TargetRootRecord( + TargetNestedRecord nested +) { +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/test/java/org/mapstruct/itest/records/module2/RecordsTest.java b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/test/java/org/mapstruct/itest/records/module2/RecordsTest.java new file mode 100644 index 0000000000..7dc5f3b38d --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/module-2/src/test/java/org/mapstruct/itest/records/module2/RecordsTest.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.module2; + +import org.junit.jupiter.api.Test; +import org.mapstruct.itest.records.module1.SourceNestedRecord; +import org.mapstruct.itest.records.module1.SourceRootRecord; + +import static org.assertj.core.api.Assertions.assertThat; + +class RecordsTest { + + @Test + void shouldMap() { + SourceRootRecord source = new SourceRootRecord( new SourceNestedRecord( "test" ) ); + TargetRootRecord target = RecordInterfaceIssueMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.nested() ).isNotNull(); + assertThat( target.nested().field() ).isEqualTo( "test" ); + } + +} diff --git a/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/pom.xml b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/pom.xml new file mode 100644 index 0000000000..120c849dca --- /dev/null +++ b/integrationtest/src/test/resources/recordsCrossModuleInterfaceTest/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + recordsCrossModuleInterfaceTest + pom + + + module-1 + module-2 + + diff --git a/integrationtest/src/test/resources/recordsCrossModuleTest/mapper/test/java/org/mapstruct/itest/records/mapper/RecordsTest.java b/integrationtest/src/test/resources/recordsCrossModuleTest/mapper/src/test/java/org/mapstruct/itest/records/mapper/RecordsTest.java similarity index 90% rename from integrationtest/src/test/resources/recordsCrossModuleTest/mapper/test/java/org/mapstruct/itest/records/mapper/RecordsTest.java rename to integrationtest/src/test/resources/recordsCrossModuleTest/mapper/src/test/java/org/mapstruct/itest/records/mapper/RecordsTest.java index 2f274792b8..49c73f2f9c 100644 --- a/integrationtest/src/test/resources/recordsCrossModuleTest/mapper/test/java/org/mapstruct/itest/records/mapper/RecordsTest.java +++ b/integrationtest/src/test/resources/recordsCrossModuleTest/mapper/src/test/java/org/mapstruct/itest/records/mapper/RecordsTest.java @@ -5,17 +5,17 @@ */ package org.mapstruct.itest.records.mapper; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.records.api.CustomerDto; import org.mapstruct.itest.records.mapper.CustomerEntity; import org.mapstruct.itest.records.mapper.CustomerMapper; -public class RecordsTest { +import static org.assertj.core.api.Assertions.assertThat; + +class RecordsTest { @Test - public void shouldMapRecord() { + void shouldMapRecord() { CustomerEntity customer = CustomerMapper.INSTANCE.fromRecord( new CustomerDto( "Kermit", "kermit@test.com" ) ); assertThat( customer ).isNotNull(); @@ -24,7 +24,7 @@ public void shouldMapRecord() { } @Test - public void shouldMapIntoRecord() { + void shouldMapIntoRecord() { CustomerEntity entity = new CustomerEntity(); entity.setName( "Kermit" ); entity.setMail( "kermit@test.com" ); diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Car.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Car.java new file mode 100644 index 0000000000..9332c47dfb --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Car.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public class Car { + + private List wheelPositions; + + public List getWheelPositions() { + return wheelPositions; + } + + public void setWheelPositions(List wheelPositions) { + this.wheelPositions = wheelPositions; + } +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CarAndWheelMapper.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CarAndWheelMapper.java new file mode 100644 index 0000000000..cc69ae7605 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CarAndWheelMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface CarAndWheelMapper { + + CarAndWheelMapper INSTANCE = Mappers.getMapper( CarAndWheelMapper.class ); + + default String stringFromWheelPosition(WheelPosition source) { + return source == null ? null : source.getPosition(); + } + + default WheelPosition wheelPositionFromString(String source) { + return source == null ? null : new WheelPosition(source); + } + + CarDto carDtoFromCar(Car source); + + Car carFromCarDto(CarDto source); +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CarDto.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CarDto.java new file mode 100644 index 0000000000..5470550060 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CarDto.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public record CarDto(List wheelPositions) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Default.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Default.java new file mode 100644 index 0000000000..b845bdd730 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Default.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Filip Hrisafov + */ +@Documented +@Target(ElementType.CONSTRUCTOR) +@Retention(RetentionPolicy.SOURCE) +public @interface Default { +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberDto.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberDto.java new file mode 100644 index 0000000000..bf3ce6cd94 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberDto.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +/** + * @author Filip Hrisafov + */ +public record MemberDto(Boolean isActive, Boolean premium) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberEntity.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberEntity.java new file mode 100644 index 0000000000..50a7b1ce00 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberEntity.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +/** + * @author Filip Hrisafov + */ +public class MemberEntity { + + private Boolean isActive; + private Boolean premium; + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean active) { + isActive = active; + } + + public Boolean getPremium() { + return premium; + } + + public void setPremium(Boolean premium) { + this.premium = premium; + } +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberMapper.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberMapper.java new file mode 100644 index 0000000000..3460aeed69 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/MemberMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface MemberMapper { + + MemberMapper INSTANCE = Mappers.getMapper( MemberMapper.class ); + + MemberEntity fromRecord(MemberDto record); + + MemberDto toRecord(MemberEntity entity); + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Task.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Task.java new file mode 100644 index 0000000000..3f990fc89a --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/Task.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import java.util.List; + +/** + * @author Oliver Erhart + */ +public record Task( String id, Long number ) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/TaskDto.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/TaskDto.java new file mode 100644 index 0000000000..1ba6eb2be9 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/TaskDto.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import java.util.List; + +/** + * @author Oliver Erhart + */ +public record TaskDto(String id, Long number) { + + @Default + TaskDto(String id) { + this( id, 1L ); + } + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/TaskMapper.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/TaskMapper.java new file mode 100644 index 0000000000..8d9da767cd --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/TaskMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Oliver Erhart + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface TaskMapper { + + TaskMapper INSTANCE = Mappers.getMapper( TaskMapper.class ); + + TaskDto toRecord(Task source); + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/WheelPosition.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/WheelPosition.java new file mode 100644 index 0000000000..fe8016ddbf --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/WheelPosition.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records; + +/** + * @author Filip Hrisafov + */ +public class WheelPosition { + + private final String position; + + public WheelPosition(String position) { + this.position = position; + } + + public String getPosition() { + return position; + } +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/Address.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/Address.java new file mode 100644 index 0000000000..fb857e90ca --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/Address.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.nested; + +/** + * @author Filip Hrisafov + */ +public record Address(String street, String city) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProvider.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProvider.java new file mode 100644 index 0000000000..a0ce13c0ba --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProvider.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.nested; + +/** + * @author Filip Hrisafov + */ +public record CareProvider(String externalId, Address address) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProviderDto.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProviderDto.java new file mode 100644 index 0000000000..d7ce7229e9 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProviderDto.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.nested; + +/** + * @author Filip Hrisafov + */ +public record CareProviderDto(String id, String street, String city) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProviderMapper.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProviderMapper.java new file mode 100644 index 0000000000..89ed688976 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/nested/CareProviderMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.nested; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface CareProviderMapper { + + CareProviderMapper INSTANCE = Mappers.getMapper( CareProviderMapper.class ); + + @Mapping(target = "id", source = "externalId") + @Mapping(target = "street", source = "address.street") + @Mapping(target = "city", source = "address.city") + CareProviderDto map(CareProvider source); +} diff --git a/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java index b404681806..0960c92a45 100644 --- a/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java +++ b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java @@ -5,17 +5,19 @@ */ package org.mapstruct.itest.records; -import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.records.CustomerDto; import org.mapstruct.itest.records.CustomerEntity; import org.mapstruct.itest.records.CustomerMapper; -public class RecordsTest { +import static org.assertj.core.api.Assertions.assertThat; + +class RecordsTest { @Test - public void shouldMapRecord() { + void shouldMapRecord() { CustomerEntity customer = CustomerMapper.INSTANCE.fromRecord( new CustomerDto( "Kermit", "kermit@test.com" ) ); assertThat( customer ).isNotNull(); @@ -24,7 +26,7 @@ public void shouldMapRecord() { } @Test - public void shouldMapIntoRecord() { + void shouldMapIntoRecord() { CustomerEntity entity = new CustomerEntity(); entity.setName( "Kermit" ); entity.setMail( "kermit@test.com" ); @@ -37,7 +39,7 @@ public void shouldMapIntoRecord() { } @Test - public void shouldMapIntoGenericRecord() { + void shouldMapIntoGenericRecord() { CustomerEntity entity = new CustomerEntity(); entity.setName( "Kermit" ); entity.setMail( "kermit@test.com" ); @@ -47,4 +49,50 @@ public void shouldMapIntoGenericRecord() { assertThat( value ).isNotNull(); assertThat( value.value() ).isEqualTo( "Kermit" ); } + + @Test + void shouldMapIntoRecordWithList() { + Car car = new Car(); + car.setWheelPositions( Arrays.asList( new WheelPosition( "left" ) ) ); + + CarDto carDto = CarAndWheelMapper.INSTANCE.carDtoFromCar(car); + + assertThat( carDto ).isNotNull(); + assertThat( carDto.wheelPositions() ) + .containsExactly( "left" ); + } + + @Test + void shouldMapMemberRecord() { + MemberEntity member = MemberMapper.INSTANCE.fromRecord( new MemberDto( true, false ) ); + + assertThat( member ).isNotNull(); + assertThat( member.getIsActive() ).isTrue(); + assertThat( member.getPremium() ).isFalse(); + } + + @Test + void shouldMapIntoMemberRecord() { + MemberEntity entity = new MemberEntity(); + entity.setIsActive( false ); + entity.setPremium( true ); + + MemberDto value = MemberMapper.INSTANCE.toRecord( entity ); + + assertThat( value ).isNotNull(); + assertThat( value.isActive() ).isFalse(); + assertThat( value.premium() ).isTrue(); + } + + @Test + void shouldUseDefaultConstructor() { + Task entity = new Task( "some-id", 1000L ); + + TaskDto value = TaskMapper.INSTANCE.toRecord( entity ); + + assertThat( value ).isNotNull(); + assertThat( value.id() ).isEqualTo( "some-id" ); + assertThat( value.number() ).isEqualTo( 1L ); + } + } diff --git a/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/nested/NestedRecordsTest.java b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/nested/NestedRecordsTest.java new file mode 100644 index 0000000000..92f074e382 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/nested/NestedRecordsTest.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.records.nested; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class NestedRecordsTest { + + @Test + void shouldMapRecord() { + CareProvider source = new CareProvider( "kermit", new Address( "Sesame Street", "New York" ) ); + CareProviderDto target = CareProviderMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.id() ).isEqualTo( "kermit" ); + assertThat( target.street() ).isEqualTo( "Sesame Street" ); + assertThat( target.city() ).isEqualTo( "New York" ); + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/pom.xml b/integrationtest/src/test/resources/sealedSubclassTest/pom.xml new file mode 100644 index 0000000000..0706425e01 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + sealedSubclassTest + jar + + + + generate-via-compiler-plugin + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + \${compiler-id} + --enable-preview + + + + org.eclipse.tycho + tycho-compiler-jdt + ${org.eclipse.tycho.compiler-jdt.version} + + + + + + + + ${project.groupId} + mapstruct-processor + ${mapstruct.version} + provided + + + + + debug-forked-javac + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + true + + --enable-preview + -J-Xdebug + -J-Xnoagent + -J-Djava.compiler=NONE + -J-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + --enable-preview + + + + + + diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Bike.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Bike.java new file mode 100644 index 0000000000..5b68f52e64 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Bike.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class Bike extends Vehicle { + private int numberOfGears; + + public int getNumberOfGears() { + return numberOfGears; + } + + public void setNumberOfGears(int numberOfGears) { + this.numberOfGears = numberOfGears; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/BikeDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/BikeDto.java new file mode 100644 index 0000000000..d51e95633b --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/BikeDto.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class BikeDto extends VehicleDto { + private int numberOfGears; + + public int getNumberOfGears() { + return numberOfGears; + } + + public void setNumberOfGears(int numberOfGears) { + this.numberOfGears = numberOfGears; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Car.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Car.java new file mode 100644 index 0000000000..0ed238e2a5 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Car.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class Car extends Vehicle { + private boolean manual; + + public boolean isManual() { + return manual; + } + + public void setManual(boolean manual) { + this.manual = manual; + } + +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/CarDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/CarDto.java new file mode 100644 index 0000000000..800bd23d39 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/CarDto.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class CarDto extends VehicleDto { + private boolean manual; + + public boolean isManual() { + return manual; + } + + public void setManual(boolean manual) { + this.manual = manual; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Davidson.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Davidson.java new file mode 100644 index 0000000000..e883c14be3 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Davidson.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class Davidson extends Motor { + private int numberOfExhausts; + + public int getNumberOfExhausts() { + return numberOfExhausts; + } + + public void setNumberOfExhausts(int numberOfExhausts) { + this.numberOfExhausts = numberOfExhausts; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/DavidsonDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/DavidsonDto.java new file mode 100644 index 0000000000..e975226e3e --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/DavidsonDto.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class DavidsonDto extends MotorDto { + private int numberOfExhausts; + + public int getNumberOfExhausts() { + return numberOfExhausts; + } + + public void setNumberOfExhausts(int numberOfExhausts) { + this.numberOfExhausts = numberOfExhausts; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Harley.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Harley.java new file mode 100644 index 0000000000..87a48034c6 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Harley.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class Harley extends Motor { + private int engineDb; + + public int getEngineDb() { + return engineDb; + } + + public void setEngineDb(int engineDb) { + this.engineDb = engineDb; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/HarleyDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/HarleyDto.java new file mode 100644 index 0000000000..2090ee7450 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/HarleyDto.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public final class HarleyDto extends MotorDto { + private int engineDb; + + public int getEngineDb() { + return engineDb; + } + + public void setEngineDb(int engineDb) { + this.engineDb = engineDb; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Motor.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Motor.java new file mode 100644 index 0000000000..fcd5f4e4dc --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Motor.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public sealed abstract class Motor extends Vehicle permits Harley, Davidson { + private int cc; + + public int getCc() { + return cc; + } + + public void setCc(int cc) { + this.cc = cc; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/MotorDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/MotorDto.java new file mode 100644 index 0000000000..bd74eb9296 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/MotorDto.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public sealed abstract class MotorDto extends VehicleDto permits HarleyDto, DavidsonDto { + private int cc; + + public int getCc() { + return cc; + } + + public void setCc(int cc) { + this.cc = cc; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/SealedSubclassMapper.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/SealedSubclassMapper.java new file mode 100644 index 0000000000..b37f623686 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/SealedSubclassMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SealedSubclassMapper { + SealedSubclassMapper INSTANCE = Mappers.getMapper( SealedSubclassMapper.class ); + + VehicleCollectionDto map(VehicleCollection vehicles); + + @SubclassMapping( source = Car.class, target = CarDto.class ) + @SubclassMapping( source = Bike.class, target = BikeDto.class ) + @SubclassMapping( source = Harley.class, target = HarleyDto.class ) + @SubclassMapping( source = Davidson.class, target = DavidsonDto.class ) + @Mapping( source = "vehicleManufacturingCompany", target = "maker") + VehicleDto map(Vehicle vehicle); + + VehicleCollection mapInverse(VehicleCollectionDto vehicles); + + @InheritInverseConfiguration + Vehicle mapInverse(VehicleDto dto); +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Vehicle.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Vehicle.java new file mode 100644 index 0000000000..2a4e7560f6 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/Vehicle.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public abstract sealed class Vehicle permits Bike, Car, Motor { + private String name; + private String vehicleManufacturingCompany; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVehicleManufacturingCompany() { + return vehicleManufacturingCompany; + } + + public void setVehicleManufacturingCompany(String vehicleManufacturingCompany) { + this.vehicleManufacturingCompany = vehicleManufacturingCompany; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleCollection.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleCollection.java new file mode 100644 index 0000000000..1ada92a298 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleCollection.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +import java.util.ArrayList; +import java.util.Collection; + +public class VehicleCollection { + private Collection vehicles = new ArrayList<>(); + + public Collection getVehicles() { + return vehicles; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleCollectionDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleCollectionDto.java new file mode 100644 index 0000000000..0cae412177 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleCollectionDto.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +import java.util.ArrayList; +import java.util.Collection; + +public class VehicleCollectionDto { + private Collection vehicles = new ArrayList<>(); + + public Collection getVehicles() { + return vehicles; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleDto.java b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleDto.java new file mode 100644 index 0000000000..8c50bdcad9 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/main/java/org/mapstruct/itest/sealedsubclass/VehicleDto.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +public abstract sealed class VehicleDto permits CarDto, BikeDto, MotorDto { + private String name; + private String maker; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMaker() { + return maker; + } + + public void setMaker(String maker) { + this.maker = maker; + } +} diff --git a/integrationtest/src/test/resources/sealedSubclassTest/src/test/java/org/mapstruct/itest/sealedsubclass/SealedSubclassTest.java b/integrationtest/src/test/resources/sealedSubclassTest/src/test/java/org/mapstruct/itest/sealedsubclass/SealedSubclassTest.java new file mode 100644 index 0000000000..4fc08b4a33 --- /dev/null +++ b/integrationtest/src/test/resources/sealedSubclassTest/src/test/java/org/mapstruct/itest/sealedsubclass/SealedSubclassTest.java @@ -0,0 +1,59 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.sealedsubclass; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SealedSubclassTest { + + @Test + void mappingIsDoneUsingSubclassMapping() { + VehicleCollection vehicles = new VehicleCollection(); + vehicles.getVehicles().add( new Car() ); + vehicles.getVehicles().add( new Bike() ); + vehicles.getVehicles().add( new Harley() ); + vehicles.getVehicles().add( new Davidson() ); + + VehicleCollectionDto result = SealedSubclassMapper.INSTANCE.map( vehicles ); + + assertThat( result.getVehicles() ).doesNotContainNull(); + assertThat( result.getVehicles() ) // remove generic so that test works. + .extracting( vehicle -> (Class) vehicle.getClass() ) + .containsExactly( CarDto.class, BikeDto.class, HarleyDto.class, DavidsonDto.class ); + } + + @Test + void inverseMappingIsDoneUsingSubclassMapping() { + VehicleCollectionDto vehicles = new VehicleCollectionDto(); + vehicles.getVehicles().add( new CarDto() ); + vehicles.getVehicles().add( new BikeDto() ); + vehicles.getVehicles().add( new HarleyDto() ); + vehicles.getVehicles().add( new DavidsonDto() ); + + VehicleCollection result = SealedSubclassMapper.INSTANCE.mapInverse( vehicles ); + + assertThat( result.getVehicles() ).doesNotContainNull(); + assertThat( result.getVehicles() ) // remove generic so that test works. + .extracting( vehicle -> (Class) vehicle.getClass() ) + .containsExactly( Car.class, Bike.class, Harley.class, Davidson.class ); + } + + @Test + void subclassMappingInheritsInverseMapping() { + VehicleCollectionDto vehiclesDto = new VehicleCollectionDto(); + CarDto carDto = new CarDto(); + carDto.setMaker( "BenZ" ); + vehiclesDto.getVehicles().add( carDto ); + + VehicleCollection result = SealedSubclassMapper.INSTANCE.mapInverse( vehiclesDto ); + + assertThat( result.getVehicles() ) + .extracting( Vehicle::getVehicleManufacturingCompany ) + .containsExactly( "BenZ" ); + } +} diff --git a/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/SourceTargetMapper.java b/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/SourceTargetMapper.java index b3fd443c6b..99f93dab7b 100644 --- a/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/SourceTargetMapper.java +++ b/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/SourceTargetMapper.java @@ -5,10 +5,10 @@ */ package org.mapstruct.itest.simple; +import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; -import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.factory.Mappers; @Mapper(uses = ReferencedCustomMapper.class) diff --git a/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/Target.java b/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/Target.java index f49e09ae32..f9bd8af76b 100644 --- a/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/Target.java +++ b/integrationtest/src/test/resources/simpleTest/src/main/java/org/mapstruct/itest/simple/Target.java @@ -6,6 +6,7 @@ package org.mapstruct.itest.simple; import java.util.List; + import org.mapstruct.itest.simple.SomeType; public class Target { diff --git a/integrationtest/src/test/resources/simpleTest/src/test/java/org/mapstruct/itest/simple/ConversionTest.java b/integrationtest/src/test/resources/simpleTest/src/test/java/org/mapstruct/itest/simple/ConversionTest.java index 2b6a0bc93b..e248e3a1ad 100644 --- a/integrationtest/src/test/resources/simpleTest/src/test/java/org/mapstruct/itest/simple/ConversionTest.java +++ b/integrationtest/src/test/resources/simpleTest/src/test/java/org/mapstruct/itest/simple/ConversionTest.java @@ -5,17 +5,17 @@ */ package org.mapstruct.itest.simple; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.simple.Source; import org.mapstruct.itest.simple.SourceTargetMapper; import org.mapstruct.itest.simple.Target; -public class ConversionTest { +import static org.assertj.core.api.Assertions.assertThat; + +class ConversionTest { @Test - public void shouldApplyConversions() { + void shouldApplyConversions() { Source source = new Source(); source.setFoo( 42 ); source.setBar( 23L ); @@ -30,18 +30,18 @@ public void shouldApplyConversions() { } @Test - public void shouldHandleNulls() { + void shouldHandleNulls() { Source source = new Source(); Target target = SourceTargetMapper.INSTANCE.sourceToTarget( source ); assertThat( target ).isNotNull(); assertThat( target.getFoo() ).isEqualTo( Long.valueOf( 1 ) ); - assertThat( target.getBar() ).isEqualTo( 0 ); + assertThat( target.getBar() ).isZero(); assertThat( target.getZip() ).isEqualTo( "0" ); } @Test - public void shouldApplyConversionsToMappedProperties() { + void shouldApplyConversionsToMappedProperties() { Source source = new Source(); source.setQax( 42 ); source.setBaz( 23L ); @@ -54,7 +54,7 @@ public void shouldApplyConversionsToMappedProperties() { } @Test - public void shouldApplyConversionsForReverseMapping() { + void shouldApplyConversionsForReverseMapping() { Target target = new Target(); target.setFoo( 42L ); target.setBar( 23 ); @@ -69,7 +69,7 @@ public void shouldApplyConversionsForReverseMapping() { } @Test - public void shouldApplyConversionsToMappedPropertiesForReverseMapping() { + void shouldApplyConversionsToMappedPropertiesForReverseMapping() { Target target = new Target(); target.setQax( 42 ); target.setBaz( 23L ); @@ -82,7 +82,7 @@ public void shouldApplyConversionsToMappedPropertiesForReverseMapping() { } @Test - public void shouldWorkWithAbstractClass() { + void shouldWorkWithAbstractClass() { Source source = new Source(); source.setFoo( 42 ); source.setBar( 23L ); diff --git a/integrationtest/src/test/resources/springTest/pom.xml b/integrationtest/src/test/resources/springTest/pom.xml index 3c1fb54552..e4210c0cba 100644 --- a/integrationtest/src/test/resources/springTest/pom.xml +++ b/integrationtest/src/test/resources/springTest/pom.xml @@ -18,6 +18,10 @@ springTest jar + + + ${org.junit6.jupiter.version} + diff --git a/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/DecoratedSourceTargetMapper.java b/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/DecoratedSourceTargetMapper.java index a020ab2b27..fbb900205b 100644 --- a/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/DecoratedSourceTargetMapper.java +++ b/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/DecoratedSourceTargetMapper.java @@ -5,9 +5,9 @@ */ package org.mapstruct.itest.spring; +import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; -import org.mapstruct.DecoratedWith; import org.mapstruct.itest.spring.other.DateMapper; @Mapper( componentModel = MappingConstants.ComponentModel.SPRING, uses = DateMapper.class ) diff --git a/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/SecondDecoratedSourceTargetMapper.java b/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/SecondDecoratedSourceTargetMapper.java index 7c04262595..c270594e2a 100644 --- a/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/SecondDecoratedSourceTargetMapper.java +++ b/integrationtest/src/test/resources/springTest/src/main/java/org/mapstruct/itest/spring/SecondDecoratedSourceTargetMapper.java @@ -5,9 +5,9 @@ */ package org.mapstruct.itest.spring; +import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; -import org.mapstruct.DecoratedWith; import org.mapstruct.itest.spring.other.DateMapper; @Mapper( componentModel = MappingConstants.ComponentModel.SPRING, uses = DateMapper.class ) diff --git a/integrationtest/src/test/resources/springTest/src/test/java/org/mapstruct/itest/spring/SpringBasedMapperTest.java b/integrationtest/src/test/resources/springTest/src/test/java/org/mapstruct/itest/spring/SpringBasedMapperTest.java index a5b3bb0e65..c0f3de4ce2 100644 --- a/integrationtest/src/test/resources/springTest/src/test/java/org/mapstruct/itest/spring/SpringBasedMapperTest.java +++ b/integrationtest/src/test/resources/springTest/src/test/java/org/mapstruct/itest/spring/SpringBasedMapperTest.java @@ -5,14 +5,14 @@ */ package org.mapstruct.itest.spring; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.itest.spring.SpringBasedMapperTest.SpringTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -22,8 +22,8 @@ * @author Andreas Gudian */ @ContextConfiguration(classes = SpringTestConfig.class) -@RunWith(SpringJUnit4ClassRunner.class) -public class SpringBasedMapperTest { +@ExtendWith(SpringExtension.class) +class SpringBasedMapperTest { @Configuration @ComponentScan(basePackageClasses = SpringBasedMapperTest.class) @@ -40,7 +40,7 @@ public static class SpringTestConfig { private SecondDecoratedSourceTargetMapper secondDecoratedMapper; @Test - public void shouldInjectSpringBasedMapper() { + void shouldInjectSpringBasedMapper() { Source source = new Source(); Target target = mapper.sourceToTarget( source ); @@ -51,7 +51,7 @@ public void shouldInjectSpringBasedMapper() { } @Test - public void shouldInjectDecorator() { + void shouldInjectDecorator() { Source source = new Source(); Target target = decoratedMapper.sourceToTarget( source ); @@ -68,7 +68,7 @@ public void shouldInjectDecorator() { } @Test - public void shouldInjectSecondDecorator() { + void shouldInjectSecondDecorator() { Source source = new Source(); Target target = secondDecoratedMapper.sourceToTarget( source ); diff --git a/integrationtest/src/test/resources/superTypeGenerationTest/generator/pom.xml b/integrationtest/src/test/resources/superTypeGenerationTest/generator/pom.xml index 1b84638ef2..f122047093 100644 --- a/integrationtest/src/test/resources/superTypeGenerationTest/generator/pom.xml +++ b/integrationtest/src/test/resources/superTypeGenerationTest/generator/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test @@ -32,7 +32,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 -proc:none diff --git a/integrationtest/src/test/resources/superTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/supertypegeneration/BaseGenerationProcessor.java b/integrationtest/src/test/resources/superTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/supertypegeneration/BaseGenerationProcessor.java index 01693d7f0e..1147d78083 100644 --- a/integrationtest/src/test/resources/superTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/supertypegeneration/BaseGenerationProcessor.java +++ b/integrationtest/src/test/resources/superTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/supertypegeneration/BaseGenerationProcessor.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.io.Writer; import java.util.Set; - import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; diff --git a/integrationtest/src/test/resources/superTypeGenerationTest/usage/pom.xml b/integrationtest/src/test/resources/superTypeGenerationTest/usage/pom.xml index ee0d556b9f..4c5e4611f0 100644 --- a/integrationtest/src/test/resources/superTypeGenerationTest/usage/pom.xml +++ b/integrationtest/src/test/resources/superTypeGenerationTest/usage/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test @@ -38,7 +38,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 -XprintProcessorInfo diff --git a/integrationtest/src/test/resources/superTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/supertypegeneration/usage/GeneratedBasesTest.java b/integrationtest/src/test/resources/superTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/supertypegeneration/usage/GeneratedBasesTest.java index 4a012283d3..d0c76874ae 100644 --- a/integrationtest/src/test/resources/superTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/supertypegeneration/usage/GeneratedBasesTest.java +++ b/integrationtest/src/test/resources/superTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/supertypegeneration/usage/GeneratedBasesTest.java @@ -5,23 +5,23 @@ */ package org.mapstruct.itest.supertypegeneration.usage; -import static org.junit.Assert.assertEquals; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mapstruct.itest.supertypegeneration.usage.Order; import org.mapstruct.itest.supertypegeneration.usage.OrderDto; import org.mapstruct.itest.supertypegeneration.usage.OrderMapper; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * Integration test for using MapStruct with another annotation processor that generates super-types of mapping source * and target types. * * @author Gunnar Morling */ -public class GeneratedBasesTest { +class GeneratedBasesTest { @Test - public void considersPropertiesOnGeneratedSourceAndTargetTypes() { + void considersPropertiesOnGeneratedSourceAndTargetTypes() { Order order = new Order(); order.setItem( "my item" ); order.setBaseName2( "my base name 2" ); diff --git a/integrationtest/src/test/resources/targetTypeGenerationTest/generator/pom.xml b/integrationtest/src/test/resources/targetTypeGenerationTest/generator/pom.xml index bf0d704851..89bd9c2d2f 100644 --- a/integrationtest/src/test/resources/targetTypeGenerationTest/generator/pom.xml +++ b/integrationtest/src/test/resources/targetTypeGenerationTest/generator/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test @@ -32,7 +32,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 -proc:none diff --git a/integrationtest/src/test/resources/targetTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/targettypegeneration/DtoGenerationProcessor.java b/integrationtest/src/test/resources/targetTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/targettypegeneration/DtoGenerationProcessor.java index 87dd299bfe..1880055ba2 100644 --- a/integrationtest/src/test/resources/targetTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/targettypegeneration/DtoGenerationProcessor.java +++ b/integrationtest/src/test/resources/targetTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/targettypegeneration/DtoGenerationProcessor.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.io.Writer; import java.util.Set; - import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; diff --git a/integrationtest/src/test/resources/targetTypeGenerationTest/usage/pom.xml b/integrationtest/src/test/resources/targetTypeGenerationTest/usage/pom.xml index da72f667a4..0726580592 100644 --- a/integrationtest/src/test/resources/targetTypeGenerationTest/usage/pom.xml +++ b/integrationtest/src/test/resources/targetTypeGenerationTest/usage/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test @@ -38,7 +38,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 -XprintProcessorInfo diff --git a/integrationtest/src/test/resources/targetTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/targettypegeneration/usage/GeneratedTargetTypeTest.java b/integrationtest/src/test/resources/targetTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/targettypegeneration/usage/GeneratedTargetTypeTest.java index d9e3f8d789..b2e30a4797 100644 --- a/integrationtest/src/test/resources/targetTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/targettypegeneration/usage/GeneratedTargetTypeTest.java +++ b/integrationtest/src/test/resources/targetTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/targettypegeneration/usage/GeneratedTargetTypeTest.java @@ -5,9 +5,9 @@ */ package org.mapstruct.itest.targettypegeneration.usage; -import static org.junit.Assert.assertEquals; +import org.junit.jupiter.api.Test; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Integration test for using MapStruct with another annotation processor that generates the target type of a mapping @@ -15,10 +15,10 @@ * * @author Gunnar Morling */ -public class GeneratedTargetTypeTest { +class GeneratedTargetTypeTest { @Test - public void considersPropertiesOnGeneratedSourceAndTargetTypes() { + void considersPropertiesOnGeneratedSourceAndTargetTypes() { Order order = new Order(); order.setItem( "my item" ); diff --git a/integrationtest/src/test/resources/usesTypeGenerationTest/generator/pom.xml b/integrationtest/src/test/resources/usesTypeGenerationTest/generator/pom.xml index 6ac3a01297..24d3436533 100644 --- a/integrationtest/src/test/resources/usesTypeGenerationTest/generator/pom.xml +++ b/integrationtest/src/test/resources/usesTypeGenerationTest/generator/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test diff --git a/integrationtest/src/test/resources/usesTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/usestypegeneration/UsesTypeGenerationProcessor.java b/integrationtest/src/test/resources/usesTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/usestypegeneration/UsesTypeGenerationProcessor.java index 2c17279ff8..48b0609d9d 100644 --- a/integrationtest/src/test/resources/usesTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/usestypegeneration/UsesTypeGenerationProcessor.java +++ b/integrationtest/src/test/resources/usesTypeGenerationTest/generator/src/main/java/org/mapstruct/itest/usestypegeneration/UsesTypeGenerationProcessor.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.io.Writer; import java.util.Set; - import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; diff --git a/integrationtest/src/test/resources/usesTypeGenerationTest/usage/pom.xml b/integrationtest/src/test/resources/usesTypeGenerationTest/usage/pom.xml index 79696df47d..d7154c6246 100644 --- a/integrationtest/src/test/resources/usesTypeGenerationTest/usage/pom.xml +++ b/integrationtest/src/test/resources/usesTypeGenerationTest/usage/pom.xml @@ -21,8 +21,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test diff --git a/integrationtest/src/test/resources/usesTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/usestypegeneration/usage/GeneratedUsesTypeTest.java b/integrationtest/src/test/resources/usesTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/usestypegeneration/usage/GeneratedUsesTypeTest.java index e715e66620..55ea60a0ad 100644 --- a/integrationtest/src/test/resources/usesTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/usestypegeneration/usage/GeneratedUsesTypeTest.java +++ b/integrationtest/src/test/resources/usesTypeGenerationTest/usage/src/test/java/org/mapstruct/itest/usestypegeneration/usage/GeneratedUsesTypeTest.java @@ -5,7 +5,7 @@ */ package org.mapstruct.itest.usestypegeneration.usage; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,10 +14,10 @@ * * @author Filip Hrisafov */ -public class GeneratedUsesTypeTest { +class GeneratedUsesTypeTest { @Test - public void considersPropertiesOnGeneratedSourceAndTargetTypes() { + void considersPropertiesOnGeneratedSourceAndTargetTypes() { Order order = new Order(); order.setItem( "my item" ); diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000000..a21f2c22c4 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk21 diff --git a/mvnw b/mvnw index 41c0f0c23d..bd8896bf22 100755 --- a/mvnw +++ b/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 86115719e5..92450f9327 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -18,165 +19,171 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/parent/pom.xml b/parent/pom.xml index bb1229fcb8..b444c1847a 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,31 +11,48 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT pom MapStruct Parent An annotation processor for generating type-safe bean mappers - http://mapstruct.org/ + https://mapstruct.org/ 2012 UTF-8 - 1.0.0.Alpha2 - 3.0.0-M3 - 3.0.0-M5 - 3.1.0 - 5.3.3 + + mapstruct/mapstruct + /tmp/repository + 1.8 + ${java.version} + ${java.version} + + ${git.commit.author.time} + + 1.0.0.Alpha3 + 3.6.2 + 3.5.4 + 3.12.0 + 7.0.3 1.6.0 - 8.36.1 - 5.8.0-M1 - 1.4.2 - + 13.0.0 + 5.14.1 + 2.2.0 + 1.24.0 1 - 3.17.2 + 3.27.7 - + jdt_apt + + 1.8 + 3.25.5 + 2.3.2 + 2.3.0 @@ -71,12 +88,12 @@ sonatype-nexus-staging Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/ sonatype-nexus-snapshots Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots/ + https://central.sonatype.com/repository/maven-snapshots/ @@ -102,7 +119,7 @@ org.freemarker freemarker - 2.3.21 + 2.3.34 org.assertj @@ -112,7 +129,7 @@ com.google.guava guava - 29.0-jre + 32.0.0-jre org.mapstruct.tools.gem @@ -124,17 +141,24 @@ gem-processor ${org.mapstruct.gem.version} - - junit - junit - 4.13.1 - com.puppycrawl.tools checkstyle ${com.puppycrawl.tools.checkstyle.version} + + org.jetbrains.kotlin + kotlin-bom + ${kotlin.version} + pom + import + + + org.jetbrains.kotlin + kotlin-metadata-jvm + ${kotlin.version} + org.junit junit-bom @@ -152,29 +176,40 @@ javax.enterprise cdi-api - 1.2 + 2.0.SP1 + + + jakarta.enterprise + jakarta.enterprise.cdi-api + 4.0.1 javax.inject javax.inject 1 + + jakarta.inject + jakarta.inject-api + 2.0.1 + org.jboss.arquillian arquillian-bom - 1.0.2.Final + 1.7.2.Final import pom org.jboss.arquillian.container arquillian-weld-se-embedded-1.1 - 1.0.0.CR7 + 1.0.0.Final org.jboss.weld - weld-core - 2.3.2.Final + weld-core-impl + 3.1.8.Final + test org.glassfish @@ -208,7 +243,7 @@ org.projectlombok lombok - 1.18.20 + 1.18.38 org.immutables @@ -223,7 +258,7 @@ com.google.protobuf protobuf-java - 3.6.0 + ${protobuf.version} org.inferred @@ -231,11 +266,42 @@ 1.14.6 + + + org.jspecify + jspecify + 1.0.0 + + joda-time joda-time - 2.9 + 2.12.5 + + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb-runtime.version} + + + + jakarta.xml.bind + jakarta.xml.bind-api + 3.0.1 + + + com.sun.xml.bind + jaxb-impl + 3.0.2 @@ -262,12 +328,12 @@ org.codehaus.plexus plexus-utils - 3.0.20 + 3.0.24 commons-io commons-io - 2.7 + 2.15.0 @@ -307,12 +373,12 @@ org.apache.maven.plugins maven-assembly-plugin - 3.1.1 + 3.8.0 org.apache.maven.plugins maven-checkstyle-plugin - 3.1.1 + 3.6.0 build-config/checkstyle.xml true @@ -344,35 +410,26 @@ org.apache.maven.plugins maven-clean-plugin - 3.1.0 + 3.5.0 org.apache.maven.plugins maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - + 3.15.0 org.apache.maven.plugins maven-dependency-plugin - 3.1.1 + 3.9.0 org.apache.maven.plugins maven-deploy-plugin - 3.0.0-M1 + 3.1.4 true - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - org.apache.maven.plugins maven-enforcer-plugin @@ -402,12 +459,12 @@ org.apache.maven.plugins maven-install-plugin - 3.0.0-M1 + 3.1.4 org.apache.maven.plugins maven-jar-plugin - 3.1.1 + 3.5.0 org.apache.maven.plugins @@ -419,35 +476,20 @@ 8 - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - -DskipTests ${add.release.arguments} - clean install - false - true - @{project.version} - true - false - release - - org.apache.maven.plugins maven-resources-plugin - 3.1.0 + 3.4.0 org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.4.0 org.apache.maven.plugins maven-site-plugin - 3.7.1 + 3.21.0 org.apache.maven.plugins @@ -461,25 +503,35 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.0 + 3.6.1 - com.mycila.maven-license-plugin - maven-license-plugin - 1.10.b1 + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + com.mycila + license-maven-plugin + 4.6 org.codehaus.mojo animal-sniffer-maven-plugin - 1.17 + 1.20 org.ow2.asm asm - 6.2.1 + 7.0 + + org.codehaus.mojo + versions-maven-plugin + 2.16.2 + org.eclipse.m2e lifecycle-mapping @@ -534,7 +586,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.14 org.jvnet.jaxb2.maven2 @@ -559,7 +611,7 @@ com.github.siom79.japicmp japicmp-maven-plugin - 0.15.2 + 0.25.4 verify @@ -599,38 +651,43 @@ - com.mycila.maven-license-plugin - maven-license-plugin + com.mycila + license-maven-plugin -

      ${basedir}/../etc/license.txt
      true - - **/.idea/** - **/.mvn/** - **/build-config/checkstyle.xml - **/build-config/import-control.xml - copyright.txt - **/LICENSE.txt - **/mapstruct.xml - **/ci-settings.xml - **/eclipse-formatter-config.xml - **/forbidden-apis.txt - **/checkstyle-for-generated-sources.xml - **/nb-configuration.xml - **/junit-platform.properties - maven-settings.xml - readme.md - CONTRIBUTING.md - .gitattributes - .gitignore - .factorypath - .checkstyle - *.yml - mvnw* - **/*.asciidoc - **/binding.xjb - **/*.flattened-pom.xml - + + +
      ${basedir}/../etc/license.txt
      + + **/.idea/** + **/.mvn/** + **/build-config/checkstyle.xml + **/build-config/import-control.xml + copyright.txt + **/LICENSE.txt + **/mapstruct.xml + **/ci-settings.xml + **/eclipse-formatter-config.xml + **/forbidden-apis.txt + **/checkstyle-for-generated-sources.xml + **/nb-configuration.xml + **/junit-platform.properties + maven-settings.xml + readme.md + CONTRIBUTING.md + NEXT_RELEASE_CHANGELOG.md + .gitattributes + .gitignore + .factorypath + .checkstyle + *.yml + mvnw* + **/*.asciidoc + **/binding.xjb + **/*.flattened-pom.xml + +
      +
      SLASHSTAR_STYLE SLASHSTAR_STYLE @@ -660,6 +717,9 @@ java18 1.0 + + org.mapstruct.ap.internal.util.IgnoreJRERequirement + @@ -679,7 +739,7 @@ - [1.8,) + [${minimum.java.version},) @@ -746,12 +806,21 @@ + + org.codehaus.mojo + versions-maven-plugin + - release + publication + + + release + + @@ -778,18 +847,94 @@ + + + + + stage + + local::file:${maven.multiModuleProjectDirectory}/target/staging-deploy + + + deploy + + + + jreleaser + + - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - + org.jreleaser + jreleaser-maven-plugin + ${jreleaser.plugin.version} + + true + + + Mapstruct + + https://mapstruct.org/ + https://mapstruct.org/documentation/stable/reference/html/ + + + + ALWAYS + + ALWAYS + true + + + + false + + + + + + RELEASE + https://central.sonatype.com/api/v1/publisher + ${maven.multiModuleProjectDirectory}/target/staging-deploy + + + org.mapstruct + mapstruct-jdk8 + false + false + + + + + + + + + {{projectVersion}} + {{projectVersion}} + + ${maven.multiModuleProjectDirectory}/NEXT_RELEASE_CHANGELOG.md + + + \d+\.\d+\.\d+\.(Alpha|Beta|M|RC)\d* + + + legacy + + + + + + ${maven.multiModuleProjectDirectory}/distribution/target/mapstruct-{{projectVersion}}-dist.tar.gz + + + ${maven.multiModuleProjectDirectory}/distribution/target/mapstruct-{{projectVersion}}-dist.zip + + + + + diff --git a/pom.xml b/pom.xml index 28c35aea74..c753e3c1e4 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT parent/pom.xml @@ -27,7 +27,6 @@ core core-jdk8 processor - integrationtest true @@ -36,10 +35,14 @@ - com.mycila.maven-license-plugin - maven-license-plugin + com.mycila + license-maven-plugin -
      etc/license.txt
      + + +
      etc/license.txt
      +
      +
      XML_STYLE SLASHSTAR_STYLE @@ -71,5 +74,17 @@ distribution
      + + test + + + release + !true + + + + integrationtest + +
      diff --git a/processor/pom.xml b/processor/pom.xml index fcc6dbbab8..e317365aec 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.5.0-SNAPSHOT + 1.7.0-SNAPSHOT ../parent/pom.xml @@ -24,6 +24,7 @@ + 21 @@ -41,11 +42,20 @@ mapstruct provided + + org.jetbrains.kotlin + kotlin-metadata-jvm + provided + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + test + org.eclipse.tycho tycho-compiler-jdt - provided - true + test @@ -84,6 +94,16 @@ javax.inject test + + jakarta.inject + jakarta.inject-api + test + + + jakarta.enterprise + jakarta.enterprise.cdi-api + test + @@ -127,6 +147,20 @@ joda-time test + + + + org.jspecify + jspecify + test + + + + jakarta.xml.bind + jakarta.xml.bind-api + provided + true + @@ -145,6 +179,13 @@
      + + org.apache.maven.plugins + maven-javadoc-plugin + + all,-missing + + org.apache.maven.plugins maven-surefire-plugin @@ -265,10 +306,49 @@ + + org.jetbrains.kotlin + + kotlin-maven-plugin + + false + + + + kotlin-compile + compile + + compile + + + ${minimum.java.version} + + src/main/kotlin + + src/main/java + + + + + kotlin-test-compile + test-compile + + test-compile + + + ${minimum.java.version} + + src/test/java + + + + + org.apache.maven.plugins maven-compiler-plugin + ${minimum.java.version} org.mapstruct.tools.gem @@ -278,6 +358,33 @@ + + + + default-compile + none + + + default-testCompile + none + + + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + org.apache.maven.plugins @@ -365,7 +472,6 @@ javax.xml.bind jaxb-api - 2.3.1 provided true diff --git a/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java b/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java index e7e52be695..50831a8f42 100644 --- a/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java @@ -17,12 +17,12 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedOptions; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -30,19 +30,21 @@ import javax.lang.model.element.QualifiedNameable; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementKindVisitor6; +import javax.lang.model.util.ElementKindVisitor8; import javax.tools.Diagnostic.Kind; +import org.mapstruct.ap.internal.gem.MapperGem; import org.mapstruct.ap.internal.model.Mapper; +import org.mapstruct.ap.internal.option.MappingOption; import org.mapstruct.ap.internal.option.Options; -import org.mapstruct.ap.internal.gem.MapperGem; -import org.mapstruct.ap.internal.gem.ReportingPolicyGem; import org.mapstruct.ap.internal.processor.DefaultModelElementProcessorContext; import org.mapstruct.ap.internal.processor.ModelElementProcessor; import org.mapstruct.ap.internal.processor.ModelElementProcessor.ProcessorContext; import org.mapstruct.ap.internal.util.AnnotationProcessingException; import org.mapstruct.ap.internal.util.AnnotationProcessorContext; import org.mapstruct.ap.internal.util.RoundContext; +import org.mapstruct.ap.internal.util.Services; +import org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider; import org.mapstruct.ap.spi.TypeHierarchyErroneousException; import static javax.lang.model.element.ElementKind.CLASS; @@ -67,9 +69,9 @@ *
    17. if no error occurred, write out the model into Java source files
    18. * *

      - * For reading annotation attributes, gems as generated with help of the Hickory tool are used. These gems allow a comfortable access to - * annotations and their attributes without depending on their class objects. + * For reading annotation attributes, gems as generated with help of Gem Tools. These gems allow comfortable access to annotations and + * their attributes without depending on their class objects. *

      * The creation of Java source files is done using the FreeMarker template engine. * Each node of the mapper model has a corresponding FreeMarker template file which provides the Java representation of @@ -80,14 +82,6 @@ * @author Gunnar Morling */ @SupportedAnnotationTypes("org.mapstruct.Mapper") -@SupportedOptions({ - MappingProcessor.SUPPRESS_GENERATOR_TIMESTAMP, - MappingProcessor.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT, - MappingProcessor.UNMAPPED_TARGET_POLICY, - MappingProcessor.DEFAULT_COMPONENT_MODEL, - MappingProcessor.DEFAULT_INJECTION_STRATEGY, - MappingProcessor.VERBOSE -}) public class MappingProcessor extends AbstractProcessor { /** @@ -95,14 +89,35 @@ public class MappingProcessor extends AbstractProcessor { */ private static final boolean ANNOTATIONS_CLAIMED_EXCLUSIVELY = false; - protected static final String SUPPRESS_GENERATOR_TIMESTAMP = "mapstruct.suppressGeneratorTimestamp"; - protected static final String SUPPRESS_GENERATOR_VERSION_INFO_COMMENT = - "mapstruct.suppressGeneratorVersionInfoComment"; - protected static final String UNMAPPED_TARGET_POLICY = "mapstruct.unmappedTargetPolicy"; - protected static final String DEFAULT_COMPONENT_MODEL = "mapstruct.defaultComponentModel"; - protected static final String DEFAULT_INJECTION_STRATEGY = "mapstruct.defaultInjectionStrategy"; - protected static final String ALWAYS_GENERATE_SERVICE_FILE = "mapstruct.alwaysGenerateServicesFile"; - protected static final String VERBOSE = "mapstruct.verbose"; + // CHECKSTYLE:OFF + // Deprecated options, kept for backwards compatibility. + // They will be removed in a future release. + @Deprecated + protected static final String SUPPRESS_GENERATOR_TIMESTAMP = MappingOption.SUPPRESS_GENERATOR_TIMESTAMP.getOptionName(); + @Deprecated + protected static final String SUPPRESS_GENERATOR_VERSION_INFO_COMMENT = MappingOption.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT.getOptionName(); + @Deprecated + protected static final String UNMAPPED_TARGET_POLICY = MappingOption.UNMAPPED_TARGET_POLICY.getOptionName(); + @Deprecated + protected static final String UNMAPPED_SOURCE_POLICY = MappingOption.UNMAPPED_SOURCE_POLICY.getOptionName(); + @Deprecated + protected static final String DEFAULT_COMPONENT_MODEL = MappingOption.DEFAULT_COMPONENT_MODEL.getOptionName(); + @Deprecated + protected static final String DEFAULT_INJECTION_STRATEGY = MappingOption.DEFAULT_INJECTION_STRATEGY.getOptionName(); + @Deprecated + protected static final String ALWAYS_GENERATE_SERVICE_FILE = MappingOption.ALWAYS_GENERATE_SERVICE_FILE.getOptionName(); + @Deprecated + protected static final String DISABLE_BUILDERS = MappingOption.DISABLE_BUILDERS.getOptionName(); + @Deprecated + protected static final String VERBOSE = MappingOption.VERBOSE.getOptionName(); + @Deprecated + protected static final String NULL_VALUE_ITERABLE_MAPPING_STRATEGY = MappingOption.NULL_VALUE_ITERABLE_MAPPING_STRATEGY.getOptionName(); + @Deprecated + protected static final String NULL_VALUE_MAP_MAPPING_STRATEGY = MappingOption.NULL_VALUE_MAP_MAPPING_STRATEGY.getOptionName(); + // CHECKSTYLE:ON + + private final Set additionalSupportedOptions; + private final String additionalSupportedOptionsError; private Options options; @@ -115,35 +130,43 @@ public class MappingProcessor extends AbstractProcessor { *

      * If the hierarchy of a mapper's source/target types is never completed (i.e. the missing super-types are not * generated by other processors), this mapper will not be generated; That's fine, the compiler will raise an error - * due to the inconsistent Java types used as source or target anyways. + * due to the inconsistent Java types used as source or target anyway. */ private Set deferredMappers = new HashSet<>(); + public MappingProcessor() { + Set additionalSupportedOptions; + String additionalSupportedOptionsError; + try { + additionalSupportedOptions = resolveAdditionalSupportedOptions(); + additionalSupportedOptionsError = null; + } + catch ( IllegalStateException ex ) { + additionalSupportedOptions = Collections.emptySet(); + additionalSupportedOptionsError = ex.getMessage(); + } + this.additionalSupportedOptions = additionalSupportedOptions; + this.additionalSupportedOptionsError = additionalSupportedOptionsError; + } + @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init( processingEnv ); - options = createOptions(); + options = new Options( processingEnv.getOptions() ); annotationProcessorContext = new AnnotationProcessorContext( processingEnv.getElementUtils(), processingEnv.getTypeUtils(), processingEnv.getMessager(), - options.isVerbose() + options.isDisableBuilders(), + options.isDisableJSpecify(), + options.isVerbose(), + resolveAdditionalOptions( processingEnv.getOptions() ) ); - } - private Options createOptions() { - String unmappedTargetPolicy = processingEnv.getOptions().get( UNMAPPED_TARGET_POLICY ); - - return new Options( - Boolean.valueOf( processingEnv.getOptions().get( SUPPRESS_GENERATOR_TIMESTAMP ) ), - Boolean.valueOf( processingEnv.getOptions().get( SUPPRESS_GENERATOR_VERSION_INFO_COMMENT ) ), - unmappedTargetPolicy != null ? ReportingPolicyGem.valueOf( unmappedTargetPolicy.toUpperCase() ) : null, - processingEnv.getOptions().get( DEFAULT_COMPONENT_MODEL ), - processingEnv.getOptions().get( DEFAULT_INJECTION_STRATEGY ), - Boolean.valueOf( processingEnv.getOptions().get( ALWAYS_GENERATE_SERVICE_FILE ) ), - Boolean.valueOf( processingEnv.getOptions().get( VERBOSE ) ) - ); + if ( additionalSupportedOptionsError != null ) { + processingEnv.getMessager().printMessage( Kind.ERROR, additionalSupportedOptionsError ); + } } @Override @@ -204,6 +227,15 @@ else if ( !deferredMappers.isEmpty() ) { return ANNOTATIONS_CLAIMED_EXCLUSIVELY; } + @Override + public Set getSupportedOptions() { + return Stream.concat( + Stream.of( MappingOption.values() ).map( MappingOption::getOptionName ), + additionalSupportedOptions.stream() + ) + .collect( Collectors.toSet() ); + } + /** * Gets fresh copies of all mappers deferred from previous rounds (the originals may contain references to * erroneous source/target type elements). @@ -233,7 +265,7 @@ private Set getMappers(final Set annotations try { Set annotatedMappers = roundEnvironment.getElementsAnnotatedWith( annotation ); - for (Element mapperElement : annotatedMappers) { + for ( Element mapperElement : annotatedMappers ) { TypeElement mapperTypeElement = asTypeElement( mapperElement ); // on some JDKs, RoundEnvironment.getElementsAnnotatedWith( ... ) returns types with @@ -261,7 +293,11 @@ private void processMapperElements(Set mapperElements, RoundContext // of one outer interface List tst = mapperElement.getEnclosedElements(); ProcessorContext context = new DefaultModelElementProcessorContext( - processingEnv, options, roundContext, getDeclaredTypesNotToBeImported( mapperElement ) + processingEnv, + options, + roundContext, + getDeclaredTypesNotToBeImported( mapperElement ), + mapperElement ); processMapperTypeElement( context, mapperElement ); @@ -340,7 +376,7 @@ private R process(ProcessorContext context, ModelElementProcessor p /** * Retrieves all model element processors, ordered by their priority value - * (with the method retrieval processor having the lowest priority value (1) + * (with the method retrieval processor having the lowest priority value (1)) * and the code generation processor the highest priority value. * * @return A list with all model element processors. @@ -360,14 +396,14 @@ private R process(ProcessorContext context, ModelElementProcessor p processors.add( processorIterator.next() ); } - Collections.sort( processors, new ProcessorComparator() ); + processors.sort( new ProcessorComparator() ); return processors; } private TypeElement asTypeElement(Element element) { return element.accept( - new ElementKindVisitor6() { + new ElementKindVisitor8() { @Override public TypeElement visitTypeAsInterface(TypeElement e, Void p) { return e; @@ -382,6 +418,35 @@ public TypeElement visitTypeAsClass(TypeElement e, Void p) { ); } + /** + * Fetch the additional supported options provided by the SPI {@link AdditionalSupportedOptionsProvider}. + * + * @return the additional supported options + */ + private static Set resolveAdditionalSupportedOptions() { + Set additionalSupportedOptions = null; + for ( AdditionalSupportedOptionsProvider optionsProvider : + Services.all( AdditionalSupportedOptionsProvider.class ) ) { + if ( additionalSupportedOptions == null ) { + additionalSupportedOptions = new HashSet<>(); + } + Set providerOptions = optionsProvider.getAdditionalSupportedOptions(); + + for ( String providerOption : providerOptions ) { + // Ensure additional options are not in the mapstruct namespace + if ( providerOption.startsWith( "mapstruct" ) ) { + throw new IllegalStateException( + "Additional SPI options cannot start with \"mapstruct\". Provider " + optionsProvider + + " provided option " + providerOption ); + } + additionalSupportedOptions.add( providerOption ); + } + + } + + return additionalSupportedOptions == null ? Collections.emptySet() : additionalSupportedOptions; + } + private static class ProcessorComparator implements Comparator> { @Override @@ -400,4 +465,16 @@ private DeferredMapper(TypeElement deferredMapperElement, Element erroneousEleme this.erroneousElement = erroneousElement; } } + + /** + * Filters only the options belonging to the declared additional supported options. + * + * @param options all processor environment options + * @return filtered options + */ + private Map resolveAdditionalOptions(Map options) { + return options.entrySet().stream() + .filter( entry -> additionalSupportedOptions.contains( entry.getKey() ) ) + .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJavaTimeToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJavaTimeToStringConversion.java index 2f530618c9..3503a0e274 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJavaTimeToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJavaTimeToStringConversion.java @@ -21,15 +21,15 @@ *

      *

      * In general each type comes with a "parse" method to convert a string to this particular type. - * For formatting a dedicated instance of {@link java.time.format.DateTimeFormatter} is used. + * For formatting a dedicated instance of {@link DateTimeFormatter} is used. *

      *

      * If no date format for mapping is specified predefined ISO* formatters from - * {@link java.time.format.DateTimeFormatter} are used. + * {@link DateTimeFormatter} are used. *

      *

      - * An overview of date and time types shipped with Java 8 can be found at - * http://docs.oracle.com/javase/tutorial/datetime/iso/index.html. + * An overview of date and time types shipped with Java 8 can be found at the + * Standard Calendar Tutorial *

      */ public abstract class AbstractJavaTimeToStringConversion extends SimpleConversion { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJodaTypeToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJodaTypeToStringConversion.java index 7f83078318..c0a3d56481 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJodaTypeToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/AbstractJodaTypeToStringConversion.java @@ -13,9 +13,9 @@ import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.util.JodaTimeConstants; -import static org.mapstruct.ap.internal.util.Collections.asSet; import static org.mapstruct.ap.internal.conversion.ConversionUtils.dateTimeFormat; import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Base class for conversions between Joda-Time types and String. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToBigIntegerConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToBigIntegerConversion.java index 9e4d61b46e..6268371480 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToBigIntegerConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToBigIntegerConversion.java @@ -12,8 +12,8 @@ import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; -import static org.mapstruct.ap.internal.util.Collections.asSet; import static org.mapstruct.ap.internal.conversion.ConversionUtils.bigDecimal; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between {@link BigDecimal} and {@link BigInteger}. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToStringConversion.java index d696cf5bb5..384013a7b3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigDecimalToStringConversion.java @@ -14,8 +14,9 @@ import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; -import static org.mapstruct.ap.internal.util.Collections.asSet; import static org.mapstruct.ap.internal.conversion.ConversionUtils.bigDecimal; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between {@link BigDecimal} and {@link String}. @@ -45,7 +46,7 @@ public String getToExpression(ConversionContext conversionContext) { public String getFromExpression(ConversionContext conversionContext) { if ( requiresDecimalFormat( conversionContext ) ) { StringBuilder sb = new StringBuilder(); - sb.append( "(" + bigDecimal( conversionContext ) + ") " ); + sb.append( "(" ).append( bigDecimal( conversionContext ) ).append( ") " ); appendDecimalFormatter( sb, conversionContext ); sb.append( ".parse( )" ); return sb.toString(); @@ -64,18 +65,31 @@ protected Set getFromConversionImportTypes(ConversionContext conversionCon public List getRequiredHelperMethods(ConversionContext conversionContext) { List helpers = new ArrayList<>(); if ( conversionContext.getNumberFormat() != null ) { - helpers.add( new CreateDecimalFormat( conversionContext.getTypeFactory() ) ); + helpers.add( new CreateDecimalFormat( + conversionContext.getTypeFactory(), + conversionContext.getLocale() != null + ) ); } return helpers; } private void appendDecimalFormatter(StringBuilder sb, ConversionContext conversionContext) { - sb.append( "createDecimalFormat( " ); + boolean withLocale = conversionContext.getLocale() != null; + sb.append( "createDecimalFormat" ); + if ( withLocale ) { + sb.append( "WithLocale" ); + } + sb.append( "( " ); if ( conversionContext.getNumberFormat() != null ) { sb.append( "\"" ); sb.append( conversionContext.getNumberFormat() ); sb.append( "\"" ); } + if ( withLocale ) { + sb.append( ", " ).append( locale( conversionContext ) ).append( ".forLanguageTag( \"" ); + sb.append( conversionContext.getLocale() ); + sb.append( "\" )" ); + } sb.append( " )" ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigIntegerToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigIntegerToStringConversion.java index 9cb1cf41cb..dc3bd29cd8 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigIntegerToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/BigIntegerToStringConversion.java @@ -15,9 +15,10 @@ import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; -import static org.mapstruct.ap.internal.util.Collections.asSet; import static org.mapstruct.ap.internal.conversion.ConversionUtils.bigDecimal; import static org.mapstruct.ap.internal.conversion.ConversionUtils.bigInteger; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between {@link BigInteger} and {@link String}. @@ -47,7 +48,7 @@ public String getToExpression(ConversionContext conversionContext) { public String getFromExpression(ConversionContext conversionContext) { if ( requiresDecimalFormat( conversionContext ) ) { StringBuilder sb = new StringBuilder(); - sb.append( "( (" + bigDecimal( conversionContext ) + ") " ); + sb.append( "( (" ).append( bigDecimal( conversionContext ) ).append( ") " ); appendDecimalFormatter( sb, conversionContext ); sb.append( ".parse( )" ); sb.append( " ).toBigInteger()" ); @@ -72,18 +73,31 @@ protected Set getFromConversionImportTypes(ConversionContext conversionCon public List getRequiredHelperMethods(ConversionContext conversionContext) { List helpers = new ArrayList<>(); if ( conversionContext.getNumberFormat() != null ) { - helpers.add( new CreateDecimalFormat( conversionContext.getTypeFactory() ) ); + helpers.add( new CreateDecimalFormat( + conversionContext.getTypeFactory(), + conversionContext.getLocale() != null + ) ); } return helpers; } private void appendDecimalFormatter(StringBuilder sb, ConversionContext conversionContext) { - sb.append( "createDecimalFormat( " ); + boolean withLocale = conversionContext.getLocale() != null; + sb.append( "createDecimalFormat" ); + if ( withLocale ) { + sb.append( "WithLocale" ); + } + sb.append( "( " ); if ( conversionContext.getNumberFormat() != null ) { sb.append( "\"" ); sb.append( conversionContext.getNumberFormat() ); sb.append( "\"" ); } + if ( withLocale ) { + sb.append( ", " ).append( locale( conversionContext ) ).append( ".forLanguageTag( \"" ); + sb.append( conversionContext.getLocale() ); + sb.append( "\" )" ); + } sb.append( " )" ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionProvider.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionProvider.java index 31d4d27585..b392281bf4 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionProvider.java @@ -46,6 +46,8 @@ public interface ConversionProvider { Assignment from(ConversionContext conversionContext); /** + * Retrieves any helper methods required for creating the conversion. + * * @param conversionContext ConversionContext providing optional information required for creating the conversion. * * @return any helper methods when required. @@ -53,6 +55,8 @@ public interface ConversionProvider { List getRequiredHelperMethods(ConversionContext conversionContext); /** + * Retrieves any fields required for creating the conversion. + * * @param conversionContext ConversionContext providing optional information required for creating the conversion. * * @return any fields when required. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java index 20fb8a2fee..e014e30f27 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java @@ -7,10 +7,13 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URI; import java.sql.Time; import java.sql.Timestamp; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; @@ -189,6 +192,17 @@ public static String zoneId(ConversionContext conversionContext) { return typeReferenceName( conversionContext, ZoneId.class ); } + /** + * Name for {@link java.time.LocalDate}. + * + * @param conversionContext Conversion context + * + * @return Name or fully-qualified name. + */ + public static String localDate(ConversionContext conversionContext) { + return typeReferenceName( conversionContext, LocalDate.class ); + } + /** * Name for {@link java.time.LocalDateTime}. * @@ -254,4 +268,27 @@ public static String stringBuilder(ConversionContext conversionContext) { public static String uuid(ConversionContext conversionContext) { return typeReferenceName( conversionContext, UUID.class ); } + + /** + * Name for {@link java.net.URI}. + * + * @param conversionContext Conversion context + * + * @return Name or fully-qualified name. + */ + public static String uri(ConversionContext conversionContext) { + return typeReferenceName( conversionContext, URI.class ); + } + + /** + * Name for {@link java.text.DecimalFormatSymbols}. + * + * @param conversionContext Conversion context + * + * @return Name or fully-qualified name. + */ + public static String decimalFormatSymbols(ConversionContext conversionContext) { + return typeReferenceName( conversionContext, DecimalFormatSymbols.class ); + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java index 4b3ab2a818..6e1f2c03aa 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java @@ -7,6 +7,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URI; +import java.net.URL; import java.sql.Time; import java.sql.Timestamp; import java.time.Duration; @@ -14,12 +16,14 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.Period; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Currency; import java.util.Date; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -40,6 +44,7 @@ public class Conversions { private final Map conversions = new HashMap<>(); private final Type enumType; private final Type stringType; + private final Type integerType; private final TypeFactory typeFactory; public Conversions(TypeFactory typeFactory) { @@ -47,6 +52,7 @@ public Conversions(TypeFactory typeFactory) { this.enumType = typeFactory.getType( Enum.class ); this.stringType = typeFactory.getType( String.class ); + this.integerType = typeFactory.getType( Integer.class ); //native types <> native types, including wrappers registerNativeTypeConversion( byte.class, Byte.class ); @@ -184,6 +190,8 @@ public Conversions(TypeFactory typeFactory) { //misc. register( Enum.class, String.class, new EnumStringConversion() ); + register( Enum.class, Integer.class, new EnumToIntegerConversion() ); + register( Enum.class, int.class, new EnumToIntegerConversion() ); register( Date.class, String.class, new DateToStringConversion() ); register( BigDecimal.class, BigInteger.class, new BigDecimalToBigIntegerConversion() ); @@ -193,6 +201,9 @@ public Conversions(TypeFactory typeFactory) { register( Currency.class, String.class, new CurrencyToStringConversion() ); register( UUID.class, String.class, new UUIDToStringConversion() ); + register( Locale.class, String.class, new LocaleToStringConversion() ); + + registerJavaNetConversions(); } private void registerJodaConversions() { @@ -226,12 +237,19 @@ private void registerJava8TimeConversions() { register( Period.class, String.class, new StaticParseToStringConversion() ); register( Duration.class, String.class, new StaticParseToStringConversion() ); - // Java 8 to Date + // Java 8 time to Date register( ZonedDateTime.class, Date.class, new JavaZonedDateTimeToDateConversion() ); register( LocalDateTime.class, Date.class, new JavaLocalDateTimeToDateConversion() ); register( LocalDate.class, Date.class, new JavaLocalDateToDateConversion() ); register( Instant.class, Date.class, new JavaInstantToDateConversion() ); + // Java 8 time + register( LocalDateTime.class, LocalDate.class, new JavaLocalDateTimeToLocalDateConversion() ); + register( ZonedDateTime.class, LocalDateTime.class, new JavaZonedDateTimeToLocalDateTimeConversion() ); + register( OffsetDateTime.class, LocalDateTime.class, new JavaOffsetDateTimeToLocalDateTimeConversion() ); + + register( ZonedDateTime.class, Instant.class, new JavaZonedDateTimeToInstantConversion() ); + register( OffsetDateTime.class, Instant.class, new JavaOffsetDateTimeToInstantConversion() ); } private void registerJavaTimeSqlConversions() { @@ -256,10 +274,10 @@ private void registerNativeTypeConversion(Class sourceType, Class targetTy if ( sourceType.isPrimitive() && targetType.isPrimitive() ) { register( sourceType, targetType, new PrimitiveToPrimitiveConversion( sourceType ) ); } - else if ( sourceType.isPrimitive() && !targetType.isPrimitive() ) { + else if ( sourceType.isPrimitive() ) { register( sourceType, targetType, new PrimitiveToWrapperConversion( sourceType, targetType ) ); } - else if ( !sourceType.isPrimitive() && targetType.isPrimitive() ) { + else if ( targetType.isPrimitive() ) { register( sourceType, targetType, inverse( new PrimitiveToWrapperConversion( targetType, sourceType ) ) ); } else { @@ -294,6 +312,17 @@ private void registerBigDecimalConversion(Class targetType) { } } + private void registerJavaNetConversions() { + if ( isJavaNetAvailable() ) { + register( URI.class, String.class, new URIToStringConversion() ); + register( URL.class, String.class, new URLToStringConversion() ); + } + } + + private boolean isJavaNetAvailable() { + return typeFactory.isTypeAvailable( "java.net.URL" ); + } + private void register(Class sourceClass, Class targetClass, ConversionProvider conversion) { Type sourceType = typeFactory.getType( sourceClass ); Type targetType = typeFactory.getType( targetClass ); @@ -311,10 +340,51 @@ private void register(String sourceTypeName, Class targetClass, ConversionPro } public ConversionProvider getConversion(Type sourceType, Type targetType) { - if ( sourceType.isEnumType() && targetType.equals( stringType ) ) { + if ( sourceType.isOptionalType() ) { + if ( targetType.isOptionalType() ) { + // We cannot convert optional to optional + return null; + } + Type sourceBaseType = sourceType.getOptionalBaseType(); + if ( sourceBaseType.equals( targetType ) ) { + // Optional -> Type + return TypeToOptionalConversion.OPTIONAL_TO_TYPE_CONVERSION; + } + + ConversionProvider conversionProvider = getInternalConversion( sourceBaseType, targetType ); + if ( conversionProvider != null ) { + return inverse( new OptionalWrapperConversionProvider( conversionProvider ) ); + } + + } + else if ( targetType.isOptionalType() ) { + // Type -> Optional + Type targetBaseType = targetType.getOptionalBaseType(); + if ( targetBaseType.equals( sourceType ) ) { + return TypeToOptionalConversion.TYPE_TO_OPTIONAL_CONVERSION; + } + ConversionProvider conversionProvider = getInternalConversion( sourceType, targetBaseType ); + if ( conversionProvider != null ) { + return new OptionalWrapperConversionProvider( conversionProvider ); + } + return null; + + } + + return getInternalConversion( sourceType, targetType ); + } + + private ConversionProvider getInternalConversion(Type sourceType, Type targetType) { + if ( sourceType.isEnumType() && + ( targetType.equals( stringType ) || + targetType.getBoxedEquivalent().equals( integerType ) ) + ) { sourceType = enumType; } - else if ( targetType.isEnumType() && sourceType.equals( stringType ) ) { + else if ( targetType.isEnumType() && + ( sourceType.equals( stringType ) || + sourceType.getBoxedEquivalent().equals( integerType ) ) + ) { targetType = enumType; } @@ -362,11 +432,7 @@ public boolean equals(Object obj) { return false; } - if ( !Objects.equals( targetType, other.targetType ) ) { - return false; - } - - return true; + return Objects.equals( targetType, other.targetType ); } } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.java index 77c59445ab..d1b49cff69 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.java @@ -5,9 +5,11 @@ */ package org.mapstruct.ap.internal.conversion; -import static org.mapstruct.ap.internal.util.Collections.asSet; - import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.Set; import org.mapstruct.ap.internal.model.HelperMethod; @@ -16,6 +18,8 @@ import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.source.MappingMethodOptions; +import static org.mapstruct.ap.internal.util.Collections.asSet; + /** * HelperMethod that creates a {@link java.text.DecimalFormat} * @@ -27,13 +31,30 @@ public class CreateDecimalFormat extends HelperMethod { private final Parameter parameter; + private final Parameter localeParameter; private final Type returnType; private final Set importTypes; - public CreateDecimalFormat(TypeFactory typeFactory) { + public CreateDecimalFormat(TypeFactory typeFactory, boolean withLocale) { this.parameter = new Parameter( "numberFormat", typeFactory.getType( String.class ) ); + this.localeParameter = withLocale ? new Parameter( "locale", typeFactory.getType( Locale.class ) ) : null; this.returnType = typeFactory.getType( DecimalFormat.class ); - this.importTypes = asSet( parameter.getType(), returnType ); + if ( withLocale ) { + this.importTypes = asSet( + parameter.getType(), + returnType, + typeFactory.getType( DecimalFormatSymbols.class ), + typeFactory.getType( Locale.class ) + ); + } + else { + this.importTypes = asSet( parameter.getType(), returnType ); + } + } + + @Override + public String getName() { + return localeParameter == null ? "createDecimalFormat" : "createDecimalFormatWithLocale"; } @Override @@ -60,4 +81,12 @@ public MappingMethodOptions getOptions() { public String describe() { return null; } + + @Override + public List getParameters() { + if ( localeParameter == null ) { + return super.getParameters(); + } + return Arrays.asList( getParameter(), localeParameter ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/CurrencyToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/CurrencyToStringConversion.java index 99797f898c..3f32994734 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/CurrencyToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/CurrencyToStringConversion.java @@ -15,6 +15,8 @@ import static org.mapstruct.ap.internal.conversion.ConversionUtils.currency; /** + * Conversion between {@link Currency} and {@link String}. + * * @author Darren Rambaud */ public class CurrencyToStringConversion extends SimpleConversion { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/DateToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/DateToStringConversion.java index 361ce9d9ab..35c7f88d74 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/DateToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/DateToStringConversion.java @@ -10,15 +10,18 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.Set; import org.mapstruct.ap.internal.model.HelperMethod; import org.mapstruct.ap.internal.model.TypeConversion; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; -import static java.util.Arrays.asList; -import static org.mapstruct.ap.internal.util.Collections.asSet; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; import static org.mapstruct.ap.internal.conversion.ConversionUtils.simpleDateFormat; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between {@link String} and {@link Date}. @@ -29,7 +32,7 @@ public class DateToStringConversion implements ConversionProvider { @Override public Assignment to(ConversionContext conversionContext) { - return new TypeConversion( asSet( conversionContext.getTypeFactory().getType( SimpleDateFormat.class ) ), + return new TypeConversion( getImportTypes( conversionContext ), Collections.emptyList(), getConversionExpression( conversionContext, "format" ) ); @@ -37,8 +40,8 @@ public Assignment to(ConversionContext conversionContext) { @Override public Assignment from(ConversionContext conversionContext) { - return new TypeConversion( asSet( conversionContext.getTypeFactory().getType( SimpleDateFormat.class ) ), - asList( conversionContext.getTypeFactory().getType( ParseException.class ) ), + return new TypeConversion( getImportTypes( conversionContext ), + Collections.singletonList( conversionContext.getTypeFactory().getType( ParseException.class ) ), getConversionExpression( conversionContext, "parse" ) ); } @@ -48,6 +51,17 @@ public List getRequiredHelperMethods(ConversionContext conversionC return Collections.emptyList(); } + private Set getImportTypes(ConversionContext conversionContext) { + if ( conversionContext.getLocale() == null ) { + return Collections.singleton( conversionContext.getTypeFactory().getType( SimpleDateFormat.class ) ); + } + + return asSet( + conversionContext.getTypeFactory().getType( SimpleDateFormat.class ), + conversionContext.getTypeFactory().getType( Locale.class ) + ); + } + private String getConversionExpression(ConversionContext conversionContext, String method) { StringBuilder conversionString = new StringBuilder( "new " ); conversionString.append( simpleDateFormat( conversionContext ) ); @@ -56,7 +70,16 @@ private String getConversionExpression(ConversionContext conversionContext, Stri if ( conversionContext.getDateFormat() != null ) { conversionString.append( " \"" ); conversionString.append( conversionContext.getDateFormat() ); - conversionString.append( "\" " ); + conversionString.append( "\"" ); + + if ( conversionContext.getLocale() != null ) { + conversionString.append( ", " ).append( locale( conversionContext ) ).append( ".forLanguageTag( \"" ); + conversionString.append( conversionContext.getLocale() ); + conversionString.append( "\" ) " ); + } + else { + conversionString.append( " " ); + } } conversionString.append( ")." ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/EnumToIntegerConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/EnumToIntegerConversion.java new file mode 100644 index 0000000000..6904d8ff63 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/EnumToIntegerConversion.java @@ -0,0 +1,38 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; + +import static org.mapstruct.ap.internal.util.Collections.asSet; + +/** + * Conversion between {@link Enum} and {@link Integer} types. + * + * @author Jose Carlos Campanero Ortiz + */ +public class EnumToIntegerConversion extends SimpleConversion { + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".ordinal()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return conversionContext.getTargetType().createReferenceName() + ".values()[ ]"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return asSet( + conversionContext.getTargetType() + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaInstantToDateConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaInstantToDateConversion.java index fab4778061..3e4fe65eb9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaInstantToDateConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaInstantToDateConversion.java @@ -5,13 +5,13 @@ */ package org.mapstruct.ap.internal.conversion; +import java.util.Date; +import java.util.Set; + import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.util.Collections; -import java.util.Date; -import java.util.Set; - import static org.mapstruct.ap.internal.conversion.ConversionUtils.date; /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaLocalDateTimeToLocalDateConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaLocalDateTimeToLocalDateConversion.java new file mode 100644 index 0000000000..9116bada4c --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaLocalDateTimeToLocalDateConversion.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.mapstruct.ap.internal.model.common.ConversionContext; + +/** + * SimpleConversion for mapping {@link LocalDateTime} to + * {@link LocalDate} and vice versa. + */ + +public class JavaLocalDateTimeToLocalDateConversion extends SimpleConversion { + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toLocalDate()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atStartOfDay()"; + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.java new file mode 100644 index 0000000000..9c5cb63990 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaOffsetDateTimeToInstantConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toInstant()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atOffset( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java new file mode 100644 index 0000000000..987c4cc625 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaOffsetDateTimeToLocalDateTimeConversion extends SimpleConversion { + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toLocalDateTime()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atOffset( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.java new file mode 100644 index 0000000000..44cc68c2a7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaZonedDateTimeToInstantConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toInstant()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atZone( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java new file mode 100644 index 0000000000..9a9275cede --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaZonedDateTimeToLocalDateTimeConversion extends SimpleConversion { + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toLocalDateTime()"; + + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atZone( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaDateTimeToCalendarConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaDateTimeToCalendarConversion.java index f9c70ff8ff..d5e74e920a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaDateTimeToCalendarConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaDateTimeToCalendarConversion.java @@ -12,8 +12,8 @@ import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; -import static org.mapstruct.ap.internal.util.Collections.asSet; import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between {@code DateTime} and {@link Calendar}. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaTimeToDateConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaTimeToDateConversion.java index 0e47085bc8..d8212e140e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaTimeToDateConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JodaTimeToDateConversion.java @@ -5,14 +5,14 @@ */ package org.mapstruct.ap.internal.conversion; -import static org.mapstruct.ap.internal.util.Collections.asSet; - import java.util.Date; import java.util.Set; import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; +import static org.mapstruct.ap.internal.util.Collections.asSet; + /** * Conversion between the following Joda types and {@link Date}: *
        diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/LocaleToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/LocaleToStringConversion.java new file mode 100644 index 0000000000..05a6da0b19 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/LocaleToStringConversion.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.util.Locale; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; + +/** + * Conversion between {@link java.util.Locale} and {@link String}. + * + * @author Jason Bodnar + */ +public class LocaleToStringConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toLanguageTag()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return locale( conversionContext ) + ".forLanguageTag( )"; + } + + @Override + protected Set getFromConversionImportTypes(final ConversionContext conversionContext) { + return Collections.asSet( conversionContext.getTypeFactory().getType( Locale.class ) ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/OptionalWrapperConversionProvider.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/OptionalWrapperConversionProvider.java new file mode 100644 index 0000000000..db9480662d --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/OptionalWrapperConversionProvider.java @@ -0,0 +1,100 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.util.List; + +import org.mapstruct.ap.internal.model.FromOptionalTypeConversion; +import org.mapstruct.ap.internal.model.HelperMethod; +import org.mapstruct.ap.internal.model.ToOptionalTypeConversion; +import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.FieldReference; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.common.TypeFactory; + +/** + * A conversion provider that wraps / unwraps the underlying conversion in Optional. + * e.g., For conversion from {@code Optional} to {@code Integer}. + * + * @author Filip Hrisafov + */ +public class OptionalWrapperConversionProvider implements ConversionProvider { + + private final ConversionProvider conversionProvider; + + public OptionalWrapperConversionProvider(ConversionProvider conversionProvider) { + this.conversionProvider = conversionProvider; + } + + @Override + public Assignment to(ConversionContext conversionContext) { + Assignment assignment = conversionProvider.to( new OptionalConversionContext( conversionContext ) ); + return new ToOptionalTypeConversion( conversionContext.getTargetType(), assignment ); + } + + @Override + public Assignment from(ConversionContext conversionContext) { + Assignment assignment = conversionProvider.to( new OptionalConversionContext( conversionContext ) ); + return new FromOptionalTypeConversion( conversionContext.getSourceType(), assignment ); + } + + @Override + public List getRequiredHelperMethods(ConversionContext conversionContext) { + return conversionProvider.getRequiredHelperMethods( conversionContext ); + } + + @Override + public List getRequiredHelperFields(ConversionContext conversionContext) { + return conversionProvider.getRequiredHelperFields( conversionContext ); + } + + private static class OptionalConversionContext implements ConversionContext { + + private final ConversionContext delegate; + + private OptionalConversionContext(ConversionContext delegate) { + this.delegate = delegate; + } + + @Override + public Type getTargetType() { + return resolveType( delegate.getTargetType() ); + } + + @Override + public Type getSourceType() { + return resolveType( delegate.getSourceType() ); + } + + private Type resolveType(Type type) { + if ( type.isOptionalType() ) { + return type.getOptionalBaseType(); + } + return type; + } + + @Override + public String getDateFormat() { + return delegate.getDateFormat(); + } + + @Override + public String getNumberFormat() { + return delegate.getNumberFormat(); + } + + @Override + public String getLocale() { + return delegate.getLocale(); + } + + @Override + public TypeFactory getTypeFactory() { + return delegate.getTypeFactory(); + } + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/PrimitiveToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/PrimitiveToStringConversion.java index fcc7241290..909ce8c0f2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/PrimitiveToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/PrimitiveToStringConversion.java @@ -6,7 +6,9 @@ package org.mapstruct.ap.internal.conversion; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.Collections; +import java.util.Locale; import java.util.Set; import org.mapstruct.ap.internal.model.common.ConversionContext; @@ -15,6 +17,9 @@ import org.mapstruct.ap.internal.util.Strings; import static org.mapstruct.ap.internal.conversion.ConversionUtils.decimalFormat; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.decimalFormatSymbols; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between primitive types such as {@code byte} or {@code long} and @@ -53,9 +58,15 @@ public String getToExpression(ConversionContext conversionContext) { @Override public Set getToConversionImportTypes(ConversionContext conversionContext) { if ( requiresDecimalFormat( conversionContext ) ) { - return Collections.singleton( - conversionContext.getTypeFactory().getType( DecimalFormat.class ) - ); + if ( conversionContext.getLocale() != null ) { + return asSet( + conversionContext.getTypeFactory().getType( DecimalFormat.class ), + conversionContext.getTypeFactory().getType( DecimalFormatSymbols.class ), + conversionContext.getTypeFactory().getType( Locale.class ) + ); + } + + return Collections.singleton( conversionContext.getTypeFactory().getType( DecimalFormat.class ) ); } return Collections.emptySet(); @@ -80,9 +91,15 @@ public String getFromExpression(ConversionContext conversionContext) { @Override protected Set getFromConversionImportTypes(ConversionContext conversionContext) { if ( requiresDecimalFormat( conversionContext ) ) { - return Collections.singleton( - conversionContext.getTypeFactory().getType( DecimalFormat.class ) - ); + if ( conversionContext.getLocale() != null ) { + return asSet( + conversionContext.getTypeFactory().getType( DecimalFormat.class ), + conversionContext.getTypeFactory().getType( DecimalFormatSymbols.class ), + conversionContext.getTypeFactory().getType( Locale.class ) + ); + } + + return Collections.singleton( conversionContext.getTypeFactory().getType( DecimalFormat.class ) ); } return Collections.emptySet(); @@ -97,6 +114,16 @@ private void appendDecimalFormatter(StringBuilder sb, ConversionContext conversi sb.append( "\"" ); sb.append( conversionContext.getNumberFormat() ); sb.append( "\"" ); + + if ( conversionContext.getLocale() != null ) { + sb.append( ", " ) + .append( decimalFormatSymbols( conversionContext ) ) + .append( ".getInstance( " ) + .append( locale( conversionContext ) ) + .append( ".forLanguageTag( \"" ) + .append( conversionContext.getLocale() ) + .append( " \" ) )" ); + } } sb.append( " )" ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ReverseConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ReverseConversion.java index 7fd7f6bbb8..cb496b0c54 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ReverseConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ReverseConversion.java @@ -5,11 +5,12 @@ */ package org.mapstruct.ap.internal.conversion; -import java.util.Collections; import java.util.List; + +import org.mapstruct.ap.internal.model.HelperMethod; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.ConversionContext; -import org.mapstruct.ap.internal.model.HelperMethod; +import org.mapstruct.ap.internal.model.common.FieldReference; /** * * A {@link ConversionProvider} which creates the inversed conversions for a @@ -41,7 +42,11 @@ public Assignment from(ConversionContext conversionContext) { @Override public List getRequiredHelperMethods(ConversionContext conversionContext) { - return Collections.emptyList(); + return conversionProvider.getRequiredHelperMethods( conversionContext ); } + @Override + public List getRequiredHelperFields(ConversionContext conversionContext) { + return conversionProvider.getRequiredHelperFields( conversionContext ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/SimpleConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/SimpleConversion.java index f0e0ccd880..91051a3e23 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/SimpleConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/SimpleConversion.java @@ -9,10 +9,10 @@ import java.util.List; import java.util.Set; +import org.mapstruct.ap.internal.model.HelperMethod; import org.mapstruct.ap.internal.model.TypeConversion; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.ConversionContext; -import org.mapstruct.ap.internal.model.HelperMethod; import org.mapstruct.ap.internal.model.common.Type; /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/TypeToOptionalConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/TypeToOptionalConversion.java new file mode 100644 index 0000000000..dbab6bc4ca --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/TypeToOptionalConversion.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.util.Collections; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Strings; + +import static org.mapstruct.ap.internal.conversion.ReverseConversion.inverse; + +/** + * Conversion between {@link java.util.Optional Optional} and its base type. + * + * @author Filip Hrisafov + */ +public class TypeToOptionalConversion extends SimpleConversion { + + static final TypeToOptionalConversion TYPE_TO_OPTIONAL_CONVERSION = new TypeToOptionalConversion(); + static final ConversionProvider OPTIONAL_TO_TYPE_CONVERSION = inverse( TYPE_TO_OPTIONAL_CONVERSION ); + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return conversionContext.getTargetType().asRawType().createReferenceName() + ".of( )"; + } + + @Override + protected Set getToConversionImportTypes(ConversionContext conversionContext) { + return Collections.singleton( conversionContext.getTargetType().asRawType() ); + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + StringBuilder sb = new StringBuilder(".get"); + Type optionalBaseType = conversionContext.getSourceType().getOptionalBaseType(); + if ( optionalBaseType.isPrimitive() ) { + sb.append( "As" ).append( Strings.capitalize( optionalBaseType.getName() ) ); + } + return sb.append( "()" ).toString(); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/URIToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/URIToStringConversion.java new file mode 100644 index 0000000000..c25383ae05 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/URIToStringConversion.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.net.URI; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.uri; +import static org.mapstruct.ap.internal.util.Collections.asSet; + +/** + * Conversion between {@link java.net.URI} and {@link String}. + * + * @author Maciej Kucharczyk + */ +public class URIToStringConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toString()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return uri( conversionContext ) + ".create( )"; + } + + @Override + protected Set getFromConversionImportTypes(final ConversionContext conversionContext) { + return asSet( conversionContext.getTypeFactory().getType( URI.class ) ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java new file mode 100644 index 0000000000..b1fd5f599a --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.conversion; + +import java.net.MalformedURLException; +import java.net.URI; +import java.util.List; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.uri; +import static org.mapstruct.ap.internal.util.Collections.asSet; + +/** + * Conversion between {@link java.net.URL} and {@link String}. + * + * @author Adam Szatyin + */ +public class URLToStringConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toString()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return uri( conversionContext ) + ".create( ).toURL()"; + } + + @Override + protected Set getFromConversionImportTypes(final ConversionContext conversionContext) { + return asSet( conversionContext.getTypeFactory().getType( URI.class ) ); + } + + @Override + protected List getFromConversionExceptionTypes(ConversionContext conversionContext) { + return java.util.Collections.singletonList( + conversionContext.getTypeFactory().getType( MalformedURLException.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/WrapperToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/WrapperToStringConversion.java index 300c901216..dd7b6bac8d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/WrapperToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/WrapperToStringConversion.java @@ -6,7 +6,9 @@ package org.mapstruct.ap.internal.conversion; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.Collections; +import java.util.Locale; import java.util.Set; import org.mapstruct.ap.internal.model.common.ConversionContext; @@ -15,6 +17,9 @@ import org.mapstruct.ap.internal.util.Strings; import static org.mapstruct.ap.internal.conversion.ConversionUtils.decimalFormat; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.decimalFormatSymbols; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.locale; +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * Conversion between wrapper types such as {@link Integer} and {@link String}. @@ -52,9 +57,15 @@ public String getToExpression(ConversionContext conversionContext) { @Override public Set getToConversionImportTypes(ConversionContext conversionContext) { if ( requiresDecimalFormat( conversionContext ) ) { - return Collections.singleton( - conversionContext.getTypeFactory().getType( DecimalFormat.class ) - ); + if ( conversionContext.getLocale() != null ) { + return asSet( + conversionContext.getTypeFactory().getType( DecimalFormat.class ), + conversionContext.getTypeFactory().getType( DecimalFormatSymbols.class ), + conversionContext.getTypeFactory().getType( Locale.class ) + ); + } + + return Collections.singleton( conversionContext.getTypeFactory().getType( DecimalFormat.class ) ); } return Collections.emptySet(); @@ -79,9 +90,15 @@ public String getFromExpression(ConversionContext conversionContext) { @Override protected Set getFromConversionImportTypes(ConversionContext conversionContext) { if ( requiresDecimalFormat( conversionContext ) ) { - return Collections.singleton( - conversionContext.getTypeFactory().getType( DecimalFormat.class ) - ); + if ( conversionContext.getLocale() != null ) { + return asSet( + conversionContext.getTypeFactory().getType( DecimalFormat.class ), + conversionContext.getTypeFactory().getType( DecimalFormatSymbols.class ), + conversionContext.getTypeFactory().getType( Locale.class ) + ); + } + + return Collections.singleton( conversionContext.getTypeFactory().getType( DecimalFormat.class ) ); } return Collections.emptySet(); @@ -96,6 +113,16 @@ private void appendDecimalFormatter(StringBuilder sb, ConversionContext conversi sb.append( "\"" ); sb.append( conversionContext.getNumberFormat() ); sb.append( "\"" ); + + if ( conversionContext.getLocale() != null ) { + sb.append( ", " ) + .append( decimalFormatSymbols( conversionContext ) ) + .append( ".getInstance( " ) + .append( locale( conversionContext ) ) + .append( ".forLanguageTag( \"" ) + .append( conversionContext.getLocale() ) + .append( " \" ) )" ); + } } sb.append( " )" ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.java new file mode 100644 index 0000000000..609c6d04e7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.gem; + +/** + * Gem for the enum {@link org.mapstruct.ClassAccessibility} + * + * @author Raimund Klein + */ +public enum ClassAccessibilityGem { + DEFAULT, PUBLIC, PACKAGE_PRIVATE +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java new file mode 100644 index 0000000000..adea4b4c1f --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.gem; + +/** + * @author Filip Hrisafov + */ +public enum ConditionStrategyGem { + + PROPERTIES, + SOURCE_PARAMETERS +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java index f8bdbc539d..3036faf2eb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java @@ -9,6 +9,8 @@ import javax.xml.bind.annotation.XmlElementRef; import org.mapstruct.AfterMapping; +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWiths; import org.mapstruct.BeanMapping; import org.mapstruct.BeforeMapping; import org.mapstruct.Builder; @@ -16,9 +18,12 @@ import org.mapstruct.Context; import org.mapstruct.DecoratedWith; import org.mapstruct.EnumMapping; +import org.mapstruct.Ignored; +import org.mapstruct.IgnoredList; import org.mapstruct.InheritConfiguration; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.IterableMapping; +import org.mapstruct.Javadoc; import org.mapstruct.MapMapping; import org.mapstruct.Mapper; import org.mapstruct.MapperConfig; @@ -28,6 +33,10 @@ import org.mapstruct.Named; import org.mapstruct.ObjectFactory; import org.mapstruct.Qualifier; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.SubclassMapping; +import org.mapstruct.SubclassMappings; +import org.mapstruct.TargetPropertyName; import org.mapstruct.TargetType; import org.mapstruct.ValueMapping; import org.mapstruct.ValueMappings; @@ -36,18 +45,28 @@ import org.mapstruct.tools.gem.GemDefinition; /** - * Triggers the generation of ge types using Hickory. + * Triggers the generation of gem types using Gem Tools. * * @author Gunnar Morling */ +@GemDefinition(Deprecated.class) +@GemDefinition(AnnotateWith.class) +@GemDefinition(AnnotateWith.Element.class) +@GemDefinition(AnnotateWiths.class) @GemDefinition(Mapper.class) @GemDefinition(Mapping.class) +@GemDefinition(Ignored.class) +@GemDefinition(IgnoredList.class) @GemDefinition(Mappings.class) @GemDefinition(IterableMapping.class) @GemDefinition(BeanMapping.class) @GemDefinition(EnumMapping.class) @GemDefinition(MapMapping.class) +@GemDefinition(SourcePropertyName.class) +@GemDefinition(SubclassMapping.class) +@GemDefinition(SubclassMappings.class) @GemDefinition(TargetType.class) +@GemDefinition(TargetPropertyName.class) @GemDefinition(MappingTarget.class) @GemDefinition(DecoratedWith.class) @GemDefinition(MapperConfig.class) @@ -63,6 +82,7 @@ @GemDefinition(Context.class) @GemDefinition(Builder.class) @GemDefinition(Condition.class) +@GemDefinition(Javadoc.class) @GemDefinition(MappingControl.class) @GemDefinition(MappingControls.class) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/InjectionStrategyGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/InjectionStrategyGem.java index f8ed93eeb4..621d07fd09 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/InjectionStrategyGem.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/InjectionStrategyGem.java @@ -13,5 +13,6 @@ public enum InjectionStrategyGem { FIELD, - CONSTRUCTOR; + CONSTRUCTOR, + SETTER; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java index b49b9d17e5..bc58024ca3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/MappingConstantsGem.java @@ -49,6 +49,10 @@ private ComponentModelGem() { public static final String SPRING = "spring"; public static final String JSR330 = "jsr330"; + + public static final String JAKARTA = "jakarta"; + + public static final String JAKARTA_CDI = "jakarta-cdi"; } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/NullValuePropertyMappingStrategyGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/NullValuePropertyMappingStrategyGem.java index 77e4aa48d6..93c98b73ac 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/NullValuePropertyMappingStrategyGem.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/NullValuePropertyMappingStrategyGem.java @@ -15,5 +15,6 @@ public enum NullValuePropertyMappingStrategyGem { SET_TO_NULL, SET_TO_DEFAULT, - IGNORE; + IGNORE, + CLEAR; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/SubclassExhaustiveStrategyGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/SubclassExhaustiveStrategyGem.java new file mode 100644 index 0000000000..2367d649a9 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/SubclassExhaustiveStrategyGem.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.gem; + +/** + * Gem for the enum {@link org.mapstruct.SubclassExhaustiveStrategy} + * + * @author Ben Zegveld + */ +public enum SubclassExhaustiveStrategyGem { + + COMPILE_ERROR( false ), + RUNTIME_EXCEPTION( true ); + + private final boolean abstractReturnTypeAllowed; + + SubclassExhaustiveStrategyGem(boolean abstractReturnTypeAllowed) { + this.abstractReturnTypeAllowed = abstractReturnTypeAllowed; + } + + public boolean isAbstractReturnTypeAllowed() { + return abstractReturnTypeAllowed; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/jakarta/JakartaGemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/jakarta/JakartaGemGenerator.java new file mode 100644 index 0000000000..93bdebeaef --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/jakarta/JakartaGemGenerator.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.gem.jakarta; + +import jakarta.xml.bind.annotation.XmlElementDecl; +import jakarta.xml.bind.annotation.XmlElementRef; +import org.mapstruct.tools.gem.GemDefinition; + +/** + * This class is a temporary solution to an issue in the Gem Tools library. + * + *

        + * This class can be merged with {@link org.mapstruct.ap.internal.gem.GemGenerator} + * after the mentioned issue is resolved. + *

        + * + * @see Gem Tools issue #10 + * @author Iaroslav Bogdanchikov + */ +@GemDefinition(XmlElementDecl.class) +@GemDefinition(XmlElementRef.class) +class JakartaGemGenerator { +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java index ba13854667..8859e542b8 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractBaseBuilder.java @@ -5,10 +5,10 @@ */ package org.mapstruct.ap.internal.model; +import java.util.function.Supplier; import javax.lang.model.element.AnnotationMirror; import org.mapstruct.ap.internal.model.common.Assignment; -import org.mapstruct.ap.internal.model.common.BuilderType; import org.mapstruct.ap.internal.model.common.ParameterBinding; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; @@ -40,7 +40,7 @@ public B method(Method sourceMethod) { } /** - * Checks if MapStruct is allowed to generate an automatic sub-mapping between {@code sourceType} and @{code + * Checks if MapStruct is allowed to generate an automatic sub-mapping between {@code sourceType} and {@code * targetType}. * This will evaluate to {@code true}, when: *
      • @@ -65,25 +65,18 @@ private boolean isDisableSubMappingMethodsGeneration() { /** * Creates a forged assignment from the provided {@code sourceRHS} and {@code forgedMethod}. If a mapping method - * for the {@code forgedMethod} already exists, then this method used for the assignment. + * for the {@code forgedMethod} already exists, this method will be used for the assignment. * * @param sourceRHS that needs to be used for the assignment * @param forgedMethod the forged method for which we want to create an {@link Assignment} * * @return See above */ - Assignment createForgedAssignment(SourceRHS sourceRHS, BuilderType builderType, ForgedMethod forgedMethod) { + Assignment createForgedAssignment(SourceRHS sourceRHS, ForgedMethod forgedMethod) { - if ( ctx.getForgedMethodsUnderCreation().containsKey( forgedMethod ) ) { - return createAssignment( sourceRHS, ctx.getForgedMethodsUnderCreation().get( forgedMethod ) ); - } - else { - ctx.getForgedMethodsUnderCreation().put( forgedMethod, forgedMethod ); - } - - MappingMethod forgedMappingMethod; + Supplier forgedMappingMethodCreator; if ( MappingMethodUtils.isEnumMapping( forgedMethod ) ) { - forgedMappingMethod = new ValueMappingMethod.Builder() + forgedMappingMethodCreator = () -> new ValueMappingMethod.Builder() .method( forgedMethod ) .valueMappings( forgedMethod.getOptions().getValueMappings() ) .enumMapping( forgedMethod.getOptions().getEnumMappingOptions() ) @@ -91,15 +84,30 @@ Assignment createForgedAssignment(SourceRHS sourceRHS, BuilderType builderType, .build(); } else { - forgedMappingMethod = new BeanMappingMethod.Builder() + forgedMappingMethodCreator = () -> new BeanMappingMethod.Builder() .forgedMethod( forgedMethod ) - .returnTypeBuilder( builderType ) .mappingContext( ctx ) .build(); } + return getOrCreateForgedAssignment( sourceRHS, forgedMethod, forgedMappingMethodCreator ); + } + + Assignment getOrCreateForgedAssignment(SourceRHS sourceRHS, ForgedMethod forgedMethod, + Supplier mappingMethodCreator) { + + if ( ctx.getForgedMethodsUnderCreation().containsKey( forgedMethod ) ) { + return createAssignment( sourceRHS, ctx.getForgedMethodsUnderCreation().get( forgedMethod ) ); + } + else { + ctx.getForgedMethodsUnderCreation().put( forgedMethod, forgedMethod ); + } + + MappingMethod forgedMappingMethod = mappingMethodCreator.get(); + Assignment forgedAssignment = createForgedAssignment( sourceRHS, forgedMethod, forgedMappingMethod ); ctx.getForgedMethodsUnderCreation().remove( forgedMethod ); + return forgedAssignment; } @@ -107,8 +115,8 @@ Assignment createForgedAssignment(SourceRHS source, ForgedMethod methodRef, Mapp if ( mappingMethod == null ) { return null; } - if (methodRef.getMappingReferences().isRestrictToDefinedMappings() || - !ctx.getMappingsToGenerate().contains( mappingMethod )) { + if ( methodRef.getMappingReferences().isRestrictToDefinedMappings() || + !ctx.getMappingsToGenerate().contains( mappingMethod ) ) { // If the mapping options are restricted only to the defined mappings, then use the mapping method. // See https://github.com/mapstruct/mapstruct/issues/1148 ctx.getMappingsToGenerate().add( mappingMethod ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java index 91b8427630..c124306105 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java @@ -5,26 +5,50 @@ */ package org.mapstruct.ap.internal.model; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.mapstruct.ap.internal.model.beanmapping.MappingReferences; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; -import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.util.Strings; -import static org.mapstruct.ap.internal.model.ForgedMethod.forElementMapping; - /** * An abstract builder that can be reused for building {@link MappingMethod}(s). * + * @param the builder itself that needs to be used for chaining + * @param the method that the builder builds * @author Filip Hrisafov */ public abstract class AbstractMappingMethodBuilder, - M extends MappingMethod> extends AbstractBaseBuilder { + M extends MappingMethod> + extends AbstractBaseBuilder { public AbstractMappingMethodBuilder(Class selfType) { super( selfType ); } + private interface ForgeMethodCreator { + ForgedMethod createMethod(String name, Type sourceType, Type returnType, Method basedOn, + ForgedMethodHistory history, boolean forgedNameBased); + + static ForgeMethodCreator forSubclassMapping(MappingReferences mappingReferences) { + return (name, sourceType, targetType, method, description, + forgedNameBased) -> ForgedMethod + .forSubclassMapping( + name, + sourceType, + targetType, + method, + mappingReferences, + description, + forgedNameBased ); + } + } + public abstract M build(); private ForgedMethodHistory description; @@ -35,6 +59,20 @@ public AbstractMappingMethodBuilder(Class selfType) { protected abstract boolean shouldUsePropertyNamesInHistory(); Assignment forgeMapping(SourceRHS sourceRHS, Type sourceType, Type targetType) { + return forgeMapping( sourceRHS, sourceType, targetType, ForgedMethod::forElementMapping ); + } + + Assignment forgeSubclassMapping(SourceRHS sourceRHS, Type sourceType, Type targetType, + MappingReferences mappingReferences) { + return forgeMapping( + sourceRHS, + sourceType, + targetType, + ForgeMethodCreator.forSubclassMapping( mappingReferences ) ); + } + + private Assignment forgeMapping(SourceRHS sourceRHS, Type sourceType, Type targetType, + ForgeMethodCreator forgeMethodCreator) { if ( !canGenerateAutoSubMappingBetween( sourceType, targetType ) ) { return null; } @@ -55,14 +93,10 @@ Assignment forgeMapping(SourceRHS sourceRHS, Type sourceType, Type targetType) { shouldUsePropertyNamesInHistory(), sourceRHS.getSourceErrorMessagePart() ); - ForgedMethod forgedMethod = forElementMapping( name, sourceType, targetType, method, description, true ); - BuilderGem builder = method.getOptions().getBeanMapping().getBuilder(); + ForgedMethod forgedMethod = + forgeMethodCreator.createMethod( name, sourceType, targetType, method, description, true ); - return createForgedAssignment( - sourceRHS, - ctx.getTypeFactory().builderTypeFor( targetType, builder ), - forgedMethod - ); + return createForgedAssignment( sourceRHS, forgedMethod ); } private String getName(Type sourceType, Type targetType) { @@ -84,4 +118,23 @@ public ForgedMethodHistory getDescription() { return description; } + public List getMethodAnnotations() { + if ( method instanceof ForgedMethod ) { + return Collections.emptyList(); + } + AdditionalAnnotationsBuilder additionalAnnotationsBuilder = + new AdditionalAnnotationsBuilder( + ctx.getElementUtils(), + ctx.getTypeFactory(), + ctx.getMessager() ); + List annotations = new ArrayList<>( + additionalAnnotationsBuilder.getProcessedAnnotations( method.getExecutable() ) + ); + + if ( method.overridesMethod() ) { + annotations.add( new Annotation( ctx.getTypeFactory().getType( Override.class ) ) ); + } + return annotations; + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AdditionalAnnotationsBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AdditionalAnnotationsBuilder.java new file mode 100644 index 0000000000..cb9289e057 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AdditionalAnnotationsBuilder.java @@ -0,0 +1,644 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; + +import org.mapstruct.ap.internal.gem.AnnotateWithGem; +import org.mapstruct.ap.internal.gem.AnnotateWithsGem; +import org.mapstruct.ap.internal.gem.DeprecatedGem; +import org.mapstruct.ap.internal.gem.ElementGem; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement.AnnotationElementType; +import org.mapstruct.ap.internal.model.annotation.EnumAnnotationElementHolder; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.common.TypeFactory; +import org.mapstruct.ap.internal.util.ElementUtils; +import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.RepeatableAnnotations; +import org.mapstruct.ap.internal.util.Strings; +import org.mapstruct.ap.spi.TypeHierarchyErroneousException; +import org.mapstruct.tools.gem.GemValue; + +import static javax.lang.model.util.ElementFilter.methodsIn; + +/** + * Helper class which is responsible for collecting all additional annotations that should be added. + * + * @author Ben Zegveld + * @since 1.5 + */ +public class AdditionalAnnotationsBuilder + extends RepeatableAnnotations { + private static final String ANNOTATE_WITH_FQN = "org.mapstruct.AnnotateWith"; + private static final String ANNOTATE_WITHS_FQN = "org.mapstruct.AnnotateWiths"; + private TypeFactory typeFactory; + private FormattingMessager messager; + + public AdditionalAnnotationsBuilder(ElementUtils elementUtils, TypeFactory typeFactory, + FormattingMessager messager) { + super( elementUtils, ANNOTATE_WITH_FQN, ANNOTATE_WITHS_FQN ); + this.typeFactory = typeFactory; + this.messager = messager; + } + + @Override + protected AnnotateWithGem singularInstanceOn(Element element) { + return AnnotateWithGem.instanceOn( element ); + } + + @Override + protected AnnotateWithsGem multipleInstanceOn(Element element) { + return AnnotateWithsGem.instanceOn( element ); + } + + @Override + protected void addInstance(AnnotateWithGem gem, Element source, Set mappings) { + buildAnnotation( gem, source ).ifPresent( t -> addAndValidateMapping( mappings, source, gem, t ) ); + } + + @Override + protected void addInstances(AnnotateWithsGem gem, Element source, Set mappings) { + for ( AnnotateWithGem annotateWithGem : gem.value().get() ) { + buildAnnotation( + annotateWithGem, + source ).ifPresent( t -> addAndValidateMapping( mappings, source, annotateWithGem, t ) ); + } + } + + @Override + public Set getProcessedAnnotations(Element source) { + Set processedAnnotations = super.getProcessedAnnotations( source ); + return addDeprecatedAnnotation( source, processedAnnotations ); + } + + private Set addDeprecatedAnnotation(Element source, Set annotations) { + DeprecatedGem deprecatedGem = DeprecatedGem.instanceOn( source ); + if ( deprecatedGem == null ) { + return annotations; + } + Type deprecatedType = typeFactory.getType( Deprecated.class ); + if ( annotations.stream().anyMatch( annotation -> annotation.getType().equals( deprecatedType ) ) ) { + messager.printMessage( + source, + deprecatedGem.mirror(), + Message.ANNOTATE_WITH_DUPLICATE, + deprecatedType.describe() ); + return annotations; + } + List annotationElements = new ArrayList<>(); + if ( deprecatedGem.since() != null && deprecatedGem.since().hasValue() ) { + annotationElements.add( new AnnotationElement( + AnnotationElementType.STRING, + "since", + Collections.singletonList( deprecatedGem.since().getValue() ) + ) ); + } + if ( deprecatedGem.forRemoval() != null && deprecatedGem.forRemoval().hasValue() ) { + annotationElements.add( new AnnotationElement( + AnnotationElementType.BOOLEAN, + "forRemoval", + Collections.singletonList( deprecatedGem.forRemoval().getValue() ) + ) ); + } + annotations.add( new Annotation(deprecatedType, annotationElements ) ); + return annotations; + } + + private void addAndValidateMapping(Set mappings, Element source, AnnotateWithGem gem, Annotation anno) { + if ( anno.getType().getTypeElement().getAnnotation( Repeatable.class ) == null ) { + if ( mappings.stream().anyMatch( existing -> existing.getType().equals( anno.getType() ) ) ) { + messager.printMessage( + source, + gem.mirror(), + Message.ANNOTATE_WITH_ANNOTATION_IS_NOT_REPEATABLE, + anno.getType().describe() ); + return; + } + } + if ( mappings.stream().anyMatch( existing -> { + return existing.getType().equals( anno.getType() ) + && existing.getProperties().equals( anno.getProperties() ); + } ) ) { + messager.printMessage( + source, + gem.mirror(), + Message.ANNOTATE_WITH_DUPLICATE, + anno.getType().describe() ); + return; + } + mappings.add( anno ); + } + + private Optional buildAnnotation(AnnotateWithGem annotationGem, Element element) { + Type annotationType = typeFactory.getType( getTypeMirror( annotationGem.value() ) ); + List eleGems = annotationGem.elements().get(); + if ( isValid( annotationType, eleGems, element, annotationGem.mirror() ) ) { + return Optional.of( new Annotation( annotationType, convertToProperties( eleGems ) ) ); + } + return Optional.empty(); + } + + private List convertToProperties(List eleGems) { + return eleGems.stream().map( gem -> convertToProperty( gem, typeFactory ) ).collect( Collectors.toList() ); + } + + private enum ConvertToProperty { + BOOLEAN( + AnnotationElementType.BOOLEAN, + (eleGem, typeFactory) -> eleGem.booleans().get(), + eleGem -> eleGem.booleans().hasValue() + ), + BYTE( + AnnotationElementType.BYTE, + (eleGem, typeFactory) -> eleGem.bytes().get(), + eleGem -> eleGem.bytes().hasValue() + ), + CHARACTER( + AnnotationElementType.CHARACTER, + (eleGem, typeFactory) -> eleGem.chars().get(), + eleGem -> eleGem.chars().hasValue() + ), + CLASSES( + AnnotationElementType.CLASS, + (eleGem, typeFactory) -> { + return eleGem.classes().get().stream().map( typeFactory::getType ).collect( Collectors.toList() ); + }, + eleGem -> eleGem.classes().hasValue() + ), + DOUBLE( + AnnotationElementType.DOUBLE, + (eleGem, typeFactory) -> eleGem.doubles().get(), + eleGem -> eleGem.doubles().hasValue() + ), + ENUM( + AnnotationElementType.ENUM, + (eleGem, typeFactory) -> { + List values = new ArrayList<>(); + for ( String enumName : eleGem.enums().get() ) { + Type type = typeFactory.getType( eleGem.enumClass().get() ); + values.add( new EnumAnnotationElementHolder( type, enumName ) ); + } + return values; + }, + eleGem -> eleGem.enums().hasValue() && eleGem.enumClass().hasValue() + ), + FLOAT( + AnnotationElementType.FLOAT, + (eleGem, typeFactory) -> eleGem.floats().get(), + eleGem -> eleGem.floats().hasValue() + ), + INT( + AnnotationElementType.INTEGER, + (eleGem, typeFactory) -> eleGem.ints().get(), + eleGem -> eleGem.ints().hasValue() + ), + LONG( + AnnotationElementType.LONG, + (eleGem, typeFactory) -> eleGem.longs().get(), + eleGem -> eleGem.longs().hasValue() + ), + SHORT( + AnnotationElementType.SHORT, + (eleGem, typeFactory) -> eleGem.shorts().get(), + eleGem -> eleGem.shorts().hasValue() + ), + STRING( + AnnotationElementType.STRING, + (eleGem, typeFactory) -> eleGem.strings().get(), + eleGem -> eleGem.strings().hasValue() + ); + + private final AnnotationElementType type; + private final BiFunction> factory; + private final Predicate usabilityChecker; + + ConvertToProperty(AnnotationElementType type, + BiFunction> factory, + Predicate usabilityChecker) { + this.type = type; + this.factory = factory; + this.usabilityChecker = usabilityChecker; + } + + AnnotationElement toProperty(ElementGem eleGem, TypeFactory typeFactory) { + return new AnnotationElement( + type, + eleGem.name().get(), + factory.apply( eleGem, typeFactory ) + ); + } + + boolean isUsable(ElementGem eleGem) { + return usabilityChecker.test( eleGem ); + } + } + + private AnnotationElement convertToProperty(ElementGem eleGem, TypeFactory typeFactory) { + for ( ConvertToProperty convertToJava : ConvertToProperty.values() ) { + if ( convertToJava.isUsable( eleGem ) ) { + return convertToJava.toProperty( eleGem, typeFactory ); + } + } + return null; + } + + private boolean isValid(Type annotationType, List eleGems, Element element, + AnnotationMirror annotationMirror) { + boolean isValid = true; + if ( !annotationIsAllowed( annotationType, element, annotationMirror ) ) { + isValid = false; + } + + List annotationElements = methodsIn( annotationType.getTypeElement() + .getEnclosedElements() ); + if ( !allRequiredElementsArePresent( annotationType, annotationElements, eleGems, element, + annotationMirror ) ) { + isValid = false; + } + if ( !allElementsAreKnownInAnnotation( annotationType, annotationElements, eleGems, element ) ) { + isValid = false; + } + if ( !allElementsAreOfCorrectType( annotationType, annotationElements, eleGems, element ) ) { + isValid = false; + } + if ( !enumConstructionIsCorrectlyUsed( eleGems, element ) ) { + isValid = false; + } + if ( !allElementsAreUnique( eleGems, element ) ) { + isValid = false; + } + return isValid; + } + + private boolean allElementsAreUnique(List eleGems, Element element) { + boolean isValid = true; + List checkedElements = new ArrayList<>(); + for ( ElementGem elementGem : eleGems ) { + String elementName = elementGem.name().get(); + if ( checkedElements.contains( elementName ) ) { + isValid = false; + messager + .printMessage( + element, + elementGem.mirror(), + Message.ANNOTATE_WITH_DUPLICATE_PARAMETER, + elementName ); + } + else { + checkedElements.add( elementName ); + } + } + return isValid; + } + + private boolean enumConstructionIsCorrectlyUsed(List eleGems, Element element) { + boolean isValid = true; + for ( ElementGem elementGem : eleGems ) { + if ( elementGem.enums().hasValue() ) { + if ( elementGem.enumClass().getValue() == null ) { + isValid = false; + messager + .printMessage( + element, + elementGem.mirror(), + Message.ANNOTATE_WITH_ENUM_CLASS_NOT_DEFINED ); + } + else { + Type type = typeFactory.getType( getTypeMirror( elementGem.enumClass() ) ); + if ( type.isEnumType() ) { + List enumConstants = type.getEnumConstants(); + for ( String enumName : elementGem.enums().get() ) { + if ( !enumConstants.contains( enumName ) ) { + isValid = false; + messager + .printMessage( + element, + elementGem.mirror(), + elementGem.enums().getAnnotationValue(), + Message.ANNOTATE_WITH_ENUM_VALUE_DOES_NOT_EXIST, + type.describe(), + enumName ); + } + } + } + } + } + else if ( elementGem.enumClass().getValue() != null ) { + isValid = false; + messager.printMessage( element, elementGem.mirror(), Message.ANNOTATE_WITH_ENUMS_NOT_DEFINED ); + } + } + return isValid; + } + + private boolean annotationIsAllowed(Type annotationType, Element element, AnnotationMirror annotationMirror) { + Target target = annotationType.getTypeElement().getAnnotation( Target.class ); + if ( target == null ) { + return true; + } + + Set annotationTargets = Stream.of( target.value() ) + // The eclipse compiler returns null for some values + // Therefore, we filter out null values + .filter( Objects::nonNull ) + .collect( Collectors.toCollection( () -> EnumSet.noneOf( ElementType.class ) ) ); + + boolean isValid = true; + if ( isTypeTarget( element ) && !annotationTargets.contains( ElementType.TYPE ) ) { + isValid = false; + messager.printMessage( + element, + annotationMirror, + Message.ANNOTATE_WITH_NOT_ALLOWED_ON_CLASS, + annotationType.describe() + ); + } + if ( isMethodTarget( element ) && !annotationTargets.contains( ElementType.METHOD ) ) { + isValid = false; + messager.printMessage( + element, + annotationMirror, + Message.ANNOTATE_WITH_NOT_ALLOWED_ON_METHODS, + annotationType.describe() + ); + } + return isValid; + } + + private boolean isTypeTarget(Element element) { + return element.getKind().isInterface() || element.getKind().isClass(); + } + + private boolean isMethodTarget(Element element) { + return element.getKind() == ElementKind.METHOD; + } + + private boolean allElementsAreKnownInAnnotation(Type annotationType, List annotationParameters, + List eleGems, Element element) { + Set allowedAnnotationParameters = annotationParameters.stream() + .map( ee -> ee.getSimpleName().toString() ) + .collect( Collectors.toSet() ); + boolean isValid = true; + for ( ElementGem eleGem : eleGems ) { + if ( eleGem.name().isValid() + && !allowedAnnotationParameters.contains( eleGem.name().get() ) ) { + isValid = false; + messager + .printMessage( + element, + eleGem.mirror(), + eleGem.name().getAnnotationValue(), + Message.ANNOTATE_WITH_UNKNOWN_PARAMETER, + eleGem.name().get(), + annotationType.describe(), + Strings.getMostSimilarWord( eleGem.name().get(), allowedAnnotationParameters ) + ); + } + } + return isValid; + } + + private boolean allRequiredElementsArePresent(Type annotationType, List annotationParameters, + List elements, Element element, + AnnotationMirror annotationMirror) { + + boolean valid = true; + for ( ExecutableElement annotationParameter : annotationParameters ) { + if ( annotationParameter.getDefaultValue() == null ) { + // Mandatory parameter, must be present in the elements + String parameterName = annotationParameter.getSimpleName().toString(); + boolean elementGemDefined = false; + for ( ElementGem elementGem : elements ) { + if ( elementGem.isValid() && elementGem.name().get().equals( parameterName ) ) { + elementGemDefined = true; + break; + } + } + + if ( !elementGemDefined ) { + valid = false; + messager + .printMessage( + element, + annotationMirror, + Message.ANNOTATE_WITH_MISSING_REQUIRED_PARAMETER, + parameterName, + annotationType.describe() + ); + } + } + } + + + return valid; + } + + private boolean allElementsAreOfCorrectType(Type annotationType, List annotationParameters, + List elements, + Element element) { + Map annotationParametersByName = + annotationParameters.stream() + .collect( Collectors.toMap( ee -> ee.getSimpleName().toString(), Function.identity() ) ); + boolean isValid = true; + for ( ElementGem eleGem : elements ) { + Type annotationParameterType = getAnnotationParameterType( annotationParametersByName, eleGem ); + Type annotationParameterTypeSingular = getNonArrayType( annotationParameterType ); + if ( annotationParameterTypeSingular == null ) { + continue; + } + if ( hasTooManyDifferentTypes( eleGem ) ) { + isValid = false; + messager.printMessage( + element, + eleGem.mirror(), + eleGem.name().getAnnotationValue(), + Message.ANNOTATE_WITH_TOO_MANY_VALUE_TYPES, + eleGem.name().get(), + annotationParameterType.describe(), + annotationType.describe() + ); + } + else { + Map elementTypes = getParameterTypes( eleGem ); + Set reportedSizeError = new HashSet<>(); + for ( Type eleGemType : elementTypes.keySet() ) { + if ( !sameTypeOrAssignableClass( annotationParameterTypeSingular, eleGemType ) ) { + isValid = false; + messager.printMessage( + element, + eleGem.mirror(), + eleGem.name().getAnnotationValue(), + Message.ANNOTATE_WITH_WRONG_PARAMETER, + eleGem.name().get(), + eleGemType.describe(), + annotationParameterType.describe(), + annotationType.describe() + ); + } + else if ( !annotationParameterType.isArrayType() + && elementTypes.get( eleGemType ) > 1 + && !reportedSizeError.contains( eleGem ) ) { + isValid = false; + messager.printMessage( + element, + eleGem.mirror(), + Message.ANNOTATE_WITH_PARAMETER_ARRAY_NOT_EXPECTED, + eleGem.name().get(), + annotationType.describe() + ); + reportedSizeError.add( eleGem ); + } + } + } + } + return isValid; + } + + private boolean hasTooManyDifferentTypes( ElementGem eleGem ) { + return Arrays.stream( ConvertToProperty.values() ) + .filter( anotationElement -> anotationElement.isUsable( eleGem ) ) + .count() > 1; + } + + private Type getNonArrayType(Type annotationParameterType) { + if ( annotationParameterType == null ) { + return null; + } + if ( annotationParameterType.isArrayType() ) { + return annotationParameterType.getComponentType(); + } + + return annotationParameterType; + } + + private boolean sameTypeOrAssignableClass(Type annotationParameterType, Type eleGemType) { + return annotationParameterType.equals( eleGemType ) + || eleGemType.isAssignableTo( getTypeBound( annotationParameterType ) ); + } + + private Type getTypeBound(Type annotationParameterType) { + List typeParameters = annotationParameterType.getTypeParameters(); + if ( typeParameters.size() != 1 ) { + return annotationParameterType; + } + return typeParameters.get( 0 ).getTypeBound(); + } + + private Map getParameterTypes(ElementGem eleGem) { + Map suppliedParameterTypes = new HashMap<>(); + if ( eleGem.booleans().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( boolean.class ), + eleGem.booleans().get().size() ); + } + if ( eleGem.bytes().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( byte.class ), + eleGem.bytes().get().size() ); + } + if ( eleGem.chars().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( char.class ), + eleGem.chars().get().size() ); + } + if ( eleGem.classes().hasValue() ) { + for ( TypeMirror mirror : eleGem.classes().get() ) { + suppliedParameterTypes.put( + typeFactory.getType( typeMirrorFromAnnotation( mirror ) ), + eleGem.classes().get().size() + ); + } + } + if ( eleGem.doubles().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( double.class ), + eleGem.doubles().get().size() ); + } + if ( eleGem.floats().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( float.class ), + eleGem.floats().get().size() ); + } + if ( eleGem.ints().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( int.class ), + eleGem.ints().get().size() ); + } + if ( eleGem.longs().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( long.class ), + eleGem.longs().get().size() ); + } + if ( eleGem.shorts().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( short.class ), + eleGem.shorts().get().size() ); + } + if ( eleGem.strings().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( String.class ), + eleGem.strings().get().size() ); + } + if ( eleGem.enums().hasValue() && eleGem.enumClass().hasValue() ) { + suppliedParameterTypes.put( + typeFactory.getType( getTypeMirror( eleGem.enumClass() ) ), + eleGem.enums().get().size() ); + } + return suppliedParameterTypes; + } + + private Type getAnnotationParameterType(Map annotationParameters, + ElementGem element) { + if ( annotationParameters.containsKey( element.name().get() ) ) { + return typeFactory.getType( annotationParameters.get( element.name().get() ).getReturnType() ); + } + else { + return null; + } + } + + private TypeMirror getTypeMirror(GemValue gemValue) { + return typeMirrorFromAnnotation( gemValue.getValue() ); + } + + private TypeMirror typeMirrorFromAnnotation(TypeMirror typeMirror) { + if ( typeMirror == null ) { + // When a class used in an annotation is created by another annotation processor + // then javac will not return correct TypeMirror with TypeKind#ERROR, but rather a string "" + // the gem tools would return a null TypeMirror in that case. + // Therefore, throw TypeHierarchyErroneousException so we can postpone the generation of the mapper + throw new TypeHierarchyErroneousException( typeMirror ); + } + + return typeMirror; + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedConstructor.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedConstructor.java index 1c38b1062b..889d602cdb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedConstructor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedConstructor.java @@ -36,14 +36,14 @@ public static AnnotatedConstructor forComponentModels(String name, if ( constructor instanceof NoArgumentConstructor ) { noArgumentConstructor = (NoArgumentConstructor) constructor; } - NoArgumentConstructor noArgConstructorToInBecluded = null; + NoArgumentConstructor noArgConstructorToBeIncluded = null; Set fragmentsToBeIncluded = Collections.emptySet(); if ( includeNoArgConstructor ) { if ( noArgumentConstructor != null ) { - noArgConstructorToInBecluded = noArgumentConstructor; + noArgConstructorToBeIncluded = noArgumentConstructor; } else { - noArgConstructorToInBecluded = new NoArgumentConstructor( name, fragmentsToBeIncluded ); + noArgConstructorToBeIncluded = new NoArgumentConstructor( name, fragmentsToBeIncluded ); } } else if ( noArgumentConstructor != null ) { @@ -53,7 +53,7 @@ else if ( noArgumentConstructor != null ) { name, mapperReferences, annotations, - noArgConstructorToInBecluded, + noArgConstructorToBeIncluded, fragmentsToBeIncluded ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedSetter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedSetter.java new file mode 100644 index 0000000000..8f60b052b9 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AnnotatedSetter.java @@ -0,0 +1,61 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.Type; + +/** + * A method in a generated type that represents a setter with annotations. + * + * @author Lucas Resch + */ +public class AnnotatedSetter extends GeneratedTypeMethod { + + private final Field field; + private final Collection methodAnnotations; + private final Collection parameterAnnotations; + + public AnnotatedSetter(Field field, Collection methodAnnotations, + Collection parameterAnnotations) { + this.field = field; + this.methodAnnotations = methodAnnotations; + this.parameterAnnotations = parameterAnnotations; + } + + @Override + public Set getImportTypes() { + Set importTypes = new HashSet<>( field.getImportTypes() ); + for ( Annotation annotation : methodAnnotations ) { + importTypes.addAll( annotation.getImportTypes() ); + } + + for ( Annotation annotation : parameterAnnotations ) { + importTypes.addAll( annotation.getImportTypes() ); + } + + return importTypes; + } + + public Type getType() { + return field.getType(); + } + + public String getFieldName() { + return field.getVariableName(); + } + + public Collection getMethodAnnotations() { + return methodAnnotations; + } + + public Collection getParameterAnnotations() { + return parameterAnnotations; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/Annotation.java b/processor/src/main/java/org/mapstruct/ap/internal/model/Annotation.java index 405958ccdf..866012c51a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/Annotation.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/Annotation.java @@ -6,9 +6,11 @@ package org.mapstruct.ap.internal.model; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement; import org.mapstruct.ap.internal.model.common.ModelElement; import org.mapstruct.ap.internal.model.common.Type; @@ -21,16 +23,13 @@ public class Annotation extends ModelElement { private final Type type; - /** - * List of annotation attributes. Quite simplistic, but it's sufficient for now. - */ - private List properties; + private List properties; public Annotation(Type type) { this( type, Collections.emptyList() ); } - public Annotation(Type type, List properties) { + public Annotation(Type type, List properties) { this.type = type; this.properties = properties; } @@ -41,10 +40,15 @@ public Type getType() { @Override public Set getImportTypes() { - return Collections.singleton( type ); + Set types = new HashSet<>(); + for ( AnnotationElement prop : properties ) { + types.addAll( prop.getImportTypes() ); + } + types.add( type ); + return types; } - public List getProperties() { + public List getProperties() { return properties; } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index 09cc20a6ed..080eab1e76 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -20,17 +20,20 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic; +import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; import org.mapstruct.ap.internal.model.PropertyMapping.ConstantMappingBuilder; @@ -40,10 +43,16 @@ import org.mapstruct.ap.internal.model.beanmapping.MappingReferences; import org.mapstruct.ap.internal.model.beanmapping.SourceReference; import org.mapstruct.ap.internal.model.beanmapping.TargetReference; +import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.BuilderType; +import org.mapstruct.ap.internal.model.common.FormattingParameters; +import org.mapstruct.ap.internal.model.common.NewInstanceCreation; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.ParameterBinding; +import org.mapstruct.ap.internal.model.common.PresenceCheck; +import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.dependency.GraphAnalyzer; import org.mapstruct.ap.internal.model.dependency.GraphAnalyzer.GraphAnalyzerBuilder; import org.mapstruct.ap.internal.model.source.BeanMappingOptions; @@ -51,12 +60,17 @@ import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.model.source.SourceMethod; +import org.mapstruct.ap.internal.model.source.SubclassMappingOptions; import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; +import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; -import org.mapstruct.ap.internal.util.accessor.ParameterElementAccessor; +import org.mapstruct.ap.internal.util.accessor.ElementAccessor; +import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; +import org.mapstruct.ap.internal.util.kotlin.KotlinMetadata; import static org.mapstruct.ap.internal.model.beanmapping.MappingReferences.forSourceMethod; import static org.mapstruct.ap.internal.util.Collections.first; @@ -79,19 +93,25 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { private final List propertyMappings; private final Map> mappingsByParameter; private final Map> constructorMappingsByParameter; + private final Map presenceChecksByParameter; private final List constantMappings; private final List constructorConstantMappings; + private final List subclassMappings; private final Type returnTypeToConstruct; + private final NewInstanceCreation newInstance; private final BuilderType returnTypeBuilder; private final MethodReference finalizerMethod; + private final String finalizedResultName; + private final String optionalResultName; + private final List beforeMappingReferencesWithFinalizedReturnType; + private final List afterMappingReferencesWithFinalizedReturnType; + private final List afterMappingReferencesWithOptionalReturnType; + private final Type subclassExhaustiveException; + private final Map sourceParametersReassignments; private final MappingReferences mappingReferences; - public static class Builder { - - private MappingBuilderContext ctx; - private Method method; - private Type userDefinedReturnType; + public static class Builder extends AbstractMappingMethodBuilder { /* returnType to construct can have a builder */ private BuilderType returnTypeBuilder; @@ -99,41 +119,38 @@ public static class Builder { private Map unprocessedTargetProperties; private Map unprocessedSourceProperties; private Set missingIgnoredSourceProperties; + private Set redundantIgnoredSourceProperties; private Set targetProperties; private final List propertyMappings = new ArrayList<>(); private final Set unprocessedSourceParameters = new HashSet<>(); private final Set existingVariableNames = new HashSet<>(); private final Map> unprocessedDefinedTargets = new LinkedHashMap<>(); + private final Map sourceParametersReassignments = new HashMap<>(); private MappingReferences mappingReferences; + private List targetThisReferences; private MethodReference factoryMethod; private boolean hasFactoryMethod; - public Builder mappingContext(MappingBuilderContext mappingContext) { - this.ctx = mappingContext; - return this; - } - - public Builder userDefinedReturnType(Type userDefinedReturnType) { - this.userDefinedReturnType = userDefinedReturnType; - return this; + public Builder() { + super( Builder.class ); } - public Builder returnTypeBuilder( BuilderType returnTypeBuilder ) { - this.returnTypeBuilder = returnTypeBuilder; - return this; + @Override + protected boolean shouldUsePropertyNamesInHistory() { + return true; } public Builder sourceMethod(SourceMethod sourceMethod) { - this.method = sourceMethod; + method( sourceMethod ); return this; } public Builder forgedMethod(ForgedMethod forgedMethod) { - this.method = forgedMethod; + method( forgedMethod ); mappingReferences = forgedMethod.getMappingReferences(); Parameter sourceParameter = first( Parameter.getSourceParameters( forgedMethod.getParameters() ) ); - for ( MappingReference mappingReference: mappingReferences.getMappingReferences() ) { + for ( MappingReference mappingReference : mappingReferences.getMappingReferences() ) { SourceReference sourceReference = mappingReference.getSourceReference(); if ( sourceReference != null ) { mappingReference.setSourceReference( new SourceReference.BuilderFromSourceReference() @@ -145,6 +162,7 @@ public Builder forgedMethod(ForgedMethod forgedMethod) { return this; } + @Override public BeanMappingMethod build() { BeanMappingOptions beanMapping = method.getOptions().getBeanMapping(); @@ -159,12 +177,32 @@ public BeanMappingMethod build() { // determine which return type to construct boolean cannotConstructReturnType = false; if ( !method.getReturnType().isVoid() ) { - Type returnTypeImpl = null; + BuilderGem builder = method.getOptions().getBeanMapping().getBuilder(); + Type returnTypeImpl; + Type userDefinedReturnType = null; + if ( selectionParameters != null && selectionParameters.getResultType() != null ) { + // This is a user-defined return type, which means we need to do some extra checks for it + userDefinedReturnType = ctx.getTypeFactory().getType( selectionParameters.getResultType() ); + returnTypeImpl = userDefinedReturnType; + returnTypeBuilder = ctx.getTypeFactory().builderTypeFor( userDefinedReturnType, builder ); + } + else { + Type methodReturnType = method.getReturnType(); + if ( methodReturnType.isOptionalType() ) { + returnTypeImpl = methodReturnType.getOptionalBaseType(); + } + else { + returnTypeImpl = methodReturnType; + } + returnTypeBuilder = ctx.getTypeFactory().builderTypeFor( returnTypeImpl, builder ); + } if ( isBuilderRequired() ) { // the userDefinedReturn type can also require a builder. That buildertype is already set returnTypeImpl = returnTypeBuilder.getBuilder(); initializeFactoryMethod( returnTypeImpl, selectionParameters ); - if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) { + if ( factoryMethod != null + || allowsAbstractReturnTypeAndIsEitherAbstractOrCanBeConstructed( returnTypeImpl ) + || doesNotAllowAbstractReturnTypeAndCanBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } else { @@ -172,7 +210,6 @@ public BeanMappingMethod build() { } } else if ( userDefinedReturnType != null ) { - returnTypeImpl = userDefinedReturnType; initializeFactoryMethod( returnTypeImpl, selectionParameters ); if ( factoryMethod != null || canResultTypeFromBeanMappingBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; @@ -182,9 +219,10 @@ else if ( userDefinedReturnType != null ) { } } else if ( !method.isUpdateMethod() ) { - returnTypeImpl = method.getReturnType(); initializeFactoryMethod( returnTypeImpl, selectionParameters ); - if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) { + if ( factoryMethod != null + || allowsAbstractReturnTypeAndIsEitherAbstractOrCanBeConstructed( returnTypeImpl ) + || doesNotAllowAbstractReturnTypeAndCanBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } else { @@ -211,9 +249,10 @@ else if ( !method.isUpdateMethod() ) { this.unprocessedTargetProperties = new LinkedHashMap<>( accessors ); + boolean constructorAccessorHadError = false; if ( !method.isUpdateMethod() && !hasFactoryMethod ) { ConstructorAccessor constructorAccessor = getConstructorAccessor( resultTypeToMap ); - if ( constructorAccessor != null ) { + if ( constructorAccessor != null && !constructorAccessor.hasError ) { this.unprocessedConstructorProperties = constructorAccessor.constructorAccessors; @@ -226,8 +265,10 @@ else if ( !method.isUpdateMethod() ) { else { this.unprocessedConstructorProperties = new LinkedHashMap<>(); } + constructorAccessorHadError = constructorAccessor != null && constructorAccessor.hasError; this.targetProperties.addAll( this.unprocessedConstructorProperties.keySet() ); + this.unprocessedTargetProperties.putAll( this.unprocessedConstructorProperties ); } else { @@ -238,37 +279,71 @@ else if ( !method.isUpdateMethod() ) { for ( Parameter sourceParameter : method.getSourceParameters() ) { unprocessedSourceParameters.add( sourceParameter ); - if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() || - sourceParameter.getType().isMapType() ) { + Type sourceParameterType = sourceParameter.getType(); + if ( sourceParameterType.isOptionalType() ) { + String sourceParameterValueName = Strings.getSafeVariableName( + sourceParameter.getName() + "Value", + existingVariableNames + ); + existingVariableNames.add( sourceParameterValueName ); + sourceParameterType = sourceParameterType.getOptionalBaseType(); + sourceParametersReassignments.put( + sourceParameter.getName(), + new Parameter( sourceParameterValueName, sourceParameter.getName(), sourceParameterType ) + ); + } + if ( sourceParameterType.isPrimitive() || sourceParameterType.isArrayType() || + sourceParameterType.isMapType() ) { continue; } - Map readAccessors = sourceParameter.getType().getPropertyReadAccessors(); + Map readAccessors = sourceParameterType.getPropertyReadAccessors(); - for ( Entry entry : readAccessors.entrySet() ) { - unprocessedSourceProperties.put( entry.getKey(), entry.getValue() ); - } + unprocessedSourceProperties.putAll( readAccessors ); } // get bean mapping (when specified as annotation ) - this.missingIgnoredSourceProperties = new HashSet<>(); - if ( beanMapping != null ) { + this.missingIgnoredSourceProperties = new LinkedHashSet<>(); + this.redundantIgnoredSourceProperties = new LinkedHashSet<>(); + if ( beanMapping != null && !beanMapping.getIgnoreUnmappedSourceProperties().isEmpty() ) { + // Get source properties explicitly mapped using @Mapping annotations + Set mappedSourceProperties = method.getOptions().getMappings().stream() + .map( MappingOptions::getSourceName ) + .filter( Objects::nonNull ) + .collect( Collectors.toSet() ); + for ( String ignoreUnmapped : beanMapping.getIgnoreUnmappedSourceProperties() ) { + // Track missing ignored properties (i.e. not in source class) if ( unprocessedSourceProperties.remove( ignoreUnmapped ) == null ) { missingIgnoredSourceProperties.add( ignoreUnmapped ); } + // Track redundant ignored properties (actually mapped despite being ignored) + if ( mappedSourceProperties.contains( ignoreUnmapped ) ) { + redundantIgnoredSourceProperties.add( ignoreUnmapped ); + } } } initializeMappingReferencesIfNeeded( resultTypeToMap ); - // map properties with mapping - boolean mappingErrorOccurred = handleDefinedMappings( resultTypeToMap ); - if ( mappingErrorOccurred ) { - return null; + boolean shouldHandledDefinedMappings = shouldHandledDefinedMappings( resultTypeToMap ); + + + if ( shouldHandledDefinedMappings ) { + // map properties with mapping + boolean mappingErrorOccurred = handleDefinedMappings( resultTypeToMap ); + if ( mappingErrorOccurred ) { + return null; + } } - if ( !mappingReferences.isRestrictToDefinedMappings() ) { + // If defined mappings should not be handled then we should not apply implicit mappings + boolean applyImplicitMappings = + shouldHandledDefinedMappings && !mappingReferences.isRestrictToDefinedMappings(); + if ( applyImplicitMappings ) { + applyImplicitMappings = beanMapping == null || !beanMapping.isIgnoredByDefault(); + } + if ( applyImplicitMappings ) { // apply name based mapping from a source reference applyTargetThisMapping(); @@ -288,12 +363,15 @@ else if ( !method.isUpdateMethod() ) { handleUnmappedConstructorProperties(); // report errors on unmapped properties - reportErrorForUnmappedTargetPropertiesIfRequired(); - reportErrorForUnmappedSourcePropertiesIfRequired(); + if ( shouldHandledDefinedMappings ) { + reportErrorForUnmappedTargetPropertiesIfRequired( resultTypeToMap, constructorAccessorHadError ); + reportErrorForUnmappedSourcePropertiesIfRequired(); + } reportErrorForMissingIgnoredSourceProperties(); reportErrorForUnusedSourceParameters(); + reportErrorForRedundantIgnoredSourceProperties(); - // mapNullToDefault + // mapNullToDefault — JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. boolean mapNullToDefault = method.getOptions() .getBeanMapping() .getNullValueMappingStrategy() @@ -310,12 +388,24 @@ else if ( !method.isUpdateMethod() ) { ctx, existingVariableNames ); + + Supplier> additionalAfterMappingParameterBindingsProvider = () -> + sourceParametersReassignments.values() + .stream() + .map( parameter -> method.getSourceParameters().size() == 1 ? + ParameterBinding.fromParameter( parameter ) : + ParameterBinding.fromTypeAndName( + parameter.getType(), + parameter.getOriginalName() + ".get()" + ) ) + .collect( Collectors.toList() ); List afterMappingMethods = LifecycleMethodResolver.afterMappingMethods( method, resultTypeToMap, selectionParameters, ctx, - existingVariableNames + existingVariableNames, + Collections::emptyList ); if ( method instanceof ForgedMethod ) { @@ -323,6 +413,14 @@ else if ( !method.isUpdateMethod() ) { if ( factoryMethod != null ) { forgedMethod.addThrownTypes( factoryMethod.getThrownTypes() ); } + for ( LifecycleCallbackMethodReference beforeMappingMethod : beforeMappingMethods ) { + forgedMethod.addThrownTypes( beforeMappingMethod.getThrownTypes() ); + } + + for ( LifecycleCallbackMethodReference afterMappingMethod : afterMappingMethods ) { + forgedMethod.addThrownTypes( afterMappingMethod.getThrownTypes() ); + } + for ( PropertyMapping propertyMapping : propertyMappings ) { if ( propertyMapping.getAssignment() != null ) { @@ -332,14 +430,102 @@ else if ( !method.isUpdateMethod() ) { } + TypeMirror subclassExhaustiveException = method.getOptions() + .getBeanMapping() + .getSubclassExhaustiveException(); + Type subclassExhaustiveExceptionType = ctx.getTypeFactory().getType( subclassExhaustiveException ); + + List subclasses = new ArrayList<>(); + for ( SubclassMappingOptions subclassMappingOptions : method.getOptions().getSubclassMappings() ) { + subclasses.add( createSubclassMapping( subclassMappingOptions ) ); + } + MethodReference finalizeMethod = null; + List beforeMappingReferencesWithFinalizedReturnType = new ArrayList<>(); + List afterMappingReferencesWithFinalizedReturnType = new ArrayList<>(); if ( shouldCallFinalizerMethod( returnTypeToConstruct ) ) { finalizeMethod = getFinalizerMethod(); + + Type finalizerReturnType = method.getReturnType(); + if ( finalizerReturnType.isOptionalType() ) { + finalizerReturnType = finalizerReturnType.getOptionalBaseType(); + } + + beforeMappingReferencesWithFinalizedReturnType.addAll( filterMappingTarget( + LifecycleMethodResolver.beforeMappingMethods( + method, + finalizerReturnType, + selectionParameters, + ctx, + existingVariableNames + ), + false + ) ); + + afterMappingReferencesWithFinalizedReturnType.addAll( LifecycleMethodResolver.afterMappingMethods( + method, + finalizerReturnType, + selectionParameters, + ctx, + existingVariableNames, + additionalAfterMappingParameterBindingsProvider + ) ); + + keepMappingReferencesUsingTarget( beforeMappingReferencesWithFinalizedReturnType, finalizerReturnType ); + keepMappingReferencesUsingTarget( afterMappingReferencesWithFinalizedReturnType, finalizerReturnType ); + } + + List afterMappingReferencesWithOptionalReturnType = new ArrayList<>(); + if ( method.getReturnType().isOptionalType() ) { + afterMappingReferencesWithOptionalReturnType.addAll( LifecycleMethodResolver.afterMappingMethods( + method, + method.getReturnType(), + selectionParameters, + ctx, + existingVariableNames, + additionalAfterMappingParameterBindingsProvider + ) ); + + keepMappingReferencesUsingTarget( + afterMappingReferencesWithOptionalReturnType, + method.getReturnType() + ); + + } + + Map presenceChecksByParameter = new LinkedHashMap<>(); + for ( Parameter sourceParameter : method.getSourceParameters() ) { + PresenceCheck parameterPresenceCheck = PresenceCheckMethodResolver.getPresenceCheckForSourceParameter( + method, + selectionParameters, + sourceParameter, + ctx + ); + if ( parameterPresenceCheck != null ) { + presenceChecksByParameter.put( sourceParameter.getName(), parameterPresenceCheck ); + } + } + + // JSpecify: a @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // The extra presence-check guard is bean-specific and required for correctness, not just an + // optimization: the bean template only emits a `return null` (and the presence-check wrapping that + // forcing mapNullToDefault would alter) when there are nullable source parameters. With none, + // getPresenceCheckByParameter would resolve to null in the single-source template branches. + // Container/Map/Stream methods instead gate this in their templates via `sourceParameterPresenceCheck??`, + // so they force unconditionally. + if ( !mapNullToDefault + && !presenceChecksByParameter.isEmpty() + && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; } return new BeanMappingMethod( method, + getMethodAnnotations(), existingVariableNames, propertyMappings, factoryMethod, @@ -348,11 +534,138 @@ else if ( !method.isUpdateMethod() ) { returnTypeBuilder, beforeMappingMethods, afterMappingMethods, + beforeMappingReferencesWithFinalizedReturnType, + afterMappingReferencesWithFinalizedReturnType, + afterMappingReferencesWithOptionalReturnType, finalizeMethod, - mappingReferences + mappingReferences, + subclasses, + presenceChecksByParameter, + subclassExhaustiveExceptionType, + sourceParametersReassignments ); } + private void keepMappingReferencesUsingTarget(List references, Type type) { + references.removeIf( reference -> { + List bindings = reference.getParameterBindings(); + if ( bindings.isEmpty() ) { + return true; + } + for ( ParameterBinding binding : bindings ) { + if ( binding.isMappingTarget() ) { + if ( type.isAssignableTo( binding.getType() ) ) { + // If the mapping target matches the type then we need to keep this + return false; + } + } + else if ( binding.isTargetType() ) { + Type targetType = binding.getType(); + List targetTypeTypeParameters = targetType.getTypeParameters(); + if ( targetTypeTypeParameters.size() == 1 ) { + if ( type.isAssignableTo( targetTypeTypeParameters.get( 0 ) ) ) { + return false; + } + } + } + } + + return true; + } ); + } + + private boolean doesNotAllowAbstractReturnTypeAndCanBeConstructed(Type returnTypeImpl) { + return !isAbstractReturnTypeAllowed() + && canReturnTypeBeConstructed( returnTypeImpl ); + } + + private boolean allowsAbstractReturnTypeAndIsEitherAbstractOrCanBeConstructed(Type returnTypeImpl) { + return isAbstractReturnTypeAllowed() + && isReturnTypeAbstractOrCanBeConstructed( returnTypeImpl ); + } + + private SubclassMapping createSubclassMapping(SubclassMappingOptions subclassMappingOptions) { + TypeFactory typeFactory = ctx.getTypeFactory(); + Type sourceType = typeFactory.getType( subclassMappingOptions.getSource() ); + Type targetType = typeFactory.getType( subclassMappingOptions.getTarget() ); + + SourceRHS rightHandSide = new SourceRHS( + "subclassMapping", + sourceType, + Collections.emptySet(), + "SubclassMapping for " + sourceType.getFullyQualifiedName() ); + SelectionCriteria criteria = + SelectionCriteria + .forSubclassMappingMethods( + subclassMappingOptions.getSelectionParameters().withSourceRHS( rightHandSide ), + subclassMappingOptions.getMappingControl( ctx.getElementUtils() ) + ); + Assignment assignment = ctx + .getMappingResolver() + .getTargetAssignment( + method, + null, + targetType, + FormattingParameters.EMPTY, + criteria, + rightHandSide, + subclassMappingOptions.getMirror(), + () -> forgeSubclassMapping( + rightHandSide, + sourceType, + targetType, + mappingReferences ) ); + String sourceArgument = null; + for ( Parameter parameter : method.getSourceParameters() ) { + if ( ctx + .getTypeUtils() + .isAssignable( sourceType.getTypeMirror(), parameter.getType().getTypeMirror() ) ) { + sourceArgument = parameter.getName(); + if ( assignment != null ) { + assignment.setSourceLocalVarName( + "(" + sourceType.createReferenceName() + ") " + sourceArgument ); + } + } + } + return new SubclassMapping( sourceType, sourceArgument, targetType, assignment ); + } + + private boolean isAbstractReturnTypeAllowed() { + return !method.getOptions().getSubclassMappings().isEmpty() + && ( method.getOptions().getBeanMapping().getSubclassExhaustiveStrategy().isAbstractReturnTypeAllowed() + || isCorrectlySealed() ); + } + + private boolean isCorrectlySealed() { + Type mappingSourceType = method.getMappingSourceType(); + return isCorrectlySealed( mappingSourceType ); + } + + private boolean isCorrectlySealed(Type mappingSourceType) { + if ( mappingSourceType.isSealed() ) { + List unusedPermittedSubclasses = + new ArrayList<>( mappingSourceType.getPermittedSubclasses() ); + method.getOptions().getSubclassMappings().forEach( subClassOption -> { + for ( Iterator iterator = unusedPermittedSubclasses.iterator(); + iterator.hasNext(); ) { + if ( ctx.getTypeUtils().isSameType( iterator.next(), subClassOption.getSource() ) ) { + iterator.remove(); + } + } + } ); + for ( Iterator iterator = unusedPermittedSubclasses.iterator(); + iterator.hasNext(); ) { + TypeMirror typeMirror = iterator.next(); + Type type = ctx.getTypeFactory().getType( typeMirror ); + if ( type.isAbstract() && isCorrectlySealed( type ) ) { + iterator.remove(); + } + } + return unusedPermittedSubclasses.isEmpty(); + } + return false; + } + private void initializeMappingReferencesIfNeeded(Type resultTypeToMap) { if ( mappingReferences == null && method instanceof SourceMethod ) { Set readAndWriteTargetProperties = new HashSet<>( unprocessedTargetProperties.keySet() ); @@ -373,8 +686,17 @@ private void initializeMappingReferencesIfNeeded(Type resultTypeToMap) { * builder is not assignable to the return type (so without building). */ private boolean isBuilderRequired() { - return returnTypeBuilder != null - && ( !method.isUpdateMethod() || !method.isMappingTargetAssignableToReturnType() ); + if ( returnTypeBuilder == null ) { + return false; + } + if ( method.isUpdateMethod() ) { + // when @MappingTarget annotated parameter is the same type as the return type. + return !method.getResultType().isAssignableTo( method.getReturnType() ); + } + else { + // For non-update methods a builder is required when returnTypeBuilder is set + return true; + } } private boolean shouldCallFinalizerMethod(Type returnTypeToConstruct ) { @@ -420,8 +742,8 @@ private void handleUnprocessedDefinedTargets() { .name( propertyName ) .build(); - Accessor targetPropertyReadAccessor = - method.getResultType().getPropertyReadAccessors().get( propertyName ); + ReadAccessor targetPropertyReadAccessor = + method.getResultType().getReadAccessor( propertyName, forceUpdateMethod ); MappingReferences mappingRefs = extractMappingReferences( propertyName, true ); PropertyMapping propertyMapping = new PropertyMappingBuilder() .mappingContext( ctx ) @@ -560,11 +882,24 @@ else if ( !returnType.hasAccessibleConstructor() ) { return error; } + private boolean isReturnTypeAbstractOrCanBeConstructed(Type returnType) { + boolean error = true; + if ( !returnType.isAbstract() && !returnType.hasAccessibleConstructor() ) { + ctx + .getMessager() + .printMessage( + method.getExecutable(), + Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, + returnType.describe() ); + error = false; + } + return error; + } + /** * Find a factory method for a return type or for a builder. * @param returnTypeImpl the return type implementation to construct * @param @selectionParameters - * @return */ private void initializeFactoryMethod(Type returnTypeImpl, SelectionParameters selectionParameters) { List> matchingFactoryMethods = @@ -604,9 +939,33 @@ else if ( matchingFactoryMethods.size() == 1 ) { } private ConstructorAccessor getConstructorAccessor(Type type) { + if ( type.isAbstract() ) { + // We cannot construct abstract classes. + // Usually we won't reach here, + // but if SubclassMapping is used with SubclassExhaustiveStrategy#RUNTIME_EXCEPTION + // then we will still generate the code. + // We shouldn't generate anything for those abstract types + return null; + } if ( type.isRecord() ) { - // If the type is a record then just get the record components and use then + + List constructors = ElementFilter.constructorsIn( type.getTypeElement() + .getEnclosedElements() ); + + for ( ExecutableElement constructor : constructors ) { + if ( constructor.getModifiers().contains( Modifier.PRIVATE ) ) { + continue; + } + + // prefer constructor annotated with @Default + if ( hasDefaultAnnotationFromAnyPackage( constructor ) ) { + return getConstructorAccessor( type, constructor ); + } + } + + + // Other than that, just get the record components and use them List recordComponents = type.getRecordComponents(); List parameterBindings = new ArrayList<>( recordComponents.size() ); Map constructorAccessors = new LinkedHashMap<>(); @@ -632,6 +991,30 @@ private ConstructorAccessor getConstructorAccessor(Type type) { return new ConstructorAccessor( parameterBindings, constructorAccessors ); } + KotlinMetadata kotlinMetadata = type.getKotlinMetadata(); + if ( kotlinMetadata != null && kotlinMetadata.isDataClass() ) { + List constructors = ElementFilter.constructorsIn( type.getTypeElement() + .getEnclosedElements() ); + + Iterator constructorIterator = constructors.iterator(); + while ( constructorIterator.hasNext() ) { + ExecutableElement constructor = constructorIterator.next(); + if ( constructor.getModifiers().contains( Modifier.PRIVATE ) ) { + constructorIterator.remove(); + continue; + } + + // prefer constructor annotated with @Default + if ( hasDefaultAnnotationFromAnyPackage( constructor ) ) { + return getConstructorAccessor( type, constructor ); + } + } + + ExecutableElement primaryConstructor = kotlinMetadata.determinePrimaryConstructor( constructors ); + + return primaryConstructor != null ? getConstructorAccessor( type, primaryConstructor ) : null; + } + List constructors = ElementFilter.constructorsIn( type.getTypeElement() .getEnclosedElements() ); @@ -699,9 +1082,16 @@ private ConstructorAccessor getConstructorAccessor(Type type) { method.getExecutable(), GENERAL_AMBIGUOUS_CONSTRUCTORS, type, - Strings.join( constructors, ", " ) + constructors.stream() + .map( ExecutableElement::getParameters ) + .map( ps -> ps.stream() + .map( VariableElement::asType ) + .map( String::valueOf ) + .collect( Collectors.joining( ", ", type.getName() + "(", ")" ) ) + ) + .collect( Collectors.joining( ", " ) ) ); - return null; + return new ConstructorAccessor( true, Collections.emptyList(), Collections.emptyMap() ); } else { return getConstructorAccessor( type, accessibleConstructors.get( 0 ) ); @@ -760,7 +1150,7 @@ else if ( constructorProperties.size() != constructorParameters.size() ) { GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS, type ); - return null; + return new ConstructorAccessor( true, Collections.emptyList(), Collections.emptyMap() ); } else { Map constructorAccessors = new LinkedHashMap<>(); @@ -794,7 +1184,7 @@ private Accessor createConstructorAccessor(Element element, TypeMirror accessedT existingVariableNames ); existingVariableNames.add( safeParameterName ); - return new ParameterElementAccessor( element, accessedType, safeParameterName ); + return new ElementAccessor( element, accessedType, safeParameterName ); } private boolean hasDefaultAnnotationFromAnyPackage(Element element) { @@ -835,6 +1225,36 @@ private List getValueAsList(AnnotationValue av) { return (List) av.getValue(); } + /** + * Determine whether defined mappings should be handled on the result type. + * They should be, if any of the following is true: + *
          + *
        • The {@code resultTypeToMap} is not abstract
        • + *
        • There is a factory method
        • + *
        • The method is an update method
        • + *
        + * Otherwise, it means that we have reached this because subclass mappings are being used + * and the chosen strategy is runtime exception. + * + * @param resultTypeToMap the type in which the defined target properties are defined + * @return {@code true} if defined mappings should be handled for the result type, {@code false} otherwise + */ + private boolean shouldHandledDefinedMappings(Type resultTypeToMap) { + if ( !resultTypeToMap.isAbstract() ) { + return true; + } + + if ( hasFactoryMethod ) { + return true; + } + + if ( method.isUpdateMethod() ) { + return true; + } + + return false; + } + /** * Iterates over all defined mapping methods ({@code @Mapping(s)}), either directly given or inherited from the * inverse mapping method. @@ -860,6 +1280,14 @@ private boolean handleDefinedMappings(Type resultTypeToMap) { for ( MappingReference mapping : mappingReferences.getMappingReferences() ) { if ( mapping.isValid() ) { String target = mapping.getTargetReference().getShallowestPropertyName(); + if ( target == null ) { + // When the shallowest property name is null then it is for @Mapping(target = ".") + if ( this.targetThisReferences == null ) { + this.targetThisReferences = new ArrayList<>(); + } + this.targetThisReferences.add( mapping ); + continue; + } if ( !handledTargets.contains( target ) ) { if ( handleDefinedMapping( mapping, resultTypeToMap, handledTargets ) ) { errorOccurred = true; @@ -943,7 +1371,10 @@ private boolean handleDefinedMapping(MappingReference mappingRef, Type resultTyp } Accessor targetWriteAccessor = unprocessedTargetProperties.get( targetPropertyName ); - Accessor targetReadAccessor = resultTypeToMap.getPropertyReadAccessors().get( targetPropertyName ); + ReadAccessor targetReadAccessor = resultTypeToMap.getReadAccessor( + targetPropertyName, + method.getSourceParameters().size() == 1 + ); if ( targetWriteAccessor == null ) { if ( targetReadAccessor == null ) { @@ -966,14 +1397,20 @@ else if ( inheritContext.isReversed() ) { return false; } } - Set readAccessors = resultTypeToMap.getPropertyReadAccessors().keySet(); - String mostSimilarProperty = Strings.getMostSimilarWord( targetPropertyName, readAccessors ); Message msg; String[] args; - if ( targetRef.getPathProperties().isEmpty() ) { - msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_RESULTTYPE; + Set readAccessors = resultTypeToMap.getPropertyReadAccessors().keySet(); + String mostSimilarProperty = Strings.getMostSimilarWord( targetPropertyName, readAccessors ); + + Element elementForMessage = mapping.getElement(); + if ( elementForMessage == null ) { + elementForMessage = method.getExecutable(); + } + + if ( mapping.isIgnored() && mapping.getElement() == null ) { + msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_IGNORED; args = new String[] { targetPropertyName, resultTypeToMap.describe(), @@ -981,25 +1418,35 @@ else if ( inheritContext.isReversed() ) { }; } else { - List pathProperties = new ArrayList<>( targetRef.getPathProperties() ); - pathProperties.add( mostSimilarProperty ); - msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_TYPE; - args = new String[] { - targetPropertyName, - resultTypeToMap.describe(), - mapping.getTargetName(), - Strings.join( pathProperties, "." ) - }; + if ( targetRef.getPathProperties().isEmpty() ) { + msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_RESULTTYPE; + args = new String[] { + targetPropertyName, + resultTypeToMap.describe(), + mostSimilarProperty + }; + } + else { + List pathProperties = new ArrayList<>( targetRef.getPathProperties() ); + pathProperties.add( mostSimilarProperty ); + msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_TYPE; + args = new String[] { + targetPropertyName, + resultTypeToMap.describe(), + mapping.getTargetName(), + Strings.join( pathProperties, "." ) + }; + } } ctx.getMessager() .printMessage( - mapping.getElement(), + elementForMessage, mapping.getMirror(), mapping.getTargetAnnotationValue(), msg, args - ); + ); return true; } else if ( mapping.getInheritContext() != null && mapping.getInheritContext().isReversed() ) { @@ -1060,7 +1507,7 @@ else if ( !mapping.isIgnored() ) { handledTargets.add( targetPropertyName ); } - // its a constant + // it's a constant // if we have an unprocessed target that means that it most probably is nested and we should // not generated any mapping for it now. Eventually it will be done though else if ( mapping.getConstant() != null ) { @@ -1080,7 +1527,7 @@ else if ( mapping.getConstant() != null ) { handledTargets.add( targetPropertyName ); } - // its an expression + // it's an expression // if we have an unprocessed target that means that it most probably is nested and we should // not generated any mapping for it now. Eventually it will be done though else if ( mapping.getJavaExpression() != null ) { @@ -1096,7 +1543,7 @@ else if ( mapping.getJavaExpression() != null ) { .build(); handledTargets.add( targetPropertyName ); } - // its a plain-old property mapping + // it's a plain-old property mapping else { SourceReference sourceRef = mappingRef.getSourceReference(); @@ -1149,6 +1596,7 @@ else if ( mapping.getJavaExpression() != null ) { if ( sourceRef != null ) { // sourceRef == null is not considered an error here if ( sourceRef.isValid() ) { + Parameter sourceParameter = sourceRef.getParameter(); // targetProperty == null can occur: we arrived here because we want as many errors // as possible before we stop analysing @@ -1157,7 +1605,8 @@ else if ( mapping.getJavaExpression() != null ) { .sourceMethod( method ) .target( targetPropertyName, targetReadAccessor, targetWriteAccessor ) .sourcePropertyName( mapping.getSourceName() ) - .sourceReference( sourceRef ) + .sourceReference( sourceRef.withParameter( + sourceParametersReassignments.get( sourceParameter.getName() ) ) ) .selectionParameters( mapping.getSelectionParameters() ) .formattingParameters( mapping.getFormattingParameters() ) .existingVariableNames( existingVariableNames ) @@ -1169,7 +1618,15 @@ else if ( mapping.getJavaExpression() != null ) { .options( mapping ) .build(); handledTargets.add( targetPropertyName ); - unprocessedSourceParameters.remove( sourceRef.getParameter() ); + unprocessedSourceParameters.remove( sourceParameter ); + // If the source parameter was directly mapped + if ( sourceRef.getPropertyEntries().isEmpty() ) { + // Ignore all of its source properties completely + ignoreSourceProperties( sourceParameter ); + } + else { + unprocessedSourceProperties.remove( sourceRef.getShallowestPropertyName() ); + } } else { errorOccured = true; @@ -1214,12 +1671,15 @@ else if ( mapping.getJavaExpression() != null ) { *

        * When a target property matches its name with the (nested) source property, it is added to the list if and * only if it is an unprocessed target property. - * + *

        * duplicates will be handled by {@link #applyPropertyNameBasedMapping(List)} */ private void applyTargetThisMapping() { + if ( this.targetThisReferences == null ) { + return; + } Set handledTargetProperties = new HashSet<>(); - for ( MappingReference targetThis : mappingReferences.getTargetThisReferences() ) { + for ( MappingReference targetThis : this.targetThisReferences ) { // handle all prior unprocessed target properties, but let duplicates fall through List sourceRefs = targetThis @@ -1283,11 +1743,13 @@ private void applyPropertyNameBasedMapping(List sourceReference continue; } - Accessor targetPropertyReadAccessor = - method.getResultType().getPropertyReadAccessors().get( targetPropertyName ); + ReadAccessor targetPropertyReadAccessor = + method.getResultType() + .getReadAccessor( targetPropertyName, method.getSourceParameters().size() == 1 ); MappingReferences mappingRefs = extractMappingReferences( targetPropertyName, false ); PropertyMapping propertyMapping = new PropertyMappingBuilder().mappingContext( ctx ) .sourceMethod( method ) + .sourcePropertyName( targetPropertyName ) .target( targetPropertyName, targetPropertyReadAccessor, targetPropertyWriteAccessor ) .sourceReference( sourceRef ) .existingVariableNames( existingVariableNames ) @@ -1326,8 +1788,9 @@ private void applyParameterNameBasedMapping() { .name( targetProperty.getKey() ) .build(); - Accessor targetPropertyReadAccessor = - method.getResultType().getPropertyReadAccessors().get( targetProperty.getKey() ); + ReadAccessor targetPropertyReadAccessor = + method.getResultType() + .getReadAccessor( targetProperty.getKey(), method.getSourceParameters().size() == 1 ); MappingReferences mappingRefs = extractMappingReferences( targetProperty.getKey(), false ); PropertyMapping propertyMapping = new PropertyMappingBuilder() .mappingContext( ctx ) @@ -1344,50 +1807,49 @@ private void applyParameterNameBasedMapping() { sourceParameters.remove(); unprocessedDefinedTargets.remove( targetProperty.getKey() ); unprocessedSourceProperties.remove( targetProperty.getKey() ); - - // The source parameter was directly mapped so ignore all of its source properties completely - if ( !sourceParameter.getType().isPrimitive() && !sourceParameter.getType().isArrayType() ) { - // We explicitly ignore source properties from primitives or array types - Map readAccessors = sourceParameter.getType().getPropertyReadAccessors(); - for ( String sourceProperty : readAccessors.keySet() ) { - unprocessedSourceProperties.remove( sourceProperty ); - } - } - unprocessedConstructorProperties.remove( targetProperty.getKey() ); + ignoreSourceProperties( sourceParameter ); } } } } + private void ignoreSourceProperties(Parameter sourceParameter) { + // The source parameter was directly mapped so ignore all of its source properties completely + if ( !sourceParameter.getType().isPrimitive() && !sourceParameter.getType().isArrayType() ) { + // We explicitly ignore source properties from primitives or array types + Map readAccessors = sourceParameter.getType() + .getPropertyReadAccessors(); + for ( String sourceProperty : readAccessors.keySet() ) { + unprocessedSourceProperties.remove( sourceProperty ); + } + } + } + private SourceReference getSourceRefByTargetName(Parameter sourceParameter, String targetPropertyName) { SourceReference sourceRef = null; - if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ) { - return sourceRef; + Type sourceParameterType = sourceParameter.getType(); + Parameter sourceParameterToUse = sourceParameter; + if ( sourceParameterType.isOptionalType() ) { + sourceParameterType = sourceParameterType.getOptionalBaseType(); + sourceParameterToUse = sourceParametersReassignments.get( sourceParameter.getName() ); } - - if ( sourceParameter.getType().isMapType() ) { - List typeParameters = sourceParameter.getType().getTypeParameters(); - if ( typeParameters.size() == 2 && typeParameters.get( 0 ).isString() ) { - return SourceReference.fromMapSource( - new String[] { targetPropertyName }, - sourceParameter - ); - } + if ( sourceParameterType.isPrimitive() || sourceParameterType.isArrayType() ) { + return sourceRef; } - Accessor sourceReadAccessor = - sourceParameter.getType().getPropertyReadAccessors().get( targetPropertyName ); + ReadAccessor sourceReadAccessor = sourceParameterType + .getReadAccessor( targetPropertyName, method.getSourceParameters().size() == 1 ); if ( sourceReadAccessor != null ) { // property mapping - Accessor sourcePresenceChecker = - sourceParameter.getType().getPropertyPresenceCheckers().get( targetPropertyName ); + PresenceCheckAccessor sourcePresenceChecker = + sourceParameterType.getPresenceChecker( targetPropertyName ); - DeclaredType declaredSourceType = (DeclaredType) sourceParameter.getType().getTypeMirror(); + DeclaredType declaredSourceType = (DeclaredType) sourceParameterType.getTypeMirror(); Type returnType = ctx.getTypeFactory().getReturnType( declaredSourceType, sourceReadAccessor ); - sourceRef = new SourceReference.BuilderFromProperty().sourceParameter( sourceParameter ) + sourceRef = new SourceReference.BuilderFromProperty().sourceParameter( sourceParameterToUse ) .type( returnType ) .readAccessor( sourceReadAccessor ) .presenceChecker( sourcePresenceChecker ) @@ -1409,95 +1871,77 @@ private ReportingPolicyGem getUnmappedTargetPolicy() { if ( mappingReferences.isForForgedMethods() ) { return ReportingPolicyGem.IGNORE; } + // If we have ignoreByDefault = true, unprocessed target properties are not an issue. + if ( method.getOptions().getBeanMapping().isIgnoredByDefault() ) { + return ReportingPolicyGem.IGNORE; + } if ( method.getOptions().getBeanMapping() != null ) { return method.getOptions().getBeanMapping().unmappedTargetPolicy(); } return method.getOptions().getMapper().unmappedTargetPolicy(); } - private void reportErrorForUnmappedTargetPropertiesIfRequired() { + private void reportErrorForUnmappedTargetPropertiesIfRequired(Type resultType, + boolean constructorAccessorHadError) { // fetch settings from element to implement ReportingPolicyGem unmappedTargetPolicy = getUnmappedTargetPolicy(); - if ( method instanceof ForgedMethod && targetProperties.isEmpty() ) { - //TODO until we solve 1140 we report this error when the target properties are empty - ForgedMethod forgedMethod = (ForgedMethod) method; - if ( forgedMethod.getHistory() == null ) { - Type sourceType = this.method.getParameters().get( 0 ).getType(); - Type targetType = this.method.getReturnType(); - ctx.getMessager().printMessage( - this.method.getExecutable(), - Message.PROPERTYMAPPING_FORGED_MAPPING_NOT_FOUND, - sourceType.describe(), - targetType.describe(), - targetType.describe(), - sourceType.describe() - ); + if ( targetProperties.isEmpty() ) { + if ( method instanceof ForgedMethod ) { + ForgedMethod forgedMethod = (ForgedMethod) method; + if ( forgedMethod.getHistory() == null ) { + Type sourceType = this.method.getParameters().get( 0 ).getType(); + Type targetType = this.method.getReturnType(); + ctx.getMessager().printMessage( + this.method.getExecutable(), + Message.PROPERTYMAPPING_FORGED_MAPPING_NOT_FOUND, + sourceType.describe(), + targetType.describe(), + targetType.describe(), + sourceType.describe() + ); + } + else { + ForgedMethodHistory history = forgedMethod.getHistory(); + ctx.getMessager().printMessage( + this.method.getExecutable(), + Message.PROPERTYMAPPING_FORGED_MAPPING_WITH_HISTORY_NOT_FOUND, + history.createSourcePropertyErrorMessage(), + history.getTargetType().describe(), + history.createTargetPropertyName(), + history.getTargetType().describe(), + history.getSourceType().describe() + ); + } } - else { - ForgedMethodHistory history = forgedMethod.getHistory(); + else if ( !constructorAccessorHadError ) { ctx.getMessager().printMessage( - this.method.getExecutable(), - Message.PROPERTYMAPPING_FORGED_MAPPING_WITH_HISTORY_NOT_FOUND, - history.createSourcePropertyErrorMessage(), - history.getTargetType().describe(), - history.createTargetPropertyName(), - history.getTargetType().describe(), - history.getSourceType().describe() + method.getExecutable(), + Message.PROPERTYMAPPING_TARGET_HAS_NO_TARGET_PROPERTIES, + resultType.describe() ); } } else if ( !unprocessedTargetProperties.isEmpty() && unmappedTargetPolicy.requiresReport() ) { - if ( !( method instanceof ForgedMethod ) ) { - Message msg = unmappedTargetPolicy.getDiagnosticKind() == Diagnostic.Kind.ERROR ? - Message.BEANMAPPING_UNMAPPED_TARGETS_ERROR : Message.BEANMAPPING_UNMAPPED_TARGETS_WARNING; - Object[] args = new Object[] { - MessageFormat.format( - "{0,choice,1#property|1 unmappedProperties, + Message unmappedPropertiesMsg, + Message unmappedForgedPropertiesMsg) { + if ( !( method instanceof ForgedMethod ) ) { Object[] args = new Object[] { MessageFormat.format( "{0,choice,1#property|1 parameterBindings; private final Map constructorAccessors; private ConstructorAccessor( List parameterBindings, Map constructorAccessors) { + this( false, parameterBindings, constructorAccessors ); + } + + private ConstructorAccessor(boolean hasError, List parameterBindings, + Map constructorAccessors) { + this.hasError = hasError; this.parameterBindings = parameterBindings; this.constructorAccessors = constructorAccessors; } @@ -1591,6 +2115,7 @@ private ConstructorAccessor( //CHECKSTYLE:OFF private BeanMappingMethod(Method method, + List annotations, Collection existingVariableNames, List propertyMappings, MethodReference factoryMethod, @@ -1599,10 +2124,19 @@ private BeanMappingMethod(Method method, BuilderType returnTypeBuilder, List beforeMappingReferences, List afterMappingReferences, + List beforeMappingReferencesWithFinalizedReturnType, + List afterMappingReferencesWithFinalizedReturnType, + List afterMappingReferencesWithOptionalReturnType, MethodReference finalizerMethod, - MappingReferences mappingReferences) { + MappingReferences mappingReferences, + List subclassMappings, + Map presenceChecksByParameter, + Type subclassExhaustiveException, + Map sourceParametersReassignments + ) { super( method, + annotations, existingVariableNames, factoryMethod, mapNullToDefault, @@ -1614,17 +2148,38 @@ private BeanMappingMethod(Method method, this.propertyMappings = propertyMappings; this.returnTypeBuilder = returnTypeBuilder; this.finalizerMethod = finalizerMethod; + this.subclassExhaustiveException = subclassExhaustiveException; + if ( this.finalizerMethod != null ) { + this.finalizedResultName = + Strings.getSafeVariableName( getResultName() + "Result", existingVariableNames ); + existingVariableNames.add( this.finalizedResultName ); + this.optionalResultName = + Strings.getSafeVariableName( getResultName() + "ResultOptional", existingVariableNames ); + existingVariableNames.add( this.optionalResultName ); + } + else { + this.finalizedResultName = null; + this.optionalResultName = + Strings.getSafeVariableName( getResultName() + "Optional", existingVariableNames ); + existingVariableNames.add( this.optionalResultName ); + } this.mappingReferences = mappingReferences; - // intialize constant mappings as all mappings, but take out the ones that can be contributed to a + this.beforeMappingReferencesWithFinalizedReturnType = beforeMappingReferencesWithFinalizedReturnType; + this.afterMappingReferencesWithFinalizedReturnType = afterMappingReferencesWithFinalizedReturnType; + this.afterMappingReferencesWithOptionalReturnType = afterMappingReferencesWithOptionalReturnType; + + // initialize constant mappings as all mappings, but take out the ones that can be contributed to a // parameter mapping. this.mappingsByParameter = new HashMap<>(); this.constantMappings = new ArrayList<>( propertyMappings.size() ); + this.presenceChecksByParameter = presenceChecksByParameter; this.constructorMappingsByParameter = new LinkedHashMap<>(); this.constructorConstantMappings = new ArrayList<>(); - Set sourceParameterNames = getSourceParameters().stream() - .map( Parameter::getName ) - .collect( Collectors.toSet() ); + Set sourceParameterNames = new HashSet<>(); + for ( Parameter sourceParameter : getSourceParameters() ) { + sourceParameterNames.add( sourceParameter.getName() ); + } for ( PropertyMapping mapping : propertyMappings ) { if ( mapping.isConstructorMapping() ) { if ( sourceParameterNames.contains( mapping.getSourceBeanName() ) ) { @@ -1646,6 +2201,15 @@ else if ( sourceParameterNames.contains( mapping.getSourceBeanName() ) ) { } } this.returnTypeToConstruct = returnTypeToConstruct; + this.newInstance = ( returnTypeToConstruct != null && getFactoryMethod() == null ) + ? NewInstanceCreation.forType( returnTypeToConstruct ) + : null; + this.subclassMappings = subclassMappings; + this.sourceParametersReassignments = sourceParametersReassignments; + } + + public Type getSubclassExhaustiveException() { + return subclassExhaustiveException; } public List getConstantMappings() { @@ -1656,6 +2220,38 @@ public List getConstructorConstantMappings() { return constructorConstantMappings; } + public List getSubclassMappings() { + return subclassMappings; + } + + public String getFinalizedResultName() { + return finalizedResultName; + } + + public Type getFinalizedReturnType() { + Type returnType = getReturnType(); + if ( returnType.isOptionalType() ) { + return returnType.getOptionalBaseType(); + } + return returnType; + } + + public String getOptionalResultName() { + return optionalResultName; + } + + public List getBeforeMappingReferencesWithFinalizedReturnType() { + return beforeMappingReferencesWithFinalizedReturnType; + } + + public List getAfterMappingReferencesWithFinalizedReturnType() { + return afterMappingReferencesWithFinalizedReturnType; + } + + public List getAfterMappingReferencesWithOptionalReturnType() { + return afterMappingReferencesWithOptionalReturnType; + } + public List propertyMappingsByParameter(Parameter parameter) { // issues: #909 and #1244. FreeMarker has problem getting values from a map when the search key is size or value return mappingsByParameter.getOrDefault( parameter.getName(), Collections.emptyList() ); @@ -1670,6 +2266,19 @@ public Type getReturnTypeToConstruct() { return returnTypeToConstruct; } + public NewInstanceCreation getNewInstance() { + return newInstance; + } + + public boolean hasSubclassMappings() { + return !subclassMappings.isEmpty(); + } + + public boolean isAbstractReturnType() { + return getFactoryMethod() == null && returnTypeToConstruct != null + && returnTypeToConstruct.isAbstract(); + } + public boolean hasConstructorMappings() { return !constructorMappingsByParameter.isEmpty() || !constructorConstantMappings.isEmpty(); } @@ -1689,55 +2298,78 @@ public Set getImportTypes() { types.addAll( propertyMapping.getTargetType().getImportTypes() ); } } + for ( SubclassMapping subclassMapping : subclassMappings ) { + types.addAll( subclassMapping.getImportTypes() ); + } if ( returnTypeToConstruct != null ) { - types.addAll( returnTypeToConstruct.getImportTypes() ); + types.addAll( newInstance != null ? newInstance.getImportTypes() : returnTypeToConstruct.getImportTypes() ); } if ( returnTypeBuilder != null ) { types.add( returnTypeBuilder.getOwningType() ); } + for ( LifecycleCallbackMethodReference reference : beforeMappingReferencesWithFinalizedReturnType ) { + types.addAll( reference.getImportTypes() ); + } + for ( LifecycleCallbackMethodReference reference : afterMappingReferencesWithFinalizedReturnType ) { + types.addAll( reference.getImportTypes() ); + } + + for ( LifecycleCallbackMethodReference reference : afterMappingReferencesWithOptionalReturnType ) { + types.addAll( reference.getImportTypes() ); + } return types; } - public List getSourceParametersExcludingPrimitives() { - return getSourceParameters().stream() - .filter( parameter -> !parameter.getType().isPrimitive() ) - .collect( Collectors.toList() ); + public Collection getSourcePresenceChecks() { + return presenceChecksByParameter.values(); } - public List getSourceParametersNeedingNullCheck() { + public Map getPresenceChecksByParameter() { + return presenceChecksByParameter; + } + + public PresenceCheck getPresenceCheckByParameter(Parameter parameter) { + return presenceChecksByParameter.get( parameter.getName() ); + } + + public List getSourceParametersNeedingPresenceCheck() { return getSourceParameters().stream() - .filter( this::needsNullCheck ) + .filter( this::needsPresenceCheck ) .collect( Collectors.toList() ); } - public List getSourceParametersNotNeedingNullCheck() { + public List getSourceParametersNotNeedingPresenceCheck() { return getSourceParameters().stream() - .filter( parameter -> !needsNullCheck( parameter ) ) + .filter( parameter -> !needsPresenceCheck( parameter ) ) .collect( Collectors.toList() ); } - private boolean needsNullCheck(Parameter parameter) { - if ( parameter.getType().isPrimitive() ) { + public Parameter getSourceParameterReassignment(Parameter parameter) { + return sourceParametersReassignments.get( parameter.getName() ); + } + + private boolean needsPresenceCheck(Parameter parameter) { + if ( !presenceChecksByParameter.containsKey( parameter.getName() ) ) { return false; } List mappings = propertyMappingsByParameter( parameter ); - if ( mappings.size() == 1 && doesNotNeedNullCheckForSourceParameter( mappings.get( 0 ) ) ) { + if ( mappings.size() == 1 && doesNotNeedPresenceCheckForSourceParameter( mappings.get( 0 ) ) ) { return false; } mappings = constructorPropertyMappingsByParameter( parameter ); - if ( mappings.size() == 1 && doesNotNeedNullCheckForSourceParameter( mappings.get( 0 ) ) ) { + if ( mappings.size() == 1 && doesNotNeedPresenceCheckForSourceParameter( mappings.get( 0 ) ) ) { return false; } return true; } - private boolean doesNotNeedNullCheckForSourceParameter(PropertyMapping mapping) { + private boolean doesNotNeedPresenceCheckForSourceParameter(PropertyMapping mapping) { if ( mapping.getAssignment().isCallingUpdateMethod() ) { // If the mapping assignment is calling an update method then we should do a null check // in the bean mapping @@ -1780,5 +2412,4 @@ public boolean equals(Object obj) { return true; } - } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java index 9b7912f752..848e2cc35f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java @@ -9,9 +9,9 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; +import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.model.common.BuilderType; import org.mapstruct.ap.internal.model.source.Method; -import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.util.Extractor; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; @@ -19,6 +19,8 @@ import static org.mapstruct.ap.internal.util.Collections.first; /** + * Factory for creating the appropriate builder finisher method. + * * @author Filip Hrisafov */ public class BuilderFinisherMethodResolver { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java index dd6db1b0ee..5c99b85588 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java @@ -5,24 +5,34 @@ */ package org.mapstruct.ap.internal.model; +import java.util.function.Predicate; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; + +import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; +import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; +import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.model.assignment.ExistingInstanceSetterWrapperForCollectionsAndMaps; import org.mapstruct.ap.internal.model.assignment.GetterWrapperForCollectionsAndMaps; +import org.mapstruct.ap.internal.model.assignment.NewInstanceSetterWrapperForCollectionsAndMaps; import org.mapstruct.ap.internal.model.assignment.SetterWrapperForCollectionsAndMaps; import org.mapstruct.ap.internal.model.assignment.SetterWrapperForCollectionsAndMapsWithNullCheck; import org.mapstruct.ap.internal.model.assignment.UpdateWrapper; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.Assignment.AssignmentType; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; -import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; -import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; -import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; import static org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem.ALWAYS; +import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.IGNORE; import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT; import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_NULL; @@ -64,6 +74,7 @@ public class CollectionAssignmentBuilder { private SourceRHS sourceRHS; private NullValueCheckStrategyGem nvcs; private NullValuePropertyMappingStrategyGem nvpms; + private NullabilityResolver.Nullability sourceJSpecifyNullability = NullabilityResolver.Nullability.UNKNOWN; public CollectionAssignmentBuilder mappingBuilderContext(MappingBuilderContext ctx) { this.ctx = ctx; @@ -125,6 +136,15 @@ public CollectionAssignmentBuilder nullValuePropertyMappingStrategy( NullValuePr return this; } + public CollectionAssignmentBuilder sourceJSpecifyNullability( + NullabilityResolver.Nullability sourceJSpecifyNullability + ) { + this.sourceJSpecifyNullability = sourceJSpecifyNullability != null + ? sourceJSpecifyNullability + : NullabilityResolver.Nullability.UNKNOWN; + return this; + } + public Assignment build() { Assignment result = assignment; @@ -154,7 +174,8 @@ public Assignment build() { targetType, true, nvpms == SET_TO_NULL && !targetType.isPrimitive(), - nvpms == SET_TO_DEFAULT + nvpms == SET_TO_DEFAULT, + false ); } else if ( method.isUpdateMethod() && !targetImmutable ) { @@ -169,7 +190,7 @@ else if ( method.isUpdateMethod() && !targetImmutable ) { targetAccessorType.isFieldAssignment() ); } - else if ( setterWrapperNeedsSourceNullCheck( result ) ) { + else if ( method.isUpdateMethod() && nvpms == IGNORE ) { result = new SetterWrapperForCollectionsAndMapsWithNullCheck( result, @@ -179,7 +200,18 @@ else if ( setterWrapperNeedsSourceNullCheck( result ) ) { targetAccessorType.isFieldAssignment() ); } - else { + else if ( setterWrapperNeedsSourceNullCheck( result ) + && canBeMappedOrDirectlyAssigned( result ) ) { + + result = new SetterWrapperForCollectionsAndMapsWithNullCheck( + result, + method.getThrownTypes(), + targetType, + ctx.getTypeFactory(), + targetAccessorType.isFieldAssignment() + ); + } + else if ( canBeMappedOrDirectlyAssigned( result ) ) { //TODO init default value // target accessor is setter, so wrap the setter in setter map/ collection handling @@ -190,6 +222,21 @@ else if ( setterWrapperNeedsSourceNullCheck( result ) ) { targetAccessorType.isFieldAssignment() ); } + else if ( hasNoArgsConstructor() ) { + result = new NewInstanceSetterWrapperForCollectionsAndMaps( + result, + method.getThrownTypes(), + targetType, + ctx.getTypeFactory(), + targetAccessorType.isFieldAssignment() ); + } + else { + ctx.getMessager().printMessage( + method.getExecutable(), + Message.PROPERTYMAPPING_NO_SUITABLE_COLLECTION_OR_MAP_CONSTRUCTOR, + targetType + ); + } } else { if ( targetImmutable ) { @@ -205,6 +252,7 @@ else if ( setterWrapperNeedsSourceNullCheck( result ) ) { result, method.getThrownTypes(), targetType, + nvpms, targetAccessorType.isFieldAssignment() ); } @@ -212,6 +260,12 @@ else if ( setterWrapperNeedsSourceNullCheck( result ) ) { return result; } + private boolean canBeMappedOrDirectlyAssigned(Assignment result) { + return result.getType() != AssignmentType.DIRECT + || hasCopyConstructor() + || targetType.isEnumSet(); + } + /** * Checks whether the setter wrapper should include a null / presence check or not * @@ -219,6 +273,14 @@ else if ( setterWrapperNeedsSourceNullCheck( result ) ) { * @return whether to include a null / presence check or not */ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs) { + // JSpecify: source @NonNull means the value is guaranteed non-null, skip the wrapper + if ( sourceJSpecifyNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK_NON_NULL_SOURCE, + targetPropertyName ); + return false; + } + if ( rhs.getSourcePresenceCheckerReference() != null ) { // If there is a source presence check then we should do a null check return true; @@ -236,4 +298,48 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs) { return false; } + private boolean hasCopyConstructor() { + return checkConstructorForPredicate( this::hasCopyConstructor ); + } + + private boolean hasNoArgsConstructor() { + return checkConstructorForPredicate( this::hasNoArgsConstructor ); + } + + private boolean checkConstructorForPredicate(Predicate predicate) { + if ( targetType.isCollectionOrMapType() ) { + if ( "java.util".equals( targetType.getPackageName() ) ) { + return true; + } + else { + Element sourceElement = targetType.getImplementationType() != null + ? targetType.getImplementationType().getTypeElement() + : targetType.getTypeElement(); + if ( sourceElement != null ) { + for ( Element element : sourceElement.getEnclosedElements() ) { + if ( element.getKind() == ElementKind.CONSTRUCTOR + && element.getModifiers().contains( Modifier.PUBLIC ) ) { + if ( predicate.test( element ) ) { + return true; + } + } + } + } + } + } + return false; + } + + private boolean hasNoArgsConstructor(Element element) { + return ( (ExecutableElement) element ).getParameters().isEmpty(); + } + + private boolean hasCopyConstructor(Element element) { + if ( element instanceof ExecutableElement ) { + ExecutableElement ee = (ExecutableElement) element; + return ee.getParameters().size() == 1 + && ctx.getTypeUtils().isAssignable( targetType.getTypeMirror(), ee.getParameters().get( 0 ).asType() ); + } + return false; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java index 382aff9737..1a807725d3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java @@ -12,6 +12,7 @@ import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; @@ -30,30 +31,50 @@ public abstract class ContainerMappingMethod extends NormalTypeMappingMethod { private final SelectionParameters selectionParameters; private final String index1Name; private final String index2Name; + private final Parameter sourceParameter; + private final PresenceCheck sourceParameterPresenceCheck; private IterableCreation iterableCreation; - ContainerMappingMethod(Method method, Collection existingVariables, Assignment parameterAssignment, + //CHECKSTYLE:OFF + ContainerMappingMethod(Method method, List annotations, + Collection existingVariables, Assignment parameterAssignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters) { - super( method, existingVariables, factoryMethod, mapNullToDefault, beforeMappingReferences, + SelectionParameters selectionParameters, PresenceCheck sourceParameterPresenceCheck) { + //CHECKSTYLE:ON + super( method, annotations, existingVariables, factoryMethod, mapNullToDefault, beforeMappingReferences, afterMappingReferences ); this.elementAssignment = parameterAssignment; this.loopVariableName = loopVariableName; - this.selectionParameters = selectionParameters; + this.selectionParameters = selectionParameters != null ? selectionParameters : SelectionParameters.empty(); + this.index1Name = Strings.getSafeVariableName( "i", existingVariables ); this.index2Name = Strings.getSafeVariableName( "j", existingVariables ); - } - public Parameter getSourceParameter() { + Parameter sourceParameter = null; for ( Parameter parameter : getParameters() ) { if ( !parameter.isMappingTarget() && !parameter.isMappingContext() ) { - return parameter; + sourceParameter = parameter; + break; } } - throw new IllegalStateException( "Method " + this + " has no source parameter." ); + if ( sourceParameter == null ) { + throw new IllegalStateException( "Method " + this + " has no source parameter." ); + } + + this.sourceParameter = sourceParameter; + this.sourceParameterPresenceCheck = sourceParameterPresenceCheck; + + } + + public Parameter getSourceParameter() { + return sourceParameter; + } + + public PresenceCheck getSourceParameterPresenceCheck() { + return sourceParameterPresenceCheck; } public IterableCreation getIterableCreation() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java index be08ac1ca4..598db971e2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java @@ -9,9 +9,12 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import javax.lang.model.element.AnnotationMirror; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.FormattingParameters; +import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; @@ -22,8 +25,6 @@ import static org.mapstruct.ap.internal.util.Collections.first; -import javax.lang.model.element.AnnotationMirror; - /** * Builder that can be used to build {@link ContainerMappingMethod}(s). * @@ -125,11 +126,19 @@ public final M build() { } assignment = getWrapper( assignment, method ); - // mapNullToDefault + // mapNullToDefault — a JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // Forcing is unconditional here (unlike BeanMappingMethod): when the source is @NonNull the template skips + // the whole guard block via `sourceParameterPresenceCheck??`, so the forced value is simply unused there. boolean mapNullToDefault = method.getOptions() .getIterableMapping() .getNullValueMappingStrategy() .isReturnDefault(); + if ( !mapNullToDefault && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } MethodReference factoryMethod = null; if ( !method.isUpdateMethod() ) { @@ -152,6 +161,11 @@ public final M build() { existingVariables ); + // Resolve presence check via JSpecify-aware resolver — returns null when source is @NonNull. + Parameter sourceParam = first( method.getSourceParameters() ); + PresenceCheck sourceParameterPresenceCheck = + PresenceCheckMethodResolver.getPresenceCheckForSourceParameter( method, null, sourceParam, ctx ); + return instantiateMappingMethod( method, existingVariables, @@ -161,7 +175,8 @@ public final M build() { loopVariableName, beforeMappingMethods, afterMappingMethods, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } @@ -178,7 +193,7 @@ protected abstract M instantiateMappingMethod(Method method, Collection boolean mapNullToDefault, String loopVariableName, List beforeMappingMethods, List afterMappingMethods, - SelectionParameters selectionParameters); + SelectionParameters selectionParameters, PresenceCheck sourceParameterPresenceCheck); protected abstract Type getElementType(Type parameterType); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/Decorator.java b/processor/src/main/java/org/mapstruct/ap/internal/model/Decorator.java index a54045dca0..df7940da31 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/Decorator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/Decorator.java @@ -7,14 +7,15 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.SortedSet; import javax.lang.model.element.TypeElement; +import org.mapstruct.ap.internal.gem.DecoratedWithGem; import org.mapstruct.ap.internal.model.common.Accessibility; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.option.Options; -import org.mapstruct.ap.internal.gem.DecoratedWithGem; import org.mapstruct.ap.internal.version.VersionInformation; /** @@ -32,6 +33,8 @@ public static class Builder extends GeneratedTypeBuilder { private boolean hasDelegateConstructor; private String implName; private String implPackage; + private boolean suppressGeneratorTimestamp; + private Set customAnnotations; public Builder() { super( Builder.class ); @@ -62,6 +65,16 @@ public Builder implPackage(String implPackage) { return this; } + public Builder suppressGeneratorTimestamp(boolean suppressGeneratorTimestamp) { + this.suppressGeneratorTimestamp = suppressGeneratorTimestamp; + return this; + } + + public Builder additionalAnnotations(Set customAnnotations) { + this.customAnnotations = customAnnotations; + return this; + } + public Decorator build() { String implementationName = implName.replace( Mapper.CLASS_NAME_PLACEHOLDER, Mapper.getFlatName( mapperElement ) ); @@ -86,9 +99,11 @@ public Decorator build() { methods, options, versionInformation, + suppressGeneratorTimestamp, Accessibility.fromModifiers( mapperElement.getModifiers() ), extraImportedTypes, - decoratorConstructor + decoratorConstructor, + customAnnotations ); } } @@ -101,8 +116,10 @@ private Decorator(TypeFactory typeFactory, String packageName, String name, Type Type mapperType, List methods, Options options, VersionInformation versionInformation, + boolean suppressGeneratorTimestamp, Accessibility accessibility, SortedSet extraImports, - DecoratorConstructor decoratorConstructor) { + DecoratorConstructor decoratorConstructor, + Set customAnnotations) { super( typeFactory, packageName, @@ -112,6 +129,7 @@ private Decorator(TypeFactory typeFactory, String packageName, String name, Type Arrays.asList( new Field( mapperType, "delegate", true ) ), options, versionInformation, + suppressGeneratorTimestamp, accessibility, extraImports, decoratorConstructor @@ -119,6 +137,11 @@ private Decorator(TypeFactory typeFactory, String packageName, String name, Type this.decoratorType = decoratorType; this.mapperType = mapperType; + + // Add custom annotations + if ( customAnnotations != null ) { + customAnnotations.forEach( this::addAnnotation ); + } } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/DefaultMapperReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/DefaultMapperReference.java index 6ac0c1c74d..2728f99323 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/DefaultMapperReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/DefaultMapperReference.java @@ -36,7 +36,7 @@ private DefaultMapperReference(Type type, boolean isAnnotatedMapper, boolean isS public static DefaultMapperReference getInstance(Type type, boolean isAnnotatedMapper, boolean isSingleton, TypeFactory typeFactory, List otherMapperReferences) { Set importTypes = Collections.asSet( type ); - if ( isAnnotatedMapper && !isSingleton) { + if ( isAnnotatedMapper && !isSingleton ) { importTypes.add( typeFactory.getType( "org.mapstruct.factory.Mappers" ) ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ForgedMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ForgedMethod.java index b97d8be111..fb7943ab24 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ForgedMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ForgedMethod.java @@ -42,6 +42,7 @@ public class ForgedMethod implements Method { private final Method basedOn; private final boolean forgedNameBased; + private MappingMethodOptions options; /** * Creates a new forged method with the given name for mapping a method parameter to a property. @@ -121,9 +122,53 @@ public static ForgedMethod forElementMapping(String name, Type sourceType, Type ); } + /** + * Creates a new forged method for mapping a SubclassMapping element + * + * @param name the (unique name) for this method + * @param sourceType the source type + * @param returnType the return type. + * @param basedOn the method that (originally) triggered this nested method generation. + * @param history a parent forged method if this is a forged method within a forged method + * @param forgedNameBased forges a name based (matched) mapping method + * + * @return a new forge method + */ + public static ForgedMethod forSubclassMapping(String name, Type sourceType, Type returnType, Method basedOn, + MappingReferences mappingReferences, ForgedMethodHistory history, + boolean forgedNameBased) { + return new ForgedMethod( + name, + sourceType, + returnType, + basedOn.getContextParameters(), + basedOn, + history, + mappingReferences == null ? MappingReferences.empty() : mappingReferences, + forgedNameBased, + MappingMethodOptions.getSubclassForgedMethodInheritedOptions( basedOn.getOptions() ) + ); + } + private ForgedMethod(String name, Type sourceType, Type returnType, List additionalParameters, Method basedOn, ForgedMethodHistory history, MappingReferences mappingReferences, boolean forgedNameBased) { + this( + name, + sourceType, + returnType, + additionalParameters, + basedOn, + history, + mappingReferences, + forgedNameBased, + MappingMethodOptions.getForgedMethodInheritedOptions( basedOn.getOptions() ) + ); + } + + private ForgedMethod(String name, Type sourceType, Type returnType, List additionalParameters, + Method basedOn, ForgedMethodHistory history, MappingReferences mappingReferences, + boolean forgedNameBased, MappingMethodOptions options) { // establish name String sourceParamSafeName; @@ -155,6 +200,8 @@ private ForgedMethod(String name, Type sourceType, Type returnType, List(); + this.thrownTypes = forgedMethod.thrownTypes; this.history = forgedMethod.history; this.sourceParameters = Parameter.getSourceParameters( parameters ); @@ -177,6 +224,8 @@ public ForgedMethod(String name, ForgedMethod forgedMethod) { this.name = name; this.forgedNameBased = forgedMethod.forgedNameBased; + + this.options = MappingMethodOptions.getForgedMethodInheritedOptions( basedOn.getOptions() ); } @Override @@ -347,7 +396,7 @@ public boolean isObjectFactory() { @Override public MappingMethodOptions getOptions() { - return basedOn.getOptions(); + return options; } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/FromOptionalTypeConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/model/FromOptionalTypeConversion.java new file mode 100644 index 0000000000..e1c320e16b --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/FromOptionalTypeConversion.java @@ -0,0 +1,115 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import java.util.List; +import java.util.Set; + +import org.mapstruct.ap.internal.model.assignment.OptionalGetWrapper; +import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.PresenceCheck; +import org.mapstruct.ap.internal.model.common.Type; + +/** + * An inline conversion from an optional source to it's value. + * + * @author Filip Hrisafov + */ +public class FromOptionalTypeConversion extends ModelElement implements Assignment { + + private final Assignment conversionAssignment; + private final Type optionalType; + + public FromOptionalTypeConversion(Type optionalType, Assignment conversionAssignment) { + this.conversionAssignment = conversionAssignment; + this.optionalType = optionalType; + } + + @Override + public Set getImportTypes() { + return conversionAssignment.getImportTypes(); + } + + @Override + public List getThrownTypes() { + return conversionAssignment.getThrownTypes(); + } + + public Assignment getAssignment() { + return conversionAssignment; + } + + @Override + public String getSourceReference() { + return conversionAssignment.getSourceReference(); + } + + @Override + public boolean isSourceReferenceParameter() { + return conversionAssignment.isSourceReferenceParameter(); + } + + @Override + public PresenceCheck getSourcePresenceCheckerReference() { + return conversionAssignment.getSourcePresenceCheckerReference(); + } + + @Override + public Type getSourceType() { + return conversionAssignment.getSourceType(); + } + + @Override + public String createUniqueVarName(String desiredName) { + return conversionAssignment.createUniqueVarName( desiredName ); + } + + @Override + public String getSourceLocalVarName() { + return conversionAssignment.getSourceLocalVarName(); + } + + @Override + public void setSourceLocalVarName(String sourceLocalVarName) { + conversionAssignment.setSourceLocalVarName( sourceLocalVarName ); + } + + @Override + public String getSourceLoopVarName() { + return conversionAssignment.getSourceLoopVarName(); + } + + @Override + public void setSourceLoopVarName(String sourceLoopVarName) { + conversionAssignment.setSourceLoopVarName( sourceLoopVarName ); + } + + @Override + public String getSourceParameterName() { + return conversionAssignment.getSourceParameterName(); + } + + @Override + public void setAssignment(Assignment assignment) { + this.conversionAssignment.setAssignment( new OptionalGetWrapper( assignment, optionalType ) ); + } + + @Override + public AssignmentType getType() { + return conversionAssignment.getType(); + } + + @Override + public boolean isCallingUpdateMethod() { + return false; + } + + @Override + public String toString() { + return conversionAssignment.toString(); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java b/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java index 620559ea24..6eb8bbfef8 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java @@ -10,15 +10,15 @@ import java.util.List; import java.util.SortedSet; import java.util.TreeSet; - import javax.lang.model.type.TypeKind; -import org.mapstruct.ap.internal.util.ElementUtils; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.model.common.Accessibility; import org.mapstruct.ap.internal.model.common.ModelElement; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.option.Options; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.version.VersionInformation; @@ -31,9 +31,9 @@ public abstract class GeneratedType extends ModelElement { private static final String JAVA_LANG_PACKAGE = "java.lang"; - protected abstract static class GeneratedTypeBuilder { + protected abstract static class GeneratedTypeBuilder> { - private T myself; + private final T myself; protected TypeFactory typeFactory; protected ElementUtils elementUtils; protected Options options; @@ -41,6 +41,7 @@ protected abstract static class GeneratedTypeBuilder extraImportedTypes; protected List methods; + protected ClassAccessibilityGem classAccessibility; GeneratedTypeBuilder(Class selfType) { myself = selfType.cast( this ); @@ -76,6 +77,10 @@ public T methods(List methods) { return myself; } + public T classAccessibility(ClassAccessibilityGem classAccessibility) { + this.classAccessibility = classAccessibility; + return myself; + } } private final String packageName; @@ -83,7 +88,7 @@ public T methods(List methods) { private final Type mapperDefinitionType; private final List annotations; - private final List methods; + private final List methods; private final SortedSet extraImportedTypes; private final boolean suppressGeneratorTimestamp; @@ -103,6 +108,7 @@ public T methods(List methods) { protected GeneratedType(TypeFactory typeFactory, String packageName, String name, Type mapperDefinitionType, List methods, List fields, Options options, VersionInformation versionInformation, + boolean suppressGeneratorTimestamp, Accessibility accessibility, SortedSet extraImportedTypes, Constructor constructor) { this.packageName = packageName; this.name = name; @@ -110,10 +116,10 @@ protected GeneratedType(TypeFactory typeFactory, String packageName, String name this.extraImportedTypes = extraImportedTypes; this.annotations = new ArrayList<>(); - this.methods = methods; + this.methods = new ArrayList<>(methods); this.fields = fields; - this.suppressGeneratorTimestamp = options.isSuppressGeneratorTimestamp(); + this.suppressGeneratorTimestamp = suppressGeneratorTimestamp; this.suppressGeneratorVersionComment = options.isSuppressGeneratorVersionComment(); this.versionInformation = versionInformation; this.accessibility = accessibility; @@ -161,7 +167,7 @@ public void addAnnotation(Annotation annotation) { annotations.add( annotation ); } - public List getMethods() { + public List getMethods() { return methods; } @@ -204,7 +210,7 @@ public SortedSet getImportTypes() { addIfImportRequired( importedTypes, mapperDefinitionType ); - for ( MappingMethod mappingMethod : methods ) { + for ( GeneratedTypeMethod mappingMethod : methods ) { for ( Type type : mappingMethod.getImportTypes() ) { addIfImportRequired( importedTypes, type ); } @@ -219,7 +225,9 @@ public SortedSet getImportTypes() { } for ( Annotation annotation : annotations ) { - addIfImportRequired( importedTypes, annotation.getType() ); + for ( Type type : annotation.getImportTypes() ) { + addIfImportRequired( importedTypes, type ); + } } for ( Type extraImport : extraImportedTypes ) { @@ -251,13 +259,19 @@ public void removeConstructor() { constructor = null; } + public Javadoc getJavadoc() { + return null; + } + protected void addIfImportRequired(Collection collection, Type typeToAdd) { if ( typeToAdd == null ) { return; } - if ( needsImportDeclaration( typeToAdd ) ) { - collection.add( typeToAdd ); + for ( Type type : typeToAdd.getImportTypes() ) { + if ( needsImportDeclaration( type ) ) { + collection.add( type ); + } } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedTypeMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedTypeMethod.java new file mode 100644 index 0000000000..8d6a999665 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedTypeMethod.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import org.mapstruct.ap.internal.model.common.ModelElement; + +/** + * Base class for methods available in a generated type. + * + * @author Filip Hrisafov + */ +public abstract class GeneratedTypeMethod extends ModelElement { + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/HelperMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/HelperMethod.java index 35b35aee73..6ff85c585f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/HelperMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/HelperMethod.java @@ -176,7 +176,7 @@ public boolean equals(Object obj) { * * @param parameter source * @param returnType target - * @return {@code true}, iff the the type variables match + * @return {@code true}, iff the type variables match */ public boolean doTypeVarsMatch(Type parameter, Type returnType) { return true; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableCreation.java b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableCreation.java index 90b50baf25..cf7bc6e2eb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableCreation.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableCreation.java @@ -9,6 +9,7 @@ import java.util.Set; import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.NewInstanceCreation; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; @@ -27,6 +28,7 @@ public class IterableCreation extends ModelElement { private final Type resultType; private final Parameter sourceParameter; private final MethodReference factoryMethod; + private final NewInstanceCreation newInstance; private final boolean canUseSize; private final boolean loadFactorAdjustment; @@ -34,6 +36,7 @@ private IterableCreation(Type resultType, Parameter sourceParameter, MethodRefer this.resultType = resultType; this.sourceParameter = sourceParameter; this.factoryMethod = factoryMethod; + this.newInstance = factoryMethod == null ? NewInstanceCreation.forType( resultType ) : null; this.canUseSize = ( sourceParameter.getType().isCollectionOrMapType() || sourceParameter.getType().isArrayType() ) && resultType.getImplementation() != null && resultType.getImplementation().hasInitialCapacityConstructor(); @@ -57,6 +60,10 @@ public MethodReference getFactoryMethod() { return this.factoryMethod; } + public NewInstanceCreation getNewInstance() { + return newInstance; + } + public boolean isCanUseSize() { return canUseSize; } @@ -68,8 +75,8 @@ public boolean isLoadFactorAdjustment() { @Override public Set getImportTypes() { Set types = new HashSet<>(); - if ( factoryMethod == null && resultType.getImplementationType() != null ) { - types.addAll( resultType.getImplementationType().getImportTypes() ); + if ( newInstance != null ) { + types.addAll( newInstance.getImportTypes() ); } if ( isEnumSet() ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java index 38ec44f1e1..220466f802 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.internal.model; -import static org.mapstruct.ap.internal.util.Collections.first; - import java.util.Collection; import java.util.List; import java.util.Set; @@ -14,10 +12,13 @@ import org.mapstruct.ap.internal.model.assignment.LocalVarWrapper; import org.mapstruct.ap.internal.model.assignment.SetterWrapper; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; +import static org.mapstruct.ap.internal.util.Collections.first; + /** * A {@link MappingMethod} implemented by a {@link Mapper} class which maps one iterable type to another. The collection * elements are mapped either by a {@link TypeConversion} or another mapping method. @@ -54,9 +55,11 @@ protected Assignment getWrapper(Assignment assignment, Method method) { protected IterableMappingMethod instantiateMappingMethod(Method method, Collection existingVariables, Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingMethods, - List afterMappingMethods, SelectionParameters selectionParameters) { + List afterMappingMethods, SelectionParameters selectionParameters, + PresenceCheck sourceParameterPresenceCheck) { return new IterableMappingMethod( method, + getMethodAnnotations(), existingVariables, assignment, factoryMethod, @@ -64,18 +67,23 @@ protected IterableMappingMethod instantiateMappingMethod(Method method, Collecti loopVariableName, beforeMappingMethods, afterMappingMethods, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } } - private IterableMappingMethod(Method method, Collection existingVariables, Assignment parameterAssignment, + //CHECKSTYLE:OFF + private IterableMappingMethod(Method method, List annotations, + Collection existingVariables, Assignment parameterAssignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters) { + SelectionParameters selectionParameters, PresenceCheck sourceParameterPresenceCheck) { + //CHECKSTYLE:ON super( method, + annotations, existingVariables, parameterAssignment, factoryMethod, @@ -83,7 +91,8 @@ private IterableMappingMethod(Method method, Collection existingVariable loopVariableName, beforeMappingReferences, afterMappingReferences, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/Javadoc.java b/processor/src/main/java/org/mapstruct/ap/internal/model/Javadoc.java new file mode 100644 index 0000000000..b8718b68e8 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/Javadoc.java @@ -0,0 +1,92 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.Type; + +/** + * Represents the javadoc information that should be generated for a {@link Mapper}. + * + * @author Jose Carlos Campanero Ortiz + */ +public class Javadoc extends ModelElement { + + public static class Builder { + + private String value; + private List authors; + private String deprecated; + private String since; + + public Builder value(String value) { + this.value = value; + return this; + } + + public Builder authors(List authors) { + this.authors = authors; + return this; + } + + public Builder deprecated(String deprecated) { + this.deprecated = deprecated; + return this; + } + + public Builder since(String since) { + this.since = since; + return this; + } + + public Javadoc build() { + return new Javadoc( + value, + authors, + deprecated, + since + ); + } + } + + private final String value; + private final List authors; + private final String deprecated; + private final String since; + + private Javadoc(String value, List authors, String deprecated, String since) { + this.value = value; + this.authors = authors != null ? Collections.unmodifiableList( authors ) : Collections.emptyList(); + this.deprecated = deprecated; + this.since = since; + } + + public String getValue() { + return value; + } + + public List getAuthors() { + return authors; + } + + public String getDeprecated() { + return deprecated; + } + + public String getSince() { + return since; + } + + @Override + public Set getImportTypes() { + return Collections.emptySet(); + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleMethodResolver.java index 87527e87f8..ada9dac470 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/LifecycleMethodResolver.java @@ -9,9 +9,11 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.ParameterBinding; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods; @@ -19,7 +21,7 @@ import org.mapstruct.ap.internal.model.source.SourceMethod; import org.mapstruct.ap.internal.model.source.selector.MethodSelectors; import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; -import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; +import org.mapstruct.ap.internal.model.source.selector.SelectionContext; /** * Factory for creating lists of appropriate {@link LifecycleCallbackMethodReference}s @@ -64,13 +66,16 @@ public static List afterMappingMethods(Method Type alternativeTarget, SelectionParameters selectionParameters, MappingBuilderContext ctx, - Set existingVariableNames) { + Set existingVariableNames, + Supplier> parameterBindingsProvider) { return collectLifecycleCallbackMethods( method, alternativeTarget, selectionParameters, filterAfterMappingMethods( getAllAvailableMethods( method, ctx.getSourceModel() ) ), ctx, - existingVariableNames ); + existingVariableNames, + parameterBindingsProvider + ); } /** @@ -132,17 +137,35 @@ private static List getAllAvailableMethods(Method method, List collectLifecycleCallbackMethods( Method method, Type targetType, SelectionParameters selectionParameters, List callbackMethods, MappingBuilderContext ctx, Set existingVariableNames) { + return collectLifecycleCallbackMethods( + method, + targetType, + selectionParameters, + callbackMethods, + ctx, + existingVariableNames, + Collections::emptyList + ); + } + + private static List collectLifecycleCallbackMethods( + Method method, Type targetType, SelectionParameters selectionParameters, List callbackMethods, + MappingBuilderContext ctx, Set existingVariableNames, + Supplier> parameterBindingsProvider) { MethodSelectors selectors = - new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getTypeFactory(), ctx.getMessager() ); + new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager(), ctx.getOptions() ); List> matchingMethods = selectors.getMatchingMethods( - method, callbackMethods, - Collections.emptyList(), - targetType, - method.getReturnType(), - SelectionCriteria.forLifecycleMethods( selectionParameters ) ); + SelectionContext.forLifecycleMethods( + method, + targetType, + selectionParameters, + ctx.getTypeFactory(), + parameterBindingsProvider + ) + ); return toLifecycleCallbackMethodRefs( method, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java index a8b68b435f..5a49e3a726 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java @@ -15,6 +15,7 @@ import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.FormattingParameters; import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; @@ -35,6 +36,8 @@ public class MapMappingMethod extends NormalTypeMappingMethod { private final Assignment keyAssignment; private final Assignment valueAssignment; + private final Parameter sourceParameter; + private final PresenceCheck sourceParameterPresenceCheck; private IterableCreation iterableCreation; public static class Builder extends AbstractMappingMethodBuilder { @@ -178,9 +181,17 @@ public MapMappingMethod build() { ctx.getMessager().note( 2, Message.MAPMAPPING_SELECT_VALUE_NOTE, valueAssignment ); } - // mapNullToDefault + // mapNullToDefault — a JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // Forcing is unconditional here (unlike BeanMappingMethod): when the source is @NonNull the template skips + // the whole guard block via `sourceParameterPresenceCheck??`, so the forced value is simply unused there. boolean mapNullToDefault = method.getOptions().getMapMapping().getNullValueMappingStrategy().isReturnDefault(); + if ( !mapNullToDefault && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } MethodReference factoryMethod = null; if ( !method.isUpdateMethod() ) { @@ -197,15 +208,21 @@ public MapMappingMethod build() { List afterMappingMethods = LifecycleMethodResolver.afterMappingMethods( method, null, ctx, existingVariables ); + Parameter sourceParam = first( method.getSourceParameters() ); + PresenceCheck sourceParameterPresenceCheck = + PresenceCheckMethodResolver.getPresenceCheckForSourceParameter( method, null, sourceParam, ctx ); + return new MapMappingMethod( method, + getMethodAnnotations(), existingVariables, keyAssignment, valueAssignment, factoryMethod, mapNullToDefault, beforeMappingMethods, - afterMappingMethods + afterMappingMethods, + sourceParameterPresenceCheck ); } @@ -224,25 +241,41 @@ protected boolean shouldUsePropertyNamesInHistory() { } - private MapMappingMethod(Method method, Collection existingVariableNames, Assignment keyAssignment, + //CHECKSTYLE:OFF + private MapMappingMethod(Method method, List annotations, + Collection existingVariableNames, Assignment keyAssignment, Assignment valueAssignment, MethodReference factoryMethod, boolean mapNullToDefault, List beforeMappingReferences, - List afterMappingReferences) { - super( method, existingVariableNames, factoryMethod, mapNullToDefault, beforeMappingReferences, + List afterMappingReferences, + PresenceCheck sourceParameterPresenceCheck) { + //CHECKSTYLE:ON + super( method, annotations, existingVariableNames, factoryMethod, mapNullToDefault, beforeMappingReferences, afterMappingReferences ); this.keyAssignment = keyAssignment; this.valueAssignment = valueAssignment; - } - - public Parameter getSourceParameter() { + Parameter sourceParameter = null; for ( Parameter parameter : getParameters() ) { if ( !parameter.isMappingTarget() && !parameter.isMappingContext() ) { - return parameter; + sourceParameter = parameter; + break; } } - throw new IllegalStateException( "Method " + this + " has no source parameter." ); + if ( sourceParameter == null ) { + throw new IllegalStateException( "Method " + this + " has no source parameter." ); + } + + this.sourceParameter = sourceParameter; + this.sourceParameterPresenceCheck = sourceParameterPresenceCheck; + } + + public Parameter getSourceParameter() { + return sourceParameter; + } + + public PresenceCheck getSourceParameterPresenceCheck() { + return sourceParameterPresenceCheck; } public List getSourceElementTypes() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java index a066b123c9..c261358455 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java @@ -8,7 +8,6 @@ import java.util.List; import java.util.Set; import java.util.SortedSet; - import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; @@ -18,6 +17,8 @@ import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.version.VersionInformation; +import static org.mapstruct.ap.internal.gem.ClassAccessibilityGem.DEFAULT; + /** * Represents a type implementing a mapper interface (annotated with {@code @Mapper}). This is the root object of the * mapper model. @@ -42,6 +43,9 @@ public static class Builder extends GeneratedTypeBuilder { private boolean customName; private String implPackage; private boolean customPackage; + private boolean suppressGeneratorTimestamp; + private Set customAnnotations; + private Javadoc javadoc; public Builder() { super( Builder.class ); @@ -62,6 +66,11 @@ public Builder constructorFragments(Set fragment return this; } + public Builder additionalAnnotations(Set customAnnotations) { + this.customAnnotations = customAnnotations; + return this; + } + public Builder decorator(Decorator decorator) { this.decorator = decorator; return this; @@ -79,6 +88,16 @@ public Builder implPackage(String implPackage) { return this; } + public Builder suppressGeneratorTimestamp(boolean suppressGeneratorTimestamp) { + this.suppressGeneratorTimestamp = suppressGeneratorTimestamp; + return this; + } + + public Builder javadoc(Javadoc javadoc) { + this.javadoc = javadoc; + return this; + } + public Mapper build() { String implementationName = implName.replace( CLASS_NAME_PLACEHOLDER, getFlatName( element ) ) + ( decorator == null ? "" : "_" ); @@ -92,6 +111,10 @@ public Mapper build() { Type definitionType = typeFactory.getType( element ); + Accessibility accessibility = + classAccessibility == DEFAULT ? Accessibility.fromModifiers( element.getModifiers() ) : + Accessibility.valueOf( classAccessibility.name() ); + return new Mapper( typeFactory, packageName, @@ -99,14 +122,17 @@ public Mapper build() { definitionType, customPackage, customName, + customAnnotations, methods, options, versionInformation, - Accessibility.fromModifiers( element.getModifiers() ), + suppressGeneratorTimestamp, + accessibility, fields, constructor, decorator, - extraImportedTypes + extraImportedTypes, + javadoc ); } @@ -115,14 +141,16 @@ public Mapper build() { private final boolean customPackage; private final boolean customImplName; private Decorator decorator; + private final Javadoc javadoc; @SuppressWarnings( "checkstyle:parameternumber" ) private Mapper(TypeFactory typeFactory, String packageName, String name, Type mapperDefinitionType, - boolean customPackage, boolean customImplName, + boolean customPackage, boolean customImplName, Set customAnnotations, List methods, Options options, VersionInformation versionInformation, + boolean suppressGeneratorTimestamp, Accessibility accessibility, List fields, Constructor constructor, - Decorator decorator, SortedSet extraImportedTypes ) { + Decorator decorator, SortedSet extraImportedTypes, Javadoc javadoc ) { super( typeFactory, @@ -133,14 +161,18 @@ private Mapper(TypeFactory typeFactory, String packageName, String name, fields, options, versionInformation, + suppressGeneratorTimestamp, accessibility, extraImportedTypes, constructor ); this.customPackage = customPackage; this.customImplName = customImplName; + customAnnotations.forEach( this::addAnnotation ); this.decorator = decorator; + + this.javadoc = javadoc; } public Decorator getDecorator() { @@ -155,6 +187,11 @@ public boolean hasCustomImplementation() { return customImplName || customPackage; } + @Override + public Javadoc getJavadoc() { + return javadoc; + } + @Override protected String getTemplateName() { return getTemplateNameForClass( GeneratedType.class ); @@ -168,12 +205,12 @@ protected String getTemplateName() { * @return the flat name for the type element */ public static String getFlatName(TypeElement element) { - if (!(element.getEnclosingElement() instanceof TypeElement)) { + if ( !(element.getEnclosingElement() instanceof TypeElement) ) { return element.getSimpleName().toString(); } StringBuilder nameBuilder = new StringBuilder( element.getSimpleName().toString() ); - for (Element enclosing = element.getEnclosingElement(); enclosing instanceof TypeElement; enclosing = - enclosing.getEnclosingElement()) { + for ( Element enclosing = element.getEnclosingElement(); enclosing instanceof TypeElement; enclosing = + enclosing.getEnclosingElement() ) { nameBuilder.insert( 0, '$' ); nameBuilder.insert( 0, enclosing.getSimpleName().toString() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MapperReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MapperReference.java index d3bfc7f17e..0179bd6324 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MapperReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MapperReference.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.internal.model; import java.util.List; + import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.SourceMethod; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java index fc82017a17..39e8558e5e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java @@ -12,8 +12,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Supplier; - import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import org.mapstruct.ap.internal.model.common.Assignment; @@ -28,8 +28,10 @@ import org.mapstruct.ap.internal.util.AccessorNamingUtils; import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.Services; import org.mapstruct.ap.internal.util.TypeUtils; +import org.mapstruct.ap.internal.version.VersionInformation; import org.mapstruct.ap.spi.EnumMappingStrategy; import org.mapstruct.ap.spi.EnumTransformationStrategy; import org.mapstruct.ap.spi.MappingExclusionProvider; @@ -109,7 +111,9 @@ Assignment getTargetAssignment(Method mappingMethod, ForgedMethodHistory descrip private final ElementUtils elementUtils; private final TypeUtils typeUtils; private final FormattingMessager messager; + private final VersionInformation versionInformation; private final AccessorNamingUtils accessorNaming; + private final NullabilityResolver nullabilityResolver; private final EnumMappingStrategy enumMappingStrategy; private final Map enumTransformationStrategies; private final Options options; @@ -126,7 +130,9 @@ public MappingBuilderContext(TypeFactory typeFactory, ElementUtils elementUtils, TypeUtils typeUtils, FormattingMessager messager, + VersionInformation versionInformation, AccessorNamingUtils accessorNaming, + NullabilityResolver nullabilityResolver, EnumMappingStrategy enumMappingStrategy, Map enumTransformationStrategies, Options options, @@ -138,7 +144,9 @@ public MappingBuilderContext(TypeFactory typeFactory, this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.messager = messager; + this.versionInformation = versionInformation; this.accessorNaming = accessorNaming; + this.nullabilityResolver = nullabilityResolver; this.enumMappingStrategy = enumMappingStrategy; this.enumTransformationStrategies = enumTransformationStrategies; this.options = options; @@ -190,10 +198,52 @@ public FormattingMessager getMessager() { return messager; } + public VersionInformation getVersionInformation() { + return versionInformation; + } + public AccessorNamingUtils getAccessorNaming() { return accessorNaming; } + public NullabilityResolver getNullabilityResolver() { + return nullabilityResolver; + } + + /** + * Resolves the JSpecify nullability of an element declared directly on the mapper (e.g. a mapping method's + * return type or one of its source parameters), using the mapper type's {@code @NullMarked} scope as the + * enclosing scope for unannotated elements. + * + * @param element the element declared on the mapper to inspect + * + * @return the resolved nullability ({@link NullabilityResolver.Nullability#UNKNOWN} when JSpecify is disabled) + */ + public NullabilityResolver.Nullability getNullabilityInMapperScope(Element element) { + return nullabilityResolver.getNullability( + element, + () -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() ); + } + + /** + * Whether the return type of the given mapping method is JSpecify {@code @NonNull} (directly or via a + * {@code @NullMarked} scope). When it is, a mapping method must not generate {@code return null}, so callers + * force {@link org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem#RETURN_DEFAULT} semantics. + *

        + * Update methods and {@code void}-returning methods never generate a {@code return null} and are excluded. + * + * @param method the mapping method to inspect + * + * @return {@code true} if the return type is {@code @NonNull}, {@code false} otherwise + */ + public boolean isJSpecifyNonNullReturn(Method method) { + if ( method.isUpdateMethod() || method.getReturnType().isVoid() ) { + return false; + } + + return getNullabilityInMapperScope( method.getExecutable() ) == NullabilityResolver.Nullability.NON_NULL; + } + public EnumMappingStrategy getEnumMappingStrategy() { return enumMappingStrategy; } @@ -220,7 +270,7 @@ public List getReservedNames() { nameSet.add( method.getName() ); } // add existing names - for ( SourceMethod method : sourceModel) { + for ( SourceMethod method : sourceModel ) { if ( method.isAbstract() ) { nameSet.add( method.getName() ); } @@ -259,11 +309,14 @@ public boolean canGenerateAutoSubMappingBetween(Type sourceType, Type targetType } /** - * @param type that MapStruct wants to use to genrate an autoamtic sub-mapping for/from + * @param type that MapStruct wants to use to generate an automatic sub-mapping for/from * * @return {@code true} if the type is not excluded from the {@link MappingExclusionProvider} */ private boolean canGenerateAutoSubMappingFor(Type type) { + if ( "java.util.Optional".equals( type.getFullyQualifiedName() ) ) { + return !SUB_MAPPING_EXCLUSION_PROVIDER.isExcluded( type.getOptionalBaseType().getTypeElement() ); + } return type.getTypeElement() != null && !SUB_MAPPING_EXCLUSION_PROVIDER.isExcluded( type.getTypeElement() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java index c7d329916f..0d04f5b6d5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java @@ -5,28 +5,28 @@ */ package org.mapstruct.ap.internal.model; -import static org.mapstruct.ap.internal.util.Strings.getSafeVariableName; -import static org.mapstruct.ap.internal.util.Strings.join; - import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import org.mapstruct.ap.internal.model.common.Accessibility; -import org.mapstruct.ap.internal.model.common.ModelElement; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; +import static org.mapstruct.ap.internal.util.Strings.getSafeVariableName; +import static org.mapstruct.ap.internal.util.Strings.join; + /** * A method implemented or referenced by a {@link Mapper} class. * * @author Gunnar Morling */ -public abstract class MappingMethod extends ModelElement { +public abstract class MappingMethod extends GeneratedTypeMethod { private final String name; private final List parameters; @@ -70,7 +70,7 @@ protected MappingMethod(Method method, List parameters, Collection parameters) { @@ -97,7 +97,11 @@ else if ( getResultType().isArrayType() ) { return name; } else { - String name = getSafeVariableName( getResultType().getName(), existingVarNames ); + Type resultType = getResultType(); + if ( resultType.isOptionalType() ) { + resultType = resultType.getOptionalBaseType(); + } + String name = getSafeVariableName( resultType.getName(), existingVarNames ); existingVarNames.add( name ); return name; } @@ -153,6 +157,16 @@ public Set getImportTypes() { types.addAll( type.getImportTypes() ); } + for ( LifecycleCallbackMethodReference reference : beforeMappingReferencesWithMappingTarget ) { + types.addAll( reference.getImportTypes() ); + } + for ( LifecycleCallbackMethodReference reference : beforeMappingReferencesWithoutMappingTarget ) { + types.addAll( reference.getImportTypes() ); + } + for ( LifecycleCallbackMethodReference reference : afterMappingReferences ) { + types.addAll( reference.getImportTypes() ); + } + return types; } @@ -175,10 +189,10 @@ public String toString() { return returnType + " " + getName() + "(" + join( parameters, ", " ) + ")"; } - private List filterMappingTarget(List methods, - boolean mustHaveMappingTargetParameter) { + protected static List filterMappingTarget( + List methods, boolean mustHaveMappingTargetParameter) { if ( methods == null ) { - return null; + return Collections.emptyList(); } List result = diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java index 78a3830f45..e2e518c55b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.internal.model; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -58,8 +59,10 @@ public class MethodReference extends ModelElement implements Assignment { private final Type definingType; private final List parameterBindings; private final Parameter providingParameter; + private final List methodsToChain; private final boolean isStatic; private final boolean isConstructor; + private final boolean isMethodChaining; /** * Creates a new reference to the given method. @@ -95,6 +98,8 @@ protected MethodReference(Method method, MapperReference declaringMapper, Parame this.isStatic = method.isStatic(); this.name = method.getName(); this.isConstructor = false; + this.methodsToChain = Collections.emptyList(); + this.isMethodChaining = false; } private MethodReference(BuiltInMethod method, ConversionContext contextParam) { @@ -111,6 +116,8 @@ private MethodReference(BuiltInMethod method, ConversionContext contextParam) { this.isStatic = method.isStatic(); this.name = method.getName(); this.isConstructor = false; + this.methodsToChain = Collections.emptyList(); + this.isMethodChaining = false; } private MethodReference(String name, Type definingType, boolean isStatic) { @@ -127,6 +134,8 @@ private MethodReference(String name, Type definingType, boolean isStatic) { this.providingParameter = null; this.isStatic = isStatic; this.isConstructor = false; + this.methodsToChain = Collections.emptyList(); + this.isMethodChaining = false; } private MethodReference(Type definingType, List parameterBindings) { @@ -142,6 +151,8 @@ private MethodReference(Type definingType, List parameterBindi this.providingParameter = null; this.isStatic = false; this.isConstructor = true; + this.methodsToChain = Collections.emptyList(); + this.isMethodChaining = false; if ( parameterBindings.isEmpty() ) { this.importTypes = Collections.emptySet(); @@ -159,6 +170,24 @@ private MethodReference(Type definingType, List parameterBindi } } + private MethodReference(MethodReference... references) { + this.name = null; + this.definingType = null; + this.sourceParameters = Collections.emptyList(); + this.returnType = null; + this.declaringMapper = null; + this.importTypes = Collections.emptySet(); + this.thrownTypes = Collections.emptyList(); + this.isUpdateMethod = false; + this.contextParam = null; + this.parameterBindings = null; + this.providingParameter = null; + this.isStatic = false; + this.isConstructor = false; + this.methodsToChain = Arrays.asList( references ); + this.isMethodChaining = true; + } + public MapperReference getDeclaringMapper() { return declaringMapper; } @@ -267,6 +296,12 @@ public Set getImportTypes() { if ( isStatic() ) { imported.add( definingType ); } + if ( isMethodChaining() ) { + for ( MethodReference methodToChain : methodsToChain ) { + imported.addAll( methodToChain.getImportTypes() ); + } + } + return imported; } @@ -276,6 +311,12 @@ public List getThrownTypes() { if ( assignment != null ) { exceptions.addAll( assignment.getThrownTypes() ); } + if ( isMethodChaining() ) { + for ( MethodReference methodToChain : methodsToChain ) { + exceptions.addAll( methodToChain.getThrownTypes() ); + } + + } return exceptions; } @@ -311,6 +352,14 @@ public boolean isConstructor() { return isConstructor; } + public boolean isMethodChaining() { + return isMethodChaining; + } + + public List getMethodsToChain() { + return methodsToChain; + } + public List getParameterBindings() { return parameterBindings; } @@ -385,6 +434,10 @@ public static MethodReference forConstructorInvocation(Type type, List.get().getXxx(); + doesNotNeedFollowUpProperty = presenceCheck == null; + if ( doesNotNeedFollowUpProperty ) { + optionalValueSource += "." + propertyEntry.getReadAccessor().getReadValueSource(); + } + } + Type optionalBaseType = previousPropertyType.getOptionalBaseType(); + safePropertyEntries.add( new SafePropertyEntry( + optionalBaseType, + optionalValueSafeName, + optionalValueSource, + new OptionalPresenceCheck( previousPropertyName, ctx.getVersionInformation(), true ) + ) ); + if ( doesNotNeedFollowUpProperty ) { + break; + } + previousPropertyName = optionalValueSafeName; + + } + else { + presenceCheck = getPresenceCheck( propertyEntry, previousPropertyName ); + if ( i > 0 && !previousEntryIsNonNull ) { + // If this is not the first property entry, + // then we need to combine the presence check with a null check of the previous property. + // JSpecify: the null check is skipped when the previous accessor returns @NonNull. + if ( presenceCheck != null ) { + presenceCheck = new AnyPresenceChecksPresenceCheck( Arrays.asList( + new NullPresenceCheck( previousPropertyName, true ), + presenceCheck + ) ); + } + else { + presenceCheck = new NullPresenceCheck( previousPropertyName, true ); + } + } + } + String safeName = Strings.getSafeVariableName( propertyEntry.getName(), existingVariableNames ); - safePropertyEntries.add( new SafePropertyEntry( propertyEntry, safeName, previousPropertyName ) ); + String source = previousPropertyName + "." + propertyEntry.getReadAccessor().getReadValueSource(); + safePropertyEntries.add( new SafePropertyEntry( + propertyEntry.getType(), + safeName, + source, + presenceCheck + ) ); existingVariableNames.add( safeName ); thrownTypes.addAll( ctx.getTypeFactory().getThrownTypes( propertyEntry.getReadAccessor() ) ); previousPropertyName = safeName; + previousPropertyType = propertyEntry.getType(); + previousEntryIsNonNull = currentEntryIsNonNull; } method.addThrownTypes( thrownTypes ); return new NestedPropertyMappingMethod( method, safePropertyEntries ); } + + private PresenceCheck getPresenceCheck(PropertyEntry propertyEntry, String previousPropertyName) { + PresenceCheckAccessor propertyPresenceChecker = propertyEntry.getPresenceChecker(); + if ( propertyPresenceChecker != null ) { + return new SuffixPresenceCheck( + previousPropertyName, + propertyPresenceChecker.getPresenceCheckSuffix(), + true + ); + } + return null; + } } private NestedPropertyMappingMethod( ForgedMethod method, List sourcePropertyEntries ) { @@ -102,8 +187,8 @@ public List getPropertyEntries() { @Override public Set getImportTypes() { Set types = super.getImportTypes(); - for ( SafePropertyEntry propertyEntry : safePropertyEntries) { - types.add( propertyEntry.getType() ); + for ( SafePropertyEntry propertyEntry : safePropertyEntries ) { + types.addAll( propertyEntry.getType().getImportTypes() ); if ( propertyEntry.getPresenceChecker() != null ) { types.addAll( propertyEntry.getPresenceChecker().getImportTypes() ); } @@ -157,43 +242,29 @@ public boolean equals( Object obj ) { public static class SafePropertyEntry { private final String safeName; - private final String readAccessorName; + private final String source; private final PresenceCheck presenceChecker; - private final String previousPropertyName; private final Type type; - public SafePropertyEntry(PropertyEntry entry, String safeName, String previousPropertyName) { + public SafePropertyEntry(Type type, String safeName, String source, PresenceCheck presenceCheck) { this.safeName = safeName; - this.readAccessorName = ValueProvider.of( entry.getReadAccessor() ).getValue(); - if ( entry.getPresenceChecker() != null ) { - this.presenceChecker = new SourceReferenceMethodPresenceCheck( - previousPropertyName, - entry.getPresenceChecker().getSimpleName() - ); - } - else { - this.presenceChecker = null; - } - this.previousPropertyName = previousPropertyName; - this.type = entry.getType(); + this.source = source; + this.presenceChecker = presenceCheck; + this.type = type; } public String getName() { return safeName; } - public String getAccessorName() { - return readAccessorName; + public String getSource() { + return source; } public PresenceCheck getPresenceChecker() { return presenceChecker; } - public String getPreviousPropertyName() { - return previousPropertyName; - } - public Type getType() { return type; } @@ -209,7 +280,7 @@ public boolean equals(Object o) { SafePropertyEntry that = (SafePropertyEntry) o; - if ( !Objects.equals( readAccessorName, that.readAccessorName ) ) { + if ( !Objects.equals( source, that.source ) ) { return false; } @@ -217,10 +288,6 @@ public boolean equals(Object o) { return false; } - if ( !Objects.equals( previousPropertyName, that.previousPropertyName ) ) { - return false; - } - if ( !Objects.equals( type, that.type ) ) { return false; } @@ -230,7 +297,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = readAccessorName != null ? readAccessorName.hashCode() : 0; + int result = source != null ? source.hashCode() : 0; result = 31 * result + ( presenceChecker != null ? presenceChecker.hashCode() : 0 ); result = 31 * result + ( type != null ? type.hashCode() : 0 ); return result; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/NestedTargetPropertyMappingHolder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/NestedTargetPropertyMappingHolder.java index 254ba62871..83464967e6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/NestedTargetPropertyMappingHolder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/NestedTargetPropertyMappingHolder.java @@ -27,6 +27,7 @@ import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.accessor.Accessor; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; import static org.mapstruct.ap.internal.util.Collections.first; @@ -63,7 +64,7 @@ public List getProcessedSourceParameters() { } /** - * @return all the targets that were hanled + * @return all the targets that were handled */ public Set getHandledTargets() { return handledTargets; @@ -150,8 +151,7 @@ public NestedTargetPropertyMappingHolder build() { entryByTP.getValue(), groupedByTP.singleTargetReferences.get( targetProperty ) ); - boolean multipleSourceParametersForTP = - groupedBySourceParam.groupedBySourceParameter.keySet().size() > 1; + boolean multipleSourceParametersForTP = groupedBySourceParam.groupedBySourceParameter.size() > 1; // All not processed mappings that should have been applied to all are part of the unprocessed // defined targets @@ -225,7 +225,7 @@ public NestedTargetPropertyMappingHolder build() { handledTargets.add( entryByTP.getKey() ); } - // For the nonNested mappings (assymetric) Mappings we also forge mappings + // For the nonNested mappings (asymmetric) Mappings we also forge mappings // However, here we do not forge name based mappings and we only // do update on the defined Mappings. if ( !groupedSourceReferences.nonNested.isEmpty() ) { @@ -361,7 +361,13 @@ private GroupedTargetReferences groupByTargetReferences( ) { Map> singleTargetReferences = new LinkedHashMap<>(); for ( MappingReference mapping : mappingReferences.getMappingReferences() ) { TargetReference targetReference = mapping.getTargetReference(); - String property = first( targetReference.getPropertyEntries() ); + List propertyEntries = targetReference.getPropertyEntries(); + if ( propertyEntries.isEmpty() ) { + // This can happen if the target property is target = ".", + // this usually happens when doing a reverse mapping + continue; + } + String property = first( propertyEntries ); MappingReference newMapping = mapping.popTargetReference(); if ( newMapping != null ) { // group properties on current name. @@ -606,7 +612,7 @@ else if ( mappingsKeyedByProperty.isEmpty() && nonNested.isEmpty() ) { * @param hasNoMappings parameter indicating whether there were any extracted mappings for this target property * @param sourceParameter the source parameter for which the grouping is being done * - * @return a list with valid single target references + * @return a set with valid single target references */ private Set extractSingleTargetReferencesToUseAndPopulateSourceParameterMappings( Set singleTargetReferences, Set sourceParameterMappings, @@ -642,7 +648,10 @@ private PropertyMapping createPropertyMappingForNestedTarget(MappingReferences m boolean forceUpdateMethod) { Accessor targetWriteAccessor = targetPropertiesWriteAccessors.get( targetPropertyName ); - Accessor targetReadAccessor = targetType.getPropertyReadAccessors().get( targetPropertyName ); + ReadAccessor targetReadAccessor = targetType.getReadAccessor( + targetPropertyName, + method.getSourceParameters().size() == 1 + ); if ( targetWriteAccessor == null ) { Set readAccessors = targetType.getPropertyReadAccessors().keySet(); String mostSimilarProperty = Strings.getMostSimilarWord( targetPropertyName, readAccessors ); @@ -745,7 +754,7 @@ public String toString() { } /** - * This class is used to group Source references in respected to the nestings that they have. + * This class is used to group Source references in respected to the nesting that they have. * * This class contains all groupings by Property Entries if they are nested, or a list of all the other options * that could not have been popped. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java index fcd8d70d52..89ee1baab2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java @@ -21,17 +21,19 @@ */ public abstract class NormalTypeMappingMethod extends MappingMethod { private final MethodReference factoryMethod; - private final boolean overridden; private final boolean mapNullToDefault; - NormalTypeMappingMethod(Method method, Collection existingVariableNames, MethodReference factoryMethod, + private final List annotations; + + NormalTypeMappingMethod(Method method, List annotations, + Collection existingVariableNames, MethodReference factoryMethod, boolean mapNullToDefault, List beforeMappingReferences, List afterMappingReferences) { super( method, existingVariableNames, beforeMappingReferences, afterMappingReferences ); this.factoryMethod = factoryMethod; - this.overridden = method.overridesMethod(); this.mapNullToDefault = mapNullToDefault; + this.annotations = annotations; } @Override @@ -45,6 +47,9 @@ public Set getImportTypes() { else if ( factoryMethod != null ) { types.addAll( factoryMethod.getImportTypes() ); } + for ( Annotation annotation : annotations ) { + types.addAll( annotation.getImportTypes() ); + } return types; } @@ -52,14 +57,14 @@ public boolean isMapNullToDefault() { return mapNullToDefault; } - public boolean isOverridden() { - return overridden; - } - public MethodReference getFactoryMethod() { return this.factoryMethod; } + public List getAnnotations() { + return annotations; + } + @Override public int hashCode() { final int prime = 31; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java index 397a001a4f..2d7da03abb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java @@ -5,12 +5,9 @@ */ package org.mapstruct.ap.internal.model; -import static org.mapstruct.ap.internal.util.Collections.first; - import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; - import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -23,10 +20,13 @@ import org.mapstruct.ap.internal.model.source.SourceMethod; import org.mapstruct.ap.internal.model.source.selector.MethodSelectors; import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; -import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; +import org.mapstruct.ap.internal.model.source.selector.SelectionContext; import org.mapstruct.ap.internal.util.Message; +import static org.mapstruct.ap.internal.util.Collections.first; + /** + * Factory for creating the appropriate object factory method. * * @author Sjaak Derksen */ @@ -74,7 +74,7 @@ public static MethodReference getFactoryMethod( Method method, ctx ); - if (matchingFactoryMethods.isEmpty()) { + if ( matchingFactoryMethods.isEmpty() ) { return null; } @@ -126,19 +126,23 @@ public static List> getMatchingFactoryMethods( Meth MappingBuilderContext ctx) { MethodSelectors selectors = - new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getTypeFactory(), ctx.getMessager() ); + new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager(), null ); return selectors.getMatchingMethods( - method, getAllAvailableMethods( method, ctx.getSourceModel() ), - java.util.Collections.emptyList(), - alternativeTarget, - alternativeTarget, - SelectionCriteria.forFactoryMethods( selectionParameters ) + SelectionContext.forFactoryMethods( method, alternativeTarget, selectionParameters, ctx.getTypeFactory() ) ); } public static MethodReference getBuilderFactoryMethod(Method method, BuilderType builder ) { + Type returnType = method.getReturnType(); + if ( returnType.isOptionalType() ) { + returnType = returnType.getOptionalBaseType(); + } + return getBuilderFactoryMethod( returnType, builder ); + } + + public static MethodReference getBuilderFactoryMethod(Type typeToBuild, BuilderType builder ) { if ( builder == null ) { return null; } @@ -149,7 +153,7 @@ public static MethodReference getBuilderFactoryMethod(Method method, BuilderType return null; } - if ( !builder.getBuildingType().isAssignableTo( method.getReturnType() ) ) { + if ( !builder.getBuildingType().isAssignableTo( typeToBuild ) ) { //TODO print error message return null; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java index 9d4910bdce..3163614871 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java @@ -6,23 +6,28 @@ package org.mapstruct.ap.internal.model; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.mapstruct.ap.internal.gem.ConditionStrategyGem; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.PresenceCheck; -import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.presence.NullPresenceCheck; +import org.mapstruct.ap.internal.model.presence.OptionalPresenceCheck; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods; import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.model.source.SourceMethod; import org.mapstruct.ap.internal.model.source.selector.MethodSelectors; import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; +import org.mapstruct.ap.internal.model.source.selector.SelectionContext; import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.NullabilityResolver; /** + * Factory for creating {@link PresenceCheck}s. + * * @author Filip Hrisafov */ public final class PresenceCheckMethodResolver { @@ -36,53 +41,80 @@ public static PresenceCheck getPresenceCheck( SelectionParameters selectionParameters, MappingBuilderContext ctx ) { - SelectedMethod matchingMethod = findMatchingPresenceCheckMethod( + List> matchingMethods = findMatchingMethods( method, - selectionParameters, + SelectionContext.forPresenceCheckMethods( method, selectionParameters, ctx.getTypeFactory() ), ctx ); - if ( matchingMethod == null ) { + if ( matchingMethods.isEmpty() ) { + return null; + } + + if ( matchingMethods.size() > 1 ) { + ctx.getMessager().printMessage( + method.getExecutable(), + Message.GENERAL_AMBIGUOUS_PRESENCE_CHECK_METHOD, + selectionParameters.getSourceRHS().getSourceType().describe(), + matchingMethods.stream() + .map( SelectedMethod::getMethod ) + .map( Method::describe ) + .collect( Collectors.joining( ", " ) ) + ); + return null; } + SelectedMethod matchingMethod = matchingMethods.get( 0 ); + MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx ); return new MethodReferencePresenceCheck( methodReference ); } - private static SelectedMethod findMatchingPresenceCheckMethod( + public static PresenceCheck getPresenceCheckForSourceParameter( Method method, SelectionParameters selectionParameters, + Parameter sourceParameter, MappingBuilderContext ctx ) { - MethodSelectors selectors = new MethodSelectors( - ctx.getTypeUtils(), - ctx.getElementUtils(), - ctx.getTypeFactory(), - ctx.getMessager() - ); - - Type booleanType = ctx.getTypeFactory().getType( Boolean.class ); - List> matchingMethods = selectors.getMatchingMethods( + List> matchingMethods = findMatchingMethods( method, - getAllAvailableMethods( method, ctx.getSourceModel() ), - Collections.emptyList(), - booleanType, - booleanType, - SelectionCriteria.forPresenceCheckMethods( selectionParameters ) + SelectionContext.forSourceParameterPresenceCheckMethods( + method, + selectionParameters, + sourceParameter, + ctx.getTypeFactory() + ), + ctx ); if ( matchingMethods.isEmpty() ) { + if ( sourceParameter.getType().isOptionalType() ) { + return new OptionalPresenceCheck( sourceParameter.getName(), ctx.getVersionInformation() ); + } + else if ( !sourceParameter.getType().isPrimitive() ) { + // If the source parameter is @NonNull (JSpecify), skip the null guard entirely. + // Resolved in the mapper's @NullMarked scope since the parameter is declared in the mapper interface. + if ( ctx.getNullabilityInMapperScope( sourceParameter.getElement() ) + == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.PROPERTYMAPPING_JSPECIFY_SKIP_METHOD_GUARD_NON_NULL_PARAM, + sourceParameter.getName() + ); + return null; + } + return new NullPresenceCheck( sourceParameter.getName() ); + } return null; } if ( matchingMethods.size() > 1 ) { ctx.getMessager().printMessage( method.getExecutable(), - Message.GENERAL_AMBIGUOUS_PRESENCE_CHECK_METHOD, - selectionParameters.getSourceRHS().getSourceType().describe(), + Message.GENERAL_AMBIGUOUS_SOURCE_PARAMETER_CHECK_METHOD, + sourceParameter.getType().describe(), matchingMethods.stream() .map( SelectedMethod::getMethod ) .map( Method::describe ) @@ -92,7 +124,30 @@ private static SelectedMethod findMatchingPresenceCheckMethod( return null; } - return matchingMethods.get( 0 ); + SelectedMethod matchingMethod = matchingMethods.get( 0 ); + + MethodReference methodReference = getPresenceCheckMethodReference( method, matchingMethod, ctx ); + + return new MethodReferencePresenceCheck( methodReference ); + + } + + private static List> findMatchingMethods( + Method method, + SelectionContext selectionContext, + MappingBuilderContext ctx + ) { + MethodSelectors selectors = new MethodSelectors( + ctx.getTypeUtils(), + ctx.getElementUtils(), + ctx.getMessager(), + null + ); + + return selectors.getMatchingMethods( + getAllAvailableMethods( method, ctx.getSourceModel(), selectionContext.getSelectionCriteria() ), + selectionContext + ); } private static MethodReference getPresenceCheckMethodReference( @@ -124,7 +179,8 @@ private static MethodReference getPresenceCheckMethodReference( } } - private static List getAllAvailableMethods(Method method, List sourceModelMethods) { + private static List getAllAvailableMethods(Method method, List sourceModelMethods, + SelectionCriteria selectionCriteria) { ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods(); if ( contextProvidedMethods.isEmpty() ) { return sourceModelMethods; @@ -137,9 +193,19 @@ private static List getAllAvailableMethods(Method method, List( methodsProvidedByParams.size() + sourceModelMethods.size() ); for ( SourceMethod methodProvidedByParams : methodsProvidedByParams ) { - // add only methods from context that do have the @Condition annotation - if ( methodProvidedByParams.isPresenceCheck() ) { - availableMethods.add( methodProvidedByParams ); + if ( selectionCriteria.isPresenceCheckRequired() ) { + // add only methods from context that do have the @Condition for properties annotation + if ( methodProvidedByParams.getConditionOptions() + .isStrategyApplicable( ConditionStrategyGem.PROPERTIES ) ) { + availableMethods.add( methodProvidedByParams ); + } + } + else if ( selectionCriteria.isSourceParameterCheckRequired() ) { + // add only methods from context that do have the @Condition for source parameters annotation + if ( methodProvidedByParams.getConditionOptions() + .isStrategyApplicable( ConditionStrategyGem.SOURCE_PARAMETERS ) ) { + availableMethods.add( methodProvidedByParams ); + } } } availableMethods.addAll( sourceModelMethods ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java index 82d2dd53fa..163c390b66 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java @@ -11,7 +11,11 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; @@ -37,8 +41,8 @@ import org.mapstruct.ap.internal.model.presence.AllPresenceChecksPresenceCheck; import org.mapstruct.ap.internal.model.presence.JavaExpressionPresenceCheck; import org.mapstruct.ap.internal.model.presence.NullPresenceCheck; -import org.mapstruct.ap.internal.model.presence.SourceReferenceContainsKeyPresenceCheck; -import org.mapstruct.ap.internal.model.presence.SourceReferenceMethodPresenceCheck; +import org.mapstruct.ap.internal.model.presence.OptionalPresenceCheck; +import org.mapstruct.ap.internal.model.presence.SuffixPresenceCheck; import org.mapstruct.ap.internal.model.source.DelegatingOptions; import org.mapstruct.ap.internal.model.source.MappingControl; import org.mapstruct.ap.internal.model.source.MappingOptions; @@ -47,10 +51,11 @@ import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.NativeTypes; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.Strings; -import org.mapstruct.ap.internal.util.ValueProvider; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; import static org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem.ALWAYS; import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.IGNORE; @@ -71,9 +76,10 @@ public class PropertyMapping extends ModelElement { private final String name; + private final String sourcePropertyName; private final String sourceBeanName; private final String targetWriteAccessorName; - private final ValueProvider targetReadAccessorProvider; + private final ReadAccessor targetReadAccessorProvider; private final Type targetType; private final Assignment assignment; private final Set dependsOn; @@ -87,7 +93,7 @@ private static class MappingBuilderBase> extends protected AccessorType targetWriteAccessorType; protected Type targetType; protected BuilderType targetBuilderType; - protected Accessor targetReadAccessor; + protected ReadAccessor targetReadAccessor; protected String targetPropertyName; protected String sourcePropertyName; @@ -103,7 +109,7 @@ public T sourceMethod(Method sourceMethod) { return super.method( sourceMethod ); } - public T target(String targetPropertyName, Accessor targetReadAccessor, Accessor targetWriteAccessor) { + public T target(String targetPropertyName, ReadAccessor targetReadAccessor, Accessor targetWriteAccessor) { this.targetPropertyName = targetPropertyName; this.targetReadAccessor = targetReadAccessor; this.targetWriteAccessor = targetWriteAccessor; @@ -269,6 +275,33 @@ public PropertyMapping build() { assignment = forge(); } + // JSpecify: raise a hard compile error when a source that is not guaranteed @NonNull + // (either explicit @Nullable or unknown nullability outside a @NullMarked scope) is + // mapped to a @NonNull constructor parameter without a default value. Neither a null + // check (which would leave the variable null, violating the @NonNull contract) nor + // passing the value through is safe here — defaultValue / defaultExpression are the + // supported remedies. + // Skip when assignment resolution already failed: the user sees the primary + // "can't find mapping" error and a duplicate would only obscure the root cause. + if ( assignment != null + && targetWriteAccessorType == AccessorType.PARAMETER + && !hasDefaultValueOrDefaultExpression() ) { + NullabilityResolver.Nullability sourceNullability = getSourceJSpecifyNullability(); + NullabilityResolver.Nullability targetNullability = ctx.getNullabilityResolver().getSetterNullability( + targetWriteAccessor.getElement(), this::targetDeclaringTypeIsNullMarked + ); + if ( sourceNullability != NullabilityResolver.Nullability.NON_NULL + && targetNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().printMessage( + method.getExecutable(), + positionHint, + Message.PROPERTYMAPPING_NULLABLE_SOURCE_TO_NON_NULL_CONSTRUCTOR_PARAM, + sourcePropertyName != null ? sourcePropertyName : targetPropertyName, + targetPropertyName + ); + } + } + Type sourceType = rightHandSide.getSourceType(); if ( assignment != null ) { ctx.getMessager().note( 2, Message.PROPERTYMAPPING_SELECT_NOTE, assignment ); @@ -287,10 +320,11 @@ else if ( targetType.isArrayType() && sourceType.isArrayType() && assignment.get } return new PropertyMapping( + sourcePropertyName, targetPropertyName, - rightHandSide.getSourceParameterName(), + sourceReference.getParameter().getOriginalName(), targetWriteAccessor.getSimpleName(), - ValueProvider.of( targetReadAccessor ), + targetReadAccessor, targetType, assignment, dependsOn, @@ -302,14 +336,15 @@ else if ( targetType.isArrayType() && sourceType.isArrayType() && assignment.get private Assignment forge( ) { Assignment assignment; Type sourceType = rightHandSide.getSourceType(); - if ( (sourceType.isCollectionType() || sourceType.isArrayType()) && targetType.isIterableType() ) { + if ( ( sourceType.isCollectionType() || sourceType.isArrayType()) && targetType.isIterableType() + || ( sourceType.isIterableType() && targetType.isCollectionType() ) ) { assignment = forgeIterableMapping( sourceType, targetType, rightHandSide ); } else if ( sourceType.isMapType() && targetType.isMapType() ) { assignment = forgeMapMapping( sourceType, targetType, rightHandSide ); } - else if ( sourceType.isMapType() && !targetType.isMapType()) { - assignment = forgeMapToBeanMapping( sourceType, targetType, rightHandSide ); + else if ( sourceType.isMapType() && !targetType.isMapType() ) { + assignment = forgeMapping( sourceType, targetType.withoutBounds(), rightHandSide ); } else if ( ( sourceType.isIterableType() && targetType.isStreamType() ) || ( sourceType.isStreamType() && targetType.isStreamType() ) @@ -420,15 +455,47 @@ private Assignment assignToPlainViaSetter(Type targetType, Assignment rhs) { Assignment factory = ObjectFactoryMethodResolver .getFactoryMethod( method, targetType, SelectionParameters.forSourceRHS( rightHandSide ), ctx ); + + if ( factory == null && targetBuilderType != null ) { + // If there is no dedicated factory method and the target has a builder we will try to use that + MethodReference builderFactoryMethod = ObjectFactoryMethodResolver.getBuilderFactoryMethod( + targetType, + targetBuilderType + ); + + if ( builderFactoryMethod != null ) { + + MethodReference finisherMethod = BuilderFinisherMethodResolver.getBuilderFinisherMethod( + method, + targetBuilderType, + ctx + ); + + if ( finisherMethod != null ) { + factory = MethodReference.forMethodChaining( builderFactoryMethod, finisherMethod ); + } + } + + } + boolean includeSourceNullCheck = !rhs.isSourceReferenceParameter(); + if ( includeSourceNullCheck ) { + // JSpecify: source @NonNull means no null check needed + NullabilityResolver.Nullability sourceNullability = getSourceJSpecifyNullability(); + if ( sourceNullability == NullabilityResolver.Nullability.NON_NULL ) { + includeSourceNullCheck = false; + } + } + reportErrorWhenSetToDefaultCannotConstructTarget( targetType, factory ); return new UpdateWrapper( rhs, method.getThrownTypes(), factory, isFieldAssignment(), targetType, - !rhs.isSourceReferenceParameter(), + includeSourceNullCheck, nvpms == SET_TO_NULL && !targetType.isPrimitive(), - nvpms == SET_TO_DEFAULT + nvpms == SET_TO_DEFAULT, + hasTwoOrMoreSettersWithName() ); } else { @@ -439,13 +506,52 @@ private Assignment assignToPlainViaSetter(Type targetType, Assignment rhs) { // however, a local var is not needed if there's no need to check for null. rhs.setSourceLocalVarName( null ); } + reportErrorWhenSetToDefaultCannotConstructTarget( targetType, null ); return new SetterWrapper( rhs, method.getThrownTypes(), isFieldAssignment(), includeSourceNullCheck, includeSourceNullCheck && nvpms == SET_TO_NULL && !targetType.isPrimitive(), - nvpms == SET_TO_DEFAULT ); + nvpms == SET_TO_DEFAULT, + hasTwoOrMoreSettersWithName(), + targetType + ); + } + } + + /** + * For {@code NullValuePropertyMappingStrategy.SET_TO_DEFAULT} the target property is reset to a new + * default instance when the source is {@code null}. When that default instance is constructed via a + * parameterless constructor ({@code new Target()}) and the target type does not provide one, MapStruct + * would generate uncompilable code such as {@code target.setLocalDate( new LocalDate() )}. Report a + * compilation error in that case instead. + * + * @param targetType the type of the target property + * @param factory the factory used to construct the default instance, or {@code null} if none is used + */ + private void reportErrorWhenSetToDefaultCannotConstructTarget(Type targetType, Assignment factory) { + if ( nvpms != SET_TO_DEFAULT || factory != null ) { + // the default instance is either not created or created through a factory / builder method + return; + } + + Type typeToConstruct = targetType.isOptionalType() ? targetType.getOptionalBaseType() : targetType; + if ( typeToConstruct.getImplementationType() != null + || typeToConstruct.isArrayType() + || typeToConstruct.isOptionalType() + || typeToConstruct.getSensibleDefault() != null ) { + // these are initialized without invoking a parameterless constructor on the target type + return; + } + + if ( !typeToConstruct.hasAccessibleParameterlessConstructor() ) { + ctx.getMessager().printMessage( + method.getExecutable(), + positionHint, + Message.PROPERTYMAPPING_NO_ACCESSIBLE_PARAMETERLESS_CONSTRUCTOR, + typeToConstruct.describe() + ); } } @@ -464,9 +570,14 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp return false; } - if ( nvcs == ALWAYS ) { - // NullValueCheckStrategy is ALWAYS -> do a null check - return true; + // JSpecify: source @NonNull means the value is guaranteed non-null, skip all checks + NullabilityResolver.Nullability sourceNullability = getSourceJSpecifyNullability(); + if ( sourceNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK_NON_NULL_SOURCE, + targetPropertyName + ); + return false; } if ( rhs.getSourcePresenceCheckerReference() != null ) { @@ -489,20 +600,103 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp return true; } - if ( defaultValue != null || defaultJavaExpression != null ) { + if ( hasDefaultValueOrDefaultExpression() ) { // If there is default value defined then a check is needed return true; } + // JSpecify annotations take precedence over NullValueCheckStrategy + NullabilityResolver resolver = ctx.getNullabilityResolver(); + NullabilityResolver.Nullability targetNullability = resolver.getSetterNullability( + targetWriteAccessor.getElement(), this::targetDeclaringTypeIsNullMarked + ); + Boolean jspecifyDecision = resolver.requiresNullCheck( sourceNullability, targetNullability ); + if ( jspecifyDecision != null ) { + ctx.getMessager().note( 2, + jspecifyDecision + ? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK + : Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK, + targetPropertyName, + sourceNullability, + targetNullability + ); + return jspecifyDecision; + } + + if ( nvcs == ALWAYS ) { + // NullValueCheckStrategy is ALWAYS -> do a null check + return true; + } + return false; } + private NullabilityResolver.Nullability getSourceJSpecifyNullability() { + if ( sourceReference == null ) { + return NullabilityResolver.Nullability.UNKNOWN; + } + List entries = sourceReference.getPropertyEntries(); + if ( !entries.isEmpty() ) { + // A source chain can only be treated as @NonNull when every accessor along the + // chain is @NonNull. If any intermediate accessor is @Nullable, the chain may + // yield null even when the deepest accessor is @NonNull. + Type enclosingType = sourceReference.getParameter().getType(); + NullabilityResolver.Nullability chain = NullabilityResolver.Nullability.NON_NULL; + for ( PropertyEntry entry : entries ) { + if ( entry.getReadAccessor() == null ) { + return NullabilityResolver.Nullability.UNKNOWN; + } + NullabilityResolver.Nullability current = ctx.getNullabilityResolver().getNullability( + entry.getReadAccessor().getElement(), enclosingType::isNullMarked + ); + if ( current == NullabilityResolver.Nullability.NULLABLE ) { + return NullabilityResolver.Nullability.NULLABLE; + } + if ( current == NullabilityResolver.Nullability.UNKNOWN ) { + chain = NullabilityResolver.Nullability.UNKNOWN; + } + enclosingType = entry.getType(); + } + return chain; + } + // Direct parameter mapping: no property entries, the source is the parameter itself. + // Use the mapper type for @NullMarked scope resolution since the parameter is declared there. + Parameter parameter = sourceReference.getParameter(); + if ( parameter != null && parameter.getElement() != null ) { + return ctx.getNullabilityInMapperScope( parameter.getElement() ); + } + return NullabilityResolver.Nullability.UNKNOWN; + } + + /** + * Resolves whether the type that declares the target write accessor (i.e. the bean that + * owns the setter or field) is in a JSpecify {@code @NullMarked} scope. This is the correct + * scope for deciding whether an unannotated setter parameter or field should be treated as + * {@code @NonNull} — walking from the property value type (e.g. {@code String}) does not + * reach the bean's declaration. + */ + private boolean targetDeclaringTypeIsNullMarked() { + Element targetElement = targetWriteAccessor.getElement(); + if ( targetElement == null ) { + return false; + } + Element declaring = targetElement.getEnclosingElement(); + if ( !( declaring instanceof TypeElement ) ) { + return false; + } + return ctx.getTypeFactory().getType( declaring.asType() ).isNullMarked(); + } + + private boolean hasDefaultValueOrDefaultExpression() { + return defaultValue != null || defaultJavaExpression != null; + } + private Assignment assignToPlainViaAdder( Assignment rightHandSide) { Assignment result = rightHandSide; String adderIteratorName = sourcePropertyName == null ? targetPropertyName : sourcePropertyName; - if ( result.getSourceType().isCollectionType() ) { + if ( result.getSourceType().isIterableType() ) { result = new AdderWrapper( result, method.getThrownTypes(), isFieldAssignment(), adderIteratorName ); } else if ( result.getSourceType().isStreamType() ) { @@ -517,12 +711,33 @@ else if ( result.getSourceType().isStreamType() ) { isFieldAssignment(), true, nvpms == SET_TO_NULL && !targetType.isPrimitive(), - nvpms == SET_TO_DEFAULT + nvpms == SET_TO_DEFAULT, + hasTwoOrMoreSettersWithName(), + targetType ); } return result; } + private boolean hasTwoOrMoreSettersWithName() { + Element enclosingClass = this.targetWriteAccessor.getElement().getEnclosingElement(); + if ( enclosingClass == null || !( ElementKind.CLASS.equals( enclosingClass.getKind() ) + || ElementKind.INTERFACE.equals( enclosingClass.getKind() ) ) ) { + return false; + } + String simpleWriteAccessorName = this.targetWriteAccessor.getSimpleName(); + boolean firstMatchFound = false; + for ( Accessor setter : ctx.getTypeFactory().getType( enclosingClass.asType() ).getSetters() ) { + if ( setter.getSimpleName().equals( simpleWriteAccessorName ) ) { + if ( firstMatchFound ) { + return true; + } + firstMatchFound = true; + } + } + return false; + } + private Assignment assignToCollection(Type targetType, AccessorType targetAccessorType, Assignment rhs) { return new CollectionAssignmentBuilder() @@ -534,8 +749,9 @@ private Assignment assignToCollection(Type targetType, AccessorType targetAccess .targetAccessorType( targetAccessorType ) .rightHandSide( rightHandSide ) .assignment( rhs ) - .nullValueCheckStrategy( nvcs ) + .nullValueCheckStrategy( hasDefaultValueOrDefaultExpression() ? ALWAYS : nvcs ) .nullValuePropertyMappingStrategy( nvpms ) + .sourceJSpecifyNullability( getSourceJSpecifyNullability() ) .build(); } @@ -559,15 +775,21 @@ private SourceRHS getSourceRHS( SourceReference sourceReference ) { // parameter reference if ( propertyEntry == null ) { - return new SourceRHS( sourceParam.getName(), - sourceParam.getType(), - existingVariableNames, - sourceReference.toString() + SourceRHS sourceRHS = new SourceRHS( + sourceParam.getName(), + sourceParam.getType(), + existingVariableNames, + sourceReference.toString() ); + sourceRHS.setSourcePresenceCheckerReference( getSourcePresenceCheckerRef( + sourceReference, + sourceRHS + ) ); + return sourceRHS; } // simple property else if ( !sourceReference.isNested() ) { - String sourceRef = sourceParam.getName() + "." + ValueProvider.of( propertyEntry.getReadAccessor() ); + String sourceRef = sourceParam.getName() + "." + propertyEntry.getReadAccessor().getReadValueSource(); SourceRHS sourceRHS = new SourceRHS( sourceParam.getName(), sourceRef, @@ -660,30 +882,34 @@ private PresenceCheck getSourcePresenceCheckerRef(SourceReference sourceReferenc // in the forged method? PropertyEntry propertyEntry = sourceReference.getShallowestProperty(); if ( propertyEntry.getPresenceChecker() != null ) { - if (propertyEntry.getPresenceChecker().getAccessorType() == AccessorType.MAP_CONTAINS ) { - return new SourceReferenceContainsKeyPresenceCheck( - sourceParam.getName(), - propertyEntry.getPresenceChecker().getSimpleName() - ); - } - List presenceChecks = new ArrayList<>(); - presenceChecks.add( new SourceReferenceMethodPresenceCheck( + presenceChecks.add( new SuffixPresenceCheck( sourceParam.getName(), - propertyEntry.getPresenceChecker().getSimpleName() + propertyEntry.getPresenceChecker().getPresenceCheckSuffix() ) ); String variableName = sourceParam.getName() + "." - + propertyEntry.getReadAccessor().getSimpleName() + "()"; - for (int i = 1; i < sourceReference.getPropertyEntries().size(); i++) { + + propertyEntry.getReadAccessor().getReadValueSource(); + Type variableType = propertyEntry.getType(); + for ( int i = 1; i < sourceReference.getPropertyEntries().size(); i++ ) { PropertyEntry entry = sourceReference.getPropertyEntries().get( i ); - if (entry.getPresenceChecker() != null && entry.getReadAccessor() != null) { - presenceChecks.add( new NullPresenceCheck( variableName ) ); - presenceChecks.add( new SourceReferenceMethodPresenceCheck( + if ( entry.getPresenceChecker() != null && entry.getReadAccessor() != null ) { + if ( variableType.isOptionalType() ) { + presenceChecks.add( new OptionalPresenceCheck( + variableName, + ctx.getVersionInformation() + ) ); + variableName = variableName + ".get()"; + } + else { + presenceChecks.add( new NullPresenceCheck( variableName ) ); + } + presenceChecks.add( new SuffixPresenceCheck( variableName, - entry.getPresenceChecker().getSimpleName() + entry.getPresenceChecker().getPresenceCheckSuffix() ) ); variableName = variableName + "." + entry.getReadAccessor().getSimpleName() + "()"; + variableType = entry.getType(); } else { break; @@ -715,11 +941,11 @@ private Assignment forgeIterableMapping(Type sourceType, Type targetType, Source private Assignment forgeWithElementMapping(Type sourceType, Type targetType, SourceRHS source, ContainerMappingMethodBuilder builder) { - + sourceType = sourceType.replaceSuperBoundWith( targetType, ctx.getTypeFactory().getType( Object.class ) ); targetType = targetType.withoutBounds(); ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "[]" ); - ContainerMappingMethod iterableMappingMethod = builder + Supplier mappingMethodCreator = () -> builder .mappingContext( ctx ) .method( methodRef ) .selectionParameters( selectionParameters ) @@ -727,7 +953,7 @@ private Assignment forgeWithElementMapping(Type sourceType, Type targetType, Sou .positionHint( positionHint ) .build(); - return createForgedAssignment( source, methodRef, iterableMappingMethod ); + return getOrCreateForgedAssignment( source, methodRef, mappingMethodCreator ); } private ForgedMethod prepareForgedMethod(Type sourceType, Type targetType, SourceRHS source, String suffix) { @@ -745,25 +971,12 @@ private Assignment forgeMapMapping(Type sourceType, Type targetType, SourceRHS s ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "{}" ); MapMappingMethod.Builder builder = new MapMappingMethod.Builder(); - MapMappingMethod mapMappingMethod = builder + Supplier mapMappingMethodCreator = () -> builder .mappingContext( ctx ) .method( methodRef ) .build(); - return createForgedAssignment( source, methodRef, mapMappingMethod ); - } - - private Assignment forgeMapToBeanMapping(Type sourceType, Type targetType, SourceRHS source) { - - targetType = targetType.withoutBounds(); - ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "{}" ); - - BeanMappingMethod.Builder builder = new BeanMappingMethod.Builder(); - final BeanMappingMethod mapToBeanMappingMethod = builder.mappingContext( ctx ) - .forgedMethod( methodRef ) - .build(); - - return createForgedAssignment( source, methodRef, mapToBeanMappingMethod ); + return getOrCreateForgedAssignment( source, methodRef, mapMappingMethodCreator ); } private Assignment forgeMapping(SourceRHS sourceRHS) { @@ -778,6 +991,10 @@ private Assignment forgeMapping(SourceRHS sourceRHS) { return null; } + return forgeMapping( sourceType, targetType, sourceRHS ); + } + + private Assignment forgeMapping(Type sourceType, Type targetType, SourceRHS sourceRHS) { //Fail fast. If we could not find the method by now, no need to try if ( sourceType.isPrimitive() || targetType.isPrimitive() ) { @@ -794,7 +1011,7 @@ private Assignment forgeMapping(SourceRHS sourceRHS) { // because we are forging a Mapping for a method with multiple source parameters. // If the target type is enum, then we can't create an update method if ( !targetType.isEnumType() && ( method.isUpdateMethod() || forceUpdateMethod ) - && targetWriteAccessorType != AccessorType.ADDER) { + && targetWriteAccessorType != AccessorType.ADDER ) { parameters.add( Parameter.forForgedMappingTarget( targetType ) ); returnType = ctx.getTypeFactory().createVoidType(); } @@ -810,7 +1027,7 @@ private Assignment forgeMapping(SourceRHS sourceRHS) { forgeMethodWithMappingReferences, forgedNamedBased ); - return createForgedAssignment( sourceRHS, targetBuilderType, forgedMethod ); + return createForgedAssignment( sourceRHS, forgedMethod ); } private ForgedMethodHistory getForgedMethodHistory(SourceRHS sourceRHS) { @@ -960,6 +1177,7 @@ public PropertyMapping build() { targetType, false, false, + false, false ); } else { @@ -1001,7 +1219,7 @@ else if ( errorMessageDetails == null ) { return new PropertyMapping( targetPropertyName, targetWriteAccessor.getSimpleName(), - ValueProvider.of( targetReadAccessor ), + targetReadAccessor, targetType, assignment, dependsOn, @@ -1067,7 +1285,7 @@ public PropertyMapping build() { return new PropertyMapping( targetPropertyName, targetWriteAccessor.getSimpleName(), - ValueProvider.of( targetReadAccessor ), + targetReadAccessor, targetType, assignment, dependsOn, @@ -1080,19 +1298,20 @@ public PropertyMapping build() { // Constructor for creating mappings of constant expressions. private PropertyMapping(String name, String targetWriteAccessorName, - ValueProvider targetReadAccessorProvider, + ReadAccessor targetReadAccessorProvider, Type targetType, Assignment propertyAssignment, Set dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) { - this( name, null, targetWriteAccessorName, targetReadAccessorProvider, + this( name, null, null, targetWriteAccessorName, targetReadAccessorProvider, targetType, propertyAssignment, dependsOn, defaultValueAssignment, constructorMapping ); } - private PropertyMapping(String name, String sourceBeanName, String targetWriteAccessorName, - ValueProvider targetReadAccessorProvider, Type targetType, - Assignment assignment, - Set dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) { + private PropertyMapping(String sourcePropertyName, String name, String sourceBeanName, + String targetWriteAccessorName, ReadAccessor targetReadAccessorProvider, Type targetType, + Assignment assignment, + Set dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) { + this.sourcePropertyName = sourcePropertyName; this.name = name; this.sourceBeanName = sourceBeanName; this.targetWriteAccessorName = targetWriteAccessorName; @@ -1100,7 +1319,7 @@ private PropertyMapping(String name, String sourceBeanName, String targetWriteAc this.targetType = targetType; this.assignment = assignment; - this.dependsOn = dependsOn != null ? dependsOn : Collections.emptySet(); + this.dependsOn = dependsOn != null ? dependsOn : Collections.emptySet(); this.defaultValueAssignment = defaultValueAssignment; this.constructorMapping = constructorMapping; } @@ -1112,6 +1331,10 @@ public String getName() { return name; } + public String getSourcePropertyName() { + return sourcePropertyName; + } + public String getSourceBeanName() { return sourceBeanName; } @@ -1121,7 +1344,7 @@ public String getTargetWriteAccessorName() { } public String getTargetReadAccessorName() { - return targetReadAccessorProvider == null ? null : targetReadAccessorProvider.getValue(); + return targetReadAccessorProvider == null ? null : targetReadAccessorProvider.getReadValueSource(); } public Type getTargetType() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ServicesEntry.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ServicesEntry.java index 727518dad8..0d97c1a523 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ServicesEntry.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ServicesEntry.java @@ -12,6 +12,8 @@ import org.mapstruct.ap.internal.model.common.Type; /** + * Represents a service entry for the service loader file. + * * @author Christophe Labouisse on 14/07/2015. */ public class ServicesEntry extends ModelElement { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java index 552040d8ce..3955bc47ce 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java @@ -15,6 +15,7 @@ import org.mapstruct.ap.internal.model.assignment.Java8FunctionWrapper; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; @@ -51,7 +52,8 @@ protected Assignment getWrapper(Assignment assignment, Method method) { protected StreamMappingMethod instantiateMappingMethod(Method method, Collection existingVariables, Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingMethods, - List afterMappingMethods, SelectionParameters selectionParameters) { + List afterMappingMethods, SelectionParameters selectionParameters, + PresenceCheck sourceParameterPresenceCheck) { Set helperImports = new HashSet<>(); if ( method.getResultType().isIterableType() ) { @@ -63,9 +65,9 @@ protected StreamMappingMethod instantiateMappingMethod(Method method, Collection sourceParameterType.isIterableType() ) { helperImports.add( ctx.getTypeFactory().getType( StreamSupport.class ) ); } - return new StreamMappingMethod( method, + getMethodAnnotations(), existingVariables, assignment, factoryMethod, @@ -74,18 +76,21 @@ protected StreamMappingMethod instantiateMappingMethod(Method method, Collection beforeMappingMethods, afterMappingMethods, selectionParameters, - helperImports + helperImports, + sourceParameterPresenceCheck ); } } - - private StreamMappingMethod(Method method, Collection existingVariables, Assignment parameterAssignment, + //CHECKSTYLE:OFF + private StreamMappingMethod(Method method, List annotations, + Collection existingVariables, Assignment parameterAssignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters, Set helperImports) { + SelectionParameters selectionParameters, Set helperImports, PresenceCheck sourceParameterPresenceCheck) { super( method, + annotations, existingVariables, parameterAssignment, factoryMethod, @@ -93,8 +98,10 @@ private StreamMappingMethod(Method method, Collection existingVariables, loopVariableName, beforeMappingReferences, afterMappingReferences, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); + //CHECKSTYLE:ON this.helperImports = helperImports; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java new file mode 100644 index 0000000000..3ce42f68e5 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java @@ -0,0 +1,69 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +import org.mapstruct.ap.internal.model.assignment.AssignmentWrapper; +import org.mapstruct.ap.internal.model.assignment.ReturnWrapper; +import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.Type; + +/** + * Represents the mapping between a Subclass and its mapping target. This will be used by a {@link BeanMappingMethod} + * that has {@link org.mapstruct.SubclassMapping} annotations applied to it. Before doing the normal mapping for that + * method it will first check if the source object is of the sourceType if so it will use the assignment instead. + * + * @author Ben Zegveld + */ +public class SubclassMapping extends ModelElement { + + private final Type sourceType; + private final Type targetType; + private final Assignment assignment; + private final String sourceArgument; + + public SubclassMapping(Type sourceType, String sourceArgument, Type targetType, Assignment assignment) { + this.sourceType = sourceType; + this.sourceArgument = sourceArgument; + this.targetType = targetType; + this.assignment = assignment; + } + + public Type getSourceType() { + return sourceType; + } + + @Override + public Set getImportTypes() { + return Collections.singleton( sourceType ); + } + + public AssignmentWrapper getAssignment() { + return new ReturnWrapper( assignment ); + } + + public String getSourceArgument() { + return sourceArgument; + } + + @Override + public boolean equals(final Object other) { + if ( !( other instanceof SubclassMapping ) ) { + return false; + } + SubclassMapping castOther = (SubclassMapping) other; + return Objects.equals( sourceType, castOther.sourceType ) && Objects.equals( targetType, castOther.targetType ); + } + + @Override + public int hashCode() { + return Objects.hash( sourceType, targetType ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/SupportingField.java b/processor/src/main/java/org/mapstruct/ap/internal/model/SupportingField.java index caf6d6160e..2cac1ef351 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/SupportingField.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/SupportingField.java @@ -47,7 +47,7 @@ public static void addAllFieldsIn(Set supportingMapping for ( SupportingMappingMethod supportingMappingMethod : supportingMappingMethods ) { Field field = supportingMappingMethod.getSupportingField(); if ( field != null ) { - targets.add( supportingMappingMethod.getSupportingField() ); + targets.add( field ); } } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ToOptionalTypeConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ToOptionalTypeConversion.java new file mode 100644 index 0000000000..45bbc9d4b0 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ToOptionalTypeConversion.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.PresenceCheck; +import org.mapstruct.ap.internal.model.common.Type; + +/** + * An inline conversion from a source to an optional of the source. + * + * @author Filip Hrisafov + */ +public class ToOptionalTypeConversion extends ModelElement implements Assignment { + + private final Assignment conversionAssignment; + private final Type targetType; + + public ToOptionalTypeConversion(Type targetType, Assignment conversionAssignment) { + this.conversionAssignment = conversionAssignment; + this.targetType = targetType; + } + + public Type getTargetType() { + return targetType; + } + + @Override + public Set getImportTypes() { + Set importTypes = new HashSet<>( conversionAssignment.getImportTypes() ); + importTypes.add( targetType ); + return importTypes; + } + + @Override + public List getThrownTypes() { + return conversionAssignment.getThrownTypes(); + } + + public Assignment getAssignment() { + return conversionAssignment; + } + + @Override + public String getSourceReference() { + return conversionAssignment.getSourceReference(); + } + + @Override + public boolean isSourceReferenceParameter() { + return conversionAssignment.isSourceReferenceParameter(); + } + + @Override + public PresenceCheck getSourcePresenceCheckerReference() { + return conversionAssignment.getSourcePresenceCheckerReference(); + } + + @Override + public Type getSourceType() { + return conversionAssignment.getSourceType(); + } + + @Override + public String createUniqueVarName(String desiredName) { + return conversionAssignment.createUniqueVarName( desiredName ); + } + + @Override + public String getSourceLocalVarName() { + return conversionAssignment.getSourceLocalVarName(); + } + + @Override + public void setSourceLocalVarName(String sourceLocalVarName) { + conversionAssignment.setSourceLocalVarName( sourceLocalVarName ); + } + + @Override + public String getSourceLoopVarName() { + return conversionAssignment.getSourceLoopVarName(); + } + + @Override + public void setSourceLoopVarName(String sourceLoopVarName) { + conversionAssignment.setSourceLoopVarName( sourceLoopVarName ); + } + + @Override + public String getSourceParameterName() { + return conversionAssignment.getSourceParameterName(); + } + + @Override + public void setAssignment(Assignment assignment) { + conversionAssignment.setAssignment( assignment ); + } + + @Override + public AssignmentType getType() { + return conversionAssignment.getType(); + } + + @Override + public boolean isCallingUpdateMethod() { + return false; + } + + @Override + public String toString() { + return targetType.getName() + ".of( " + conversionAssignment.toString() + " )"; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java index 0d71ed5506..82346fa8ac 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.internal.model; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -25,6 +26,7 @@ import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.TypeUtils; +import org.mapstruct.ap.internal.version.VersionInformation; import org.mapstruct.ap.spi.EnumTransformationStrategy; import static org.mapstruct.ap.internal.gem.MappingConstantsGem.ANY_REMAINING; @@ -41,9 +43,11 @@ */ public class ValueMappingMethod extends MappingMethod { + private final List annotations; private final List valueMappings; private final MappingEntry defaultTarget; private final MappingEntry nullTarget; + private final VersionInformation versionInformation; private final Type unexpectedValueMappingException; @@ -116,15 +120,32 @@ else if ( sourceType.isString() && targetType.isEnumType() ) { LifecycleMethodResolver.beforeMappingMethods( method, selectionParameters, ctx, existingVariables ); List afterMappingMethods = LifecycleMethodResolver.afterMappingMethods( method, selectionParameters, ctx, existingVariables ); + List annotations; + if ( method instanceof ForgedMethod ) { + annotations = Collections.emptyList(); + } + else { + annotations = new ArrayList<>(); + AdditionalAnnotationsBuilder additionalAnnotationsBuilder = + new AdditionalAnnotationsBuilder( + ctx.getElementUtils(), + ctx.getTypeFactory(), + ctx.getMessager() + ); + annotations.addAll( additionalAnnotationsBuilder.getProcessedAnnotations( method.getExecutable() ) ); + } // finally return a mapping - return new ValueMappingMethod( method, + return new ValueMappingMethod( + method, + annotations, mappingEntries, valueMappings.nullValueTarget, valueMappings.defaultTargetValue, determineUnexpectedValueMappingException(), beforeMappingMethods, - afterMappingMethods + afterMappingMethods, + ctx.getVersionInformation() ); } @@ -138,8 +159,10 @@ private void initializeEnumTransformationStrategy() { String nameTransformationStrategy = enumMapping.getNameTransformationStrategy(); if ( enumTransformationStrategies.containsKey( nameTransformationStrategy ) ) { - enumTransformationInvoker = new EnumTransformationStrategyInvoker( enumTransformationStrategies.get( - nameTransformationStrategy ), enumMapping.getNameTransformationConfiguration() ); + enumTransformationInvoker = new EnumTransformationStrategyInvoker( + enumTransformationStrategies.get( + nameTransformationStrategy ), enumMapping.getNameTransformationConfiguration() + ); } } @@ -164,12 +187,12 @@ private String transform(String source) { } } - private List enumToEnumMapping(Method method, Type sourceType, Type targetType ) { + private List enumToEnumMapping(Method method, Type sourceType, Type targetType) { List mappings = new ArrayList<>(); List unmappedSourceConstants = new ArrayList<>( sourceType.getEnumConstants() ); - boolean sourceErrorOccurred = !reportErrorIfMappedSourceEnumConstantsDontExist( method, sourceType ); - boolean targetErrorOccurred = !reportErrorIfMappedTargetEnumConstantsDontExist( method, targetType ); + boolean sourceErrorOccurred = reportErrorIfMappedSourceEnumConstantsDontExist( method, sourceType ); + boolean targetErrorOccurred = reportErrorIfMappedTargetEnumConstantsDontExist( method, targetType ); if ( sourceErrorOccurred || targetErrorOccurred ) { return mappings; } @@ -236,7 +259,8 @@ else if ( NULL.equals( targetConstant ) ) { } // all sources should now be matched, there's no default to fall back to, so if sources remain, // we have an issue. - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), Message.VALUEMAPPING_UNMAPPED_SOURCES, sourceErrorMessage, targetErrorMessage, @@ -248,17 +272,17 @@ else if ( NULL.equals( targetConstant ) ) { return mappings; } - private List enumToStringMapping(Method method, Type sourceType ) { + private List enumToStringMapping(Method method, Type sourceType) { List mappings = new ArrayList<>(); List unmappedSourceConstants = new ArrayList<>( sourceType.getEnumConstants() ); - boolean sourceErrorOccurred = !reportErrorIfMappedSourceEnumConstantsDontExist( method, sourceType ); - boolean anyRemainingUsedError = !reportErrorIfSourceEnumConstantsContainsAnyRemaining( method ); + boolean sourceErrorOccurred = reportErrorIfMappedSourceEnumConstantsDontExist( method, sourceType ); + boolean anyRemainingUsedError = reportErrorIfSourceEnumConstantsContainsAnyRemaining( method ); if ( sourceErrorOccurred || anyRemainingUsedError ) { return mappings; } - // Start to fill the mappings with the defined valuemappings + // Start to fill the mappings with the defined valueMappings for ( ValueMappingOptions valueMapping : valueMappings.regularValueMappings ) { mappings.add( new MappingEntry( valueMapping.getSource(), valueMapping.getTarget() ) ); unmappedSourceConstants.remove( valueMapping.getSource() ); @@ -278,18 +302,18 @@ private List enumToStringMapping(Method method, Type sourceType ) return mappings; } - private List stringToEnumMapping(Method method, Type targetType ) { + private List stringToEnumMapping(Method method, Type targetType) { List mappings = new ArrayList<>(); List unmappedSourceConstants = new ArrayList<>( targetType.getEnumConstants() ); - boolean sourceErrorOccurred = !reportErrorIfMappedTargetEnumConstantsDontExist( method, targetType ); + boolean sourceErrorOccurred = reportErrorIfMappedTargetEnumConstantsDontExist( method, targetType ); reportWarningIfAnyRemainingOrAnyUnMappedMissing( method ); if ( sourceErrorOccurred ) { return mappings; } Set mappedSources = new LinkedHashSet<>(); - // Start to fill the mappings with the defined valuemappings + // Start to fill the mappings with the defined value mappings for ( ValueMappingOptions valueMapping : valueMappings.regularValueMappings ) { mappedSources.add( valueMapping.getSource() ); mappings.add( new MappingEntry( valueMapping.getSource(), valueMapping.getTarget() ) ); @@ -356,7 +380,7 @@ else if ( !sourceEnumConstants.contains( mappedConstant.getSource() ) ) { foundIncorrectMapping = true; } } - return !foundIncorrectMapping; + return foundIncorrectMapping; } private boolean reportErrorIfSourceEnumConstantsContainsAnyRemaining(Method method) { @@ -372,7 +396,7 @@ private boolean reportErrorIfSourceEnumConstantsContainsAnyRemaining(Method meth ); foundIncorrectMapping = true; } - return !foundIncorrectMapping; + return foundIncorrectMapping; } private void reportWarningIfAnyRemainingOrAnyUnMappedMissing(Method method) { @@ -423,7 +447,8 @@ private boolean reportErrorIfMappedTargetEnumConstantsDontExist(Method method, T if ( valueMappings.nullTarget != null && NULL.equals( valueMappings.nullTarget.getTarget() ) && !targetEnumConstants.contains( valueMappings.nullTarget.getTarget() ) ) { - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), valueMappings.nullTarget.getMirror(), valueMappings.nullTarget.getTargetAnnotationValue(), Message.VALUEMAPPING_NON_EXISTING_CONSTANT, @@ -446,7 +471,7 @@ else if ( valueMappings.nullTarget == null && valueMappings.nullValueTarget != n ); } - return !foundIncorrectMapping; + return foundIncorrectMapping; } private Type determineUnexpectedValueMappingException() { @@ -532,18 +557,35 @@ String getValue(ValueMappingOptions valueMapping) { } private ValueMappingMethod(Method method, + List annotations, List enumMappings, String nullTarget, String defaultTarget, Type unexpectedValueMappingException, List beforeMappingMethods, - List afterMappingMethods) { + List afterMappingMethods, + VersionInformation versionInformation) { super( method, beforeMappingMethods, afterMappingMethods ); this.valueMappings = enumMappings; this.nullTarget = new MappingEntry( null, nullTarget ); - this.defaultTarget = new MappingEntry( null, defaultTarget != null ? defaultTarget : THROW_EXCEPTION); + this.defaultTarget = new MappingEntry( null, defaultTarget != null ? defaultTarget : THROW_EXCEPTION ); this.unexpectedValueMappingException = unexpectedValueMappingException; this.overridden = method.overridesMethod(); + this.annotations = annotations; + this.versionInformation = versionInformation; + } + + public boolean isDefaultTargetRequired() { + if ( !versionInformation.isSourceVersionAtLeast14() ) { + return true; + } + + Type sourceType = getSourceParameter().getType(); + if ( !sourceType.isEnumType() ) { + return true; + } + + return sourceType.getEnumConstants().size() != getValueMappings().size(); } @Override @@ -551,12 +593,15 @@ public Set getImportTypes() { Set importTypes = super.getImportTypes(); if ( unexpectedValueMappingException != null && !unexpectedValueMappingException.isJavaLangType() ) { - if ( defaultTarget.isTargetAsException() || nullTarget.isTargetAsException() || + if ( ( isDefaultTargetRequired() && defaultTarget.isTargetAsException() ) || + nullTarget.isTargetAsException() || hasMappingWithTargetAsException() ) { importTypes.addAll( unexpectedValueMappingException.getImportTypes() ); } } - + for ( Annotation annotation : annotations ) { + importTypes.addAll( annotation.getImportTypes() ); + } return importTypes; } @@ -578,6 +623,10 @@ public MappingEntry getNullTarget() { return nullTarget; } + public VersionInformation getVersionInformation() { + return versionInformation; + } + public Type getUnexpectedValueMappingException() { return unexpectedValueMappingException; } @@ -590,6 +639,10 @@ public boolean isOverridden() { return overridden; } + public List getAnnotations() { + return annotations; + } + public static class MappingEntry { private final String source; private final String target; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/annotation/AnnotationElement.java b/processor/src/main/java/org/mapstruct/ap/internal/model/annotation/AnnotationElement.java new file mode 100644 index 0000000000..f242b51f47 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/annotation/AnnotationElement.java @@ -0,0 +1,130 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.annotation; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.Type; + +/** + * Represents an annotation element. + * + * @author Ben Zegveld + */ +public class AnnotationElement extends ModelElement { + public enum AnnotationElementType { + BOOLEAN, BYTE, CHARACTER, CLASS, DOUBLE, ENUM, FLOAT, INTEGER, LONG, SHORT, STRING + } + + private final String elementName; + private final List values; + private final AnnotationElementType type; + + public AnnotationElement(AnnotationElementType type, List values) { + this( type, null, values ); + } + + public AnnotationElement(AnnotationElementType type, String elementName, List values) { + this.type = type; + this.elementName = elementName; + this.values = values; + } + + public String getElementName() { + return elementName; + } + + public List getValues() { + return values; + } + + @Override + public Set getImportTypes() { + Set importTypes = null; + for ( Object value : values ) { + if ( value instanceof ModelElement ) { + if ( importTypes == null ) { + importTypes = new HashSet<>(); + } + importTypes.addAll( ( (ModelElement) value ).getImportTypes() ); + } + } + + return importTypes == null ? Collections.emptySet() : importTypes; + } + + public boolean isBoolean() { + return type == AnnotationElementType.BOOLEAN; + } + + public boolean isByte() { + return type == AnnotationElementType.BYTE; + } + + public boolean isCharacter() { + return type == AnnotationElementType.CHARACTER; + } + + public boolean isClass() { + return type == AnnotationElementType.CLASS; + } + + public boolean isDouble() { + return type == AnnotationElementType.DOUBLE; + } + + public boolean isEnum() { + return type == AnnotationElementType.ENUM; + } + + public boolean isFloat() { + return type == AnnotationElementType.FLOAT; + } + + public boolean isInteger() { + return type == AnnotationElementType.INTEGER; + } + + public boolean isLong() { + return type == AnnotationElementType.LONG; + } + + public boolean isShort() { + return type == AnnotationElementType.SHORT; + } + + public boolean isString() { + return type == AnnotationElementType.STRING; + } + + @Override + public int hashCode() { + return Objects.hash( elementName, type, values ); + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) { + return true; + } + if ( obj == null ) { + return false; + } + if ( getClass() != obj.getClass() ) { + return false; + } + AnnotationElement other = (AnnotationElement) obj; + return Objects.equals( elementName, other.elementName ) + && type == other.type + && Objects.equals( values, other.values ); + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/annotation/EnumAnnotationElementHolder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/annotation/EnumAnnotationElementHolder.java new file mode 100644 index 0000000000..de87f32525 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/annotation/EnumAnnotationElementHolder.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.annotation; + +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.Type; + +public class EnumAnnotationElementHolder extends ModelElement { + + private final Type enumClass; + private final String name; + + public EnumAnnotationElementHolder(Type enumClass, String name) { + this.enumClass = enumClass; + this.name = name; + } + + public Type getEnumClass() { + return enumClass; + } + + public String getName() { + return name; + } + + @Override + public Set getImportTypes() { + return enumClass.getImportTypes(); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/AdderWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/AdderWrapper.java index c5778628e9..13dfe832c9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/AdderWrapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/AdderWrapper.java @@ -39,7 +39,15 @@ public AdderWrapper( Assignment rhs, // localVar is iteratorVariable String desiredName = Nouns.singularize( adderIteratorName ); rhs.setSourceLoopVarName( rhs.createUniqueVarName( desiredName ) ); - adderType = first( getSourceType().determineTypeArguments( Collection.class ) ); + if ( getSourceType().isCollectionType() ) { + adderType = first( getSourceType().determineTypeArguments( Collection.class ) ); + } + else if ( getSourceType().isArrayType() ) { + adderType = getSourceType().getComponentType(); + } + else { // iterable + adderType = first( getSourceType().determineTypeArguments( Iterable.class ) ); + } } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.java index a334312007..eaa2fe68ff 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.java @@ -12,6 +12,7 @@ import org.mapstruct.ap.internal.model.common.Type; /** + * Decorates the assignment as an {@link Enum} constant access. * * @author Sjaak Derksen */ diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.java index 06a2712f00..ce347321c9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.java @@ -5,24 +5,23 @@ */ package org.mapstruct.ap.internal.model.assignment; -import static org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem.ALWAYS; -import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.IGNORE; -import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT; - import java.util.HashSet; import java.util.List; import java.util.Set; +import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; +import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; -import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; -import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; + +import static org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem.ALWAYS; +import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.IGNORE; /** * This wrapper handles the situation where an assignment is done for an update method. * - * In case of a pre-existing target the wrapper checks if there is an collection or map initialized on the target bean + * In case of a pre-existing target the wrapper checks if there is a collection or map initialized on the target bean * (not null). If so it uses the addAll (for collections) or putAll (for maps). The collection / map is cleared in case * of a pre-existing target {@link org.mapstruct.MappingTarget }before adding the source entries. * @@ -34,8 +33,8 @@ public class ExistingInstanceSetterWrapperForCollectionsAndMaps extends SetterWrapperForCollectionsAndMapsWithNullCheck { - private final boolean includeElseBranch; - private final boolean mapNullToDefault; + private final NullValuePropertyMappingStrategyGem nvpms; + private final NullValueCheckStrategyGem nvcs; private final Type targetType; public ExistingInstanceSetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment, @@ -53,26 +52,30 @@ public ExistingInstanceSetterWrapperForCollectionsAndMaps(Assignment decoratedAs typeFactory, fieldAssignment ); - this.mapNullToDefault = SET_TO_DEFAULT == nvpms; + this.nvcs = nvcs; + this.nvpms = nvpms; this.targetType = targetType; - this.includeElseBranch = ALWAYS != nvcs && IGNORE != nvpms; } @Override public Set getImportTypes() { Set imported = new HashSet<>( super.getImportTypes() ); if ( isMapNullToDefault() && ( targetType.getImplementationType() != null ) ) { - imported.add( targetType.getImplementationType() ); + imported.addAll( getNewInstance().getImportTypes() ); } return imported; } public boolean isIncludeElseBranch() { - return includeElseBranch; + return nvcs != ALWAYS && nvpms != IGNORE; } public boolean isMapNullToDefault() { - return mapNullToDefault; + return nvpms == NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT; + } + + public boolean isMapNullToClear() { + return nvpms == NullValuePropertyMappingStrategyGem.CLEAR; } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.java index 40e195dcd0..e1c0c8cb20 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.java @@ -9,14 +9,17 @@ import java.util.List; import java.util.Set; +import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.Type; +import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.IGNORE; + /** * This wrapper handles the situation were an assignment must be done via a target getter method because there * is no setter available. * - * The wrapper checks if there is an collection or map initialized on the target bean (not null). If so it uses the + * The wrapper checks if there is a collection or map initialized on the target bean (not null). If so it uses the * addAll (for collections) or putAll (for maps). The collection / map is cleared in case of a pre-existing target * {@link org.mapstruct.MappingTarget }before adding the source entries. The goal is that the same collection / map * is used as target. @@ -26,6 +29,14 @@ * @author Sjaak Derksen */ public class GetterWrapperForCollectionsAndMaps extends WrapperForCollectionsAndMaps { + private final boolean ignoreMapNull; + + public GetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment, + List thrownTypesToExclude, + Type targetType, + boolean fieldAssignment) { + this( decoratedAssignment, thrownTypesToExclude, targetType, null, fieldAssignment ); + } /** * @param decoratedAssignment source RHS @@ -36,6 +47,7 @@ public class GetterWrapperForCollectionsAndMaps extends WrapperForCollectionsAnd public GetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment, List thrownTypesToExclude, Type targetType, + NullValuePropertyMappingStrategyGem nvpms, boolean fieldAssignment) { super( @@ -44,6 +56,7 @@ public GetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment, targetType, fieldAssignment ); + this.ignoreMapNull = nvpms == IGNORE; } @Override @@ -54,4 +67,8 @@ public Set getImportTypes() { } return imported; } + + public boolean isIgnoreMapNull() { + return ignoreMapNull; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/NewInstanceSetterWrapperForCollectionsAndMaps.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/NewInstanceSetterWrapperForCollectionsAndMaps.java new file mode 100644 index 0000000000..f0e23470a7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/NewInstanceSetterWrapperForCollectionsAndMaps.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.assignment; + +import java.util.List; + +import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.common.TypeFactory; + +/** + * This wrapper handles the situation where an assignment is done via the setter, while creating the collection or map + * using a no-args constructor. + * + * @author Ben Zegveld + */ +public class NewInstanceSetterWrapperForCollectionsAndMaps extends SetterWrapperForCollectionsAndMapsWithNullCheck { + + private String instanceVar; + + public NewInstanceSetterWrapperForCollectionsAndMaps(Assignment decoratedAssignment, + List thrownTypesToExclude, + Type targetType, + TypeFactory typeFactory, + boolean fieldAssignment) { + + super( + decoratedAssignment, + thrownTypesToExclude, + targetType, + typeFactory, + fieldAssignment + ); + this.instanceVar = decoratedAssignment.createUniqueVarName( targetType.getName() ); + } + + public String getInstanceVar() { + return instanceVar; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/OptionalGetWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/OptionalGetWrapper.java new file mode 100644 index 0000000000..6c405c871d --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/OptionalGetWrapper.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.assignment; + +import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Strings; + +/** + * Decorates the assignment as an {@link java.util.Optional#get()} call. + * + * @author Filip Hrisafov + */ +public class OptionalGetWrapper extends AssignmentWrapper { + + private final Type optionalType; + + public OptionalGetWrapper(Assignment decoratedAssignment, Type optionalType) { + super( decoratedAssignment, false ); + this.optionalType = optionalType; + } + + public Type getOptionalType() { + return optionalType; + } + + @Override + public String toString() { + if ( optionalType.getFullyQualifiedName().equals( "java.util.Optional" ) ) { + return getAssignment() + ".get()"; + } + return getAssignment() + ".getAs" + Strings.capitalize( optionalType.getOptionalBaseType().getName() ) + "()"; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.java new file mode 100644 index 0000000000..76e2c19fa7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.assignment; + +import org.mapstruct.ap.internal.model.common.Assignment; + +/** + * Decorates an assignment as a return variable. + * + * @author Ben Zegveld + */ +public class ReturnWrapper extends AssignmentWrapper { + + public ReturnWrapper(Assignment decoratedAssignment) { + super( decoratedAssignment, false ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java index 7be948c857..94b031c622 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java @@ -6,7 +6,9 @@ package org.mapstruct.ap.internal.model.assignment; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.Type; @@ -22,19 +24,25 @@ public class SetterWrapper extends AssignmentWrapper { private final boolean includeSourceNullCheck; private final boolean setExplicitlyToNull; private final boolean setExplicitlyToDefault; + private final boolean mustCastForNull; + private final Type nullCastType; public SetterWrapper(Assignment rhs, List thrownTypesToExclude, boolean fieldAssignment, boolean includeSourceNullCheck, boolean setExplicitlyToNull, - boolean setExplicitlyToDefault) { + boolean setExplicitlyToDefault, + boolean mustCastForNull, + Type nullCastType) { super( rhs, fieldAssignment ); this.thrownTypesToExclude = thrownTypesToExclude; this.includeSourceNullCheck = includeSourceNullCheck; this.setExplicitlyToDefault = setExplicitlyToDefault; this.setExplicitlyToNull = setExplicitlyToNull; + this.mustCastForNull = mustCastForNull; + this.nullCastType = nullCastType; } public SetterWrapper(Assignment rhs, List thrownTypesToExclude, boolean fieldAssignment ) { @@ -43,6 +51,8 @@ public SetterWrapper(Assignment rhs, List thrownTypesToExclude, boolean fi this.includeSourceNullCheck = false; this.setExplicitlyToNull = false; this.setExplicitlyToDefault = false; + this.mustCastForNull = false; + this.nullCastType = null; } @Override @@ -59,6 +69,15 @@ public List getThrownTypes() { return result; } + @Override + public Set getImportTypes() { + Set imported = new HashSet<>( super.getImportTypes() ); + if ( isSetExplicitlyToNull() && isMustCastForNull() ) { + imported.add( nullCastType ); + } + return imported; + } + public boolean isSetExplicitlyToNull() { return setExplicitlyToNull; } @@ -71,4 +90,7 @@ public boolean isIncludeSourceNullCheck() { return includeSourceNullCheck; } + public boolean isMustCastForNull() { + return mustCastForNull; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.java index 5e3f4d63fe..8ef71f7c0d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.java @@ -5,17 +5,18 @@ */ package org.mapstruct.ap.internal.model.assignment; -import static org.mapstruct.ap.internal.model.common.Assignment.AssignmentType.DIRECT; - import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.NewInstanceCreation; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; +import static org.mapstruct.ap.internal.model.common.Assignment.AssignmentType.DIRECT; + /** * This wrapper handles the situation where an assignment is done via the setter and a null check is needed. * This is needed when a direct assignment is used, or if the user has chosen the appropriate strategy @@ -26,6 +27,7 @@ public class SetterWrapperForCollectionsAndMapsWithNullCheck extends WrapperForC private final Type targetType; private final TypeFactory typeFactory; + private final NewInstanceCreation newInstance; public SetterWrapperForCollectionsAndMapsWithNullCheck(Assignment decoratedAssignment, List thrownTypesToExclude, @@ -40,35 +42,34 @@ public SetterWrapperForCollectionsAndMapsWithNullCheck(Assignment decoratedAssig ); this.targetType = targetType; this.typeFactory = typeFactory; + this.newInstance = NewInstanceCreation.forType( targetType ); } @Override public Set getImportTypes() { Set imported = new HashSet<>( super.getImportTypes() ); if ( isDirectAssignment() ) { - if ( targetType.getImplementationType() != null ) { - imported.addAll( targetType.getImplementationType().getImportTypes() ); - } - else { - imported.addAll( targetType.getImportTypes() ); - } - + imported.addAll( newInstance.getImportTypes() ); if ( isEnumSet() ) { imported.add( typeFactory.getType( EnumSet.class ) ); } } - if (isDirectAssignment() || getSourcePresenceCheckerReference() == null ) { + if ( isDirectAssignment() || getSourcePresenceCheckerReference() == null ) { imported.addAll( getNullCheckLocalVarType().getImportTypes() ); } return imported; } + public NewInstanceCreation getNewInstance() { + return newInstance; + } + public boolean isDirectAssignment() { return getType() == DIRECT; } public boolean isEnumSet() { - return "java.util.EnumSet".equals( targetType.getFullyQualifiedName() ); + return targetType.isEnumSet(); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.java index cf0140e8ce..c80b5293c8 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.java @@ -11,6 +11,7 @@ import java.util.Set; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.NewInstanceCreation; import org.mapstruct.ap.internal.model.common.Type; /** @@ -22,10 +23,11 @@ public class UpdateWrapper extends AssignmentWrapper { private final List thrownTypesToExclude; private final Assignment factoryMethod; - private final Type targetImplementationType; + private final NewInstanceCreation newInstance; private final boolean includeSourceNullCheck; private final boolean setExplicitlyToNull; private final boolean setExplicitlyToDefault; + private final boolean mustCastForNull; public UpdateWrapper( Assignment decoratedAssignment, List thrownTypesToExclude, @@ -34,28 +36,16 @@ public UpdateWrapper( Assignment decoratedAssignment, Type targetType, boolean includeSourceNullCheck, boolean setExplicitlyToNull, - boolean setExplicitlyToDefault ) { + boolean setExplicitlyToDefault, + boolean mustCastForNull) { super( decoratedAssignment, fieldAssignment ); this.thrownTypesToExclude = thrownTypesToExclude; this.factoryMethod = factoryMethod; - this.targetImplementationType = determineImplType( factoryMethod, targetType ); + this.newInstance = ( factoryMethod == null ) ? NewInstanceCreation.forType( targetType ) : null; this.includeSourceNullCheck = includeSourceNullCheck; this.setExplicitlyToDefault = setExplicitlyToDefault; this.setExplicitlyToNull = setExplicitlyToNull; - } - - private static Type determineImplType(Assignment factoryMethod, Type targetType) { - if ( factoryMethod != null ) { - //If we have factory method then we won't use the targetType - return null; - } - if ( targetType.getImplementationType() != null ) { - // it's probably a collection or something - return targetType.getImplementationType(); - } - - // no factory method means we create a new instance ourself and thus need to import the type - return targetType; + this.mustCastForNull = mustCastForNull; } @Override @@ -78,9 +68,8 @@ public Set getImportTypes() { if ( factoryMethod != null ) { imported.addAll( factoryMethod.getImportTypes() ); } - if ( targetImplementationType != null ) { - imported.add( targetImplementationType ); - imported.addAll( targetImplementationType.getTypeParameters() ); + if ( newInstance != null ) { + imported.addAll( newInstance.getImportTypes() ); } return imported; } @@ -89,6 +78,10 @@ public Assignment getFactoryMethod() { return factoryMethod; } + public NewInstanceCreation getNewInstance() { + return newInstance; + } + public boolean isIncludeSourceNullCheck() { return includeSourceNullCheck; } @@ -100,4 +93,8 @@ public boolean isSetExplicitlyToNull() { public boolean isSetExplicitlyToDefault() { return setExplicitlyToDefault; } + + public boolean isMustCastForNull() { + return mustCastForNull; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReference.java index e2501ea952..9baa84dc77 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReference.java @@ -45,7 +45,7 @@ public TargetReference getTargetReference() { public MappingReference popTargetReference() { if ( targetReference != null ) { TargetReference newTargetReference = targetReference.pop(); - if (newTargetReference != null ) { + if ( newTargetReference != null ) { return new MappingReference(mapping, newTargetReference, sourceReference ); } } @@ -55,7 +55,7 @@ public MappingReference popTargetReference() { public MappingReference popSourceReference() { if ( sourceReference != null ) { SourceReference newSourceReference = sourceReference.pop(); - if (newSourceReference != null ) { + if ( newSourceReference != null ) { return new MappingReference(mapping, targetReference, newSourceReference ); } } @@ -71,7 +71,37 @@ public boolean equals(Object o) { return false; } MappingReference that = (MappingReference) o; - return mapping.equals( that.mapping ); + if ( ".".equals( that.mapping.getTargetName() ) ) { + // target this will never be equal to any other target this or any other. + return false; + } + + if ( !Objects.equals( mapping.getTargetName(), that.mapping.getTargetName() ) ) { + return false; + } + + if ( !Objects.equals( mapping.getConstant(), that.mapping.getConstant() ) ) { + return false; + } + + if ( !Objects.equals( mapping.getJavaExpression(), that.mapping.getJavaExpression() ) ) { + return false; + } + + if ( sourceReference == null ) { + return that.sourceReference == null; + } + + if ( that.sourceReference == null ) { + return false; + } + + + if ( !Objects.equals( sourceReference.getPropertyEntries(), that.sourceReference.getPropertyEntries() ) ) { + return false; + } + + return true; } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReferences.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReferences.java index 569c75e0eb..edc5b3157e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReferences.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/MappingReferences.java @@ -5,10 +5,8 @@ */ package org.mapstruct.ap.internal.model.beanmapping; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; -import java.util.List; import java.util.Objects; import java.util.Set; @@ -23,7 +21,6 @@ public class MappingReferences { private static final MappingReferences EMPTY = new MappingReferences( Collections.emptySet(), false ); private final Set mappingReferences; - private final List targetThisReferences; private final boolean restrictToDefinedMappings; private final boolean forForgedMethods; @@ -38,7 +35,6 @@ public static MappingReferences forSourceMethod(SourceMethod sourceMethod, TypeFactory typeFactory) { Set references = new LinkedHashSet<>(); - List targetThisReferences = new ArrayList<>( ); for ( MappingOptions mapping : sourceMethod.getOptions().getMappings() ) { @@ -61,30 +57,16 @@ public static MappingReferences forSourceMethod(SourceMethod sourceMethod, // add when inverse is also valid MappingReference mappingReference = new MappingReference( mapping, targetReference, sourceReference ); if ( isValidWhenInversed( mappingReference ) ) { - if ( ".".equals( mapping.getTargetName() ) ) { - targetThisReferences.add( mappingReference ); - } - else { - references.add( mappingReference ); - } + references.add( mappingReference ); } } - return new MappingReferences( references, targetThisReferences, false ); - } - - public MappingReferences(Set mappingReferences, List targetThisReferences, - boolean restrictToDefinedMappings) { - this.mappingReferences = mappingReferences; - this.restrictToDefinedMappings = restrictToDefinedMappings; - this.forForgedMethods = restrictToDefinedMappings; - this.targetThisReferences = targetThisReferences; + return new MappingReferences( references, false ); } public MappingReferences(Set mappingReferences, boolean restrictToDefinedMappings) { this.mappingReferences = mappingReferences; this.restrictToDefinedMappings = restrictToDefinedMappings; this.forForgedMethods = restrictToDefinedMappings; - this.targetThisReferences = Collections.emptyList(); } public MappingReferences(Set mappingReferences, boolean restrictToDefinedMappings, @@ -92,7 +74,6 @@ public MappingReferences(Set mappingReferences, boolean restri this.mappingReferences = mappingReferences; this.restrictToDefinedMappings = restrictToDefinedMappings; this.forForgedMethods = forForgedMethods; - this.targetThisReferences = Collections.emptyList(); } public Set getMappingReferences() { @@ -128,7 +109,7 @@ public boolean hasNestedTargetReferences() { for ( MappingReference mappingRef : mappingReferences ) { TargetReference targetReference = mappingRef.getTargetReference(); - if ( targetReference.isNested()) { + if ( targetReference.isNested() ) { return true; } @@ -136,10 +117,6 @@ public boolean hasNestedTargetReferences() { return false; } - public List getTargetThisReferences() { - return targetThisReferences; - } - @Override public boolean equals(Object o) { if ( this == o ) { @@ -160,9 +137,6 @@ public boolean equals(Object o) { if ( !Objects.equals( mappingReferences, that.mappingReferences ) ) { return false; } - if ( Objects.equals( targetThisReferences, that.targetThisReferences ) ) { - return false; - } return true; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java index 8ac5e46671..a3c8a74e3f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/PropertyEntry.java @@ -9,17 +9,18 @@ import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.util.Strings; -import org.mapstruct.ap.internal.util.accessor.Accessor; +import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; /** * A PropertyEntry contains information on the name, readAccessor and presenceCheck (for source) - * and return type of a property. + * and return type of property. */ public class PropertyEntry { private final String[] fullName; - private final Accessor readAccessor; - private final Accessor presenceChecker; + private final ReadAccessor readAccessor; + private final PresenceCheckAccessor presenceChecker; private final Type type; /** @@ -29,7 +30,8 @@ public class PropertyEntry { * @param readAccessor * @param type */ - private PropertyEntry(String[] fullName, Accessor readAccessor, Accessor presenceChecker, Type type) { + private PropertyEntry(String[] fullName, ReadAccessor readAccessor, PresenceCheckAccessor presenceChecker, + Type type) { this.fullName = fullName; this.readAccessor = readAccessor; this.presenceChecker = presenceChecker; @@ -45,8 +47,8 @@ private PropertyEntry(String[] fullName, Accessor readAccessor, Accessor presenc * @param type type of the property * @return the property entry for given parameters. */ - public static PropertyEntry forSourceReference(String[] name, Accessor readAccessor, - Accessor presenceChecker, Type type) { + public static PropertyEntry forSourceReference(String[] name, ReadAccessor readAccessor, + PresenceCheckAccessor presenceChecker, Type type) { return new PropertyEntry( name, readAccessor, presenceChecker, type ); } @@ -54,11 +56,11 @@ public String getName() { return fullName[fullName.length - 1]; } - public Accessor getReadAccessor() { + public ReadAccessor getReadAccessor() { return readAccessor; } - public Accessor getPresenceChecker() { + public PresenceCheckAccessor getPresenceChecker() { return presenceChecker; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java index 15b10b5782..c37b12e311 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java @@ -7,16 +7,13 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; @@ -26,9 +23,8 @@ import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; -import org.mapstruct.ap.internal.util.accessor.Accessor; -import org.mapstruct.ap.internal.util.accessor.MapValueAccessor; -import org.mapstruct.ap.internal.util.accessor.MapValuePresenceChecker; +import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; import static org.mapstruct.ap.internal.model.beanmapping.PropertyEntry.forSourceReference; import static org.mapstruct.ap.internal.util.Collections.last; @@ -51,32 +47,13 @@ *

      • {@code propertyEntries[1]} will describe {@code propB}
      • *
      * - * After building, {@link #isValid()} will return true when when no problems are detected during building. + * After building, {@link #isValid()} will return true when no problems are detected during building. * * @author Sjaak Derksen + * @author Filip Hrisafov */ public class SourceReference extends AbstractReference { - public static SourceReference fromMapSource(String[] segments, Parameter parameter) { - Type parameterType = parameter.getType(); - Type valueType = parameterType.getTypeParameters().get( 1 ); - - TypeElement typeElement = parameterType.getTypeElement(); - TypeMirror typeMirror = valueType.getTypeMirror(); - String simpleName = String.join( ".", segments ); - - MapValueAccessor mapValueAccessor = new MapValueAccessor( typeElement, typeMirror, simpleName ); - MapValuePresenceChecker mapValuePresenceChecker = new MapValuePresenceChecker( - typeElement, - typeMirror, - simpleName - ); - List entries = Collections.singletonList( - PropertyEntry.forSourceReference( segments, mapValueAccessor, mapValuePresenceChecker, valueType ) - ); - return new SourceReference( parameter, entries, true ); - } - /** * Builds a {@link SourceReference} from an {@code @Mappping}. */ @@ -166,29 +143,36 @@ public SourceReference build() { * the parameter name to avoid ambiguity * * consider: {@code Target map( Source1 source1 )} - * entries in an @Mapping#source can be "source1.propx" or just "propx" to be valid + * entries in a @Mapping#source can be "source1.propx" or just "propx" to be valid * * @param segments the segments of @Mapping#source * @param parameter the one and only parameter * @return the source reference */ private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) { - - if ( canBeTreatedAsMapSourceType( parameter.getType() ) ) { - return fromMapSource( segments, parameter ); - } - boolean foundEntryMatch; + boolean allowedMapToBean = false; + if ( segments.length > 0 ) { + if ( parameter.getType().isMapType() ) { + // When the parameter type is a map and the parameter name matches the first segment + // then the first segment should not be treated as a property of the map + allowedMapToBean = !segments[0].equals( parameter.getName() ); + } + } String[] propertyNames = segments; - List entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames ); + List entries = matchWithSourceAccessorTypes( + parameter.getType(), + propertyNames, + allowedMapToBean + ); foundEntryMatch = ( entries.size() == propertyNames.length ); if ( !foundEntryMatch ) { //Lets see if the expression contains the parameterName, so parameterName.propName1.propName2 if ( getSourceParameterFromMethodOrTemplate( segments[0] ) != null ) { propertyNames = Arrays.copyOfRange( segments, 1, segments.length ); - entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames ); + entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames, true ); foundEntryMatch = ( entries.size() == propertyNames.length ); } else { @@ -214,14 +198,6 @@ private SourceReference buildFromSingleSourceParameters(String[] segments, Param */ private SourceReference buildFromMultipleSourceParameters(String[] segments, Parameter parameter) { - if (parameter != null && canBeTreatedAsMapSourceType( parameter.getType() )) { - String[] propertyNames = new String[0]; - if ( segments.length > 1 ) { - propertyNames = Arrays.copyOfRange( segments, 1, segments.length ); - } - return fromMapSource( propertyNames, parameter ); - } - boolean foundEntryMatch; String[] propertyNames = new String[0]; @@ -229,7 +205,7 @@ private SourceReference buildFromMultipleSourceParameters(String[] segments, Par if ( segments.length > 1 && parameter != null ) { propertyNames = Arrays.copyOfRange( segments, 1, segments.length ); - entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames ); + entries = matchWithSourceAccessorTypes( parameter.getType(), propertyNames, true ); foundEntryMatch = ( entries.size() == propertyNames.length ); } else { @@ -244,21 +220,12 @@ private SourceReference buildFromMultipleSourceParameters(String[] segments, Par return new SourceReference( parameter, entries, foundEntryMatch ); } - private boolean canBeTreatedAsMapSourceType(Type type) { - if ( !type.isMapType() ) { - return false; - } - - List typeParameters = type.getTypeParameters(); - return typeParameters.size() == 2 && typeParameters.get( 0 ).isString(); - } - /** - * When there are more than one source parameters, the first segment name of the propery + * When there are more than one source parameters, the first segment name of the property * needs to match the parameter name to avoid ambiguity * * consider: {@code Target map( Source1 source1, Source2 source2 )} - * entries in an @Mapping#source need to be "source1.propx" or "source2.propy.propz" to be valid + * entries in a @Mapping#source need to be "source1.propx" or "source2.propy.propz" to be valid * * @param segments the segments of @Mapping#source * @return parameter that matches with first segment of @Mapping#source @@ -351,29 +318,30 @@ private void reportErrorOnNoMatch( Parameter parameter, String[] propertyNames, } } - private List matchWithSourceAccessorTypes(Type type, String[] entryNames) { + private List matchWithSourceAccessorTypes(Type type, String[] entryNames, + boolean allowedMapToBean) { List sourceEntries = new ArrayList<>(); Type newType = type; for ( int i = 0; i < entryNames.length; i++ ) { boolean matchFound = false; - Map sourceReadAccessors = newType.getPropertyReadAccessors(); - Map sourcePresenceCheckers = newType.getPropertyPresenceCheckers(); - - for ( Map.Entry getter : sourceReadAccessors.entrySet() ) { - if ( getter.getKey().equals( entryNames[i] ) ) { - newType = typeFactory.getReturnType( - (DeclaredType) newType.getTypeMirror(), - getter.getValue() - ); - sourceEntries.add( forSourceReference( - Arrays.copyOf( entryNames, i + 1 ), - getter.getValue(), - sourcePresenceCheckers.get( entryNames[i] ), - newType - ) ); - matchFound = true; - break; - } + Type noBoundsType = newType.withoutBounds(); + if ( noBoundsType.isOptionalType() ) { + noBoundsType = noBoundsType.getOptionalBaseType(); + } + ReadAccessor readAccessor = noBoundsType.getReadAccessor( entryNames[i], i > 0 || allowedMapToBean ); + if ( readAccessor != null ) { + PresenceCheckAccessor presenceChecker = noBoundsType.getPresenceChecker( entryNames[i] ); + newType = typeFactory.getReturnType( + (DeclaredType) noBoundsType.getTypeMirror(), + readAccessor + ); + sourceEntries.add( forSourceReference( + Arrays.copyOf( entryNames, i + 1 ), + readAccessor, + presenceChecker, + newType + ) ); + matchFound = true; } if ( !matchFound ) { break; @@ -393,8 +361,8 @@ private void reportMappingError(Message msg, Object... objects) { public static class BuilderFromProperty { private String name; - private Accessor readAccessor; - private Accessor presenceChecker; + private ReadAccessor readAccessor; + private PresenceCheckAccessor presenceChecker; private Type type; private Parameter sourceParameter; @@ -403,12 +371,12 @@ public BuilderFromProperty name(String name) { return this; } - public BuilderFromProperty readAccessor(Accessor readAccessor) { + public BuilderFromProperty readAccessor(ReadAccessor readAccessor) { this.readAccessor = readAccessor; return this; } - public BuilderFromProperty presenceChecker(Accessor presenceChecker) { + public BuilderFromProperty presenceChecker(PresenceCheckAccessor presenceChecker) { this.presenceChecker = presenceChecker; return this; } @@ -480,10 +448,10 @@ public List push(TypeFactory typeFactory, FormattingMessager me PropertyEntry deepestProperty = getDeepestProperty(); if ( deepestProperty != null ) { Type type = deepestProperty.getType(); - Map newDeepestReadAccessors = type.getPropertyReadAccessors(); + Map newDeepestReadAccessors = type.getPropertyReadAccessors(); String parameterName = getParameter().getName(); String deepestPropertyFullName = deepestProperty.getFullName(); - for ( Map.Entry newDeepestReadAccessorEntry : newDeepestReadAccessors.entrySet() ) { + for ( Map.Entry newDeepestReadAccessorEntry : newDeepestReadAccessors.entrySet() ) { // Always include the parameter name in the new full name. // Otherwise multi source parameters might be reported incorrectly String newFullName = @@ -500,4 +468,11 @@ public List push(TypeFactory typeFactory, FormattingMessager me return result; } + public SourceReference withParameter(Parameter parameter) { + if ( parameter == null || getParameter() == parameter ) { + return this; + } + return new SourceReference( parameter, getPropertyEntries(), isValid() ); + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java index 39baf29a56..1988e4f90b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.internal.model.common; import java.util.Set; - import javax.lang.model.element.Modifier; /** @@ -15,7 +14,7 @@ * @author Andreas Gudian */ public enum Accessibility { - PRIVATE( "private" ), DEFAULT( "" ), PROTECTED( "protected" ), PUBLIC( "public" ); + PRIVATE( "private" ), PACKAGE_PRIVATE( "" ), PROTECTED( "protected" ), PUBLIC( "public" ); private final String keyword; @@ -38,6 +37,6 @@ else if ( modifiers.contains( Modifier.PRIVATE ) ) { return PRIVATE; } - return DEFAULT; + return PACKAGE_PRIVATE; } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Assignment.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Assignment.java index 293df9516b..7ed0140d81 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Assignment.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Assignment.java @@ -147,7 +147,6 @@ public boolean isConverted() { */ void setSourceLoopVarName(String sourceLoopVarName); - /** * Returns whether the type of assignment * diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java index b94bb82efd..2b9c6d626b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java @@ -9,11 +9,14 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.TypeUtils; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.spi.BuilderInfo; /** + * Represents the information about a builder. + * How it can be constructed, the type it is building etc. + * * @author Filip Hrisafov */ public class BuilderType { @@ -105,7 +108,7 @@ else if ( typeUtils.isSameType( builder.getTypeMirror(), builderCreationOwner ) // When the builderCreationMethod is constructor, its return type is Void. In this case the // builder type should be the owner type. - if (builderInfo.getBuilderCreationMethod().getKind() == ElementKind.CONSTRUCTOR) { + if ( builderInfo.getBuilderCreationMethod().getKind() == ElementKind.CONSTRUCTOR ) { builder = owner; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ConversionContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ConversionContext.java index c2aa73f307..d3a29e30a4 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ConversionContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ConversionContext.java @@ -21,6 +21,13 @@ public interface ConversionContext { */ Type getTargetType(); + /** + * Returns the source type of this conversion. + * + * @return The source type of this conversion. + */ + Type getSourceType(); + /** * Returns the date format if this conversion or built-in method is from String to a date type (e.g. {@link Date}) * or vice versa. @@ -32,6 +39,8 @@ public interface ConversionContext { String getNumberFormat(); + String getLocale(); + TypeFactory getTypeFactory(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactory.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactory.java index 09e4957aad..4d54f3c3e9 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactory.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactory.java @@ -61,13 +61,8 @@ else if ( isJodaDateTimeSupposed( sourceType, targetType ) ) { dateFormatValidator = new JodaTimeDateFormatValidator(); } else { - dateFormatValidator = new DateFormatValidator() { - @Override - public DateFormatValidationResult validate(String dateFormat) { - return new DateFormatValidationResult( true, Message.GENERAL_UNSUPPORTED_DATE_FORMAT_CHECK, - sourceType, targetType ); - } - }; + dateFormatValidator = dateFormat -> new DateFormatValidationResult( + true, Message.GENERAL_UNSUPPORTED_DATE_FORMAT_CHECK, sourceType, targetType ); } return dateFormatValidator; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/DefaultConversionContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/DefaultConversionContext.java index f5a9fcc761..4e5eed47a8 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/DefaultConversionContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/DefaultConversionContext.java @@ -21,6 +21,7 @@ public class DefaultConversionContext implements ConversionContext { private final FormattingParameters formattingParameters; private final String dateFormat; private final String numberFormat; + private final String locale; private final TypeFactory typeFactory; public DefaultConversionContext(TypeFactory typeFactory, FormattingMessager messager, Type sourceType, @@ -32,6 +33,7 @@ public DefaultConversionContext(TypeFactory typeFactory, FormattingMessager mess this.formattingParameters = formattingParameters; this.dateFormat = this.formattingParameters.getDate(); this.numberFormat = this.formattingParameters.getNumber(); + this.locale = this.formattingParameters.getLocale(); validateDateFormat(); } @@ -59,11 +61,21 @@ public Type getTargetType() { return targetType; } + @Override + public Type getSourceType() { + return sourceType; + } + @Override public String getNumberFormat() { return numberFormat; } + @Override + public String getLocale() { + return locale != null ? locale.toString() : null; + } + @Override public String getDateFormat() { return dateFormat; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/FormattingParameters.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/FormattingParameters.java index e21f5d74fa..7ef818630a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/FormattingParameters.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/FormattingParameters.java @@ -10,26 +10,29 @@ import javax.lang.model.element.Element; /** + * Represent information about the configured formatting. * * @author Sjaak Derksen */ public class FormattingParameters { - public static final FormattingParameters EMPTY = new FormattingParameters( null, null, null, null, null ); + public static final FormattingParameters EMPTY = new FormattingParameters( null, null, null, null, null, null ); private final String date; private final String number; private final AnnotationMirror mirror; private final AnnotationValue dateAnnotationValue; private final Element element; + private final String locale; public FormattingParameters(String date, String number, AnnotationMirror mirror, - AnnotationValue dateAnnotationValue, Element element) { + AnnotationValue dateAnnotationValue, Element element, String locale) { this.date = date; this.number = number; this.mirror = mirror; this.dateAnnotationValue = dateAnnotationValue; this.element = element; + this.locale = locale; } public String getDate() { @@ -51,4 +54,8 @@ public AnnotationValue getDateAnnotationValue() { public Element getElement() { return element; } + + public String getLocale() { + return locale; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ImplementationType.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ImplementationType.java index 45b73c492f..af8c7ecfb3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ImplementationType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ImplementationType.java @@ -16,23 +16,34 @@ public class ImplementationType { private final Type type; private final boolean initialCapacityConstructor; private final boolean loadFactorAdjustment; + private final String factoryMethodName; - private ImplementationType(Type type, boolean initialCapacityConstructor, boolean loadFactorAdjustment) { + private ImplementationType( + Type type, + boolean initialCapacityConstructor, + boolean loadFactorAdjustment, + String factoryMethodName + ) { this.type = type; this.initialCapacityConstructor = initialCapacityConstructor; this.loadFactorAdjustment = loadFactorAdjustment; + this.factoryMethodName = factoryMethodName; } public static ImplementationType withDefaultConstructor(Type type) { - return new ImplementationType( type, false, false ); + return new ImplementationType( type, false, false, null ); } public static ImplementationType withInitialCapacity(Type type) { - return new ImplementationType( type, true, false ); + return new ImplementationType( type, true, false, null ); } public static ImplementationType withLoadFactorAdjustment(Type type) { - return new ImplementationType( type, true, true ); + return new ImplementationType( type, true, true, null ); + } + + public static ImplementationType withFactoryMethod(Type type, String factoryMethodName) { + return new ImplementationType( type, true, false, factoryMethodName ); } /** @@ -44,7 +55,7 @@ public static ImplementationType withLoadFactorAdjustment(Type type) { * @return a new implementation type with the given {@code type} */ public ImplementationType createNew(Type type) { - return new ImplementationType( type, initialCapacityConstructor, loadFactorAdjustment ); + return new ImplementationType( type, initialCapacityConstructor, loadFactorAdjustment, factoryMethodName ); } /** @@ -71,4 +82,8 @@ public boolean hasInitialCapacityConstructor() { public boolean isLoadFactorAdjustment() { return loadFactorAdjustment; } + + public String getFactoryMethodName() { + return factoryMethodName; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/NegatePresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/NegatePresenceCheck.java new file mode 100644 index 0000000000..78357affb8 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/NegatePresenceCheck.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.common; + +import java.util.Objects; +import java.util.Set; + +/** + * A {@link PresenceCheck} that negates the result of another presence check. + * + * @author Filip Hrisafov + */ +public class NegatePresenceCheck extends ModelElement implements PresenceCheck { + + private final PresenceCheck presenceCheck; + + public NegatePresenceCheck(PresenceCheck presenceCheck) { + this.presenceCheck = presenceCheck; + } + + public PresenceCheck getPresenceCheck() { + return presenceCheck; + } + + @Override + public Set getImportTypes() { + return presenceCheck.getImportTypes(); + } + + @Override + public PresenceCheck negate() { + return presenceCheck; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + NegatePresenceCheck that = (NegatePresenceCheck) o; + return Objects.equals( presenceCheck, that.presenceCheck ); + } + + @Override + public int hashCode() { + return Objects.hash( presenceCheck ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/NewInstanceCreation.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/NewInstanceCreation.java new file mode 100644 index 0000000000..a91d7242cb --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/NewInstanceCreation.java @@ -0,0 +1,63 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.common; + +import java.util.Set; + +/** + * Model element representing the head of a {@code new T<>(...)} expression. It always renders the diamond + * operator for generic types, leaving any type-argument inference to the surrounding Java context. + *

      + * The caller is responsible for emitting the parenthesised argument list after the model. + *

      + * Imports contributed by this element only include the raw constructor type, not its type parameters: the + * generated source never references the parameter classes through this expression, so they are not needed + * here. If the surrounding code references a type parameter elsewhere (e.g. in a variable declaration or a + * method signature), that reference contributes its own imports through its own model element. + * + * @author Filip Hrisafov + */ +public class NewInstanceCreation extends ModelElement { + + private final Type type; + private final Type rawType; + + private NewInstanceCreation(Type type) { + this.type = type; + this.rawType = type.asRawType(); + } + + /** + * Creates a {@link NewInstanceCreation} for the given target type. If the target has an implementation + * type (e.g. {@code Collection} -> {@code ArrayList}), the implementation type is used. + * + * @param targetType the target type to be instantiated; must not be {@code null} + * @return a new model + */ + public static NewInstanceCreation forType(Type targetType) { + Type effective = targetType.getImplementationType() != null + ? targetType.getImplementationType() + : targetType; + return new NewInstanceCreation( effective ); + } + + public Type getType() { + return type; + } + + public Type getRawType() { + return rawType; + } + + public boolean isGeneric() { + return !type.getTypeParameters().isEmpty(); + } + + @Override + public Set getImportTypes() { + return rawType.getImportTypes(); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index dc62018be5..c5091869a6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -14,6 +14,8 @@ import org.mapstruct.ap.internal.gem.ContextGem; import org.mapstruct.ap.internal.gem.MappingTargetGem; +import org.mapstruct.ap.internal.gem.SourcePropertyNameGem; +import org.mapstruct.ap.internal.gem.TargetPropertyNameGem; import org.mapstruct.ap.internal.gem.TargetTypeGem; import org.mapstruct.ap.internal.util.Collections; @@ -31,6 +33,8 @@ public class Parameter extends ModelElement { private final boolean mappingTarget; private final boolean targetType; private final boolean mappingContext; + private final boolean sourcePropertyName; + private final boolean targetPropertyName; private final boolean varArgs; @@ -42,23 +46,33 @@ private Parameter(Element element, Type type, boolean varArgs) { this.mappingTarget = MappingTargetGem.instanceOn( element ) != null; this.targetType = TargetTypeGem.instanceOn( element ) != null; this.mappingContext = ContextGem.instanceOn( element ) != null; + this.sourcePropertyName = SourcePropertyNameGem.instanceOn( element ) != null; + this.targetPropertyName = TargetPropertyNameGem.instanceOn( element ) != null; this.varArgs = varArgs; } - private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext, + private Parameter(String name, String originalName, Type type, boolean mappingTarget, boolean targetType, + boolean mappingContext, + boolean sourcePropertyName, boolean targetPropertyName, boolean varArgs) { this.element = null; this.name = name; - this.originalName = name; + this.originalName = originalName; this.type = type; this.mappingTarget = mappingTarget; this.targetType = targetType; this.mappingContext = mappingContext; + this.sourcePropertyName = sourcePropertyName; + this.targetPropertyName = targetPropertyName; this.varArgs = varArgs; } public Parameter(String name, Type type) { - this( name, type, false, false, false, false ); + this( name, name, type ); + } + + public Parameter(String name, String originalName, Type type) { + this( name, originalName, type, false, false, false, false, false, false ); } public Element getElement() { @@ -94,6 +108,8 @@ private String format() { return ( mappingTarget ? "@MappingTarget " : "" ) + ( targetType ? "@TargetType " : "" ) + ( mappingContext ? "@Context " : "" ) + + ( sourcePropertyName ? "@SourcePropertyName " : "" ) + + ( targetPropertyName ? "@TargetPropertyName " : "" ) + "%s " + name; } @@ -110,10 +126,40 @@ public boolean isMappingContext() { return mappingContext; } + public boolean isTargetPropertyName() { + return targetPropertyName; + } + + public boolean isSourcePropertyName() { + return sourcePropertyName; + } + public boolean isVarArgs() { return varArgs; } + public boolean isSourceParameter() { + return !isMappingTarget() && + !isTargetType() && + !isMappingContext() && + !isSourcePropertyName() && + !isTargetPropertyName(); + } + + public Parameter withName(String name) { + return new Parameter( + name, + this.name, + type, + mappingTarget, + targetType, + mappingContext, + sourcePropertyName, + targetPropertyName, + varArgs + ); + } + @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; @@ -149,11 +195,14 @@ public static Parameter forElementAndType(VariableElement element, Type paramete public static Parameter forForgedMappingTarget(Type parameterType) { return new Parameter( + "mappingTarget", "mappingTarget", parameterType, true, false, false, + false, + false, false ); } @@ -195,8 +244,12 @@ public static Parameter getTargetTypeParameter(List parameters) { return parameters.stream().filter( Parameter::isTargetType ).findAny().orElse( null ); } - private static boolean isSourceParameter( Parameter parameter ) { - return !parameter.isMappingTarget() && !parameter.isTargetType() && !parameter.isMappingContext(); + public static Parameter getSourcePropertyNameParameter(List parameters) { + return parameters.stream().filter( Parameter::isSourcePropertyName ).findAny().orElse( null ); + } + + public static Parameter getTargetPropertyNameParameter(List parameters) { + return parameters.stream().filter( Parameter::isTargetPropertyName ).findAny().orElse( null ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java index 36e4d3e864..c1a594c73a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java @@ -6,7 +6,9 @@ package org.mapstruct.ap.internal.model.common; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Set; @@ -19,18 +21,14 @@ public class ParameterBinding { private final Type type; private final String variableName; - private final boolean targetType; - private final boolean mappingTarget; - private final boolean mappingContext; private final SourceRHS sourceRHS; + private final Collection bindingTypes; - private ParameterBinding(Type parameterType, String variableName, boolean mappingTarget, boolean targetType, - boolean mappingContext, SourceRHS sourceRHS) { + private ParameterBinding(Type parameterType, String variableName, Collection bindingTypes, + SourceRHS sourceRHS) { this.type = parameterType; this.variableName = variableName; - this.targetType = targetType; - this.mappingTarget = mappingTarget; - this.mappingContext = mappingContext; + this.bindingTypes = bindingTypes; this.sourceRHS = sourceRHS; } @@ -41,25 +39,47 @@ public String getVariableName() { return variableName; } + public boolean isSourceParameter() { + return bindingTypes.contains( BindingType.PARAMETER ); + } + /** * @return {@code true}, if the parameter being bound is a {@code @TargetType} parameter. */ public boolean isTargetType() { - return targetType; + return bindingTypes.contains( BindingType.TARGET_TYPE ); } /** * @return {@code true}, if the parameter being bound is a {@code @MappingTarget} parameter. */ public boolean isMappingTarget() { - return mappingTarget; + return bindingTypes.contains( BindingType.MAPPING_TARGET ); } /** * @return {@code true}, if the parameter being bound is a {@code @MappingContext} parameter. */ public boolean isMappingContext() { - return mappingContext; + return bindingTypes.contains( BindingType.CONTEXT ); + } + + public boolean isForSourceRhs() { + return bindingTypes.contains( BindingType.SOURCE_RHS ); + } + + /** + * @return {@code true}, if the parameter being bound is a {@code @SourcePropertyName} parameter. + */ + public boolean isSourcePropertyName() { + return bindingTypes.contains( BindingType.SOURCE_PROPERTY_NAME ); + } + + /** + * @return {@code true}, if the parameter being bound is a {@code @TargetPropertyName} parameter. + */ + public boolean isTargetPropertyName() { + return bindingTypes.contains( BindingType.TARGET_PROPERTY_NAME ); } /** @@ -77,7 +97,7 @@ public SourceRHS getSourceRHS() { } public Set getImportTypes() { - if ( targetType ) { + if ( isTargetType() ) { return type.getImportTypes(); } @@ -93,12 +113,31 @@ public Set getImportTypes() { * @return a parameter binding reflecting the given parameter as being used as argument for a method call */ public static ParameterBinding fromParameter(Parameter parameter) { + EnumSet bindingTypes = EnumSet.of( BindingType.PARAMETER ); + if ( parameter.isMappingTarget() ) { + bindingTypes.add( BindingType.MAPPING_TARGET ); + } + + if ( parameter.isTargetType() ) { + bindingTypes.add( BindingType.TARGET_TYPE ); + } + + if ( parameter.isMappingContext() ) { + bindingTypes.add( BindingType.CONTEXT ); + } + + if ( parameter.isSourcePropertyName() ) { + bindingTypes.add( BindingType.SOURCE_PROPERTY_NAME ); + } + + if ( parameter.isTargetPropertyName() ) { + bindingTypes.add( BindingType.TARGET_PROPERTY_NAME ); + } + return new ParameterBinding( parameter.getType(), parameter.getName(), - parameter.isMappingTarget(), - parameter.isTargetType(), - parameter.isMappingContext(), + bindingTypes, null ); } @@ -115,9 +154,7 @@ public static ParameterBinding fromTypeAndName(Type parameterType, String parame return new ParameterBinding( parameterType, parameterName, - false, - false, - false, + Collections.emptySet(), null ); } @@ -127,7 +164,31 @@ public static ParameterBinding fromTypeAndName(Type parameterType, String parame * @return a parameter binding representing a target type parameter */ public static ParameterBinding forTargetTypeBinding(Type classTypeOf) { - return new ParameterBinding( classTypeOf, null, false, true, false, null ); + return new ParameterBinding( classTypeOf, null, Collections.singleton( BindingType.TARGET_TYPE ), null ); + } + + /** + * @return a parameter binding representing a target property name parameter + */ + public static ParameterBinding forTargetPropertyNameBinding(Type classTypeOf) { + return new ParameterBinding( + classTypeOf, + null, + Collections.singleton( BindingType.TARGET_PROPERTY_NAME ), + null + ); + } + + /** + * @return a parameter binding representing a source property name parameter + */ + public static ParameterBinding forSourcePropertyNameBinding(Type classTypeOf) { + return new ParameterBinding( + classTypeOf, + null, + Collections.singleton( BindingType.SOURCE_PROPERTY_NAME ), + null + ); } /** @@ -135,7 +196,7 @@ public static ParameterBinding forTargetTypeBinding(Type classTypeOf) { * @return a parameter binding representing a mapping target parameter */ public static ParameterBinding forMappingTargetBinding(Type resultType) { - return new ParameterBinding( resultType, null, true, false, false, null ); + return new ParameterBinding( resultType, null, Collections.singleton( BindingType.MAPPING_TARGET ), null ); } /** @@ -143,10 +204,27 @@ public static ParameterBinding forMappingTargetBinding(Type resultType) { * @return a parameter binding representing a mapping source type */ public static ParameterBinding forSourceTypeBinding(Type sourceType) { - return new ParameterBinding( sourceType, null, false, false, false, null ); + return new ParameterBinding( sourceType, null, Collections.singleton( BindingType.SOURCE_TYPE ), null ); } public static ParameterBinding fromSourceRHS(SourceRHS sourceRHS) { - return new ParameterBinding( sourceRHS.getSourceType(), null, false, false, false, sourceRHS ); + return new ParameterBinding( + sourceRHS.getSourceType(), + null, + Collections.singleton( BindingType.SOURCE_RHS ), + sourceRHS + ); + } + + enum BindingType { + PARAMETER, + FROM_TYPE_AND_NAME, + TARGET_TYPE, + TARGET_PROPERTY_NAME, + SOURCE_PROPERTY_NAME, + MAPPING_TARGET, + CONTEXT, + SOURCE_TYPE, + SOURCE_RHS } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/PresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/PresenceCheck.java index e77e24f218..ec286480d9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/PresenceCheck.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/PresenceCheck.java @@ -21,4 +21,6 @@ public interface PresenceCheck { */ Set getImportTypes(); + PresenceCheck negate(); + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java index b73d1ecf44..b4d422c797 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/SourceRHS.java @@ -149,6 +149,12 @@ public Type getSourceTypeForMatching() { else if ( sourceType.isStreamType() ) { return first( sourceType.determineTypeArguments( Stream.class ) ); } + else if ( sourceType.isArrayType() ) { + return sourceType.getComponentType(); + } + else if ( sourceType.isIterableType() ) { + return first( sourceType.determineTypeArguments( Iterable.class ) ); + } } return sourceType; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java index 4da9128f98..0c02870352 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java @@ -5,27 +5,37 @@ */ package org.mapstruct.ap.internal.model.common; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Deque; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; - +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -34,18 +44,33 @@ import javax.lang.model.util.ElementFilter; import javax.lang.model.util.SimpleTypeVisitor8; +import kotlin.Metadata; +import kotlin.metadata.Attributes; +import kotlin.metadata.KmClass; +import kotlin.metadata.KmConstructor; +import kotlin.metadata.Modality; +import kotlin.metadata.jvm.JvmExtensionsKt; +import kotlin.metadata.jvm.JvmMethodSignature; +import kotlin.metadata.jvm.KotlinClassMetadata; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.util.AccessorNamingUtils; import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.Executables; import org.mapstruct.ap.internal.util.Filters; +import org.mapstruct.ap.internal.util.JSpecifyConstants; import org.mapstruct.ap.internal.util.JavaStreamConstants; import org.mapstruct.ap.internal.util.NativeTypes; import org.mapstruct.ap.internal.util.Nouns; import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; +import org.mapstruct.ap.internal.util.accessor.ElementAccessor; +import org.mapstruct.ap.internal.util.accessor.MapValueAccessor; +import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; +import org.mapstruct.ap.internal.util.kotlin.KotlinMetadata; +import static java.util.Collections.emptyList; import static org.mapstruct.ap.internal.util.Collections.first; /** @@ -57,8 +82,32 @@ * through {@link TypeFactory}. * * @author Gunnar Morling + * @author Filip Hrisafov */ public class Type extends ModelElement implements Comparable { + private static final Method SEALED_PERMITTED_SUBCLASSES_METHOD; + private static final boolean KOTLIN_METADATA_JVM_PRESENT; + + static { + Method permittedSubclassesMethod; + try { + permittedSubclassesMethod = TypeElement.class.getMethod( "getPermittedSubclasses" ); + } + catch ( NoSuchMethodException e ) { + permittedSubclassesMethod = null; + } + SEALED_PERMITTED_SUBCLASSES_METHOD = permittedSubclassesMethod; + + boolean kotlinMetadataJvmPresent; + try { + Class.forName( "kotlin.metadata.jvm.KotlinClassMetadata", false, ModelElement.class.getClassLoader() ); + kotlinMetadataJvmPresent = true; + } + catch ( ClassNotFoundException e ) { + kotlinMetadataJvmPresent = false; + } + KOTLIN_METADATA_JVM_PRESENT = kotlinMetadataJvmPresent; + } private final TypeUtils typeUtils; private final ElementUtils elementUtils; @@ -71,9 +120,11 @@ public class Type extends ModelElement implements Comparable { private final ImplementationType implementationType; private final Type componentType; + private final Type topLevelType; private final String packageName; private final String name; + private final String nameWithTopLevelTypeName; private final String qualifiedName; private final boolean isInterface; @@ -93,8 +144,8 @@ public class Type extends ModelElement implements Comparable { private final Map notToBeImportedTypes; private Boolean isToBeImported; - private Map readAccessors = null; - private Map presenceCheckers = null; + private Map readAccessors = null; + private Map presenceCheckers = null; private List allMethods = null; private List allFields = null; @@ -103,13 +154,16 @@ public class Type extends ModelElement implements Comparable { private List setters = null; private List adders = null; private List alternativeTargetAccessors = null; - private Map constructorAccessors = null; private Type boundingBase = null; + private List boundTypes = null; private Type boxedEquivalent = null; private Boolean hasAccessibleConstructor; + private Boolean isNullMarked; + private KotlinMetadata kotlinMetadata; + private boolean kotlinMetadataInitialized; private final Filters filters; @@ -172,6 +226,19 @@ public Type(TypeUtils typeUtils, ElementUtils elementUtils, TypeFactory typeFact this.filters = new Filters( accessorNaming, typeUtils, typeMirror ); this.loggingVerbose = loggingVerbose; + + TypeElement typeElementForTopLevel; + if ( Boolean.TRUE.equals( isToBeImported ) ) { + // If the is to be imported is explicitly set to true then we shouldn't look for the top level type + typeElementForTopLevel = null; + } + else { + // The top level type for an array type is the top level type of the component type + typeElementForTopLevel = + this.componentType == null ? this.typeElement : this.componentType.getTypeElement(); + } + this.topLevelType = topLevelType( typeElementForTopLevel, this.typeFactory ); + this.nameWithTopLevelTypeName = nameWithTopLevelTypeName( typeElementForTopLevel, this.name ); } //CHECKSTYLE:ON @@ -199,11 +266,33 @@ public String getName() { *

      * If the {@code java.time} variant is referred to first, the {@code java.time.LocalDateTime} will be imported * and the {@code org.joda} variant will be referred to with its FQN. + *

      + * If the type is nested and its top level type is to be imported + * then the name including its top level type will be returned. * - * @return Just the name if this {@link Type} will be imported, otherwise the fully-qualified name. + * @return Just the name if this {@link Type} will be imported, the name up to the top level {@link Type} + * (if the top level type is important, otherwise the fully-qualified name. */ public String createReferenceName() { - return isToBeImported() ? name : ( shouldUseSimpleName() ? name : qualifiedName ); + if ( isToBeImported() ) { + // isToBeImported() returns true for arrays. + // Therefore, we need to check the top level type when creating the reference + if ( isTopLevelTypeToBeImported() ) { + return nameWithTopLevelTypeName != null ? nameWithTopLevelTypeName : name; + } + + return name; + } + + if ( shouldUseSimpleName() ) { + return name; + } + + if ( isTopLevelTypeToBeImported() && nameWithTopLevelTypeName != null ) { + return nameWithTopLevelTypeName; + } + + return qualifiedName; } public List getTypeParameters() { @@ -238,6 +327,53 @@ public boolean isString() { return String.class.getName().equals( getFullyQualifiedName() ); } + /** + * Whether this type is within a JSpecify {@code @NullMarked} scope. Walks the enclosing-element + * chain (this type, outer classes, package) and returns at the first {@code @NullMarked} or + * {@code @NullUnmarked} encountered — the closest annotation wins. Module-level annotations + * are only reached when the compiler populates {@code PackageElement.getEnclosingElement()} + * with a {@link javax.lang.model.element.ModuleElement} (JPMS only). + *

      + * The result is memoized on this {@code Type} instance. {@link TypeFactory#getType} does not + * intern {@code Type} instances, so callers that invoke this repeatedly should cache the + * {@code Type} reference or the result. + * + * @return {@code true} if the closest enclosing annotation is {@code @NullMarked}; + * {@code false} if it is {@code @NullUnmarked} or if no such annotation was found + */ + public boolean isNullMarked() { + if ( isNullMarked == null ) { + isNullMarked = resolveNullMarked(); + } + return isNullMarked; + } + + private boolean resolveNullMarked() { + if ( typeElement == null ) { + return false; + } + Element current = typeElement; + while ( current != null ) { + for ( AnnotationMirror mirror : current.getAnnotationMirrors() ) { + Element annotationElement = mirror.getAnnotationType().asElement(); + if ( !( annotationElement instanceof TypeElement ) ) { + // Defensive: unresolved annotations (e.g. ErrorType during incremental + // builds) can produce a non-TypeElement. Skip instead of crashing. + continue; + } + String fqn = ( (TypeElement) annotationElement ).getQualifiedName().toString(); + if ( JSpecifyConstants.NULL_MARKED_FQN.equals( fqn ) ) { + return true; + } + if ( JSpecifyConstants.NULL_UNMARKED_FQN.equals( fqn ) ) { + return false; + } + } + current = current.getEnclosingElement(); + } + return false; + } + /** * @return this type's enum constants in case it is an enum, an empty list otherwise. */ @@ -288,6 +424,17 @@ public boolean isMapType() { return isMapType; } + private boolean hasStringMapSignature() { + if ( isMapType() ) { + List typeParameters = getTypeParameters(); + if ( typeParameters.size() == 2 && typeParameters.get( 0 ).isString() ) { + return true; + } + } + + return false; + } + public boolean isCollectionOrMapType() { return isCollectionType || isMapType; } @@ -296,10 +443,44 @@ public boolean isArrayType() { return componentType != null; } + private boolean isType(Class type) { + return type.getName().equals( getFullyQualifiedName() ); + } + + public boolean isOptionalType() { + return isType( Optional.class ) || isType( OptionalInt.class ) || isType( OptionalDouble.class ) || + isType( OptionalLong.class ); + } + + public Type getOptionalBaseType() { + if ( isType( Optional.class ) ) { + return getTypeParameters().get( 0 ); + } + + if ( isType( OptionalInt.class ) ) { + return typeFactory.getType( int.class ); + } + + if ( isType( OptionalDouble.class ) ) { + return typeFactory.getType( double.class ); + } + + if ( isType( OptionalLong.class ) ) { + return typeFactory.getType( long.class ); + } + + throw new IllegalStateException( "getOptionalBaseType should only be called for Optional types." ); + + } + public boolean isTypeVar() { return (typeMirror.getKind() == TypeKind.TYPEVAR); } + public boolean isIntersection() { + return typeMirror.getKind() == TypeKind.INTERSECTION; + } + public boolean isJavaLangType() { return packageName != null && packageName.startsWith( "java." ); } @@ -320,7 +501,7 @@ public boolean isStreamType() { /** * A wild card type can have two types of bounds (mutual exclusive): extends and super. * - * @return true if the bound has a wild card super bound (e.g. ? super Number) + * @return true if the bound has a wild card super bound (e.g. {@code ? super Number}) */ public boolean hasSuperBound() { boolean result = false; @@ -334,7 +515,7 @@ public boolean hasSuperBound() { /** * A wild card type can have two types of bounds (mutual exclusive): extends and super. * - * @return true if the bound has a wild card super bound (e.g. ? extends Number) + * @return true if the bound has a wild card extends bound (e.g. {@code ? extends Number}) */ public boolean hasExtendsBound() { boolean result = false; @@ -402,6 +583,10 @@ public Set getImportTypes() { result.addAll( componentType.getImportTypes() ); } + if ( topLevelType != null ) { + result.addAll( topLevelType.getImportTypes() ); + } + for ( Type parameter : typeParameters ) { result.addAll( parameter.getImportTypes() ); } @@ -413,6 +598,10 @@ public Set getImportTypes() { return result; } + protected boolean isTopLevelTypeToBeImported() { + return topLevelType != null && topLevelType.isToBeImported(); + } + /** * Whether this type is to be imported by means of an import statement in the currently generated source file * (it can be referenced in the generated source using its simple name) or not (referenced using the FQN). @@ -435,7 +624,7 @@ public boolean isToBeImported() { isToBeImported = true; } } - else { + else if ( typeElement == null || !typeElement.getNestingKind().isNested() ) { toBeImportedTypes.put( trimmedName, trimmedQualifiedName ); isToBeImported = true; } @@ -444,8 +633,11 @@ public boolean isToBeImported() { } private boolean shouldUseSimpleName() { - String fqn = notToBeImportedTypes.get( name ); - return this.qualifiedName.equals( fqn ); + // Using trimSimpleClassName since the same is used in the isToBeImported() + // to check whether notToBeImportedTypes contains it + String trimmedName = trimSimpleClassName( name ); + String fqn = notToBeImportedTypes.get( trimmedName ); + return trimSimpleClassName( this.qualifiedName ).equals( fqn ); } public Type erasure() { @@ -476,6 +668,80 @@ public Type erasure() { ); } + public Type replaceSuperBoundWith( Type compare, Type replacement ) { + if ( typeParameters.isEmpty() ) { + return this; + } + List targetTypeParameters = compare.getTypeParameters(); + if ( targetTypeParameters.size() != typeParameters.size() ) { + return this; + } + TypeMirror replacementMirror = replacement.getTypeMirror(); + boolean noChange = true; + List bounds = new ArrayList<>( typeParameters.size() ); + TypeMirror[] mirrors = new TypeMirror[ typeParameters.size() ]; + for ( int x = 0; x < typeParameters.size(); x++ ) { + Type type = typeParameters.get( x ); + if ( !type.hasSuperBound() || type.isRawAssignableTo( targetTypeParameters.get( x ) ) ) { + bounds.add( type ); + mirrors[ x ] = type.getTypeMirror(); + } + else { + bounds.add( replacement ); + mirrors[x] = replacementMirror; + noChange = false; + } + } + + if ( noChange ) { + return this; + } + + TypeElement typeElementWithoutBounds; + Type componentTypeWithoutBounds; + TypeMirror typeMirrorWithoutBounds; + if ( this.componentType != null ) { + typeElementWithoutBounds = null; + componentTypeWithoutBounds = this.componentType.replaceSuperBoundWith( compare, replacement ); + typeMirrorWithoutBounds = typeUtils.getArrayType( componentTypeWithoutBounds.getTypeMirror() ); + } + else { + DeclaredType declaredType = typeUtils.getDeclaredType( + typeElement, + mirrors + ); + typeMirrorWithoutBounds = declaredType; + typeElementWithoutBounds = (TypeElement) declaredType.asElement(); + componentTypeWithoutBounds = null; + } + + return new Type( + typeUtils, + elementUtils, + typeFactory, + accessorNaming, + typeMirrorWithoutBounds, + typeElementWithoutBounds, + bounds, + implementationType, + componentTypeWithoutBounds, + packageName, + name, + qualifiedName, + isInterface, + isEnumType, + isIterableType, + isCollectionType, + isMapType, + isStream, + toBeImportedTypes, + notToBeImportedTypes, + isToBeImported, + isLiteral, + loggingVerbose + ); + } + public Type withoutBounds() { if ( typeParameters.isEmpty() ) { return this; @@ -488,20 +754,34 @@ public Type withoutBounds() { mirrors.add( typeParameter.getTypeBound().getTypeMirror() ); } - DeclaredType declaredType = typeUtils.getDeclaredType( - typeElement, - mirrors.toArray( new TypeMirror[] {} ) - ); + TypeElement typeElementWithoutBounds; + Type componentTypeWithoutBounds; + TypeMirror typeMirrorWithoutBounds; + if ( this.componentType != null ) { + typeElementWithoutBounds = null; + componentTypeWithoutBounds = this.componentType.withoutBounds(); + typeMirrorWithoutBounds = typeUtils.getArrayType( componentTypeWithoutBounds.getTypeMirror() ); + } + else { + DeclaredType declaredType = typeUtils.getDeclaredType( + typeElement, + mirrors.toArray( new TypeMirror[] {} ) + ); + typeMirrorWithoutBounds = declaredType; + typeElementWithoutBounds = (TypeElement) declaredType.asElement(); + componentTypeWithoutBounds = null; + } + return new Type( typeUtils, elementUtils, typeFactory, accessorNaming, - declaredType, - (TypeElement) declaredType.asElement(), + typeMirrorWithoutBounds, + typeElementWithoutBounds, bounds, implementationType, - componentType, + componentTypeWithoutBounds, packageName, name, qualifiedName, @@ -519,6 +799,21 @@ public Type withoutBounds() { ); } + private Type replaceGeneric(Type oldGenericType, Type newType) { + if ( !typeParameters.contains( oldGenericType ) || newType == null || oldGenericType.equals( newType ) ) { + return this; + } + newType = newType.getBoxedEquivalent(); + TypeMirror[] replacedTypeMirrors = new TypeMirror[typeParameters.size()]; + for ( int i = 0; i < typeParameters.size(); i++ ) { + Type typeParameter = typeParameters.get( i ); + replacedTypeMirrors[i] = + typeParameter.equals( oldGenericType ) ? newType.typeMirror : typeParameter.typeMirror; + } + + return typeFactory.getType( typeUtils.getDeclaredType( typeElement, replacedTypeMirrors ) ); + } + /** * Whether this type is assignable to the given other type, considering the "extends / upper bounds" * as well. @@ -528,11 +823,14 @@ public Type withoutBounds() { * @return {@code true} if and only if this type is assignable to the given other type. */ public boolean isAssignableTo(Type other) { + TypeMirror otherMirror = other.typeMirror; + if ( otherMirror.getKind() == TypeKind.WILDCARD ) { + otherMirror = typeUtils.erasure( other.typeMirror ); + } if ( TypeKind.WILDCARD == typeMirror.getKind() ) { - return typeUtils.contains( typeMirror, other.typeMirror ); + return typeUtils.contains( typeMirror, otherMirror ); } - - return typeUtils.isAssignable( typeMirror, other.typeMirror ); + return typeUtils.isAssignable( typeMirror, otherMirror ); } /** @@ -566,21 +864,64 @@ public Type asRawType() { } } + public ReadAccessor getReadAccessor(String propertyName, boolean allowedMapToBean) { + if ( allowedMapToBean && hasStringMapSignature() ) { + ExecutableElement getMethod = getAllMethods() + .stream() + .filter( m -> m.getSimpleName().contentEquals( "get" ) ) + .filter( m -> m.getParameters().size() == 1 ) + .findAny() + .orElse( null ); + return new MapValueAccessor( getMethod, typeParameters.get( 1 ).getTypeMirror(), propertyName ); + } + + Map readAccessors = getPropertyReadAccessors(); + + return readAccessors.get( propertyName ); + } + + public PresenceCheckAccessor getPresenceChecker(String propertyName) { + if ( hasStringMapSignature() ) { + return PresenceCheckAccessor.mapContainsKey( propertyName ); + } + + Map presenceCheckers = getPropertyPresenceCheckers(); + return presenceCheckers.get( propertyName ); + } + /** * getPropertyReadAccessors * * @return an unmodifiable map of all read accessors (including 'is' for booleans), indexed by property name */ - public Map getPropertyReadAccessors() { + public Map getPropertyReadAccessors() { if ( readAccessors == null ) { - List getterList = filters.getterMethodsIn( getAllMethods() ); - Map modifiableGetters = new LinkedHashMap<>(); - for ( Accessor getter : getterList ) { + + Map recordAccessors = filters.recordAccessorsIn( getRecordComponents() ); + Map modifiableGetters = new LinkedHashMap<>(recordAccessors); + + List getterList = filters.getterMethodsIn( getAllMethods() ); + for ( ReadAccessor getter : getterList ) { + String simpleName = getter.getSimpleName(); + if ( recordAccessors.containsKey( simpleName ) ) { + // If there is already a record accessor that contains the simple name + // then it means that the getter is actually a record component. + // In that case we need to ignore it. + // e.g. record component named isActive. + // The DefaultAccessorNamingStrategy will return active as property name, + // but the property name is isActive, since it is a record + continue; + } String propertyName = getPropertyName( getter ); + + if ( recordAccessors.containsKey( propertyName ) ) { + // If there is already a record accessor, the property needs to be ignored + continue; + } if ( modifiableGetters.containsKey( propertyName ) ) { // In the DefaultAccessorNamingStrategy, this can only be the case for Booleans: isFoo() and // getFoo(); The latter is preferred. - if ( !getter.getSimpleName().startsWith( "is" ) ) { + if ( !simpleName.startsWith( "is" ) ) { modifiableGetters.put( propertyName, getter ); } @@ -590,13 +931,8 @@ public Map getPropertyReadAccessors() { } } - Map recordAccessors = filters.recordAccessorsIn( getRecordComponents() ); - for ( Map.Entry recordEntry : recordAccessors.entrySet() ) { - modifiableGetters.putIfAbsent( recordEntry.getKey(), recordEntry.getValue() ); - } - - List fieldsList = filters.fieldsIn( getAllFields() ); - for ( Accessor field : fieldsList ) { + List fieldsList = filters.fieldsIn( getAllFields(), ReadAccessor::fromField ); + for ( ReadAccessor field : fieldsList ) { String propertyName = getPropertyName( field ); // If there was no getter or is method for booleans, then resort to the field. // If a field was already added do not add it again. @@ -612,12 +948,15 @@ public Map getPropertyReadAccessors() { * * @return an unmodifiable map of all presence checkers, indexed by property name */ - public Map getPropertyPresenceCheckers() { + public Map getPropertyPresenceCheckers() { if ( presenceCheckers == null ) { - List checkerList = filters.presenceCheckMethodsIn( getAllMethods() ); - Map modifiableCheckers = new LinkedHashMap<>(); - for ( Accessor checker : checkerList ) { - modifiableCheckers.put( getPropertyName( checker ), checker ); + List checkerList = filters.presenceCheckMethodsIn( getAllMethods() ); + Map modifiableCheckers = new LinkedHashMap<>(); + for ( ExecutableElement checker : checkerList ) { + modifiableCheckers.put( + getPropertyName( checker ), + PresenceCheckAccessor.methodInvocation( checker ) + ); } presenceCheckers = Collections.unmodifiableMap( modifiableCheckers ); } @@ -638,6 +977,10 @@ public Map getPropertyPresenceCheckers() { * @return an unmodifiable map of all write accessors indexed by property name */ public Map getPropertyWriteAccessors( CollectionMappingStrategyGem cmStrategy ) { + if ( isRecord() ) { + // Records do not have setters, so we return an empty map + return Collections.emptyMap(); + } // collect all candidate target accessors List candidates = new ArrayList<>( getSetters() ); candidates.addAll( getAlternativeTargetAccessors() ); @@ -675,6 +1018,7 @@ else if ( candidate.getAccessorType() == AccessorType.GETTER ) { // an adder has been found (according strategy) so overrule current choice. candidate = adderMethod; } + } else if ( candidate.getAccessorType() == AccessorType.FIELD && ( Executables.isFinal( candidate ) || result.containsKey( targetPropertyName ) ) ) { @@ -682,6 +1026,16 @@ else if ( candidate.getAccessorType() == AccessorType.FIELD && ( Executables.is continue; } + if ( candidate.getAccessorType() == AccessorType.GETTER ) { + // When the candidate is a getter then it can't be used in the following cases: + // 1. The collection mapping strategy is target immutable + // 2. The target type is a stream (streams are immutable) + if ( cmStrategy == CollectionMappingStrategyGem.TARGET_IMMUTABLE || + targetType != null && targetType.isStreamType() ) { + continue; + } + } + Accessor previousCandidate = result.get( targetPropertyName ); if ( previousCandidate == null || preferredType == null || ( targetType != null && typeUtils.isAssignable( preferredType.getTypeMirror(), targetType.getTypeMirror() ) ) ) { @@ -746,13 +1100,17 @@ private List nullSafeTypeElementListConversion(Function getAccessorCandidates(Type property, Class superclass) List adderList = getAdders(); List candidateList = new ArrayList<>(); for ( Accessor adder : adderList ) { - ExecutableElement executable = (ExecutableElement) adder.getElement(); - VariableElement arg = executable.getParameters().get( 0 ); - if ( typeUtils.isSameType( boxed( arg.asType() ), boxed( typeArg ) ) ) { + TypeMirror adderParameterType = determineTargetType( adder ).getTypeMirror(); + if ( typeUtils.isSameType( boxed( adderParameterType ), boxed( typeArg ) ) ) { candidateList.add( adder ); } } @@ -842,7 +1199,7 @@ private TypeMirror boxed(TypeMirror possiblePrimitive) { * * @return an unmodifiable list of all setters */ - private List getSetters() { + public List getSetters() { if ( setters == null ) { setters = Collections.unmodifiableList( filters.setterMethodsIn( getAllMethods() ) ); } @@ -874,6 +1231,13 @@ private List getAdders() { * @return an unmodifiable list of alternative target accessors. */ private List getAlternativeTargetAccessors() { + if ( alternativeTargetAccessors != null ) { + return alternativeTargetAccessors; + } + + if ( isRecord() ) { + alternativeTargetAccessors = Collections.emptyList(); + } if ( alternativeTargetAccessors == null ) { @@ -881,7 +1245,7 @@ private List getAlternativeTargetAccessors() { List setterMethods = getSetters(); List readAccessors = new ArrayList<>( getPropertyReadAccessors().values() ); // All the fields are also alternative accessors - readAccessors.addAll( filters.fieldsIn( getAllFields() ) ); + readAccessors.addAll( filters.fieldsIn( getAllFields(), ElementAccessor::new ) ); // there could be a read accessor (field or method) for a list/map that is not present as setter. // an accessor could substitute the setter in that case and act as setter. @@ -1000,6 +1364,10 @@ else if ( !method.getModifiers().contains( Modifier.PUBLIC ) ) { * FTL. */ public String getNull() { + if ( isOptionalType() ) { + return createReferenceName() + ".empty()"; + } + if ( !isPrimitive() || isArrayType() ) { return "null"; } @@ -1073,8 +1441,8 @@ public boolean equals(Object obj) { Type other = (Type) obj; if ( this.isWildCardBoundByTypeVar() && other.isWildCardBoundByTypeVar() ) { - return ( this.hasExtendsBound() == this.hasExtendsBound() - || this.hasSuperBound() == this.hasSuperBound() ) + return ( this.hasExtendsBound() == other.hasExtendsBound() + || this.hasSuperBound() == other.hasSuperBound() ) && typeUtils.isSameType( getTypeBound().getTypeMirror(), other.getTypeBound().getTypeMirror() ); } else { @@ -1101,7 +1469,7 @@ public String describe() { } else { // name allows for inner classes - String name = getFullyQualifiedName().replaceFirst( "^" + getPackageName() + ".", "" ); + String name = getNameKeepingInnerClasses(); List typeParams = getTypeParameters(); if ( typeParams.isEmpty() ) { return name; @@ -1113,6 +1481,15 @@ public String describe() { } } + private String getNameKeepingInnerClasses() { + String packageNamePrefix = getPackageName() + "."; + String fullyQualifiedName = getFullyQualifiedName(); + if ( fullyQualifiedName.startsWith( packageNamePrefix ) ) { + return fullyQualifiedName.substring( packageNamePrefix.length() ); + } + return fullyQualifiedName; + } + /** * * @return an identification that can be used as part in a forged method name. @@ -1146,6 +1523,29 @@ public Type getTypeBound() { return boundingBase; } + public List getTypeBounds() { + if ( this.boundTypes != null ) { + return boundTypes; + } + Type bound = getTypeBound(); + if ( bound == null ) { + this.boundTypes = Collections.emptyList(); + } + else if ( !bound.isIntersection() ) { + this.boundTypes = Collections.singletonList( bound ); + } + else { + List bounds = ( (IntersectionType) bound.typeMirror ).getBounds(); + this.boundTypes = new ArrayList<>( bounds.size() ); + for ( TypeMirror mirror : bounds ) { + boundTypes.add( typeFactory.getType( mirror ) ); + } + } + + return this.boundTypes; + + } + public boolean hasAccessibleConstructor() { if ( hasAccessibleConstructor == null ) { hasAccessibleConstructor = false; @@ -1160,6 +1560,47 @@ public boolean hasAccessibleConstructor() { return hasAccessibleConstructor; } + /** + * Whether this type can be instantiated through an accessible parameterless constructor, i.e. via + * {@code new Type()}. Unlike {@link #hasAccessibleConstructor()} this returns {@code false} for types that + * only expose constructors with parameters (e.g. {@code BigDecimal}). + * + * @return {@code true} if a {@code new Type()} call would compile, {@code false} otherwise + */ + public boolean hasAccessibleParameterlessConstructor() { + if ( typeElement == null || isInterface() || isAbstract() || isEnumType() ) { + return false; + } + List constructors = ElementFilter.constructorsIn( typeElement.getEnclosedElements() ); + if ( constructors.isEmpty() ) { + // no declared constructors means the implicit, accessible default constructor is available + return true; + } + for ( ExecutableElement constructor : constructors ) { + if ( !constructor.getModifiers().contains( Modifier.PRIVATE ) && constructor.getParameters().isEmpty() ) { + return true; + } + } + return false; + } + + public KotlinMetadata getKotlinMetadata() { + if ( !kotlinMetadataInitialized ) { + kotlinMetadataInitialized = true; + if ( typeElement != null && KOTLIN_METADATA_JVM_PRESENT ) { + Metadata metadataAnnotation = typeElement.getAnnotation( Metadata.class ); + if ( metadataAnnotation != null ) { + KotlinClassMetadata classMetadata = KotlinClassMetadata.readLenient( metadataAnnotation ); + if ( classMetadata instanceof KotlinClassMetadata.Class ) { + kotlinMetadata = new KotlinMetadataImpl( (KotlinClassMetadata.Class) classMetadata ); + } + } + } + } + + return kotlinMetadata; + } + /** * Returns the direct supertypes of a type. The interface types, if any, * will appear last in the list. @@ -1210,9 +1651,9 @@ public boolean isLiteral() { /** * Steps through the declaredType in order to find a match for this typeVar Type. It aligns with - * the provided parameterized type where this typeVar type is used. - * - * For example: + * the provided parameterized type where this typeVar type is used.
      + *
      + * For example:

            * {@code
            * this: T
            * declaredType: JAXBElement
      @@ -1225,12 +1666,13 @@ public boolean isLiteral() {
            * parameterizedType: Callable
            * return: BigDecimal
            * }
      +     * 
      * * @param declared the type * @param parameterized the parameterized type * - * @return - the same type when this is not a type var in the broadest sense (T, T[], or ? extends T) - * - the matching parameter in the parameterized type when this is a type var when found + * @return - the same type when this is not a type var in the broadest sense (T, T[], or ? extends T)
      + * - the matching parameter in the parameterized type when this is a type var when found
      * - null in all other cases */ public ResolvedPair resolveParameterToType(Type declared, Type parameterized) { @@ -1241,6 +1683,96 @@ public ResolvedPair resolveParameterToType(Type declared, Type parameterized) { return new ResolvedPair( this, this ); } + /** + * Resolves generic types using the declared and parameterized types as input.
      + *
      + * For example: + *
      +     * {@code
      +     * this: T
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: Integer
      +     *
      +     * this: List
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: List
      +     *
      +     * this: List
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: List
      +     *
      +     * this: List>
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: List>
      +     * }
      +     * 
      + * It also works for partial matching.
      + *
      + * For example: + *
      +     * {@code
      +     * this: Map
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: Map
      +     * }
      +     * 
      + * It also works with multiple parameters at both sides.
      + *
      + * For example when reversing Key/Value for a Map: + *
      +     * {@code
      +     * this: Map
      +     * declaredType: HashMap
      +     * parameterizedType: HashMap
      +     * result: Map
      +     * }
      +     * 
      + * + * Mismatch result examples: + *
      +     * {@code
      +     * this: T
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: null
      +     *
      +     * this: List
      +     * declaredType: JAXBElement
      +     * parameterizedType: JAXBElement
      +     * result: List
      +     * }
      +     * 
      + * + * @param declared the type + * @param parameterized the parameterized type + * + * @return - the result of {@link #resolveParameterToType(Type, Type)} when this type itself is a type var.
      + * - the type but then with the matching type parameters replaced.
      + * - the same type when this type does not contain matching type parameters. + */ + public Type resolveGenericTypeParameters(Type declared, Type parameterized) { + if ( isTypeVar() || isArrayTypeVar() || isWildCardBoundByTypeVar() ) { + return resolveParameterToType( declared, parameterized ).getMatch(); + } + Type resultType = this; + for ( Type generic : getTypeParameters() ) { + if ( generic.isTypeVar() || generic.isArrayTypeVar() || generic.isWildCardBoundByTypeVar() ) { + ResolvedPair resolveParameterToType = generic.resolveParameterToType( declared, parameterized ); + resultType = resultType.replaceGeneric( generic, resolveParameterToType.getMatch() ); + } + else { + Type replacementType = generic.resolveParameterToType( declared, parameterized ).getMatch(); + resultType = resultType.replaceGeneric( generic, replacementType ); + } + } + return resultType; + } + public boolean isWildCardBoundByTypeVar() { return ( hasExtendsBound() || hasSuperBound() ) && getTypeBound().isTypeVar(); } @@ -1295,7 +1827,7 @@ else if ( typeToMatch.hasSuperBound() && parameterized.getSuperBound() != null return new ResolvedPair( typeFactory.getType( parameterized ), declared ); } } - else if (parameterized.getSuperBound() != null ) { + else if ( parameterized.getSuperBound() != null ) { ResolvedPair match = visit( parameterized.getSuperBound(), declared ); if ( match.match != null ) { return new ResolvedPair( typeFactory.getType( parameterized ), declared ); @@ -1334,6 +1866,13 @@ else if ( types.isSameType( types.erasure( parameterized ), types.erasure( decla for ( int i = 0; i < parameterized.getTypeArguments().size(); i++ ) { TypeMirror parameterizedTypeArg = parameterized.getTypeArguments().get( i ); Type declaredTypeArg = declared.getTypeParameters().get( i ); + if ( parameterizedTypeArg.getKind() == TypeKind.DECLARED && + !types.isSameType( types.erasure( parameterizedTypeArg ), + types.erasure( declaredTypeArg.getTypeMirror() ) ) ) { + // Comparable can never be assigned Comparable + // Generics enforce exact matches for declared types + return DEFAULT_VALUE; + } ResolvedPair result = visit( parameterizedTypeArg, declaredTypeArg ); if ( result != super.DEFAULT_VALUE ) { results.add( result ); @@ -1468,4 +2007,209 @@ private String trimSimpleClassName(String className) { return trimmedClassName; } + private static String nameWithTopLevelTypeName(TypeElement element, String name) { + if ( element == null ) { + return null; + } + if ( !element.getNestingKind().isNested() ) { + return name; + } + + Deque elements = new ArrayDeque<>(); + elements.addFirst( name ); + Element parent = element.getEnclosingElement(); + while ( parent != null && parent.getKind() != ElementKind.PACKAGE ) { + elements.addFirst( parent.getSimpleName() ); + parent = parent.getEnclosingElement(); + } + + return String.join( ".", elements ); + } + + private static Type topLevelType(TypeElement typeElement, TypeFactory typeFactory) { + if ( typeElement == null || typeElement.getNestingKind() == NestingKind.TOP_LEVEL ) { + return null; + } + + Element parent = typeElement.getEnclosingElement(); + while ( parent != null ) { + if ( parent.getEnclosingElement() != null && + parent.getEnclosingElement().getKind() == ElementKind.PACKAGE ) { + break; + } + parent = parent.getEnclosingElement(); + } + return parent == null ? null : typeFactory.getType( parent.asType() ); + } + + public boolean isEnumSet() { + return "java.util.EnumSet".equals( getFullyQualifiedName() ); + } + + /** + * return true if this type is a java 17+ sealed class + */ + public boolean isSealed() { + KotlinMetadata kotlinMetadata = getKotlinMetadata(); + if ( kotlinMetadata != null ) { + return kotlinMetadata.isSealedClass(); + } + return typeElement.getModifiers().stream().map( Modifier::name ).anyMatch( "SEALED"::equals ); + } + + /** + * return the list of permitted TypeMirrors for the java 17+ sealed class + */ + @SuppressWarnings( "unchecked" ) + public List getPermittedSubclasses() { + KotlinMetadata kotlinMetadata = getKotlinMetadata(); + if ( kotlinMetadata != null ) { + return kotlinMetadata.getPermittedSubclasses(); + } + if ( SEALED_PERMITTED_SUBCLASSES_METHOD == null ) { + return emptyList(); + } + try { + return (List) SEALED_PERMITTED_SUBCLASSES_METHOD.invoke( typeElement ); + } + catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) { + return emptyList(); + } + } + + private class KotlinMetadataImpl implements KotlinMetadata { + + private final KotlinClassMetadata.Class kotlinClassMetadata; + + private KotlinMetadataImpl(KotlinClassMetadata.Class kotlinClassMetadata) { + this.kotlinClassMetadata = kotlinClassMetadata; + } + + @Override + public boolean isDataClass() { + return Attributes.isData( kotlinClassMetadata.getKmClass() ); + } + + @Override + public boolean isSealedClass() { + return Attributes.getModality( kotlinClassMetadata.getKmClass() ) == Modality.SEALED; + } + + @Override + public ExecutableElement determinePrimaryConstructor(List constructors) { + if ( constructors.size() == 1 ) { + // If we have one constructor, that this constructor is the primary one + return constructors.get( 0 ); + } + KmClass kmClass = kotlinClassMetadata.getKmClass(); + KmConstructor primaryKmConstructor = null; + for ( KmConstructor constructor : kmClass.getConstructors() ) { + if ( !Attributes.isSecondary( constructor ) ) { + primaryKmConstructor = constructor; + } + + } + + if ( primaryKmConstructor == null ) { + return null; + } + + List sameParametersSizeConstructors = new ArrayList<>(); + for ( ExecutableElement constructor : constructors ) { + if ( constructor.getParameters().size() == primaryKmConstructor.getValueParameters().size() ) { + sameParametersSizeConstructors.add( constructor ); + } + } + + if ( sameParametersSizeConstructors.size() == 1 ) { + return sameParametersSizeConstructors.get( 0 ); + } + + JvmMethodSignature signature = JvmExtensionsKt.getSignature( primaryKmConstructor ); + if ( signature == null ) { + return null; + } + + String signatureDescriptor = signature.getDescriptor(); + for ( ExecutableElement constructor : constructors ) { + String constructorDescriptor = buildJvmConstructorDescriptor( constructor ); + if ( signatureDescriptor.equals( constructorDescriptor ) ) { + return constructor; + } + } + + return null; + } + + @Override + public List getPermittedSubclasses() { + List sealedSubclassNames = kotlinClassMetadata.getKmClass().getSealedSubclasses(); + List permittedSubclasses = new ArrayList<>( sealedSubclassNames.size() ); + for ( String sealedSubclassName : sealedSubclassNames ) { + Type subclassType = typeFactory.getType( sealedSubclassName.replace( '/', '.' ) ); + permittedSubclasses.add( subclassType.getTypeMirror() ); + } + + return permittedSubclasses; + } + + private String buildJvmConstructorDescriptor(ExecutableElement constructor) { + StringBuilder signature = new StringBuilder( "(" ); + + for ( VariableElement param : constructor.getParameters() ) { + signature.append( getJvmTypeDescriptor( param.asType() ) ); + } + + signature.append( ")V" ); + return signature.toString(); + } + + private String getJvmTypeDescriptor(TypeMirror type) { + return type.accept( + new SimpleTypeVisitor8() { + @Override + public String visitPrimitive(PrimitiveType t, Void p) { + switch ( t.getKind() ) { + case BOOLEAN: + return "Z"; + case BYTE: + return "B"; + case SHORT: + return "S"; + case INT: + return "I"; + case LONG: + return "J"; + case CHAR: + return "C"; + case FLOAT: + return "F"; + case DOUBLE: + return "D"; + default: + return ""; + } + } + + @Override + public String visitDeclared(DeclaredType t, Void p) { + TypeElement element = (TypeElement) t.asElement(); + String binaryName = elementUtils.getBinaryName( element ).toString(); + return "L" + binaryName.replace( '.', '/' ) + ";"; + } + + @Override + public String visitArray(ArrayType t, Void p) { + return "[" + getJvmTypeDescriptor( t.getComponentType() ); + } + + @Override + protected String defaultAction(TypeMirror e, Void p) { + return ""; + } + }, null + ); + } + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java index 127ec74270..8d79780d35 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java @@ -38,26 +38,29 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.type.WildcardType; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.util.AnnotationProcessingException; import org.mapstruct.ap.internal.util.Collections; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.Extractor; import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.JavaCollectionConstants; import org.mapstruct.ap.internal.util.JavaStreamConstants; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.NativeTypes; import org.mapstruct.ap.internal.util.RoundContext; import org.mapstruct.ap.internal.util.Strings; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.util.accessor.Accessor; +import org.mapstruct.ap.internal.version.VersionInformation; import org.mapstruct.ap.spi.AstModifyingAnnotationProcessor; import org.mapstruct.ap.spi.BuilderInfo; import org.mapstruct.ap.spi.MoreThanOneBuilderCreationMethodException; import org.mapstruct.ap.spi.TypeHierarchyErroneousException; import static org.mapstruct.ap.internal.model.common.ImplementationType.withDefaultConstructor; +import static org.mapstruct.ap.internal.model.common.ImplementationType.withFactoryMethod; import static org.mapstruct.ap.internal.model.common.ImplementationType.withInitialCapacity; import static org.mapstruct.ap.internal.model.common.ImplementationType.withLoadFactorAdjustment; @@ -82,6 +85,8 @@ public class TypeFactory { sb.append( ')' ); return sb.toString(); }; + private static final String LINKED_HASH_SET_FACTORY_METHOD_NAME = "newLinkedHashSet"; + private static final String LINKED_HASH_MAP_FACTORY_METHOD_NAME = "newLinkedHashMap"; private final ElementUtils elementUtils; private final TypeUtils typeUtils; @@ -100,7 +105,8 @@ public class TypeFactory { private final boolean loggingVerbose; public TypeFactory(ElementUtils elementUtils, TypeUtils typeUtils, FormattingMessager messager, - RoundContext roundContext, Map notToBeImportedTypes, boolean loggingVerbose) { + RoundContext roundContext, Map notToBeImportedTypes, boolean loggingVerbose, + VersionInformation versionInformation) { this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.messager = messager; @@ -118,11 +124,22 @@ public TypeFactory(ElementUtils elementUtils, TypeUtils typeUtils, FormattingMes implementationTypes.put( Collection.class.getName(), withInitialCapacity( getType( ArrayList.class ) ) ); implementationTypes.put( List.class.getName(), withInitialCapacity( getType( ArrayList.class ) ) ); - implementationTypes.put( Set.class.getName(), withLoadFactorAdjustment( getType( LinkedHashSet.class ) ) ); + boolean sourceVersionAtLeast19 = versionInformation.isSourceVersionAtLeast19(); + implementationTypes.put( + Set.class.getName(), + sourceVersionAtLeast19 ? + withFactoryMethod( getType( LinkedHashSet.class ), LINKED_HASH_SET_FACTORY_METHOD_NAME ) : + withLoadFactorAdjustment( getType( LinkedHashSet.class ) ) + ); implementationTypes.put( SortedSet.class.getName(), withDefaultConstructor( getType( TreeSet.class ) ) ); implementationTypes.put( NavigableSet.class.getName(), withDefaultConstructor( getType( TreeSet.class ) ) ); - implementationTypes.put( Map.class.getName(), withLoadFactorAdjustment( getType( LinkedHashMap.class ) ) ); + implementationTypes.put( + Map.class.getName(), + sourceVersionAtLeast19 ? + withFactoryMethod( getType( LinkedHashMap.class ), LINKED_HASH_MAP_FACTORY_METHOD_NAME ) : + withLoadFactorAdjustment( getType( LinkedHashMap.class ) ) + ); implementationTypes.put( SortedMap.class.getName(), withDefaultConstructor( getType( TreeMap.class ) ) ); implementationTypes.put( NavigableMap.class.getName(), withDefaultConstructor( getType( TreeMap.class ) ) ); implementationTypes.put( @@ -133,6 +150,14 @@ public TypeFactory(ElementUtils elementUtils, TypeUtils typeUtils, FormattingMes ConcurrentNavigableMap.class.getName(), withDefaultConstructor( getType( ConcurrentSkipListMap.class ) ) ); + implementationTypes.put( + JavaCollectionConstants.SEQUENCED_SET_FQN, + withFactoryMethod( getType( LinkedHashSet.class ), LINKED_HASH_SET_FACTORY_METHOD_NAME ) + ); + implementationTypes.put( + JavaCollectionConstants.SEQUENCED_MAP_FQN, + withFactoryMethod( getType( LinkedHashMap.class ), LINKED_HASH_MAP_FACTORY_METHOD_NAME ) + ); this.loggingVerbose = loggingVerbose; } @@ -193,7 +218,24 @@ public Type getType(TypeMirror mirror) { return getType( mirror, false ); } + /** + * Return a type that is always going to be imported. + * This is useful when using it in {@code Mapper#imports} + * for types that should be used in expressions. + * + * @param mirror the type mirror for which we need a type + * + * @return the type + */ + public Type getAlwaysImportedType(TypeMirror mirror) { + return getType( mirror, false, true ); + } + private Type getType(TypeMirror mirror, boolean isLiteral) { + return getType( mirror, isLiteral, null ); + } + + private Type getType(TypeMirror mirror, boolean isLiteral, Boolean alwaysImport) { if ( !canBeProcessed( mirror ) ) { throw new TypeHierarchyErroneousException( mirror ); } @@ -212,7 +254,7 @@ private Type getType(TypeMirror mirror, boolean isLiteral) { String qualifiedName; TypeElement typeElement; Type componentType; - Boolean toBeImported = null; + Boolean toBeImported = alwaysImport; if ( mirror.getKind() == TypeKind.DECLARED ) { DeclaredType declaredType = (DeclaredType) mirror; @@ -252,7 +294,7 @@ else if ( mirror.getKind() == TypeKind.ARRAY ) { packageName = elementUtils.getPackageOf( componentTypeElement ).getQualifiedName().toString(); qualifiedName = componentTypeElement.getQualifiedName().toString() + arraySuffix; } - else if (componentTypeMirror.getKind().isPrimitive()) { + else if ( componentTypeMirror.getKind().isPrimitive() ) { // When the component type is primitive and is annotated with ElementType.TYPE_USE then // the typeMirror#toString returns (@CustomAnnotation :: byte) for the javac compiler name = NativeTypes.getName( componentTypeMirror.getKind() ) + builder.toString(); @@ -278,7 +320,17 @@ else if (componentTypeMirror.getKind().isPrimitive()) { isInterface = false; // When the component type is primitive and is annotated with ElementType.TYPE_USE then // the typeMirror#toString returns (@CustomAnnotation :: byte) for the javac compiler - name = mirror.getKind().isPrimitive() ? NativeTypes.getName( mirror.getKind() ) : mirror.toString(); + if ( mirror.getKind().isPrimitive() ) { + name = NativeTypes.getName( mirror.getKind() ); + } + // When the component type is type var and is annotated with ElementType.TYPE_USE then + // the typeMirror#toString returns (@CustomAnnotation T) for the errorprone javac compiler + else if ( mirror.getKind() == TypeKind.TYPEVAR ) { + name = ( (TypeVariable) mirror ).asElement().getSimpleName().toString(); + } + else { + name = mirror.toString(); + } packageName = null; qualifiedName = name; typeElement = null; @@ -445,10 +497,14 @@ public List getThrownTypes(ExecutableType method) { } public List getThrownTypes(Accessor accessor) { - if (accessor.getAccessorType().isFieldAssignment()) { + if ( accessor.getAccessorType().isFieldAssignment() ) { return new ArrayList<>(); } - return extractTypes( ( (ExecutableElement) accessor.getElement() ).getThrownTypes() ); + Element element = accessor.getElement(); + if ( element instanceof ExecutableElement ) { + return extractTypes( ( (ExecutableElement) element ).getThrownTypes() ); + } + return new ArrayList<>(); } private List extractTypes(List typeMirrors) { @@ -462,20 +518,20 @@ private List extractTypes(List typeMirrors) { } private List getTypeParameters(TypeMirror mirror, boolean isImplementationType) { + while ( mirror.getKind() == TypeKind.ARRAY ) { + mirror = getComponentType( mirror ); + } if ( mirror.getKind() != TypeKind.DECLARED ) { return java.util.Collections.emptyList(); } DeclaredType declaredType = (DeclaredType) mirror; - List typeParameters = new ArrayList<>( declaredType.getTypeArguments().size() ); + List typeArguments = declaredType.getTypeArguments(); + List typeParameters = new ArrayList<>( typeArguments.size() ); - for ( TypeMirror typeParameter : declaredType.getTypeArguments() ) { - if ( isImplementationType ) { - typeParameters.add( getType( typeParameter ).getTypeBound() ); - } - else { - typeParameters.add( getType( typeParameter ) ); - } + for ( TypeMirror typeParameter : typeArguments ) { + Type type = getType( typeParameter ); + typeParameters.add( isImplementationType ? type.getTypeBound() : type ); } return typeParameters; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java index df8b008032..d71f67a2a4 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AllPresenceChecksPresenceCheck.java @@ -11,10 +11,13 @@ import java.util.Set; import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.NegatePresenceCheck; import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; /** + * A {@link PresenceCheck} that checks if all the given presence checks are present. + * * @author Filip Hrisafov */ public class AllPresenceChecksPresenceCheck extends ModelElement implements PresenceCheck { @@ -29,6 +32,11 @@ public Collection getPresenceChecks() { return presenceChecks; } + @Override + public PresenceCheck negate() { + return new NegatePresenceCheck( this ); + } + @Override public Set getImportTypes() { Set importTypes = new HashSet<>(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AnyPresenceChecksPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AnyPresenceChecksPresenceCheck.java new file mode 100644 index 0000000000..5f8fac9623 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/AnyPresenceChecksPresenceCheck.java @@ -0,0 +1,66 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.presence; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.NegatePresenceCheck; +import org.mapstruct.ap.internal.model.common.PresenceCheck; +import org.mapstruct.ap.internal.model.common.Type; + +/** + * A {@link PresenceCheck} that checks if any of the given presence checks are present. + * + * @author Filip Hrisafov + */ +public class AnyPresenceChecksPresenceCheck extends ModelElement implements PresenceCheck { + + private final Collection presenceChecks; + + public AnyPresenceChecksPresenceCheck(Collection presenceChecks) { + this.presenceChecks = presenceChecks; + } + + public Collection getPresenceChecks() { + return presenceChecks; + } + + @Override + public PresenceCheck negate() { + return new NegatePresenceCheck( this ); + } + + @Override + public Set getImportTypes() { + Set importTypes = new HashSet<>(); + for ( PresenceCheck presenceCheck : presenceChecks ) { + importTypes.addAll( presenceCheck.getImportTypes() ); + } + + return importTypes; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + AnyPresenceChecksPresenceCheck that = (AnyPresenceChecksPresenceCheck) o; + return Objects.equals( presenceChecks, that.presenceChecks ); + } + + @Override + public int hashCode() { + return Objects.hash( presenceChecks ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java index d4f52c9f27..0c7c132023 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/JavaExpressionPresenceCheck.java @@ -10,10 +10,13 @@ import java.util.Set; import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.NegatePresenceCheck; import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; /** + * A {@link PresenceCheck} that calls a Java expression. + * * @author Filip Hrisafov */ public class JavaExpressionPresenceCheck extends ModelElement implements PresenceCheck { @@ -28,6 +31,11 @@ public String getJavaExpression() { return javaExpression; } + @Override + public PresenceCheck negate() { + return new NegatePresenceCheck( this ); + } + @Override public Set getImportTypes() { return Collections.emptySet(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.java index c9cae61b7c..2da4ae4023 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.java @@ -14,25 +14,43 @@ import org.mapstruct.ap.internal.model.common.Type; /** + * A presence check that checks if the source reference is null. + * * @author Filip Hrisafov */ public class NullPresenceCheck extends ModelElement implements PresenceCheck { private final String sourceReference; + private final boolean negate; public NullPresenceCheck(String sourceReference) { this.sourceReference = sourceReference; + this.negate = false; + } + + public NullPresenceCheck(String sourceReference, boolean negate) { + this.sourceReference = sourceReference; + this.negate = negate; } public String getSourceReference() { return sourceReference; } + public boolean isNegate() { + return negate; + } + @Override public Set getImportTypes() { return Collections.emptySet(); } + @Override + public PresenceCheck negate() { + return new NullPresenceCheck( sourceReference, !negate ); + } + @Override public boolean equals(Object o) { if ( this == o ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.java new file mode 100644 index 0000000000..d727c30f24 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.java @@ -0,0 +1,77 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.presence; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.PresenceCheck; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.version.VersionInformation; + +/** + * Presence checker for {@link java.util.Optional} types. + * + * @author Ken Wang + */ +public class OptionalPresenceCheck extends ModelElement implements PresenceCheck { + + private final String sourceReference; + private final VersionInformation versionInformation; + private final boolean negate; + + public OptionalPresenceCheck(String sourceReference, VersionInformation versionInformation) { + this( sourceReference, versionInformation, false ); + } + + public OptionalPresenceCheck(String sourceReference, VersionInformation versionInformation, boolean negate) { + this.sourceReference = sourceReference; + this.versionInformation = versionInformation; + this.negate = negate; + } + + public String getSourceReference() { + return sourceReference; + } + + public VersionInformation getVersionInformation() { + return versionInformation; + } + + @Override + public Set getImportTypes() { + return Collections.emptySet(); + } + + public boolean isNegate() { + return negate; + } + + @Override + public PresenceCheck negate() { + return new OptionalPresenceCheck( sourceReference, versionInformation, !negate ); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + OptionalPresenceCheck that = (OptionalPresenceCheck) o; + return Objects.equals( sourceReference, that.sourceReference ); + } + + @Override + public int hashCode() { + return Objects.hash( sourceReference ); + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceMethodPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceMethodPresenceCheck.java deleted file mode 100644 index 374eac0c8e..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceMethodPresenceCheck.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.model.presence; - -import java.util.Collections; -import java.util.Objects; -import java.util.Set; - -import org.mapstruct.ap.internal.model.common.ModelElement; -import org.mapstruct.ap.internal.model.common.PresenceCheck; -import org.mapstruct.ap.internal.model.common.Type; - -/** - * @author Filip Hrisafov - */ -public class SourceReferenceMethodPresenceCheck extends ModelElement implements PresenceCheck { - - private final String sourceReference; - private final String methodName; - - public SourceReferenceMethodPresenceCheck(String sourceReference, String methodName) { - this.sourceReference = sourceReference; - this.methodName = methodName; - } - - public String getSourceReference() { - return sourceReference; - } - - public String getMethodName() { - return methodName; - } - - @Override - public Set getImportTypes() { - return Collections.emptySet(); - } - - @Override - public boolean equals(Object o) { - if ( this == o ) { - return true; - } - if ( o == null || getClass() != o.getClass() ) { - return false; - } - SourceReferenceMethodPresenceCheck that = (SourceReferenceMethodPresenceCheck) o; - return Objects.equals( sourceReference, that.sourceReference ) && - Objects.equals( methodName, that.methodName ); - } - - @Override - public int hashCode() { - return Objects.hash( sourceReference, methodName ); - } -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SuffixPresenceCheck.java similarity index 53% rename from processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.java rename to processor/src/main/java/org/mapstruct/ap/internal/model/presence/SuffixPresenceCheck.java index d4c011a051..d35e89fce2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/SuffixPresenceCheck.java @@ -10,28 +10,46 @@ import java.util.Set; import org.mapstruct.ap.internal.model.common.ModelElement; +import org.mapstruct.ap.internal.model.common.NegatePresenceCheck; import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; /** + * A {@link PresenceCheck} that calls the suffix on the source reference. + * * @author Filip Hrisafov */ -public class SourceReferenceContainsKeyPresenceCheck extends ModelElement implements PresenceCheck { +public class SuffixPresenceCheck extends ModelElement implements PresenceCheck { private final String sourceReference; - private final String propertyName; + private final String suffix; + private final boolean negate; + + public SuffixPresenceCheck(String sourceReference, String suffix) { + this( sourceReference, suffix, false ); + } - public SourceReferenceContainsKeyPresenceCheck(String sourceReference, String propertyName) { + public SuffixPresenceCheck(String sourceReference, String suffix, boolean negate) { this.sourceReference = sourceReference; - this.propertyName = propertyName; + this.suffix = suffix; + this.negate = negate; } public String getSourceReference() { return sourceReference; } - public String getPropertyName() { - return propertyName; + public String getSuffix() { + return suffix; + } + + public boolean isNegate() { + return negate; + } + + @Override + public PresenceCheck negate() { + return new NegatePresenceCheck( this ); } @Override @@ -47,13 +65,13 @@ public boolean equals(Object o) { if ( o == null || getClass() != o.getClass() ) { return false; } - SourceReferenceContainsKeyPresenceCheck that = (SourceReferenceContainsKeyPresenceCheck) o; - return Objects.equals( sourceReference, that.sourceReference ) && - Objects.equals( propertyName, that.propertyName ); + SuffixPresenceCheck that = (SuffixPresenceCheck) o; + return Objects.equals( sourceReference, that.sourceReference ) + && Objects.equals( suffix, that.suffix ); } @Override public int hashCode() { - return Objects.hash( sourceReference, propertyName ); + return Objects.hash( sourceReference, suffix ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java index 9567dd80bf..73a4d7c12d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java @@ -11,19 +11,20 @@ import java.util.Optional; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.gem.ReportingPolicyGem; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; - -import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.gem.BeanMappingGem; import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; +import org.mapstruct.ap.internal.gem.ReportingPolicyGem; +import org.mapstruct.ap.internal.gem.SubclassExhaustiveStrategyGem; +import org.mapstruct.ap.internal.model.common.TypeFactory; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.tools.gem.GemValue; /** @@ -34,31 +35,59 @@ public class BeanMappingOptions extends DelegatingOptions { private final SelectionParameters selectionParameters; + private final List ignoreUnmappedSourceProperties; private final BeanMappingGem beanMapping; /** * creates a mapping for inheritance. Will set * * @param beanMapping the bean mapping options that should be used + * @param isInverse whether the inheritance is inverse * * @return new mapping */ - public static BeanMappingOptions forInheritance(BeanMappingOptions beanMapping) { + public static BeanMappingOptions forInheritance(BeanMappingOptions beanMapping, boolean isInverse) { BeanMappingOptions options = new BeanMappingOptions( SelectionParameters.forInheritance( beanMapping.selectionParameters ), + isInverse ? Collections.emptyList() : beanMapping.ignoreUnmappedSourceProperties, beanMapping.beanMapping, beanMapping ); return options; } + public static BeanMappingOptions forForgedMethods(BeanMappingOptions beanMapping) { + BeanMappingOptions options = new BeanMappingOptions( + beanMapping.selectionParameters != null ? + SelectionParameters.withoutResultType( beanMapping.selectionParameters ) : SelectionParameters.empty(), + Collections.emptyList(), + beanMapping.beanMapping, + beanMapping + ); + return options; + } + + public static BeanMappingOptions forSubclassForgedMethods(BeanMappingOptions beanMapping) { + + return new BeanMappingOptions( + beanMapping.selectionParameters != null ? + SelectionParameters.withoutResultType( beanMapping.selectionParameters ) : null, + beanMapping.ignoreUnmappedSourceProperties, + beanMapping.beanMapping, + beanMapping + ); + } + + public static BeanMappingOptions empty(DelegatingOptions delegatingOptions) { + return new BeanMappingOptions( SelectionParameters.empty(), Collections.emptyList(), null, delegatingOptions ); + } + public static BeanMappingOptions getInstanceOn(BeanMappingGem beanMapping, MapperOptions mapperOptions, ExecutableElement method, FormattingMessager messager, TypeUtils typeUtils, TypeFactory typeFactory ) { if ( beanMapping == null || !isConsistent( beanMapping, method, messager ) ) { - BeanMappingOptions options = new BeanMappingOptions( null, null, mapperOptions ); - return options; + return empty( mapperOptions ); } Objects.requireNonNull( method ); @@ -77,6 +106,7 @@ public static BeanMappingOptions getInstanceOn(BeanMappingGem beanMapping, Mappe //TODO Do we want to add the reporting policy to the BeanMapping as well? To give more granular support? BeanMappingOptions options = new BeanMappingOptions( selectionParameters, + beanMapping.ignoreUnmappedSourceProperties().get(), beanMapping, mapperOptions ); @@ -86,13 +116,16 @@ public static BeanMappingOptions getInstanceOn(BeanMappingGem beanMapping, Mappe private static boolean isConsistent(BeanMappingGem gem, ExecutableElement method, FormattingMessager messager) { if ( !gem.resultType().hasValue() + && !gem.mappingControl().hasValue() && !gem.qualifiedBy().hasValue() && !gem.qualifiedByName().hasValue() && !gem.ignoreUnmappedSourceProperties().hasValue() && !gem.nullValueCheckStrategy().hasValue() && !gem.nullValuePropertyMappingStrategy().hasValue() && !gem.nullValueMappingStrategy().hasValue() + && !gem.subclassExhaustiveStrategy().hasValue() && !gem.unmappedTargetPolicy().hasValue() + && !gem.unmappedSourcePolicy().hasValue() && !gem.ignoreByDefault().hasValue() && !gem.builder().hasValue() ) { @@ -103,10 +136,12 @@ private static boolean isConsistent(BeanMappingGem gem, ExecutableElement method } private BeanMappingOptions(SelectionParameters selectionParameters, + List ignoreUnmappedSourceProperties, BeanMappingGem beanMapping, DelegatingOptions next) { super( next ); this.selectionParameters = selectionParameters; + this.ignoreUnmappedSourceProperties = ignoreUnmappedSourceProperties; this.beanMapping = beanMapping; } @@ -139,6 +174,23 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { .orElse( next().getNullValueMappingStrategy() ); } + @Override + public SubclassExhaustiveStrategyGem getSubclassExhaustiveStrategy() { + return Optional.ofNullable( beanMapping ).map( BeanMappingGem::subclassExhaustiveStrategy ) + .filter( GemValue::hasValue ) + .map( GemValue::getValue ) + .map( SubclassExhaustiveStrategyGem::valueOf ) + .orElse( next().getSubclassExhaustiveStrategy() ); + } + + @Override + public TypeMirror getSubclassExhaustiveException() { + return Optional.ofNullable( beanMapping ).map( BeanMappingGem::subclassExhaustiveException ) + .filter( GemValue::hasValue ) + .map( GemValue::getValue ) + .orElse( next().getSubclassExhaustiveException() ); + } + @Override public ReportingPolicyGem unmappedTargetPolicy() { return Optional.ofNullable( beanMapping ).map( BeanMappingGem::unmappedTargetPolicy ) @@ -148,6 +200,15 @@ public ReportingPolicyGem unmappedTargetPolicy() { .orElse( next().unmappedTargetPolicy() ); } + @Override + public ReportingPolicyGem unmappedSourcePolicy() { + return Optional.ofNullable( beanMapping ).map( BeanMappingGem::unmappedSourcePolicy ) + .filter( GemValue::hasValue ) + .map( GemValue::getValue ) + .map( ReportingPolicyGem::valueOf ) + .orElse( next().unmappedSourcePolicy() ); + } + @Override public BuilderGem getBuilder() { return Optional.ofNullable( beanMapping ).map( BeanMappingGem::builder ) @@ -171,16 +232,14 @@ public SelectionParameters getSelectionParameters() { return selectionParameters; } - public boolean isignoreByDefault() { + public boolean isIgnoredByDefault() { return Optional.ofNullable( beanMapping ).map( BeanMappingGem::ignoreByDefault ) .map( GemValue::get ) .orElse( false ); } public List getIgnoreUnmappedSourceProperties() { - return Optional.ofNullable( beanMapping ).map( BeanMappingGem::ignoreUnmappedSourceProperties ) - .map( GemValue::get ) - .orElse( Collections.emptyList() ); + return ignoreUnmappedSourceProperties; } public AnnotationMirror getMirror() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java new file mode 100644 index 0000000000..dfb0865ecc --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionMethodOptions.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source; + +import java.util.Collection; +import java.util.Collections; + +import org.mapstruct.ap.internal.gem.ConditionStrategyGem; + +/** + * Encapsulates all options specific for a condition check method. + * + * @author Filip Hrisafov + */ +public class ConditionMethodOptions { + + private static final ConditionMethodOptions EMPTY = new ConditionMethodOptions( Collections.emptyList() ); + + private final Collection conditionOptions; + + public ConditionMethodOptions(Collection conditionOptions) { + this.conditionOptions = conditionOptions; + } + + public boolean isStrategyApplicable(ConditionStrategyGem strategy) { + for ( ConditionOptions conditionOption : conditionOptions ) { + if ( conditionOption.getConditionStrategies().contains( strategy ) ) { + return true; + } + } + + return false; + } + + public boolean isAnyStrategyApplicable() { + return !conditionOptions.isEmpty(); + } + + public static ConditionMethodOptions empty() { + return EMPTY; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java new file mode 100644 index 0000000000..83e2bf2819 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java @@ -0,0 +1,172 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import org.mapstruct.ap.internal.gem.ConditionGem; +import org.mapstruct.ap.internal.gem.ConditionStrategyGem; +import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.Message; + +/** + * Represents a condition configuration as configured via {@code @Condition}. + * + * @author Filip Hrisafov + */ +public class ConditionOptions { + + private final Set conditionStrategies; + + private ConditionOptions(Set conditionStrategies) { + this.conditionStrategies = conditionStrategies; + } + + public Collection getConditionStrategies() { + return conditionStrategies; + } + + public static ConditionOptions getInstanceOn(ConditionGem condition, ExecutableElement method, + List parameters, + FormattingMessager messager) { + if ( condition == null ) { + return null; + } + + TypeMirror returnType = method.getReturnType(); + TypeKind returnTypeKind = returnType.getKind(); + // We only allow methods that return boolean or Boolean to be condition methods + if ( returnTypeKind != TypeKind.BOOLEAN ) { + if ( returnTypeKind != TypeKind.DECLARED ) { + return null; + } + DeclaredType declaredType = (DeclaredType) returnType; + TypeElement returnTypeElement = (TypeElement) declaredType.asElement(); + if ( !returnTypeElement.getQualifiedName().contentEquals( Boolean.class.getCanonicalName() ) ) { + return null; + } + } + + Set strategies = condition.appliesTo().get() + .stream() + .map( ConditionStrategyGem::valueOf ) + .collect( Collectors.toCollection( () -> EnumSet.noneOf( ConditionStrategyGem.class ) ) ); + + if ( strategies.isEmpty() ) { + messager.printMessage( + method, + condition.mirror(), + condition.appliesTo().getAnnotationValue(), + Message.CONDITION_MISSING_APPLIES_TO_STRATEGY + ); + + return null; + } + + boolean allStrategiesValid = true; + + for ( ConditionStrategyGem strategy : strategies ) { + boolean isStrategyValid = isValid( strategy, condition, method, parameters, messager ); + allStrategiesValid &= isStrategyValid; + } + + return allStrategiesValid ? new ConditionOptions( strategies ) : null; + } + + protected static boolean isValid(ConditionStrategyGem strategy, ConditionGem condition, + ExecutableElement method, List parameters, + FormattingMessager messager) { + if ( strategy == ConditionStrategyGem.SOURCE_PARAMETERS ) { + return hasValidStrategyForSourceProperties( condition, method, parameters, messager ); + } + else if ( strategy == ConditionStrategyGem.PROPERTIES ) { + return hasValidStrategyForProperties( condition, method, parameters, messager ); + } + else { + throw new IllegalStateException( "Invalid condition strategy: " + strategy ); + } + } + + protected static boolean hasValidStrategyForSourceProperties(ConditionGem condition, ExecutableElement method, + List parameters, + FormattingMessager messager) { + for ( Parameter parameter : parameters ) { + if ( parameter.isSourceParameter() ) { + // source parameter is a valid parameter for a source condition check + continue; + } + + if ( parameter.isMappingContext() ) { + // mapping context parameter is a valid parameter for a source condition check + continue; + } + + messager.printMessage( + method, + condition.mirror(), + Message.CONDITION_SOURCE_PARAMETERS_INVALID_PARAMETER, + parameter.describe() + ); + return false; + } + return true; + } + + protected static boolean hasValidStrategyForProperties(ConditionGem condition, ExecutableElement method, + List parameters, + FormattingMessager messager) { + for ( Parameter parameter : parameters ) { + if ( parameter.isSourceParameter() ) { + // source parameter is a valid parameter for a property condition check + continue; + } + + if ( parameter.isMappingContext() ) { + // mapping context parameter is a valid parameter for a property condition check + continue; + } + + if ( parameter.isTargetType() ) { + // target type parameter is a valid parameter for a property condition check + continue; + } + + if ( parameter.isMappingTarget() ) { + // mapping target parameter is a valid parameter for a property condition check + continue; + } + + if ( parameter.isSourcePropertyName() ) { + // source property name parameter is a valid parameter for a property condition check + continue; + } + + if ( parameter.isTargetPropertyName() ) { + // target property name parameter is a valid parameter for a property condition check + continue; + } + + messager.printMessage( + method, + condition.mirror(), + Message.CONDITION_PROPERTIES_INVALID_PARAMETER, + parameter + ); + return false; + } + return true; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java index 58f03ad55d..d958272f9b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java @@ -9,10 +9,9 @@ import java.util.Set; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MapperGem; @@ -21,6 +20,9 @@ import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; +import org.mapstruct.ap.internal.gem.SubclassExhaustiveStrategyGem; +import org.mapstruct.ap.internal.option.Options; +import org.mapstruct.ap.internal.util.ElementUtils; public class DefaultOptions extends DelegatingOptions { @@ -55,14 +57,19 @@ public Set imports() { @Override public ReportingPolicyGem unmappedTargetPolicy() { - if ( options.getUnmappedTargetPolicy() != null ) { - return options.getUnmappedTargetPolicy(); + ReportingPolicyGem unmappedTargetPolicy = options.getUnmappedTargetPolicy(); + if ( unmappedTargetPolicy != null ) { + return unmappedTargetPolicy; } return ReportingPolicyGem.valueOf( mapper.unmappedTargetPolicy().getDefaultValue() ); } @Override public ReportingPolicyGem unmappedSourcePolicy() { + ReportingPolicyGem unmappedSourcePolicy = options.getUnmappedSourcePolicy(); + if ( unmappedSourcePolicy != null ) { + return unmappedSourcePolicy; + } return ReportingPolicyGem.valueOf( mapper.unmappedSourcePolicy().getDefaultValue() ); } @@ -73,12 +80,20 @@ public ReportingPolicyGem typeConversionPolicy() { @Override public String componentModel() { - if ( options.getDefaultComponentModel() != null ) { - return options.getDefaultComponentModel(); + String defaultComponentModel = options.getDefaultComponentModel(); + if ( defaultComponentModel != null ) { + return defaultComponentModel; } return mapper.componentModel().getDefaultValue(); } + public boolean suppressTimestampInGenerated() { + if ( mapper.suppressTimestampInGenerated().hasValue() ) { + return mapper.suppressTimestampInGenerated().getValue(); + } + return options.isSuppressGeneratorTimestamp(); + } + @Override public MappingInheritanceStrategyGem getMappingInheritanceStrategy() { return MappingInheritanceStrategyGem.valueOf( mapper.mappingInheritanceStrategy().getDefaultValue() ); @@ -86,8 +101,9 @@ public MappingInheritanceStrategyGem getMappingInheritanceStrategy() { @Override public InjectionStrategyGem getInjectionStrategy() { - if ( options.getDefaultInjectionStrategy() != null ) { - return InjectionStrategyGem.valueOf( options.getDefaultInjectionStrategy().toUpperCase() ); + String defaultInjectionStrategy = options.getDefaultInjectionStrategy(); + if ( defaultInjectionStrategy != null ) { + return InjectionStrategyGem.valueOf( defaultInjectionStrategy.toUpperCase() ); } return InjectionStrategyGem.valueOf( mapper.injectionStrategy().getDefaultValue() ); } @@ -116,6 +132,35 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { return NullValueMappingStrategyGem.valueOf( mapper.nullValueMappingStrategy().getDefaultValue() ); } + public SubclassExhaustiveStrategyGem getSubclassExhaustiveStrategy() { + return SubclassExhaustiveStrategyGem.valueOf( mapper.subclassExhaustiveStrategy().getDefaultValue() ); + } + + public TypeMirror getSubclassExhaustiveException() { + return mapper.subclassExhaustiveException().getDefaultValue(); + } + + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + NullValueMappingStrategyGem nullValueIterableMappingStrategy = options.getNullValueIterableMappingStrategy(); + if ( nullValueIterableMappingStrategy != null ) { + return nullValueIterableMappingStrategy; + } + return NullValueMappingStrategyGem.valueOf( mapper.nullValueIterableMappingStrategy().getDefaultValue() ); + } + + public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + NullValueMappingStrategyGem nullValueMapMappingStrategy = options.getNullValueMapMappingStrategy(); + if ( nullValueMapMappingStrategy != null ) { + return nullValueMapMappingStrategy; + } + return NullValueMappingStrategyGem.valueOf( mapper.nullValueMapMappingStrategy().getDefaultValue() ); + } + + @Override + public ClassAccessibilityGem accessibility() { + return ClassAccessibilityGem.valueOf( mapper.accessibility().getDefaultValue() ); + } + public BuilderGem getBuilder() { // TODO: I realized this is not correct, however it needs to be null in order to keep downward compatibility // but assuming a default @Builder will make testcases fail. Not having a default means that you need to diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java index 564358ec98..e97f527745 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java @@ -10,9 +10,9 @@ import java.util.Set; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MappingInheritanceStrategyGem; @@ -20,6 +20,8 @@ import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; +import org.mapstruct.ap.internal.gem.SubclassExhaustiveStrategyGem; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.spi.TypeHierarchyErroneousException; /** @@ -67,6 +69,10 @@ public String componentModel() { return next.componentModel(); } + public boolean suppressTimestampInGenerated() { + return next.suppressTimestampInGenerated(); + } + public MappingInheritanceStrategyGem getMappingInheritanceStrategy() { return next.getMappingInheritanceStrategy(); } @@ -79,6 +85,10 @@ public Boolean isDisableSubMappingMethodsGeneration() { return next.isDisableSubMappingMethodsGeneration(); } + public ClassAccessibilityGem accessibility() { + return next.accessibility(); + } + // BeanMapping and Mapping public CollectionMappingStrategyGem getCollectionMappingStrategy() { @@ -97,6 +107,22 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { return next.getNullValueMappingStrategy(); } + public SubclassExhaustiveStrategyGem getSubclassExhaustiveStrategy() { + return next.getSubclassExhaustiveStrategy(); + } + + public TypeMirror getSubclassExhaustiveException() { + return next.getSubclassExhaustiveException(); + } + + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + return next.getNullValueIterableMappingStrategy(); + } + + public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + return next.getNullValueMapMappingStrategy(); + } + public BuilderGem getBuilder() { return next.getBuilder(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java index 85734c3083..41b74aa13a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/EnumMappingOptions.java @@ -21,6 +21,8 @@ import static org.mapstruct.ap.internal.util.Message.ENUMMAPPING_NO_ELEMENTS; /** + * Represents an enum mapping as configured via {@code @EnumMapping}. + * * @author Filip Hrisafov */ public class EnumMappingOptions extends DelegatingOptions { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/IterableMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/IterableMappingOptions.java index 40b5b25353..50fca3e4d2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/IterableMappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/IterableMappingOptions.java @@ -8,14 +8,14 @@ import java.util.Optional; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; -import org.mapstruct.ap.internal.model.common.FormattingParameters; import org.mapstruct.ap.internal.gem.IterableMappingGem; import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; +import org.mapstruct.ap.internal.model.common.FormattingParameters; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.tools.gem.GemValue; /** @@ -30,11 +30,16 @@ public class IterableMappingOptions extends DelegatingOptions { private final IterableMappingGem iterableMapping; public static IterableMappingOptions fromGem(IterableMappingGem iterableMapping, - MapperOptions mappperOptions, ExecutableElement method, + MapperOptions mapperOptions, ExecutableElement method, FormattingMessager messager, TypeUtils typeUtils) { if ( iterableMapping == null || !isConsistent( iterableMapping, method, messager ) ) { - IterableMappingOptions options = new IterableMappingOptions( null, null, null, mappperOptions ); + IterableMappingOptions options = new IterableMappingOptions( + null, + SelectionParameters.empty(), + null, + mapperOptions + ); return options; } @@ -50,11 +55,12 @@ public static IterableMappingOptions fromGem(IterableMappingGem iterableMapping, iterableMapping.numberFormat().get(), iterableMapping.mirror(), iterableMapping.dateFormat().getAnnotationValue(), - method + method, + iterableMapping.locale().getValue() ); IterableMappingOptions options = - new IterableMappingOptions( formatting, selection, iterableMapping, mappperOptions ); + new IterableMappingOptions( formatting, selection, iterableMapping, mapperOptions ); return options; } @@ -99,7 +105,7 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { .filter( GemValue::hasValue ) .map( GemValue::getValue ) .map( NullValueMappingStrategyGem::valueOf ) - .orElse( next().getNullValueMappingStrategy() ); + .orElse( next().getNullValueIterableMappingStrategy() ); } public MappingControl getElementMappingControl(ElementUtils elementUtils) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapMappingOptions.java index c35a8e363d..9f3d12faf3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapMappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapMappingOptions.java @@ -8,14 +8,14 @@ import java.util.Optional; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; -import org.mapstruct.ap.internal.model.common.FormattingParameters; import org.mapstruct.ap.internal.gem.MapMappingGem; import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; +import org.mapstruct.ap.internal.model.common.FormattingParameters; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.tools.gem.GemValue; /** @@ -38,15 +38,17 @@ public static MapMappingOptions fromGem(MapMappingGem mapMapping, MapperOptions if ( mapMapping == null || !isConsistent( mapMapping, method, messager ) ) { MapMappingOptions options = new MapMappingOptions( null, + SelectionParameters.empty(), null, - null, - null, + SelectionParameters.empty(), null, mapperOptions ); return options; } + String locale = mapMapping.locale().getValue(); + SelectionParameters keySelection = new SelectionParameters( mapMapping.keyQualifiedBy().get(), mapMapping.keyQualifiedByName().get(), @@ -66,7 +68,8 @@ public static MapMappingOptions fromGem(MapMappingGem mapMapping, MapperOptions mapMapping.keyNumberFormat().get(), mapMapping.mirror(), mapMapping.keyDateFormat().getAnnotationValue(), - method + method, + locale ); FormattingParameters valueFormatting = new FormattingParameters( @@ -74,7 +77,8 @@ public static MapMappingOptions fromGem(MapMappingGem mapMapping, MapperOptions mapMapping.valueNumberFormat().get(), mapMapping.mirror(), mapMapping.valueDateFormat().getAnnotationValue(), - method + method, + locale ); MapMappingOptions options = new MapMappingOptions( @@ -144,7 +148,7 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { .filter( GemValue::hasValue ) .map( GemValue::getValue ) .map( NullValueMappingStrategyGem::valueOf ) - .orElse( next().getNullValueMappingStrategy() ); + .orElse( next().getNullValueMapMappingStrategy() ); } public MappingControl getKeyMappingControl(ElementUtils elementUtils) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java index 7f8df7bf42..b51008b713 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java @@ -8,9 +8,9 @@ import java.util.Set; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MapperConfigGem; @@ -19,6 +19,8 @@ import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; +import org.mapstruct.ap.internal.gem.SubclassExhaustiveStrategyGem; +import org.mapstruct.ap.internal.util.ElementUtils; public class MapperConfigOptions extends DelegatingOptions { @@ -75,6 +77,13 @@ public String componentModel() { return mapperConfig.componentModel().hasValue() ? mapperConfig.componentModel().get() : next().componentModel(); } + @Override + public boolean suppressTimestampInGenerated() { + return mapperConfig.suppressTimestampInGenerated().hasValue() ? + mapperConfig.suppressTimestampInGenerated().get() : + next().suppressTimestampInGenerated(); + } + @Override public MappingInheritanceStrategyGem getMappingInheritanceStrategy() { return mapperConfig.mappingInheritanceStrategy().hasValue() ? @@ -126,6 +135,48 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { next().getNullValueMappingStrategy(); } + @Override + public SubclassExhaustiveStrategyGem getSubclassExhaustiveStrategy() { + return mapperConfig.subclassExhaustiveStrategy().hasValue() ? + SubclassExhaustiveStrategyGem.valueOf( mapperConfig.subclassExhaustiveStrategy().get() ) : + next().getSubclassExhaustiveStrategy(); + } + + public TypeMirror getSubclassExhaustiveException() { + return mapperConfig.subclassExhaustiveException().hasValue() ? + mapperConfig.subclassExhaustiveException().get() : + next().getSubclassExhaustiveException(); + } + + @Override + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + if ( mapperConfig.nullValueIterableMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapperConfig.nullValueIterableMappingStrategy().get() ); + } + if ( mapperConfig.nullValueMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapperConfig.nullValueMappingStrategy().get() ); + } + return next().getNullValueIterableMappingStrategy(); + } + + @Override + public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + if ( mapperConfig.nullValueMapMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapperConfig.nullValueMapMappingStrategy().get() ); + } + if ( mapperConfig.nullValueMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapperConfig.nullValueMappingStrategy().get() ); + } + return next().getNullValueMapMappingStrategy(); + } + + @Override + public ClassAccessibilityGem accessibility() { + return mapperConfig.accessibility().hasValue() ? + ClassAccessibilityGem.valueOf( mapperConfig.accessibility().get() ) : + next().accessibility(); + } + @Override public BuilderGem getBuilder() { return mapperConfig.builder().hasValue() ? mapperConfig.builder().get() : next().getBuilder(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java index 82d17a9846..b30b8c95fa 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java @@ -12,10 +12,9 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MapperConfigGem; @@ -25,6 +24,9 @@ import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; +import org.mapstruct.ap.internal.gem.SubclassExhaustiveStrategyGem; +import org.mapstruct.ap.internal.option.Options; +import org.mapstruct.ap.internal.util.ElementUtils; public class MapperOptions extends DelegatingOptions { @@ -104,6 +106,13 @@ public String componentModel() { return mapper.componentModel().hasValue() ? mapper.componentModel().get() : next().componentModel(); } + @Override + public boolean suppressTimestampInGenerated() { + return mapper.suppressTimestampInGenerated().hasValue() ? + mapper.suppressTimestampInGenerated().get() : + next().suppressTimestampInGenerated(); + } + @Override public MappingInheritanceStrategyGem getMappingInheritanceStrategy() { return mapper.mappingInheritanceStrategy().hasValue() ? @@ -155,6 +164,49 @@ public NullValueMappingStrategyGem getNullValueMappingStrategy() { next().getNullValueMappingStrategy(); } + @Override + public SubclassExhaustiveStrategyGem getSubclassExhaustiveStrategy() { + return mapper.subclassExhaustiveStrategy().hasValue() ? + SubclassExhaustiveStrategyGem.valueOf( mapper.subclassExhaustiveStrategy().get() ) : + next().getSubclassExhaustiveStrategy(); + } + + @Override + public ClassAccessibilityGem accessibility() { + return mapper.accessibility().hasValue() ? + ClassAccessibilityGem.valueOf( mapper.accessibility().get() ) : + next().accessibility(); + } + + @Override + public TypeMirror getSubclassExhaustiveException() { + return mapper.subclassExhaustiveException().hasValue() ? + mapper.subclassExhaustiveException().get() : + next().getSubclassExhaustiveException(); + } + + @Override + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + if ( mapper.nullValueIterableMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapper.nullValueIterableMappingStrategy().get() ); + } + if ( mapper.nullValueMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapper.nullValueMappingStrategy().get() ); + } + return next().getNullValueIterableMappingStrategy(); + } + + @Override + public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + if ( mapper.nullValueMapMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapper.nullValueMapMappingStrategy().get() ); + } + if ( mapper.nullValueMappingStrategy().hasValue() ) { + return NullValueMappingStrategyGem.valueOf( mapper.nullValueMappingStrategy().get() ); + } + return next().getNullValueMapMappingStrategy(); + } + @Override public BuilderGem getBuilder() { return mapper.builder().hasValue() ? mapper.builder().get() : next().getBuilder(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingControl.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingControl.java index ab2957086f..87448bfb65 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingControl.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingControl.java @@ -14,11 +14,11 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.gem.MappingControlGem; import org.mapstruct.ap.internal.gem.MappingControlUseGem; import org.mapstruct.ap.internal.gem.MappingControlsGem; +import org.mapstruct.ap.internal.util.ElementUtils; public class MappingControl { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java index 859a4c6f5f..caf7639977 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodOptions.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.lang.model.element.AnnotationMirror; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.model.common.Type; @@ -34,7 +35,9 @@ public class MappingMethodOptions { null, null, null, - Collections.emptyList() + Collections.emptyList(), + Collections.emptySet(), + null ); private MapperOptions mapper; @@ -45,12 +48,16 @@ public class MappingMethodOptions { private EnumMappingOptions enumMappingOptions; private List valueMappings; private boolean fullyInitialized; + private Set subclassMappings; + + private SubclassValidator subclassValidator; public MappingMethodOptions(MapperOptions mapper, Set mappings, IterableMappingOptions iterableMapping, MapMappingOptions mapMapping, BeanMappingOptions beanMapping, EnumMappingOptions enumMappingOptions, - List valueMappings) { + List valueMappings, + Set subclassMappings, SubclassValidator subclassValidator) { this.mapper = mapper; this.mappings = mappings; this.iterableMapping = iterableMapping; @@ -58,6 +65,8 @@ public MappingMethodOptions(MapperOptions mapper, Set mappings, this.beanMapping = beanMapping; this.enumMappingOptions = enumMappingOptions; this.valueMappings = valueMappings; + this.subclassMappings = subclassMappings; + this.subclassValidator = subclassValidator; } /** @@ -97,6 +106,10 @@ public List getValueMappings() { return valueMappings; } + public Set getSubclassMappings() { + return subclassMappings; + } + public void setIterableMapping(IterableMappingOptions iterableMapping) { this.iterableMapping = iterableMapping; } @@ -136,10 +149,13 @@ public void markAsFullyInitialized() { /** * Merges in all the mapping options configured, giving the already defined options precedence. * + * @param sourceMethod the method which inherits the options. * @param templateMethod the template method with the options to inherit, may be {@code null} * @param isInverse if {@code true}, the specified options are from an inverse method + * @param annotationMirror the annotation on which the compile errors will be shown. */ - public void applyInheritedOptions(SourceMethod templateMethod, boolean isInverse) { + public void applyInheritedOptions(SourceMethod sourceMethod, SourceMethod templateMethod, boolean isInverse, + AnnotationMirror annotationMirror) { MappingMethodOptions templateOptions = templateMethod.getOptions(); if ( null != templateOptions ) { if ( !getIterableMapping().hasAnnotation() && templateOptions.getIterableMapping().hasAnnotation() ) { @@ -151,7 +167,7 @@ public void applyInheritedOptions(SourceMethod templateMethod, boolean isInverse } if ( !getBeanMapping().hasAnnotation() && templateOptions.getBeanMapping().hasAnnotation() ) { - setBeanMapping( BeanMappingOptions.forInheritance( templateOptions.getBeanMapping( ) ) ); + setBeanMapping( BeanMappingOptions.forInheritance( templateOptions.getBeanMapping( ), isInverse ) ); } if ( !getEnumMappingOptions().hasAnnotation() && templateOptions.getEnumMappingOptions().hasAnnotation() ) { @@ -176,7 +192,7 @@ public void applyInheritedOptions(SourceMethod templateMethod, boolean isInverse } else { if ( templateOptions.getValueMappings() != null ) { - // iff there are also inherited mappings, we inverse and add them. + // if there are also inherited mappings, we inverse and add them. for ( ValueMappingOptions inheritedValueMapping : templateOptions.getValueMappings() ) { ValueMappingOptions valueMapping = isInverse ? inheritedValueMapping.inverse() : inheritedValueMapping; @@ -188,6 +204,21 @@ public void applyInheritedOptions(SourceMethod templateMethod, boolean isInverse } } + if ( isInverse ) { + List inheritedMappings = SubclassMappingOptions.copyForInverseInheritance( + templateOptions.getSubclassMappings(), + getBeanMapping() ); + addAllNonRedefined( sourceMethod, annotationMirror, inheritedMappings ); + } + else if ( methodsHaveIdenticalSignature( templateMethod, sourceMethod ) ) { + List inheritedMappings = + SubclassMappingOptions + .copyForInheritance( + templateOptions.getSubclassMappings(), + getBeanMapping() ); + addAllNonRedefined( sourceMethod, annotationMirror, inheritedMappings ); + } + Set newMappings = new LinkedHashSet<>(); for ( MappingOptions mapping : templateOptions.getMappings() ) { if ( isInverse ) { @@ -208,21 +239,54 @@ public void applyInheritedOptions(SourceMethod templateMethod, boolean isInverse } } + private boolean methodsHaveIdenticalSignature(SourceMethod templateMethod, SourceMethod sourceMethod) { + return parametersAreOfIdenticalTypeAndOrder( templateMethod, sourceMethod ) + && resultTypeIsTheSame( templateMethod, sourceMethod ); + } + + private boolean parametersAreOfIdenticalTypeAndOrder(SourceMethod templateMethod, SourceMethod sourceMethod) { + if ( templateMethod.getParameters().size() != sourceMethod.getParameters().size() ) { + return false; + } + for ( int i = 0; i < templateMethod.getParameters().size(); i++ ) { + if ( !templateMethod.getParameters().get( i ).getType().equals( + sourceMethod.getParameters().get( i ).getType() ) ) { + return false; + } + } + return true; + } + + private boolean resultTypeIsTheSame(SourceMethod templateMethod, SourceMethod sourceMethod) { + return templateMethod.getResultType().equals( sourceMethod.getResultType() ); + } + + private void addAllNonRedefined(SourceMethod sourceMethod, AnnotationMirror annotationMirror, + List inheritedMappings) { + Set redefinedSubclassMappings = new HashSet<>( subclassMappings ); + for ( SubclassMappingOptions subclassMappingOption : inheritedMappings ) { + if ( !redefinedSubclassMappings.contains( subclassMappingOption ) ) { + if ( subclassValidator.isValidUsage( + sourceMethod.getExecutable(), + annotationMirror, + subclassMappingOption.getSource() ) ) { + subclassMappings.add( subclassMappingOption ); + } + } + } + } + private void addAllNonRedefined(Set inheritedMappings) { - Set redefinedSources = new HashSet<>(); + // We are only adding the targets here since this mappings have already been reversed Set redefinedTargets = new HashSet<>(); for ( MappingOptions redefinedMappings : mappings ) { - if ( redefinedMappings.getSourceName() != null ) { - redefinedSources.add( redefinedMappings.getSourceName() ); - } if ( redefinedMappings.getTargetName() != null ) { redefinedTargets.add( redefinedMappings.getTargetName() ); } } for ( MappingOptions inheritedMapping : inheritedMappings ) { if ( inheritedMapping.isIgnored() - || ( !isRedefined( redefinedSources, inheritedMapping.getSourceName() ) - && !isRedefined( redefinedTargets, inheritedMapping.getTargetName() ) ) + || !isRedefined( redefinedTargets, inheritedMapping.getTargetName() ) ) { mappings.add( inheritedMapping ); } @@ -315,4 +379,35 @@ private String getFirstTargetPropertyName(MappingOptions mapping) { return getPropertyEntries( mapping )[0]; } + /** + * SubclassMappingOptions are not inherited to forged methods. They would result in an infinite loop if they were. + * + * @return a MappingMethodOptions without SubclassMappingOptions or SubclassValidator. + */ + public static MappingMethodOptions getForgedMethodInheritedOptions(MappingMethodOptions options) { + return new MappingMethodOptions( + options.mapper, + options.mappings, + options.iterableMapping, + options.mapMapping, + BeanMappingOptions.forForgedMethods( options.beanMapping ), + options.enumMappingOptions, + options.valueMappings, + Collections.emptySet(), + null ); + } + + public static MappingMethodOptions getSubclassForgedMethodInheritedOptions(MappingMethodOptions options) { + return new MappingMethodOptions( + options.mapper, + options.mappings, + options.iterableMapping, + options.mapMapping, + BeanMappingOptions.forSubclassForgedMethods( options.beanMapping ), + options.enumMappingOptions, + options.valueMappings, + Collections.emptySet(), + null ); + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodUtils.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodUtils.java index 50ea5df6e0..7d0ab3a7ec 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingMethodUtils.java @@ -10,6 +10,8 @@ import static org.mapstruct.ap.internal.util.Collections.first; /** + * Utility class for mapping methods. + * * @author Filip Hrisafov */ public final class MappingMethodUtils { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java index cb496a3a31..746aca5a32 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MappingOptions.java @@ -18,16 +18,16 @@ import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.gem.MappingGem; import org.mapstruct.ap.internal.gem.MappingsGem; import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem; import org.mapstruct.ap.internal.model.common.FormattingParameters; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.tools.gem.GemValue; /** @@ -37,7 +37,7 @@ */ public class MappingOptions extends DelegatingOptions { - private static final Pattern JAVA_EXPRESSION = Pattern.compile( "^java\\((.*)\\)$" ); + private static final Pattern JAVA_EXPRESSION = Pattern.compile( "^\\s*java\\((.*)\\)\\s*$", Pattern.DOTALL ); private final String sourceName; private final String constant; @@ -118,10 +118,12 @@ public static void addInstance(MappingGem mapping, ExecutableElement method, String conditionExpression = getConditionExpression( mapping, method, messager ); String dateFormat = mapping.dateFormat().getValue(); String numberFormat = mapping.numberFormat().getValue(); + String locale = mapping.locale().getValue(); + String defaultValue = mapping.defaultValue().getValue(); Set dependsOn = mapping.dependsOn().hasValue() ? - new LinkedHashSet( mapping.dependsOn().getValue() ) : + new LinkedHashSet<>( mapping.dependsOn().getValue() ) : Collections.emptySet(); FormattingParameters formattingParam = new FormattingParameters( @@ -129,7 +131,8 @@ public static void addInstance(MappingGem mapping, ExecutableElement method, numberFormat, mapping.mirror(), mapping.dateFormat().getAnnotationValue(), - method + method, + locale ); SelectionParameters selectionParams = new SelectionParameters( mapping.qualifiedBy().get(), @@ -182,7 +185,7 @@ public static MappingOptions forIgnore(String targetName) { null, true, null, - null, + SelectionParameters.empty(), Collections.emptySet(), null, null, @@ -207,10 +210,13 @@ private static boolean isConsistent(MappingGem gem, ExecutableElement method, if ( gem.source().hasValue() && gem.constant().hasValue() ) { message = Message.PROPERTYMAPPING_SOURCE_AND_CONSTANT_BOTH_DEFINED; } + else if ( gem.expression().hasValue() && gem.conditionQualifiedByName().hasValue() ) { + message = Message.PROPERTYMAPPING_EXPRESSION_AND_CONDITION_QUALIFIED_BY_NAME_BOTH_DEFINED; + } else if ( gem.source().hasValue() && gem.expression().hasValue() ) { message = Message.PROPERTYMAPPING_SOURCE_AND_EXPRESSION_BOTH_DEFINED; } - else if (gem.expression().hasValue() && gem.constant().hasValue() ) { + else if ( gem.expression().hasValue() && gem.constant().hasValue() ) { message = Message.PROPERTYMAPPING_EXPRESSION_AND_CONSTANT_BOTH_DEFINED; } else if ( gem.expression().hasValue() && gem.defaultValue().hasValue() ) { @@ -222,9 +228,15 @@ else if ( gem.constant().hasValue() && gem.defaultValue().hasValue() ) { else if ( gem.expression().hasValue() && gem.defaultExpression().hasValue() ) { message = Message.PROPERTYMAPPING_EXPRESSION_AND_DEFAULT_EXPRESSION_BOTH_DEFINED; } + else if ( gem.expression().hasValue() && gem.conditionExpression().hasValue() ) { + message = Message.PROPERTYMAPPING_EXPRESSION_AND_CONDITION_EXPRESSION_BOTH_DEFINED; + } else if ( gem.constant().hasValue() && gem.defaultExpression().hasValue() ) { message = Message.PROPERTYMAPPING_CONSTANT_AND_DEFAULT_EXPRESSION_BOTH_DEFINED; } + else if ( gem.constant().hasValue() && gem.conditionExpression().hasValue() ) { + message = Message.PROPERTYMAPPING_CONSTANT_AND_CONDITION_EXPRESSION_BOTH_DEFINED; + } else if ( gem.defaultValue().hasValue() && gem.defaultExpression().hasValue() ) { message = Message.PROPERTYMAPPING_DEFAULT_VALUE_AND_DEFAULT_EXPRESSION_BOTH_DEFINED; } @@ -248,6 +260,12 @@ else if ( gem.nullValuePropertyMappingStrategy().hasValue() && gem.ignore().hasValue() && gem.ignore().getValue() ) { message = Message.PROPERTYMAPPING_IGNORE_AND_NVPMS; } + else if ( ".".equals( gem.target().get() ) && gem.ignore().hasValue() && gem.ignore().getValue() ) { + message = Message.PROPERTYMAPPING_TARGET_THIS_AND_IGNORE; + } + else if ( ".".equals( gem.target().get() ) && !gem.source().hasValue() ) { + message = Message.PROPERTYMAPPING_TARGET_THIS_NO_SOURCE; + } if ( message == null ) { return true; @@ -464,13 +482,12 @@ public MappingControl getMappingControl(ElementUtils elementUtils) { } /** - * mapping can only be inversed if the source was not a constant nor an expression nor a nested property - * and the mapping is not a 'target-source-ignore' mapping + * Mapping can only be inversed if the source was not a constant nor an expression * * @return true when the above applies */ public boolean canInverse() { - return constant == null && javaExpression == null && !( isIgnored && sourceName == null ); + return constant == null && javaExpression == null; } public MappingOptions copyForInverseInheritance(SourceMethod templateMethod, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java index 0c60f41364..d9c9ce41fc 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/Method.java @@ -83,21 +83,13 @@ public interface Method { Parameter getMappingTargetParameter(); /** - * Returns whether the meethod is designated as bean factory for + * Returns whether the method is designated as bean factory for * mapping target {@link org.mapstruct.ObjectFactory } * * @return true if it is a target bean factory. */ boolean isObjectFactory(); - /** - * Returns whether the method is designated as a presence check method - * @return {@code true} if it is a presence check method - */ - default boolean isPresenceCheck() { - return false; - } - /** * Returns the parameter designated as target type (if present) {@link org.mapstruct.TargetType } * @@ -105,7 +97,6 @@ default boolean isPresenceCheck() { */ Parameter getTargetTypeParameter(); - /** * Returns the {@link Accessibility} of this method. * @@ -187,13 +178,8 @@ default boolean isPresenceCheck() { */ MappingMethodOptions getOptions(); - /** - * - * @return true when @MappingTarget annotated parameter is the same type as the return type. The method has - * to be an update method in order for this to be true. - */ - default boolean isMappingTargetAssignableToReturnType() { - return isUpdateMethod() && getResultType().isAssignableTo( getReturnType() ); + default ConditionMethodOptions getConditionOptions() { + return ConditionMethodOptions.empty(); } /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MethodMatcher.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MethodMatcher.java index f08e4a9152..c62012472c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MethodMatcher.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MethodMatcher.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; - import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; @@ -83,6 +82,17 @@ boolean matches(List sourceTypes, Type targetType) { // (the relation target / target type, target type being a class) if ( !analyser.candidateReturnType.isVoid() ) { + if ( targetType.isPrimitive() ) { + // If the target type is primitive + // then we are going to check if its boxed equivalent + // is assignable to the candidate return type + // This is done because primitives can be assigned from their own narrower counterparts + // directly without any casting. + // e.g. a Long is assignable to a primitive double + // However, in order not to lose information we are not going to allow this + return targetType.getBoxedEquivalent() + .isAssignableTo( analyser.candidateReturnType.getBoxedEquivalent() ); + } if ( !( analyser.candidateReturnType.isAssignableTo( targetType ) ) ) { return false; } @@ -155,7 +165,7 @@ private boolean lineUp() { // Represent result as map. Map resolvedPairs = new HashMap<>(); for ( TypeVarCandidate candidate : methodParCandidates.values() ) { - for ( Type.ResolvedPair pair : candidate.pairs) { + for ( Type.ResolvedPair pair : candidate.pairs ) { resolvedPairs.put( pair.getParameter(), pair.getMatch() ); } } @@ -239,6 +249,8 @@ boolean getCandidates(Type aCandidateMethodType, Type matchingType, Map String method( T par ); @@ -256,6 +268,8 @@ boolean getCandidates(Type aCandidateMethodType, Type matchingType, Map methodParCandidates ) { for ( Map.Entry entry : methodParCandidates.entrySet() ) { - Type bound = entry.getKey().getTypeBound(); - if ( bound != null ) { + for ( Type bound : entry.getKey().getTypeBounds() ) { for ( Type.ResolvedPair pair : entry.getValue().pairs ) { if ( entry.getKey().hasUpperBound() ) { if ( !pair.getMatch().asRawType().isAssignableTo( bound.asRawType() ) ) { @@ -371,7 +384,9 @@ else if ( typeFromCandidateMethodTypeParameter.isWildCardBoundByTypeVar() // something went wrong return null; } - typeArgs[i] = matchingType.getTypeMirror(); + // Use the boxed equivalent for the type arguments, + // because a primitive type cannot be a type argument + typeArgs[i] = matchingType.getBoxedEquivalent().getTypeMirror(); } else { // it is not a type var (e.g. Map ), String is not a type var diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java index fb76c13629..35cdcd9cc0 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SelectionParameters.java @@ -9,9 +9,9 @@ import java.util.List; import java.util.Objects; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.model.common.SourceRHS; +import org.mapstruct.ap.internal.util.TypeUtils; /** * Holding parameters common to the selection process, common to IterableMapping, BeanMapping, PropertyMapping and @@ -21,6 +21,16 @@ */ public class SelectionParameters { + private static final SelectionParameters EMPTY = new SelectionParameters( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + null, + null, + null + ); + private final List qualifiers; private final List qualifyingNames; private final List conditionQualifiers; @@ -39,6 +49,10 @@ public class SelectionParameters { * @return the selection parameters based on the given ones */ public static SelectionParameters forInheritance(SelectionParameters selectionParameters) { + return withoutResultType( selectionParameters ); + } + + public static SelectionParameters withoutResultType(SelectionParameters selectionParameters) { return new SelectionParameters( selectionParameters.qualifiers, selectionParameters.qualifyingNames, @@ -221,4 +235,9 @@ public static SelectionParameters forSourceRHS(SourceRHS sourceRHS) { sourceRHS ); } + + public static SelectionParameters empty() { + return EMPTY; + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java index 86f19618d4..8427dd3eca 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SourceMethod.java @@ -15,16 +15,14 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; -import org.mapstruct.ap.internal.gem.ConditionGem; -import org.mapstruct.ap.internal.util.TypeUtils; - +import org.mapstruct.ap.internal.gem.ObjectFactoryGem; import org.mapstruct.ap.internal.model.common.Accessibility; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; -import org.mapstruct.ap.internal.gem.ObjectFactoryGem; import org.mapstruct.ap.internal.util.Executables; import org.mapstruct.ap.internal.util.Strings; +import org.mapstruct.ap.internal.util.TypeUtils; import static org.mapstruct.ap.internal.model.source.MappingMethodUtils.isEnumMapping; import static org.mapstruct.ap.internal.util.Collections.first; @@ -48,12 +46,14 @@ public class SourceMethod implements Method { private final List parameters; private final Parameter mappingTargetParameter; private final Parameter targetTypeParameter; + private final Parameter sourcePropertyNameParameter; + private final Parameter targetPropertyNameParameter; private final boolean isObjectFactory; - private final boolean isPresenceCheck; private final Type returnType; private final Accessibility accessibility; private final List exceptionTypes; private final MappingMethodOptions mappingMethodOptions; + private final ConditionMethodOptions conditionMethodOptions; private final List prototypeMethods; private final Type mapperToImplement; @@ -94,9 +94,12 @@ public static class Builder { private List valueMappings; private EnumMappingOptions enumMappingOptions; private ParameterProvidedMethods contextProvidedMethods; + private Set conditionOptions; private List typeParameters; + private Set subclassMappings; private boolean verboseLogging; + private SubclassValidator subclassValidator; public Builder setDeclaringMapper(Type declaringMapper) { this.declaringMapper = declaringMapper; @@ -153,6 +156,16 @@ public Builder setEnumMappingOptions(EnumMappingOptions enumMappingOptions) { return this; } + public Builder setSubclassMappings(Set subclassMappings) { + this.subclassMappings = subclassMappings; + return this; + } + + public Builder setSubclassValidator(SubclassValidator subclassValidator) { + this.subclassValidator = subclassValidator; + return this; + } + public Builder setTypeUtils(TypeUtils typeUtils) { this.typeUtils = typeUtils; return this; @@ -173,7 +186,7 @@ public Builder setPrototypeMethods(List prototypeMethods) { return this; } - public Builder setDefininingType(Type definingType) { + public Builder setDefiningType(Type definingType) { this.definingType = definingType; return this; } @@ -183,6 +196,11 @@ public Builder setContextProvidedMethods(ParameterProvidedMethods contextProvide return this; } + public Builder setConditionOptions(Set conditionOptions) { + this.conditionOptions = conditionOptions; + return this; + } + public Builder setVerboseLogging(boolean verboseLogging) { this.verboseLogging = verboseLogging; return this; @@ -194,6 +212,10 @@ public SourceMethod build() { mappings = Collections.emptySet(); } + if ( subclassMappings == null ) { + subclassMappings = Collections.emptySet(); + } + MappingMethodOptions mappingMethodOptions = new MappingMethodOptions( mapper, mappings, @@ -201,20 +223,27 @@ public SourceMethod build() { mapMapping, beanMapping, enumMappingOptions, - valueMappings + valueMappings, + subclassMappings, + subclassValidator ); + ConditionMethodOptions conditionMethodOptions = + conditionOptions != null ? new ConditionMethodOptions( conditionOptions ) : + ConditionMethodOptions.empty(); + this.typeParameters = this.executable.getTypeParameters() .stream() .map( Element::asType ) .map( typeFactory::getType ) .collect( Collectors.toList() ); - return new SourceMethod( this, mappingMethodOptions ); + return new SourceMethod( this, mappingMethodOptions, conditionMethodOptions ); } } - private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions) { + private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions, + ConditionMethodOptions conditionMethodOptions) { this.declaringMapper = builder.declaringMapper; this.executable = builder.executable; this.parameters = builder.parameters; @@ -223,6 +252,7 @@ private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions) this.accessibility = Accessibility.fromModifiers( builder.executable.getModifiers() ); this.mappingMethodOptions = mappingMethodOptions; + this.conditionMethodOptions = conditionMethodOptions; this.sourceParameters = Parameter.getSourceParameters( parameters ); this.contextParameters = Parameter.getContextParameters( parameters ); @@ -231,9 +261,10 @@ private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions) this.mappingTargetParameter = Parameter.getMappingTargetParameter( parameters ); this.targetTypeParameter = Parameter.getTargetTypeParameter( parameters ); + this.sourcePropertyNameParameter = Parameter.getSourcePropertyNameParameter( parameters ); + this.targetPropertyNameParameter = Parameter.getTargetPropertyNameParameter( parameters ); this.hasObjectFactoryAnnotation = ObjectFactoryGem.instanceOn( executable ) != null; this.isObjectFactory = determineIfIsObjectFactory(); - this.isPresenceCheck = determineIfIsPresenceCheck(); this.typeUtils = builder.typeUtils; this.typeFactory = builder.typeFactory; @@ -246,24 +277,13 @@ private SourceMethod(Builder builder, MappingMethodOptions mappingMethodOptions) private boolean determineIfIsObjectFactory() { boolean hasNoSourceParameters = getSourceParameters().isEmpty(); boolean hasNoMappingTargetParam = getMappingTargetParameter() == null; + boolean hasNoSourcePropertyNameParam = getSourcePropertyNameParameter() == null; + boolean hasNoTargetPropertyNameParam = getTargetPropertyNameParameter() == null; return !isLifecycleCallbackMethod() && !returnType.isVoid() - && hasNoMappingTargetParam + && hasNoMappingTargetParam && hasNoSourcePropertyNameParam && hasNoTargetPropertyNameParam && ( hasObjectFactoryAnnotation || hasNoSourceParameters ); } - private boolean determineIfIsPresenceCheck() { - if ( returnType.isPrimitive() ) { - if ( !returnType.getName().equals( "boolean" ) ) { - return false; - } - } - else if ( !returnType.getFullyQualifiedName().equals( Boolean.class.getCanonicalName() ) ) { - return false; - } - - return ConditionGem.instanceOn( executable ) != null; - } - @Override public Type getDeclaringMapper() { return declaringMapper; @@ -362,6 +382,14 @@ public Parameter getTargetTypeParameter() { return targetTypeParameter; } + public Parameter getSourcePropertyNameParameter() { + return sourcePropertyNameParameter; + } + + public Parameter getTargetPropertyNameParameter() { + return targetPropertyNameParameter; + } + public boolean isIterableMapping() { if ( isIterableMapping == null ) { isIterableMapping = getSourceParameters().size() == 1 @@ -516,6 +544,11 @@ public MappingMethodOptions getOptions() { return mappingMethodOptions; } + @Override + public ConditionMethodOptions getConditionOptions() { + return conditionMethodOptions; + } + @Override public boolean isStatic() { return executable.getModifiers().contains( Modifier.STATIC ); @@ -536,11 +569,6 @@ public boolean isLifecycleCallbackMethod() { return Executables.isLifecycleCallbackMethod( getExecutable() ); } - @Override - public boolean isPresenceCheck() { - return isPresenceCheck; - } - public boolean isAfterMappingMethod() { return Executables.isAfterMappingMethod( getExecutable() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SubclassMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SubclassMappingOptions.java new file mode 100644 index 0000000000..2b0e6dd13e --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SubclassMappingOptions.java @@ -0,0 +1,235 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; + +import org.mapstruct.ap.internal.gem.SubclassMappingGem; +import org.mapstruct.ap.internal.gem.SubclassMappingsGem; +import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.TypeUtils; +import org.mapstruct.ap.spi.TypeHierarchyErroneousException; + +import static org.mapstruct.ap.internal.util.Message.SUBCLASSMAPPING_ILLEGAL_SUBCLASS; +import static org.mapstruct.ap.internal.util.Message.SUBCLASSMAPPING_NO_VALID_SUPERCLASS; +import static org.mapstruct.ap.internal.util.Message.SUBCLASSMAPPING_UPDATE_METHODS_NOT_SUPPORTED; + +/** + * Represents a subclass mapping as configured via {@code @SubclassMapping}. + * + * @author Ben Zegveld + */ +public class SubclassMappingOptions extends DelegatingOptions { + + private final TypeMirror source; + private final TypeMirror target; + private final TypeUtils typeUtils; + private final SelectionParameters selectionParameters; + private final SubclassMappingGem subclassMapping; + + public SubclassMappingOptions(TypeMirror source, TypeMirror target, TypeUtils typeUtils, DelegatingOptions next, + SelectionParameters selectionParameters, SubclassMappingGem subclassMapping) { + super( next ); + this.source = source; + this.target = target; + this.typeUtils = typeUtils; + this.selectionParameters = selectionParameters; + this.subclassMapping = subclassMapping; + } + + @Override + public boolean hasAnnotation() { + return source != null && target != null; + } + + private static boolean isConsistent(SubclassMappingGem gem, ExecutableElement method, FormattingMessager messager, + TypeUtils typeUtils, List sourceParameters, Type resultType, + SubclassValidator subclassValidator) { + + if ( resultType == null ) { + messager.printMessage( method, gem.mirror(), SUBCLASSMAPPING_UPDATE_METHODS_NOT_SUPPORTED ); + return false; + } + + TypeMirror sourceSubclass = gem.source().getValue(); + TypeMirror targetSubclass = gem.target().getValue(); + TypeMirror targetParentType = resultType.getTypeMirror(); + validateTypeMirrors( sourceSubclass, targetSubclass, targetParentType ); + + boolean isConsistent = true; + + boolean isChildOfAParameter = false; + for ( Parameter sourceParameter : sourceParameters ) { + TypeMirror sourceParentType = sourceParameter.getType().getTypeMirror(); + validateTypeMirrors( sourceParentType ); + isChildOfAParameter = isChildOfAParameter || isChildOfParent( typeUtils, sourceSubclass, sourceParentType ); + } + if ( !isChildOfAParameter ) { + messager + .printMessage( + method, + gem.mirror(), + SUBCLASSMAPPING_NO_VALID_SUPERCLASS, + sourceSubclass.toString() ); + isConsistent = false; + } + if ( !isChildOfParent( typeUtils, targetSubclass, targetParentType ) ) { + messager + .printMessage( + method, + gem.mirror(), + SUBCLASSMAPPING_ILLEGAL_SUBCLASS, + targetParentType.toString(), + targetSubclass.toString() ); + isConsistent = false; + } + if ( !subclassValidator.isValidUsage( method, gem.mirror(), sourceSubclass ) ) { + isConsistent = false; + } + return isConsistent; + } + + private static void validateTypeMirrors(TypeMirror... typeMirrors) { + for ( TypeMirror typeMirror : typeMirrors ) { + if ( typeMirror == null ) { + // When a class used in uses or imports is created by another annotation processor + // then javac will not return correct TypeMirror with TypeKind#ERROR, but rather a string "" + // the gem tools would return a null TypeMirror in that case. + // Therefore throw TypeHierarchyErroneousException so we can postpone the generation of the mapper + throw new TypeHierarchyErroneousException( typeMirror ); + } + } + } + + private static boolean isChildOfParent(TypeUtils typeUtils, TypeMirror childType, TypeMirror parentType) { + return typeUtils.isSubtype( childType, parentType ); + } + + public TypeMirror getSource() { + return source; + } + + public TypeMirror getTarget() { + return target; + } + + public SelectionParameters getSelectionParameters() { + return selectionParameters; + } + + public AnnotationMirror getMirror() { + return Optional.ofNullable( subclassMapping ).map( SubclassMappingGem::mirror ).orElse( null ); + } + + public static void addInstances(SubclassMappingsGem gem, ExecutableElement method, + BeanMappingOptions beanMappingOptions, FormattingMessager messager, + TypeUtils typeUtils, Set mappings, + List sourceParameters, Type resultType, + SubclassValidator subclassValidator) { + for ( SubclassMappingGem subclassMapping : gem.value().get() ) { + addInstance( + subclassMapping, + method, + beanMappingOptions, + messager, + typeUtils, + mappings, + sourceParameters, + resultType, + subclassValidator ); + } + } + + public static void addInstance(SubclassMappingGem subclassMapping, ExecutableElement method, + BeanMappingOptions beanMappingOptions, FormattingMessager messager, + TypeUtils typeUtils, Set mappings, + List sourceParameters, Type resultType, + SubclassValidator subclassValidator) { + if ( !isConsistent( + subclassMapping, + method, + messager, + typeUtils, + sourceParameters, + resultType, + subclassValidator ) ) { + return; + } + + TypeMirror sourceSubclass = subclassMapping.source().getValue(); + TypeMirror targetSubclass = subclassMapping.target().getValue(); + SelectionParameters selectionParameters = new SelectionParameters( + subclassMapping.qualifiedBy().get(), + subclassMapping.qualifiedByName().get(), + targetSubclass, + typeUtils + ); + + mappings + .add( + new SubclassMappingOptions( + sourceSubclass, + targetSubclass, + typeUtils, + beanMappingOptions, + selectionParameters, + subclassMapping + ) ); + } + + public static List copyForInverseInheritance(Set mappings, + BeanMappingOptions beanMappingOptions) { + // we are not interested in keeping it unique at this point. + return mappings.stream().map( mapping -> new SubclassMappingOptions( + mapping.target, + mapping.source, + mapping.typeUtils, + beanMappingOptions, + mapping.selectionParameters, + mapping.subclassMapping + ) ).collect( Collectors.toCollection( ArrayList::new ) ); + } + + public static List copyForInheritance(Set subclassMappings, + BeanMappingOptions beanMappingOptions) { + // we are not interested in keeping it unique at this point. + List mappings = new ArrayList<>(); + for ( SubclassMappingOptions subclassMapping : subclassMappings ) { + mappings.add( + new SubclassMappingOptions( + subclassMapping.source, + subclassMapping.target, + subclassMapping.typeUtils, + beanMappingOptions, + subclassMapping.selectionParameters, + subclassMapping.subclassMapping ) ); + } + return mappings; + } + + @Override + public boolean equals(Object obj) { + if ( obj == null || !( obj instanceof SubclassMappingOptions ) ) { + return false; + } + SubclassMappingOptions other = (SubclassMappingOptions) obj; + return typeUtils.isSameType( source, other.source ); + } + + @Override + public int hashCode() { + return 1; // use a stable value because TypeMirror is not safe to use for hashCode. + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/SubclassValidator.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SubclassValidator.java new file mode 100644 index 0000000000..601b588e42 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/SubclassValidator.java @@ -0,0 +1,62 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source; + +import java.util.ArrayList; +import java.util.List; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; + +import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.TypeUtils; + +/** + * Handles the validation of multiple @SubclassMapping annotations on the same method. + * + * @author Ben Zegveld + */ +public class SubclassValidator { + + private final FormattingMessager messager; + private final List handledSubclasses = new ArrayList<>(); + private final TypeUtils typeUtils; + + public SubclassValidator(FormattingMessager messager, TypeUtils typeUtils) { + this.messager = messager; + this.typeUtils = typeUtils; + } + + public boolean isValidUsage(Element e, AnnotationMirror annotation, TypeMirror sourceType) { + for ( TypeMirror typeMirror : handledSubclasses ) { + if ( typeUtils.isSameType( sourceType, typeMirror ) ) { + messager + .printMessage( + e, + annotation, + Message.SUBCLASSMAPPING_DOUBLE_SOURCE_SUBCLASS, + sourceType ); + return false; + } + if ( typeUtils.isAssignable( sourceType, typeMirror ) ) { + messager + .printMessage( + e, + annotation, + Message.SUBCLASSMAPPING_ILLOGICAL_ORDER, + sourceType, + typeMirror, + sourceType, + typeMirror ); + return false; + } + } + handledSubclasses.add( sourceType ); + return true; + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ValueMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ValueMappingOptions.java index 963683b5c5..9c412733c4 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ValueMappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ValueMappingOptions.java @@ -5,8 +5,8 @@ */ package org.mapstruct.ap.internal.model.source; -import java.util.List; import java.util.Objects; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.ExecutableElement; @@ -34,7 +34,7 @@ public class ValueMappingOptions { private final AnnotationValue targetAnnotationValue; public static void fromMappingsGem(ValueMappingsGem mappingsGem, ExecutableElement method, - FormattingMessager messager, List mappings) { + FormattingMessager messager, Set mappings) { boolean anyFound = false; for ( ValueMappingGem mappingGem : mappingsGem.value().get() ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/AbstractToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/AbstractToXmlGregorianCalendar.java index e2ccc3f476..87a4648d49 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/AbstractToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/AbstractToXmlGregorianCalendar.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.internal.model.source.builtin; -import static org.mapstruct.ap.internal.util.Collections.asSet; - import java.util.Set; import org.mapstruct.ap.internal.model.common.ConstructorFragment; @@ -16,7 +14,11 @@ import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.util.XmlConstants; +import static org.mapstruct.ap.internal.util.Collections.asSet; + /** + * Base class for built-in methods for converting from a particular type to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public abstract class AbstractToXmlGregorianCalendar extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMappingMethods.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMappingMethods.java index 0edae7f10f..6cd1605b23 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMappingMethods.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMappingMethods.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.util.JaxbConstants; import org.mapstruct.ap.internal.util.JodaTimeConstants; @@ -24,7 +25,7 @@ public class BuiltInMappingMethods { public BuiltInMappingMethods(TypeFactory typeFactory) { boolean isXmlGregorianCalendarPresent = isXmlGregorianCalendarAvailable( typeFactory ); - builtInMethods = new ArrayList<>( 20 ); + builtInMethods = new ArrayList<>( 21 ); if ( isXmlGregorianCalendarPresent ) { builtInMethods.add( new DateToXmlGregorianCalendar( typeFactory ) ); builtInMethods.add( new XmlGregorianCalendarToDate( typeFactory ) ); @@ -39,8 +40,14 @@ public BuiltInMappingMethods(TypeFactory typeFactory) { builtInMethods.add( new XmlGregorianCalendarToLocalDateTime( typeFactory ) ); } - if ( isJaxbAvailable( typeFactory ) ) { - builtInMethods.add( new JaxbElemToValue( typeFactory ) ); + if ( isJavaxJaxbAvailable( typeFactory ) ) { + Type type = typeFactory.getType( JaxbConstants.JAVAX_JAXB_ELEMENT_FQN ); + builtInMethods.add( new JaxbElemToValue( type ) ); + } + + if ( isJakartaJaxbAvailable( typeFactory ) ) { + Type type = typeFactory.getType( JaxbConstants.JAKARTA_JAXB_ELEMENT_FQN ); + builtInMethods.add( new JaxbElemToValue( type ) ); } builtInMethods.add( new ZonedDateTimeToCalendar( typeFactory ) ); @@ -58,8 +65,12 @@ public BuiltInMappingMethods(TypeFactory typeFactory) { } } - private static boolean isJaxbAvailable(TypeFactory typeFactory) { - return typeFactory.isTypeAvailable( JaxbConstants.JAXB_ELEMENT_FQN ); + private static boolean isJavaxJaxbAvailable(TypeFactory typeFactory) { + return typeFactory.isTypeAvailable( JaxbConstants.JAVAX_JAXB_ELEMENT_FQN ); + } + + private static boolean isJakartaJaxbAvailable(TypeFactory typeFactory) { + return typeFactory.isTypeAvailable( JaxbConstants.JAKARTA_JAXB_ELEMENT_FQN ); } private static boolean isXmlGregorianCalendarAvailable(TypeFactory typeFactory) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java index f46b201576..de034d2011 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/BuiltInMethod.java @@ -5,14 +5,11 @@ */ package org.mapstruct.ap.internal.model.source.builtin; -import static org.mapstruct.ap.internal.util.Collections.first; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; - import javax.lang.model.element.ExecutableElement; import org.mapstruct.ap.internal.model.common.Accessibility; @@ -26,6 +23,8 @@ import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods; import org.mapstruct.ap.internal.util.Strings; +import static org.mapstruct.ap.internal.util.Collections.first; + /** * Represents a "built-in" mapping method which will be added as private method to the generated mapper. Built-in * methods are used in cases where a simple conversation doesn't suffice, e.g. as several lines of source code or a @@ -190,7 +189,7 @@ public boolean equals(Object obj) { * * @param parameter source * @param returnType target - * @return {@code true}, iff the the type variables match + * @return {@code true}, iff the type variables match */ public boolean doTypeVarsMatch(Type parameter, Type returnType) { return true; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/CalendarToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/CalendarToXmlGregorianCalendar.java index c8cd89169b..3461538c41 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/CalendarToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/CalendarToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@link Calendar} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class CalendarToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/DateToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/DateToXmlGregorianCalendar.java index 036edd83a2..fb556e68a1 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/DateToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/DateToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@link Date} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class DateToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java index 0d3d4c30af..2a5d1639e6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JaxbElemToValue.java @@ -5,26 +5,23 @@ */ package org.mapstruct.ap.internal.model.source.builtin; -import static org.mapstruct.ap.internal.util.Collections.asSet; - import java.util.Set; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; -import org.mapstruct.ap.internal.model.common.TypeFactory; -import org.mapstruct.ap.internal.util.JaxbConstants; + +import static org.mapstruct.ap.internal.util.Collections.asSet; /** * @author Sjaak Derksen */ -public class JaxbElemToValue extends BuiltInMethod { +class JaxbElemToValue extends BuiltInMethod { private final Parameter parameter; private final Type returnType; private final Set importTypes; - public JaxbElemToValue(TypeFactory typeFactory) { - Type type = typeFactory.getType( JaxbConstants.JAXB_ELEMENT_FQN ); + JaxbElemToValue(Type type) { this.parameter = new Parameter( "element", type ); this.returnType = type.getTypeParameters().get( 0 ); this.importTypes = asSet( parameter.getType() ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaDateTimeToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaDateTimeToXmlGregorianCalendar.java index d0b06300c4..ef57e03175 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaDateTimeToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaDateTimeToXmlGregorianCalendar.java @@ -13,6 +13,8 @@ import org.mapstruct.ap.internal.util.JodaTimeConstants; /** + * A built-in method for converting from Joda {@code DateTime} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class JodaDateTimeToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateTimeToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateTimeToXmlGregorianCalendar.java index 80fa9ba326..285ebb29fe 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateTimeToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateTimeToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from Joda {@code LocalDateTime} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class JodaLocalDateTimeToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateToXmlGregorianCalendar.java index 37c89ef360..8163aa2fb2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalDateToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from Joda {@code LocalDate} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class JodaLocalDateToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalTimeToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalTimeToXmlGregorianCalendar.java index b29c7aec54..4fca4e0ed3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalTimeToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/JodaLocalTimeToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from Joda {@code LocalTime} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class JodaLocalTimeToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateTimeToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateTimeToXmlGregorianCalendar.java index 178a77b5fa..b52c7fb586 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateTimeToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateTimeToXmlGregorianCalendar.java @@ -17,6 +17,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@link LocalDateTime} to {@code XMLGregorianCalendar}. + * * @author Andrei Arlou */ public class LocalDateTimeToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateToXmlGregorianCalendar.java index 2e2544ade0..e2ec7d7696 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/LocalDateToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@link LocalDate} to {@code XMLGregorianCalendar}. + * * @author Gunnar Morling */ public class LocalDateToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/StringToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/StringToXmlGregorianCalendar.java index 9f0d2ad207..635853d0fe 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/StringToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/StringToXmlGregorianCalendar.java @@ -19,6 +19,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@link String} to {@code XMLGregorianCalendar}. + * * @author Sjaak Derksen */ public class StringToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToCalendar.java index 03927fd869..e52b306af0 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to {@link Calendar}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToCalendar extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToDate.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToDate.java index 38f76a66ee..fd3d9a33a6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToDate.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToDate.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to {@link Date}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToDate extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaDateTime.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaDateTime.java index ab6658bfc5..93ec3e8f96 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaDateTime.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaDateTime.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.internal.model.source.builtin; +import java.util.Calendar; import java.util.Set; import org.mapstruct.ap.internal.model.common.Parameter; @@ -16,6 +17,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from Joda {@code DateTime} to {@link Calendar}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToJodaDateTime extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDate.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDate.java index f45488755c..9a9c1561fc 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDate.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDate.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to Joda {@code LocalDate}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToJodaLocalDate extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDateTime.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDateTime.java index af7fd9e9fd..a8d36a13cf 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDateTime.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalDateTime.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to Joda {@code LocalDateTime}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToJodaLocalDateTime extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalTime.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalTime.java index 3725691134..b26450d560 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalTime.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToJodaLocalTime.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * Conversion from {@code XMLGregorianCalendar} to Joda {@code LocalTime}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToJodaLocalTime extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDate.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDate.java index 2c9ced3413..7bcd5902a5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDate.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDate.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to {@link LocalDate}. + * * @author Gunnar Morling */ public class XmlGregorianCalendarToLocalDate extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDateTime.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDateTime.java index b676bd03a4..21b4c57854 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDateTime.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToLocalDateTime.java @@ -17,6 +17,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to {@link LocalDateTime}. + * * @author Andrei Arlou */ public class XmlGregorianCalendarToLocalDateTime extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToString.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToString.java index c932da6675..e293a9c80b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToString.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/XmlGregorianCalendarToString.java @@ -18,6 +18,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@code XMLGregorianCalendar} to {@link String}. + * * @author Sjaak Derksen */ public class XmlGregorianCalendarToString extends BuiltInMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/ZonedDateTimeToXmlGregorianCalendar.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/ZonedDateTimeToXmlGregorianCalendar.java index 27a39cd2b7..d9e39b388e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/ZonedDateTimeToXmlGregorianCalendar.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/builtin/ZonedDateTimeToXmlGregorianCalendar.java @@ -16,6 +16,8 @@ import static org.mapstruct.ap.internal.util.Collections.asSet; /** + * A built-in method for converting from {@link ZonedDateTime} to {@code XMLGregorianCalendar}. + * * @author Christian Bandowski */ public class ZonedDateTimeToXmlGregorianCalendar extends AbstractToXmlGregorianCalendar { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java index ed1c72ab2a..b6e5ca0a53 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/CreateOrUpdateSelector.java @@ -8,7 +8,6 @@ import java.util.ArrayList; import java.util.List; -import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; /** @@ -29,14 +28,11 @@ public class CreateOrUpdateSelector implements MethodSelector { @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { - + public List> getMatchingMethods(List> methods, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); if ( criteria.isLifecycleCallbackRequired() || criteria.isObjectFactoryRequired() + || criteria.isSourceParameterCheckRequired() || criteria.isPresenceCheckRequired() ) { return methods; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/FactoryParameterSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/FactoryParameterSelector.java index 41d37e8b69..50896195f1 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/FactoryParameterSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/FactoryParameterSelector.java @@ -8,7 +8,6 @@ import java.util.ArrayList; import java.util.List; -import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; /** @@ -21,11 +20,9 @@ public class FactoryParameterSelector implements MethodSelector { @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List> methods, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); if ( !criteria.isObjectFactoryRequired() || methods.size() <= 1 ) { return methods; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/InheritanceSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/InheritanceSelector.java index a624c1accb..7b56564ba9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/InheritanceSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/InheritanceSelector.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.internal.model.source.selector; -import static org.mapstruct.ap.internal.util.Collections.first; - import java.util.ArrayList; import java.util.List; @@ -14,6 +12,8 @@ import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; +import static org.mapstruct.ap.internal.util.Collections.first; + /** * Selects on inheritance distance, e.g. the amount of inheritance steps from the parameter type. * @@ -22,26 +22,22 @@ public class InheritanceSelector implements MethodSelector { @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List> methods, + SelectionContext context) { - if ( sourceTypes.size() != 1 ) { + Type sourceType = context.getSourceType(); + if ( sourceType == null ) { return methods; } - Type singleSourceType = first( sourceTypes ); - List> candidatesWithBestMatchingSourceType = new ArrayList<>(); int bestMatchingSourceTypeDistance = Integer.MAX_VALUE; - // find the methods with the minimum distance regarding getParameter getParameter type + // Find methods with the minimum inheritance distance from the source parameter type for ( SelectedMethod method : methods ) { Parameter singleSourceParam = first( method.getMethod().getSourceParameters() ); - int sourceTypeDistance = singleSourceType.distanceTo( singleSourceParam.getType() ); + int sourceTypeDistance = sourceType.distanceTo( singleSourceParam.getType() ); bestMatchingSourceTypeDistance = addToCandidateListIfMinimal( candidatesWithBestMatchingSourceType, @@ -53,17 +49,17 @@ public List> getMatchingMethods(Method mapp return candidatesWithBestMatchingSourceType; } - private int addToCandidateListIfMinimal(List> candidatesWithBestMathingType, + private int addToCandidateListIfMinimal(List> candidatesWithBestMatchingType, int bestMatchingTypeDistance, SelectedMethod method, int currentTypeDistance) { if ( currentTypeDistance == bestMatchingTypeDistance ) { - candidatesWithBestMathingType.add( method ); + candidatesWithBestMatchingType.add( method ); } else if ( currentTypeDistance < bestMatchingTypeDistance ) { bestMatchingTypeDistance = currentTypeDistance; - candidatesWithBestMathingType.clear(); - candidatesWithBestMathingType.add( method ); + candidatesWithBestMatchingType.clear(); + candidatesWithBestMatchingType.add( method ); } return bestMatchingTypeDistance; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/JakartaXmlElementDeclSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/JakartaXmlElementDeclSelector.java new file mode 100644 index 0000000000..d5595ef6b7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/JakartaXmlElementDeclSelector.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source.selector; + +import javax.lang.model.element.Element; + +import org.mapstruct.ap.internal.gem.jakarta.XmlElementDeclGem; +import org.mapstruct.ap.internal.gem.jakarta.XmlElementRefGem; +import org.mapstruct.ap.internal.util.TypeUtils; + +/** + * The concrete implementation of the {@link XmlElementDeclSelector} that + * works with {@link jakarta.xml.bind.annotation.XmlElementRef} and + * {@link jakarta.xml.bind.annotation.XmlElementDecl}. + * + * @author Iaroslav Bogdanchikov + */ +class JakartaXmlElementDeclSelector extends XmlElementDeclSelector { + + JakartaXmlElementDeclSelector(TypeUtils typeUtils) { + super( typeUtils ); + } + + @Override + XmlElementDeclInfo getXmlElementDeclInfo(Element element) { + XmlElementDeclGem gem = XmlElementDeclGem.instanceOn( element ); + + if ( gem == null ) { + return null; + } + + return new XmlElementDeclInfo( gem.name().get(), gem.scope().get() ); + } + + @Override + XmlElementRefInfo getXmlElementRefInfo(Element element) { + XmlElementRefGem gem = XmlElementRefGem.instanceOn( element ); + + if ( gem == null ) { + return null; + } + + return new XmlElementRefInfo( gem.name().get(), gem.type().get() ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/JavaxXmlElementDeclSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/JavaxXmlElementDeclSelector.java new file mode 100644 index 0000000000..912da3e696 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/JavaxXmlElementDeclSelector.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source.selector; + +import javax.lang.model.element.Element; + +import org.mapstruct.ap.internal.gem.XmlElementDeclGem; +import org.mapstruct.ap.internal.gem.XmlElementRefGem; +import org.mapstruct.ap.internal.util.TypeUtils; + +/** + * The concrete implementation of the {@link XmlElementDeclSelector} that + * works with {@link javax.xml.bind.annotation.XmlElementRef} and + * {@link javax.xml.bind.annotation.XmlElementDecl}. + * + * @author Iaroslav Bogdanchikov + */ +class JavaxXmlElementDeclSelector extends XmlElementDeclSelector { + + JavaxXmlElementDeclSelector(TypeUtils typeUtils) { + super( typeUtils ); + } + + @Override + XmlElementDeclInfo getXmlElementDeclInfo(Element element) { + XmlElementDeclGem gem = XmlElementDeclGem.instanceOn( element ); + + if ( gem == null ) { + return null; + } + + return new XmlElementDeclInfo( gem.name().get(), gem.scope().get() ); + } + + @Override + XmlElementRefInfo getXmlElementRefInfo(Element element) { + XmlElementRefGem gem = XmlElementRefGem.instanceOn( element ); + + if ( gem == null ) { + return null; + } + + return new XmlElementRefInfo( gem.name().get(), gem.type().get() ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/LifecycleOverloadDeduplicateSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/LifecycleOverloadDeduplicateSelector.java new file mode 100644 index 0000000000..7ce23af186 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/LifecycleOverloadDeduplicateSelector.java @@ -0,0 +1,129 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source.selector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.ParameterBinding; +import org.mapstruct.ap.internal.model.source.Method; + +/** + * Selector for deduplicating overloaded lifecycle callback methods + * whose parameter signatures differ only by type hierarchy. + *

      + * In the context of lifecycle callback method selection + * (such as @BeforeMapping or @AfterMapping), it is possible to have multiple overloaded methods + * whose parameter lists are structurally identical except for the specific types, + * where those types are related by inheritance (e.g., one parameter is a superclass or subclass of another). + *

      + * This selector groups such methods by their effective parameter signature + * (ignoring differences only in type hierarchy), and, within each group, + * retains only the method whose parameter types have the closest inheritance distance + * to the actual invocation types. + * This ensures that, for each group of nearly identical overloads, + * only the most specific and appropriate method is selected. + *

      + * Example (see Issue3849Test): + * + *

      {@code
      + * @AfterMapping
      + * default void afterMapping(Parent source, @MappingTarget ParentDto target) { ... }
      + * @AfterMapping
      + * default void afterMapping(Parent source, @MappingTarget ChildDto target) { ... }
      + * }
      + * When mapping a Child to a ChildDto, + * only the method with ChildDto is selected, even though both methods match by signature + * except for the target type's inheritance relationship. + */ +public class LifecycleOverloadDeduplicateSelector implements MethodSelector { + @Override + public List> getMatchingMethods(List> methods, + SelectionContext context) { + if ( !context.getSelectionCriteria().isLifecycleCallbackRequired() || methods.size() <= 1 ) { + return methods; + } + Collection>> methodSignatureGroups = + methods.stream() + .collect( Collectors.groupingBy( + LifecycleOverloadDeduplicateSelector::buildSignatureKey, + LinkedHashMap::new, + Collectors.toList() + ) ) + .values(); + List> deduplicatedMethods = new ArrayList<>( methods.size() ); + for ( List> signatureGroup : methodSignatureGroups ) { + if ( signatureGroup.size() == 1 ) { + deduplicatedMethods.add( signatureGroup.get( 0 ) ); + continue; + } + SelectedMethod bestInheritanceMethod = signatureGroup.get( 0 ); + for ( int i = 1; i < signatureGroup.size(); i++ ) { + SelectedMethod candidateMethod = signatureGroup.get( i ); + if ( isInheritanceBetter( candidateMethod, bestInheritanceMethod ) ) { + bestInheritanceMethod = candidateMethod; + } + } + deduplicatedMethods.add( bestInheritanceMethod ); + } + return deduplicatedMethods; + } + + /** + * Builds a grouping key for a method based on its defining type, + * method name, and a detailed breakdown of each parameter binding. + *

      + * The key consists of: + *

        + *
      • The type that defines the method
      • + *
      • The method name
      • + *
      • parameter bindings
      • + *
      + * This ensures that methods are grouped together only if all these aspects match, + * except for differences in type hierarchy, which are handled separately. + */ + private static List buildSignatureKey(SelectedMethod method) { + List key = new ArrayList<>(); + key.add( method.getMethod().getDefiningType() ); + key.add( method.getMethod().getName() ); + for ( ParameterBinding binding : method.getParameterBindings() ) { + key.add( binding.getType() ); + key.add( binding.getVariableName() ); + } + return key; + } + + /** + * Compare the inheritance distance of parameters between two methods to determine if candidateMethod is better. + * Compares parameters in order, returns as soon as a better one is found. + */ + private boolean isInheritanceBetter(SelectedMethod candidateMethod, + SelectedMethod currentBestMethod) { + List candidateBindings = candidateMethod.getParameterBindings(); + List bestBindings = currentBestMethod.getParameterBindings(); + List candidateParams = candidateMethod.getMethod().getParameters(); + List bestParams = currentBestMethod.getMethod().getParameters(); + int paramCount = candidateBindings.size(); + + for ( int i = 0; i < paramCount; i++ ) { + int candidateDistance = candidateBindings.get( i ) + .getType() + .distanceTo( candidateParams.get( i ).getType() ); + int bestDistance = bestBindings.get( i ).getType().distanceTo( bestParams.get( i ).getType() ); + if ( candidateDistance < bestDistance ) { + return true; + } + else if ( candidateDistance > bestDistance ) { + return false; + } + } + return false; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java index 37502ec56d..691199b49d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodFamilySelector.java @@ -8,7 +8,7 @@ import java.util.ArrayList; import java.util.List; -import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.gem.ConditionStrategyGem; import org.mapstruct.ap.internal.model.source.Method; /** @@ -20,18 +20,28 @@ public class MethodFamilySelector implements MethodSelector { @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List> methods, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); List> result = new ArrayList<>( methods.size() ); for ( SelectedMethod method : methods ) { - if ( method.getMethod().isObjectFactory() == criteria.isObjectFactoryRequired() + if ( criteria.isPresenceCheckRequired() ) { + if ( method.getMethod() + .getConditionOptions() + .isStrategyApplicable( ConditionStrategyGem.PROPERTIES ) ) { + result.add( method ); + } + } + else if ( criteria.isSourceParameterCheckRequired() ) { + if ( method.getMethod() + .getConditionOptions() + .isStrategyApplicable( ConditionStrategyGem.SOURCE_PARAMETERS ) ) { + result.add( method ); + } + } + else if ( method.getMethod().isObjectFactory() == criteria.isObjectFactoryRequired() && method.getMethod().isLifecycleCallbackMethod() == criteria.isLifecycleCallbackRequired() - && method.getMethod().isPresenceCheck() == criteria.isPresenceCheckRequired() ) { result.add( method ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelector.java index 86d35bb3aa..375d5a9bf8 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelector.java @@ -7,7 +7,6 @@ import java.util.List; -import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; /** @@ -23,18 +22,10 @@ interface MethodSelector { * Selects those methods which match the given types and other criteria * * @param either SourceMethod or BuiltInMethod - * @param mappingMethod mapping method, defined in Mapper for which this selection is carried out * @param candidates list of available methods - * @param sourceTypes parameter type(s) that should be matched - * @param mappingTargetType mappingTargetType that should be matched - * @param returnType return type that should be matched - * @param criteria criteria used in the selection process + * @param context the context for the matching * @return list of methods that passes the matching process */ - List> getMatchingMethods(Method mappingMethod, - List> candidates, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria); + List> getMatchingMethods(List> candidates, + SelectionContext context); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java index de429174da..774f25a8c3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MethodSelectors.java @@ -9,9 +9,8 @@ import java.util.Arrays; import java.util.List; -import org.mapstruct.ap.internal.model.common.Type; -import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.source.Method; +import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.TypeUtils; @@ -25,38 +24,40 @@ public class MethodSelectors { private final List selectors; - public MethodSelectors(TypeUtils typeUtils, ElementUtils elementUtils, TypeFactory typeFactory, - FormattingMessager messager) { - selectors = Arrays.asList( + public MethodSelectors(TypeUtils typeUtils, ElementUtils elementUtils, + FormattingMessager messager, Options options) { + List selectorList = new ArrayList<>( Arrays.asList( new MethodFamilySelector(), - new TypeSelector( typeFactory, messager ), + new TypeSelector( messager ), new QualifierSelector( typeUtils, elementUtils ), new TargetTypeSelector( typeUtils ), - new XmlElementDeclSelector( typeUtils ), - new InheritanceSelector(), + new JavaxXmlElementDeclSelector( typeUtils ), + new JakartaXmlElementDeclSelector( typeUtils ), + new InheritanceSelector() + ) ); + if ( options != null && !options.isDisableLifecycleOverloadDeduplicateSelector() ) { + selectorList.add( new LifecycleOverloadDeduplicateSelector() ); + } + + selectorList.addAll( Arrays.asList( new CreateOrUpdateSelector(), new SourceRhsSelector(), - new FactoryParameterSelector() ); + new FactoryParameterSelector(), + new MostSpecificResultTypeSelector() + ) ); + this.selectors = selectorList; } /** * Selects those methods which match the given types and other criteria * * @param either SourceMethod or BuiltInMethod - * @param mappingMethod mapping method, defined in Mapper for which this selection is carried out * @param methods list of available methods - * @param sourceTypes parameter type(s) that should be matched - * @param mappingTargetType the mapping target type that should be matched - * @param returnType return type that should be matched - * @param criteria criteria used in the selection process + * @param context the selection context that should be used in the matching process * @return list of methods that passes the matching process */ - public List> getMatchingMethods(Method mappingMethod, - List methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List methods, + SelectionContext context) { List> candidates = new ArrayList<>( methods.size() ); for ( T method : methods ) { @@ -64,13 +65,7 @@ public List> getMatchingMethods(Method mapp } for ( MethodSelector selector : selectors ) { - candidates = selector.getMatchingMethods( - mappingMethod, - candidates, - sourceTypes, - mappingTargetType, - returnType, - criteria ); + candidates = selector.getMatchingMethods( candidates, context ); } return candidates; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MostSpecificResultTypeSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MostSpecificResultTypeSelector.java new file mode 100644 index 0000000000..88e302eba0 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/MostSpecificResultTypeSelector.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source.selector; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.source.Method; + +/** + * A {@link MethodSelector} that selects the most specific result type. + * + * @author Filip Hrisafov + */ +public class MostSpecificResultTypeSelector implements MethodSelector { + + @Override + public List> getMatchingMethods(List> candidates, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); + Type mappingTargetType = context.getMappingTargetType(); + if ( candidates.size() < 2 || !criteria.isForMapping() || criteria.getQualifyingResultType() != null ) { + return candidates; + } + + List> result = new ArrayList<>(); + + for ( SelectedMethod candidate : candidates ) { + if ( candidate.getMethod() + .getResultType() + .getBoxedEquivalent() + .equals( mappingTargetType.getBoxedEquivalent() ) ) { + // If the result type is the same as the target type + // then this candidate has the most specific match and should be used + result.add( candidate ); + } + } + + + // If not most specific types were found then return the current candidates + return result.isEmpty() ? candidates : result; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/QualifierSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/QualifierSelector.java index 210e462e3e..744c03d77c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/QualifierSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/QualifierSelector.java @@ -12,14 +12,14 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; +import org.mapstruct.ap.internal.gem.NamedGem; +import org.mapstruct.ap.internal.gem.QualifierGem; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SourceMethod; -import org.mapstruct.ap.internal.gem.NamedGem; -import org.mapstruct.ap.internal.gem.QualifierGem; +import org.mapstruct.ap.internal.util.ElementUtils; +import org.mapstruct.ap.internal.util.TypeUtils; /** * This selector selects a best match based on qualifier annotations. @@ -49,12 +49,9 @@ public QualifierSelector(TypeUtils typeUtils, ElementUtils elementUtils ) { } @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List> methods, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); int numberOfQualifiersToMatch = 0; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java index 7acb1af210..1702a8d4d6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectedMethod.java @@ -14,6 +14,8 @@ /** * A selected method with additional metadata that might be required for further usage of the selected method. * + * @param the type of the method + * * @author Andreas Gudian */ public class SelectedMethod { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java new file mode 100644 index 0000000000..79351ae868 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java @@ -0,0 +1,302 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.model.source.selector; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.ParameterBinding; +import org.mapstruct.ap.internal.model.common.SourceRHS; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.model.common.TypeFactory; +import org.mapstruct.ap.internal.model.source.Method; +import org.mapstruct.ap.internal.model.source.SelectionParameters; + +/** + * Context passed to the selectors to get the information they need. + * + * @author Filip Hrisafov + */ +public class SelectionContext { + + private final Type sourceType; + private final SelectionCriteria selectionCriteria; + private final Method mappingMethod; + private final Type mappingTargetType; + private final Type returnType; + private final Supplier> parameterBindingsProvider; + private List parameterBindings; + + private SelectionContext(Type sourceType, SelectionCriteria selectionCriteria, Method mappingMethod, + Type mappingTargetType, Type returnType, + Supplier> parameterBindingsProvider) { + this.sourceType = sourceType; + this.selectionCriteria = selectionCriteria; + this.mappingMethod = mappingMethod; + this.mappingTargetType = mappingTargetType; + this.returnType = returnType; + this.parameterBindingsProvider = parameterBindingsProvider; + } + + /** + * @return the source type that should be matched + */ + public Type getSourceType() { + return sourceType; + } + + /** + * @return the criteria used in the selection process + */ + public SelectionCriteria getSelectionCriteria() { + return selectionCriteria; + } + + /** + * @return the mapping target type that should be matched + */ + public Type getMappingTargetType() { + return mappingTargetType; + } + + /** + * @return the return type that should be matched + */ + public Type getReturnType() { + return returnType; + } + + /** + * @return the available parameter bindings for the matching + */ + public List getAvailableParameterBindings() { + if ( this.parameterBindings == null ) { + this.parameterBindings = this.parameterBindingsProvider.get(); + } + return parameterBindings; + } + + /** + * @return the mapping method, defined in Mapper for which this selection is carried out + */ + public Method getMappingMethod() { + return mappingMethod; + } + + public static SelectionContext forMappingMethods(Method mappingMethod, Type source, Type target, + SelectionCriteria criteria, TypeFactory typeFactory) { + return new SelectionContext( + source, + criteria, + mappingMethod, + target, + target, + () -> getAvailableParameterBindingsFromSourceType( + source, + target, + mappingMethod, + typeFactory + ) + ); + } + + public static SelectionContext forLifecycleMethods(Method mappingMethod, Type targetType, + SelectionParameters selectionParameters, + TypeFactory typeFactory, + Supplier> additionalParameterBindingsProvider) { + SelectionCriteria criteria = SelectionCriteria.forLifecycleMethods( selectionParameters ); + return new SelectionContext( + null, + criteria, + mappingMethod, + targetType, + mappingMethod.getResultType(), + () -> { + List parameterBindings = getAvailableParameterBindingsFromMethod( + mappingMethod, + targetType, + criteria.getSourceRHS(), + typeFactory + ); + parameterBindings.addAll( additionalParameterBindingsProvider.get() ); + return parameterBindings; + } + ); + } + + public static SelectionContext forFactoryMethods(Method mappingMethod, Type alternativeTarget, + SelectionParameters selectionParameters, + TypeFactory typeFactory) { + SelectionCriteria criteria = SelectionCriteria.forFactoryMethods( selectionParameters ); + return new SelectionContext( + null, + criteria, + mappingMethod, + alternativeTarget, + alternativeTarget, + () -> getAvailableParameterBindingsFromMethod( + mappingMethod, + alternativeTarget, + criteria.getSourceRHS(), + typeFactory + ) + ); + } + + public static SelectionContext forPresenceCheckMethods(Method mappingMethod, + SelectionParameters selectionParameters, + TypeFactory typeFactory) { + SelectionCriteria criteria = SelectionCriteria.forPresenceCheckMethods( selectionParameters ); + Type booleanType = typeFactory.getType( Boolean.class ); + return new SelectionContext( + null, + criteria, + mappingMethod, + booleanType, + booleanType, + () -> getAvailableParameterBindingsFromMethod( + mappingMethod, + booleanType, + criteria.getSourceRHS(), + typeFactory + ) + ); + } + + public static SelectionContext forSourceParameterPresenceCheckMethods(Method mappingMethod, + SelectionParameters selectionParameters, + Parameter sourceParameter, + TypeFactory typeFactory) { + SelectionCriteria criteria = SelectionCriteria.forSourceParameterCheckMethods( selectionParameters ); + Type booleanType = typeFactory.getType( Boolean.class ); + return new SelectionContext( + null, + criteria, + mappingMethod, + booleanType, + booleanType, + () -> getParameterBindingsForSourceParameterPresenceCheck( + mappingMethod, + booleanType, + sourceParameter, + typeFactory + ) + ); + } + + private static List getParameterBindingsForSourceParameterPresenceCheck(Method method, + Type targetType, + Parameter sourceParameter, + TypeFactory typeFactory) { + + List availableParams = new ArrayList<>( method.getParameters().size() + 3 ); + + availableParams.add( ParameterBinding.fromParameter( sourceParameter ) ); + availableParams.add( ParameterBinding.forTargetTypeBinding( typeFactory.classTypeOf( targetType ) ) ); + for ( Parameter parameter : method.getParameters() ) { + if ( !parameter.isSourceParameter( ) ) { + availableParams.add( ParameterBinding.fromParameter( parameter ) ); + } + } + + return availableParams; + } + + private static List getAvailableParameterBindingsFromMethod(Method method, Type targetType, + SourceRHS sourceRHS, + TypeFactory typeFactory) { + List availableParams = new ArrayList<>( method.getParameters().size() + 3 ); + + if ( sourceRHS != null ) { + availableParams.addAll( ParameterBinding.fromParameters( method.getParameters() ) ); + availableParams.add( ParameterBinding.fromSourceRHS( sourceRHS ) ); + addSourcePropertyNameBindings( availableParams, sourceRHS.getSourceType(), typeFactory ); + } + else { + availableParams.addAll( ParameterBinding.fromParameters( method.getParameters() ) ); + } + + addTargetRelevantBindings( availableParams, targetType, typeFactory ); + + return availableParams; + } + + private static List getAvailableParameterBindingsFromSourceType(Type sourceType, + Type targetType, + Method mappingMethod, + TypeFactory typeFactory) { + + List availableParams = new ArrayList<>(); + + availableParams.add( ParameterBinding.forSourceTypeBinding( sourceType ) ); + addSourcePropertyNameBindings( availableParams, sourceType, typeFactory ); + + for ( Parameter param : mappingMethod.getParameters() ) { + if ( param.isMappingContext() ) { + availableParams.add( ParameterBinding.fromParameter( param ) ); + } + } + + addTargetRelevantBindings( availableParams, targetType, typeFactory ); + + return availableParams; + } + + private static void addSourcePropertyNameBindings(List availableParams, Type sourceType, + TypeFactory typeFactory) { + + boolean sourcePropertyNameAvailable = false; + for ( ParameterBinding pb : availableParams ) { + if ( pb.isSourcePropertyName() ) { + sourcePropertyNameAvailable = true; + break; + } + } + if ( !sourcePropertyNameAvailable ) { + availableParams.add( ParameterBinding.forSourcePropertyNameBinding( typeFactory.getType( String.class ) ) ); + } + + } + + /** + * Adds default parameter bindings for the mapping-target and target-type if not already available. + * + * @param availableParams Already available params, new entries will be added to this list + * @param targetType Target type + */ + private static void addTargetRelevantBindings(List availableParams, Type targetType, + TypeFactory typeFactory) { + boolean mappingTargetAvailable = false; + boolean targetTypeAvailable = false; + boolean targetPropertyNameAvailable = false; + + // search available parameter bindings if mapping-target and/or target-type is available + for ( ParameterBinding pb : availableParams ) { + if ( pb.isMappingTarget() ) { + mappingTargetAvailable = true; + } + else if ( pb.isTargetType() ) { + targetTypeAvailable = true; + } + else if ( pb.isTargetPropertyName() ) { + targetPropertyNameAvailable = true; + } + } + + if ( !mappingTargetAvailable ) { + availableParams.add( ParameterBinding.forMappingTargetBinding( targetType ) ); + } + if ( !targetTypeAvailable ) { + availableParams.add( ParameterBinding.forTargetTypeBinding( typeFactory.classTypeOf( targetType ) ) ); + } + if ( !targetPropertyNameAvailable ) { + availableParams.add( ParameterBinding.forTargetPropertyNameBinding( typeFactory.getType( String.class ) ) ); + } + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java index b70e94b6d1..45e11fe6ea 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionCriteria.java @@ -5,9 +5,8 @@ */ package org.mapstruct.ap.internal.model.source.selector; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; - import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.model.common.SourceRHS; @@ -21,49 +20,39 @@ */ public class SelectionCriteria { - private final List qualifiers = new ArrayList<>(); - private final List qualifiedByNames = new ArrayList<>(); + private final QualifyingInfo qualifyingInfo; private final String targetPropertyName; - private final TypeMirror qualifyingResultType; private final SourceRHS sourceRHS; + private boolean ignoreQualifiers = false; private Type type; - private final boolean allowDirect; - private final boolean allowConversion; - private final boolean allowMappingMethod; - private final boolean allow2Steps; + private final MappingControl mappingControl; public SelectionCriteria(SelectionParameters selectionParameters, MappingControl mappingControl, String targetPropertyName, Type type) { - if ( selectionParameters != null ) { - if ( type == Type.PRESENCE_CHECK ) { - qualifiers.addAll( selectionParameters.getConditionQualifiers() ); - qualifiedByNames.addAll( selectionParameters.getConditionQualifyingNames() ); - } - else { - qualifiers.addAll( selectionParameters.getQualifiers() ); - qualifiedByNames.addAll( selectionParameters.getQualifyingNames() ); - } - qualifyingResultType = selectionParameters.getResultType(); - sourceRHS = selectionParameters.getSourceRHS(); - } - else { - this.qualifyingResultType = null; - sourceRHS = null; - } - if ( mappingControl != null ) { - this.allowDirect = mappingControl.allowDirect(); - this.allowConversion = mappingControl.allowTypeConversion(); - this.allowMappingMethod = mappingControl.allowMappingMethod(); - this.allow2Steps = mappingControl.allowBy2Steps(); - } - else { - this.allowDirect = true; - this.allowConversion = true; - this.allowMappingMethod = true; - this.allow2Steps = true; - } + this( + QualifyingInfo.fromSelectionParameters( selectionParameters ), + selectionParameters != null ? selectionParameters.getSourceRHS() : null, + mappingControl, + targetPropertyName, + type + ); + } + + private SelectionCriteria(QualifyingInfo qualifyingInfo, SourceRHS sourceRHS, MappingControl mappingControl, + String targetPropertyName, Type type) { + this.qualifyingInfo = qualifyingInfo; this.targetPropertyName = targetPropertyName; + this.sourceRHS = sourceRHS; this.type = type; + this.mappingControl = mappingControl; + } + + /** + * + * @return {@code true} if only mapping methods should be selected + */ + public boolean isForMapping() { + return type == null || type == Type.PREFER_UPDATE_MAPPING; } /** @@ -87,12 +76,23 @@ public boolean isPresenceCheckRequired() { return type == Type.PRESENCE_CHECK; } + /** + * @return {@code true} if source parameter check methods should be selected, {@code false} otherwise + */ + public boolean isSourceParameterCheckRequired() { + return type == Type.SOURCE_PARAMETER_CHECK; + } + + public void setIgnoreQualifiers(boolean ignoreQualifiers) { + this.ignoreQualifiers = ignoreQualifiers; + } + public List getQualifiers() { - return qualifiers; + return ignoreQualifiers ? Collections.emptyList() : qualifyingInfo.qualifiers(); } public List getQualifiedByNames() { - return qualifiedByNames; + return ignoreQualifiers ? Collections.emptyList() : qualifyingInfo.qualifiedByNames(); } public String getTargetPropertyName() { @@ -100,7 +100,7 @@ public String getTargetPropertyName() { } public TypeMirror getQualifyingResultType() { - return qualifyingResultType; + return qualifyingInfo.qualifyingResultType(); } public boolean isPreferUpdateMapping() { @@ -116,23 +116,27 @@ public void setPreferUpdateMapping(boolean preferUpdateMapping) { } public boolean hasQualfiers() { - return !qualifiedByNames.isEmpty() || !qualifiers.isEmpty(); + return !qualifyingInfo.qualifiedByNames().isEmpty() || !qualifyingInfo.qualifiers().isEmpty(); } public boolean isAllowDirect() { - return allowDirect; + return mappingControl == null || mappingControl.allowDirect(); } public boolean isAllowConversion() { - return allowConversion; + return mappingControl == null || mappingControl.allowTypeConversion(); } public boolean isAllowMappingMethod() { - return allowMappingMethod; + return mappingControl == null || mappingControl.allowMappingMethod(); } public boolean isAllow2Steps() { - return allow2Steps; + return mappingControl == null || mappingControl.allowBy2Steps(); + } + + public boolean isSelfAllowed() { + return type != Type.SELF_NOT_ALLOWED; } public static SelectionCriteria forMappingMethods(SelectionParameters selectionParameters, @@ -156,13 +160,83 @@ public static SelectionCriteria forLifecycleMethods(SelectionParameters selectio } public static SelectionCriteria forPresenceCheckMethods(SelectionParameters selectionParameters) { - return new SelectionCriteria( selectionParameters, null, null, Type.PRESENCE_CHECK ); + SourceRHS sourceRHS = selectionParameters.getSourceRHS(); + Type type; + QualifyingInfo qualifyingInfo = new QualifyingInfo( + selectionParameters.getConditionQualifiers(), + selectionParameters.getConditionQualifyingNames(), + selectionParameters.getResultType() + ); + if ( sourceRHS != null && sourceRHS.isSourceReferenceParameter() ) { + // If the source reference is for a source parameter, + // then the presence check should be for the source parameter + type = Type.SOURCE_PARAMETER_CHECK; + } + else { + type = Type.PRESENCE_CHECK; + } + return new SelectionCriteria( qualifyingInfo, sourceRHS, null, null, type ); } + public static SelectionCriteria forSourceParameterCheckMethods(SelectionParameters selectionParameters) { + return new SelectionCriteria( selectionParameters, null, null, Type.SOURCE_PARAMETER_CHECK ); + } + + public static SelectionCriteria forSubclassMappingMethods(SelectionParameters selectionParameters, + MappingControl mappingControl) { + return new SelectionCriteria( selectionParameters, mappingControl, null, Type.SELF_NOT_ALLOWED ); + } + + private static class QualifyingInfo { + + private static final QualifyingInfo EMPTY = new QualifyingInfo( + Collections.emptyList(), + Collections.emptyList(), + null + ); + + private final List qualifiers; + private final List qualifiedByNames; + private final TypeMirror qualifyingResultType; + + private QualifyingInfo(List qualifiers, List qualifiedByNames, + TypeMirror qualifyingResultType) { + this.qualifiers = qualifiers; + this.qualifiedByNames = qualifiedByNames; + this.qualifyingResultType = qualifyingResultType; + } + + public List qualifiers() { + return qualifiers; + } + + public List qualifiedByNames() { + return qualifiedByNames; + } + + public TypeMirror qualifyingResultType() { + return qualifyingResultType; + } + + private static QualifyingInfo fromSelectionParameters(SelectionParameters selectionParameters) { + if ( selectionParameters == null ) { + return EMPTY; + } + return new QualifyingInfo( + selectionParameters.getQualifiers(), + selectionParameters.getQualifyingNames(), + selectionParameters.getResultType() + ); + } + } + + public enum Type { PREFER_UPDATE_MAPPING, OBJECT_FACTORY, LIFECYCLE_CALLBACK, PRESENCE_CHECK, + SOURCE_PARAMETER_CHECK, + SELF_NOT_ALLOWED, } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java index 930ce2d403..91a30902e0 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SourceRhsSelector.java @@ -9,21 +9,19 @@ import java.util.List; import org.mapstruct.ap.internal.model.common.ParameterBinding; -import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; /** - * Selector that tries to resolve an ambiquity between methods that contain source parameters and + * Selector that tries to resolve an ambiguity between methods that contain source parameters and * {@link org.mapstruct.ap.internal.model.common.SourceRHS SourceRHS} type parameters. * @author Filip Hrisafov */ public class SourceRhsSelector implements MethodSelector { @Override - public List> getMatchingMethods(Method mappingMethod, - List> candidates, - List sourceTypes, Type mappingTargetType, - Type returnType, SelectionCriteria criteria) { + public List> getMatchingMethods(List> candidates, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); if ( candidates.size() < 2 || criteria.getSourceRHS() == null ) { return candidates; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TargetTypeSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TargetTypeSelector.java index 38a907aedb..181c1f883b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TargetTypeSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TargetTypeSelector.java @@ -7,12 +7,10 @@ import java.util.ArrayList; import java.util.List; - import javax.lang.model.type.TypeMirror; -import org.mapstruct.ap.internal.util.TypeUtils; -import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; +import org.mapstruct.ap.internal.util.TypeUtils; /** * This selector selects a best match based on the result type. @@ -31,12 +29,9 @@ public TargetTypeSelector( TypeUtils typeUtils ) { } @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List> methods, + SelectionContext context) { + SelectionCriteria criteria = context.getSelectionCriteria(); TypeMirror qualifyingTypeMirror = criteria.getQualifyingResultType(); if ( qualifyingTypeMirror != null && !criteria.isLifecycleCallbackRequired() ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java index 74e518c84d..1a2389eba1 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/TypeSelector.java @@ -11,9 +11,7 @@ import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.ParameterBinding; -import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; -import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.MethodMatcher; import org.mapstruct.ap.internal.util.FormattingMessager; @@ -29,44 +27,24 @@ */ public class TypeSelector implements MethodSelector { - private TypeFactory typeFactory; private FormattingMessager messager; - public TypeSelector(TypeFactory typeFactory, FormattingMessager messager) { - this.typeFactory = typeFactory; + public TypeSelector(FormattingMessager messager) { this.messager = messager; } @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { - + public List> getMatchingMethods(List> methods, + SelectionContext context) { if ( methods.isEmpty() ) { return methods; } + Type returnType = context.getReturnType(); + List> result = new ArrayList<>(); - List availableBindings; - if ( sourceTypes.isEmpty() ) { - // if no source types are given, we have a factory or lifecycle method - availableBindings = getAvailableParameterBindingsFromMethod( - mappingMethod, - mappingTargetType, - criteria.getSourceRHS() - ); - } - else { - availableBindings = getAvailableParameterBindingsFromSourceTypes( - sourceTypes, - mappingTargetType, - mappingMethod - ); - } + List availableBindings = context.getAvailableParameterBindings(); for ( SelectedMethod method : methods ) { List> parameterBindingPermutations = @@ -74,7 +52,7 @@ public List> getMatchingMethods(Method mapp if ( parameterBindingPermutations != null ) { SelectedMethod matchingMethod = - getMatchingParameterBinding( returnType, mappingMethod, method, parameterBindingPermutations ); + getMatchingParameterBinding( returnType, context, method, parameterBindingPermutations ); if ( matchingMethod != null ) { result.add( matchingMethod ); @@ -84,73 +62,8 @@ public List> getMatchingMethods(Method mapp return result; } - private List getAvailableParameterBindingsFromMethod(Method method, Type targetType, - SourceRHS sourceRHS) { - List availableParams = new ArrayList<>( method.getParameters().size() + 3 ); - - if ( sourceRHS != null ) { - availableParams.addAll( ParameterBinding.fromParameters( method.getParameters() ) ); - availableParams.add( ParameterBinding.fromSourceRHS( sourceRHS ) ); - } - else { - availableParams.addAll( ParameterBinding.fromParameters( method.getParameters() ) ); - } - - addMappingTargetAndTargetTypeBindings( availableParams, targetType ); - - return availableParams; - } - - private List getAvailableParameterBindingsFromSourceTypes(List sourceTypes, - Type targetType, Method mappingMethod) { - - List availableParams = new ArrayList<>( sourceTypes.size() + 2 ); - - for ( Type sourceType : sourceTypes ) { - availableParams.add( ParameterBinding.forSourceTypeBinding( sourceType ) ); - } - - for ( Parameter param : mappingMethod.getParameters() ) { - if ( param.isMappingContext() ) { - availableParams.add( ParameterBinding.fromParameter( param ) ); - } - } - - addMappingTargetAndTargetTypeBindings( availableParams, targetType ); - - return availableParams; - } - - /** - * Adds default parameter bindings for the mapping-target and target-type if not already available. - * - * @param availableParams Already available params, new entries will be added to this list - * @param targetType Target type - */ - private void addMappingTargetAndTargetTypeBindings(List availableParams, Type targetType) { - boolean mappingTargetAvailable = false; - boolean targetTypeAvailable = false; - - // search available parameter bindings if mapping-target and/or target-type is available - for ( ParameterBinding pb : availableParams ) { - if ( pb.isMappingTarget() ) { - mappingTargetAvailable = true; - } - else if ( pb.isTargetType() ) { - targetTypeAvailable = true; - } - } - - if ( !mappingTargetAvailable ) { - availableParams.add( ParameterBinding.forMappingTargetBinding( targetType ) ); - } - if ( !targetTypeAvailable ) { - availableParams.add( ParameterBinding.forTargetTypeBinding( typeFactory.classTypeOf( targetType ) ) ); - } - } - private SelectedMethod getMatchingParameterBinding(Type returnType, - Method mappingMethod, SelectedMethod selectedMethodInfo, + SelectionContext context, SelectedMethod selectedMethodInfo, List> parameterAssignmentVariants) { List> matchingParameterAssignmentVariants = new ArrayList<>( @@ -193,7 +106,7 @@ else if ( matchingParameterAssignmentVariants.size() == 1 ) { messager.printMessage( selectedMethod.getExecutable(), Message.LIFECYCLEMETHOD_AMBIGUOUS_PARAMETERS, - mappingMethod + context.getMappingMethod() ); return null; @@ -301,7 +214,9 @@ private static List findCandidateBindingsForParameter(List *
    19. Name and Scope matches
    20. @@ -34,28 +32,29 @@ * the given method is not annotated with {@code XmlElementDecl} it will be considered as matching. * * @author Sjaak Derksen + * + * @see JavaxXmlElementDeclSelector + * @see JakartaXmlElementDeclSelector */ -public class XmlElementDeclSelector implements MethodSelector { +abstract class XmlElementDeclSelector implements MethodSelector { private final TypeUtils typeUtils; - public XmlElementDeclSelector(TypeUtils typeUtils) { + XmlElementDeclSelector(TypeUtils typeUtils) { this.typeUtils = typeUtils; } @Override - public List> getMatchingMethods(Method mappingMethod, - List> methods, - List sourceTypes, - Type mappingTargetType, - Type returnType, - SelectionCriteria criteria) { + public List> getMatchingMethods(List> methods, + SelectionContext context) { + Type resultType = context.getMappingMethod().getResultType(); + String targetPropertyName = context.getSelectionCriteria().getTargetPropertyName(); List> nameMatches = new ArrayList<>(); List> scopeMatches = new ArrayList<>(); List> nameAndScopeMatches = new ArrayList<>(); XmlElementRefInfo xmlElementRefInfo = - findXmlElementRef( mappingMethod.getResultType(), criteria.getTargetPropertyName() ); + findXmlElementRef( resultType, targetPropertyName ); for ( SelectedMethod candidate : methods ) { if ( !( candidate.getMethod() instanceof SourceMethod ) ) { @@ -63,15 +62,14 @@ public List> getMatchingMethods(Method mapp } SourceMethod candidateMethod = (SourceMethod) candidate.getMethod(); - XmlElementDeclGem xmlElementDecl = - XmlElementDeclGem.instanceOn( candidateMethod.getExecutable() ); + XmlElementDeclInfo xmlElementDeclInfo = getXmlElementDeclInfo( candidateMethod.getExecutable() ); - if ( xmlElementDecl == null ) { + if ( xmlElementDeclInfo == null ) { continue; } - String name = xmlElementDecl.name().get(); - TypeMirror scope = xmlElementDecl.scope().getValue(); + String name = xmlElementDeclInfo.nameValue(); + TypeMirror scope = xmlElementDeclInfo.scopeType(); boolean nameIsSetAndMatches = name != null && name.equals( xmlElementRefInfo.nameValue() ); boolean scopeIsSetAndMatches = @@ -142,9 +140,9 @@ private XmlElementRefInfo findXmlElementRef(Type resultType, String targetProper for ( Element enclosed : currentElement.getEnclosedElements() ) { if ( enclosed.getKind().equals( ElementKind.FIELD ) && enclosed.getSimpleName().contentEquals( targetPropertyName ) ) { - XmlElementRefGem xmlElementRef = XmlElementRefGem.instanceOn( enclosed ); - if ( xmlElementRef != null ) { - return new XmlElementRefInfo( xmlElementRef.name().get(), currentMirror ); + XmlElementRefInfo xmlElementRefInfo = getXmlElementRefInfo( enclosed ); + if ( xmlElementRefInfo != null ) { + return new XmlElementRefInfo( xmlElementRefInfo.nameValue(), currentMirror ); } } } @@ -154,7 +152,11 @@ private XmlElementRefInfo findXmlElementRef(Type resultType, String targetProper return defaultInfo; } - private static class XmlElementRefInfo { + abstract XmlElementDeclInfo getXmlElementDeclInfo(Element element); + + abstract XmlElementRefInfo getXmlElementRefInfo(Element element); + + static class XmlElementRefInfo { private final String nameValue; private final TypeMirror sourceType; @@ -163,12 +165,37 @@ private static class XmlElementRefInfo { this.sourceType = sourceType; } - public String nameValue() { + String nameValue() { return nameValue; } - public TypeMirror sourceType() { + TypeMirror sourceType() { return sourceType; } } + + /** + * A class, whose purpose is to combine the use of + * {@link org.mapstruct.ap.internal.gem.XmlElementDeclGem} + * and + * {@link org.mapstruct.ap.internal.gem.jakarta.XmlElementDeclGem}. + */ + static class XmlElementDeclInfo { + + private final String nameValue; + private final TypeMirror scopeType; + + XmlElementDeclInfo(String nameValue, TypeMirror scopeType) { + this.nameValue = nameValue; + this.scopeType = scopeType; + } + + String nameValue() { + return nameValue; + } + + TypeMirror scopeType() { + return scopeType; + } + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/option/MappingOption.java b/processor/src/main/java/org/mapstruct/ap/internal/option/MappingOption.java new file mode 100644 index 0000000000..4607fc1ecf --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/option/MappingOption.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.option; + +/** + * The different compiler mapping options that are available in MapStruct. + * + * @author Filip Hrisafov + */ +public enum MappingOption { + + // CHECKSTYLE:OFF + SUPPRESS_GENERATOR_TIMESTAMP( "mapstruct.suppressGeneratorTimestamp" ), + SUPPRESS_GENERATOR_VERSION_INFO_COMMENT( "mapstruct.suppressGeneratorVersionInfoComment" ), + UNMAPPED_TARGET_POLICY("mapstruct.unmappedTargetPolicy"), + UNMAPPED_SOURCE_POLICY("mapstruct.unmappedSourcePolicy"), + DEFAULT_COMPONENT_MODEL("mapstruct.defaultComponentModel"), + DEFAULT_INJECTION_STRATEGY("mapstruct.defaultInjectionStrategy"), + ALWAYS_GENERATE_SERVICE_FILE("mapstruct.alwaysGenerateServicesFile"), + DISABLE_BUILDERS("mapstruct.disableBuilders"), + VERBOSE("mapstruct.verbose"), + NULL_VALUE_ITERABLE_MAPPING_STRATEGY("mapstruct.nullValueIterableMappingStrategy"), + NULL_VALUE_MAP_MAPPING_STRATEGY("mapstruct.nullValueMapMappingStrategy"), + DISABLE_LIFECYCLE_OVERLOAD_DEDUPLICATE_SELECTOR("mapstruct.disableLifecycleOverloadDeduplicateSelector"), + DISABLE_JSPECIFY("mapstruct.disableJSpecify"), + ; + // CHECKSTYLE:ON + + private final String optionName; + + MappingOption(String optionName) { + this.optionName = optionName; + } + + /** + * Returns the name of the option, which can be used in the compiler arguments. + * + * @return the name of the option + */ + public String getOptionName() { + return optionName; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java b/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java index 54b69c28b3..1fd3585301 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/option/Options.java @@ -5,6 +5,10 @@ */ package org.mapstruct.ap.internal.option; +import java.util.Locale; +import java.util.Map; + +import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; import org.mapstruct.ap.internal.gem.ReportingPolicyGem; /** @@ -12,54 +16,83 @@ * * @author Andreas Gudian * @author Gunnar Morling + * @author Filip Hrisafov */ public class Options { - private final boolean suppressGeneratorTimestamp; - private final boolean suppressGeneratorVersionComment; - private final ReportingPolicyGem unmappedTargetPolicy; - private final boolean alwaysGenerateSpi; - private final String defaultComponentModel; - private final String defaultInjectionStrategy; - private final boolean verbose; - - public Options(boolean suppressGeneratorTimestamp, boolean suppressGeneratorVersionComment, - ReportingPolicyGem unmappedTargetPolicy, - String defaultComponentModel, String defaultInjectionStrategy, - boolean alwaysGenerateSpi, boolean verbose) { - this.suppressGeneratorTimestamp = suppressGeneratorTimestamp; - this.suppressGeneratorVersionComment = suppressGeneratorVersionComment; - this.unmappedTargetPolicy = unmappedTargetPolicy; - this.defaultComponentModel = defaultComponentModel; - this.defaultInjectionStrategy = defaultInjectionStrategy; - this.alwaysGenerateSpi = alwaysGenerateSpi; - this.verbose = verbose; + + private final Map options; + + public Options(Map options) { + this.options = options; } public boolean isSuppressGeneratorTimestamp() { - return suppressGeneratorTimestamp; + return parseBoolean( MappingOption.SUPPRESS_GENERATOR_TIMESTAMP ); } public boolean isSuppressGeneratorVersionComment() { - return suppressGeneratorVersionComment; + return parseBoolean( MappingOption.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT ); } public ReportingPolicyGem getUnmappedTargetPolicy() { - return unmappedTargetPolicy; + return parseEnum( MappingOption.UNMAPPED_TARGET_POLICY, ReportingPolicyGem.class ); + } + + public ReportingPolicyGem getUnmappedSourcePolicy() { + return parseEnum( MappingOption.UNMAPPED_SOURCE_POLICY, ReportingPolicyGem.class ); } public String getDefaultComponentModel() { - return defaultComponentModel; + return options.get( MappingOption.DEFAULT_COMPONENT_MODEL.getOptionName() ); } public String getDefaultInjectionStrategy() { - return defaultInjectionStrategy; + return options.get( MappingOption.DEFAULT_INJECTION_STRATEGY.getOptionName() ); } public boolean isAlwaysGenerateSpi() { - return alwaysGenerateSpi; + return parseBoolean( MappingOption.ALWAYS_GENERATE_SERVICE_FILE ); + } + + public boolean isDisableBuilders() { + return parseBoolean( MappingOption.DISABLE_BUILDERS ); } public boolean isVerbose() { - return verbose; + return parseBoolean( MappingOption.VERBOSE ); + } + + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { + return parseEnum( MappingOption.NULL_VALUE_ITERABLE_MAPPING_STRATEGY, NullValueMappingStrategyGem.class ); + } + + public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { + return parseEnum( MappingOption.NULL_VALUE_MAP_MAPPING_STRATEGY, NullValueMappingStrategyGem.class ); + } + + public boolean isDisableLifecycleOverloadDeduplicateSelector() { + return parseBoolean( MappingOption.DISABLE_LIFECYCLE_OVERLOAD_DEDUPLICATE_SELECTOR ); + } + + public boolean isDisableJSpecify() { + return parseBoolean( MappingOption.DISABLE_JSPECIFY ); + } + + private boolean parseBoolean(MappingOption option) { + if ( options.isEmpty() ) { + return false; + } + return Boolean.parseBoolean( options.get( option.getOptionName() ) ); + } + + private > E parseEnum(MappingOption option, Class enumType) { + if ( options.isEmpty() ) { + return null; + } + String value = options.get( option.getOptionName() ); + if ( value == null ) { + return null; + } + return Enum.valueOf( enumType, value.toUpperCase( Locale.ROOT ) ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/AnnotationBasedComponentModelProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/AnnotationBasedComponentModelProcessor.java index d68936f545..bbd695412a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/AnnotationBasedComponentModelProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/AnnotationBasedComponentModelProcessor.java @@ -11,10 +11,12 @@ import java.util.List; import java.util.ListIterator; import java.util.Set; - +import java.util.stream.Collectors; import javax.lang.model.element.TypeElement; +import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.model.AnnotatedConstructor; +import org.mapstruct.ap.internal.model.AnnotatedSetter; import org.mapstruct.ap.internal.model.Annotation; import org.mapstruct.ap.internal.model.AnnotationMapperReference; import org.mapstruct.ap.internal.model.Decorator; @@ -23,7 +25,6 @@ import org.mapstruct.ap.internal.model.MapperReference; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; -import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.model.source.MapperOptions; /** @@ -77,6 +78,9 @@ else if ( mapper.getDecorator() != null ) { if ( injectionStrategy == InjectionStrategyGem.CONSTRUCTOR ) { buildConstructors( mapper ); } + else if ( injectionStrategy == InjectionStrategyGem.SETTER ) { + buildSetters( mapper ); + } return mapper; } @@ -84,7 +88,7 @@ else if ( mapper.getDecorator() != null ) { protected void adjustDecorator(Mapper mapper, InjectionStrategyGem injectionStrategy) { Decorator decorator = mapper.getDecorator(); - for ( Annotation typeAnnotation : getDecoratorAnnotations() ) { + for ( Annotation typeAnnotation : getDecoratorAnnotations( decorator ) ) { decorator.addAnnotation( typeAnnotation ); } @@ -110,6 +114,42 @@ private List toMapperReferences(List fields) { return mapperReferences; } + private void buildSetters(Mapper mapper) { + List mapperReferences = toMapperReferences( mapper.getFields() ); + for ( MapperReference mapperReference : mapperReferences ) { + if ( mapperReference.isUsed() ) { + AnnotatedSetter setter = new AnnotatedSetter( + mapperReference, + getMapperReferenceAnnotations(), + Collections.emptyList() + ); + mapper.getMethods().add( setter ); + } + } + + Decorator decorator = mapper.getDecorator(); + if ( decorator != null ) { + List mapperReferenceAnnotations = getMapperReferenceAnnotations(); + Set mapperReferenceAnnotationsTypes = mapperReferenceAnnotations + .stream() + .map( Annotation::getType ) + .collect( Collectors.toSet() ); + for ( Field field : decorator.getFields() ) { + if ( field instanceof AnnotationMapperReference ) { + + List fieldAnnotations = ( (AnnotationMapperReference) field ).getAnnotations(); + + List qualifiers = extractMissingAnnotations( + fieldAnnotations, + mapperReferenceAnnotationsTypes + ); + + decorator.getMethods().add( new AnnotatedSetter( field, mapperReferenceAnnotations, qualifiers ) ); + } + } + } + } + private void buildConstructors(Mapper mapper) { if ( !toMapperReferences( mapper.getFields() ).isEmpty() ) { AnnotatedConstructor annotatedConstructor = buildAnnotatedConstructorForMapper( mapper ); @@ -197,17 +237,33 @@ private void removeDuplicateAnnotations(List annotati AnnotationMapperReference annotationMapperReference = mapperReferenceIterator.next(); mapperReferenceIterator.remove(); - List qualifiers = new ArrayList<>(); - for ( Annotation annotation : annotationMapperReference.getAnnotations() ) { - if ( !mapperReferenceAnnotationsTypes.contains( annotation.getType() ) ) { - qualifiers.add( annotation ); - } - } + List qualifiers = extractMissingAnnotations( + annotationMapperReference.getAnnotations(), + mapperReferenceAnnotationsTypes + ); mapperReferenceIterator.add( annotationMapperReference.withNewAnnotations( qualifiers ) ); } } + /** + * Extract all annotations from {@code annotations} that do not have a type in {@code annotationTypes}. + * + * @param annotations the annotations from which we need to extract information + * @param annotationTypes the annotation types to ignore + * @return the annotations that are not in the {@code annotationTypes} + */ + private List extractMissingAnnotations(List annotations, + Set annotationTypes) { + List qualifiers = new ArrayList<>(); + for ( Annotation annotation : annotations ) { + if ( !annotationTypes.contains( annotation.getType() ) ) { + qualifiers.add( annotation ); + } + } + return qualifiers; + } + protected boolean additionalPublicEmptyConstructor() { return false; } @@ -252,7 +308,7 @@ protected Field replacementMapperReference(Field originalReference, List getDecoratorAnnotations() { + protected List getDecoratorAnnotations(Decorator decorator) { return Collections.emptyList(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/CdiComponentProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/CdiComponentProcessor.java index 74ff2b118b..d8f136034b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/CdiComponentProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/CdiComponentProcessor.java @@ -12,6 +12,8 @@ import org.mapstruct.ap.internal.gem.MappingConstantsGem; import org.mapstruct.ap.internal.model.Annotation; import org.mapstruct.ap.internal.model.Mapper; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.AnnotationProcessingException; /** * A {@link ModelElementProcessor} which converts the given {@link Mapper} @@ -30,13 +32,13 @@ protected String getComponentModelIdentifier() { @Override protected List getTypeAnnotations(Mapper mapper) { return Collections.singletonList( - new Annotation( getTypeFactory().getType( "javax.enterprise.context.ApplicationScoped" ) ) + new Annotation( getType( "ApplicationScoped" ) ) ); } @Override protected List getMapperReferenceAnnotations() { - return Arrays.asList( new Annotation( getTypeFactory().getType( "javax.inject.Inject" ) ) ); + return Arrays.asList( new Annotation( getType( "Inject" ) ) ); } @Override @@ -48,4 +50,24 @@ protected boolean requiresGenerationOfDecoratorClass() { protected boolean additionalPublicEmptyConstructor() { return true; } + + private Type getType(String simpleName) { + String javaxPrefix = "javax.inject."; + String jakartaPrefix = "jakarta.inject."; + if ( "ApplicationScoped".equals( simpleName ) ) { + javaxPrefix = "javax.enterprise.context."; + jakartaPrefix = "jakarta.enterprise.context."; + } + if ( getTypeFactory().isTypeAvailable( javaxPrefix + simpleName ) ) { + return getTypeFactory().getType( javaxPrefix + simpleName ); + } + + if ( getTypeFactory().isTypeAvailable( jakartaPrefix + simpleName ) ) { + return getTypeFactory().getType( jakartaPrefix + simpleName ); + } + + throw new AnnotationProcessingException( + "Couldn't find any of the CDI or Jakarta CDI Dependency types." + + " Are you missing a dependency on your classpath?" ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java index d6a05cec32..7a0596d4c2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java @@ -13,6 +13,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic.Kind; import org.mapstruct.ap.internal.model.common.TypeFactory; @@ -22,6 +23,7 @@ import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.RoundContext; import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.version.VersionInformation; @@ -43,17 +45,19 @@ public class DefaultModelElementProcessorContext implements ProcessorContext { private final TypeUtils delegatingTypes; private final ElementUtils delegatingElements; private final AccessorNamingUtils accessorNaming; + private final NullabilityResolver nullabilityResolver; private final RoundContext roundContext; public DefaultModelElementProcessorContext(ProcessingEnvironment processingEnvironment, Options options, - RoundContext roundContext, Map notToBeImported) { + RoundContext roundContext, Map notToBeImported, TypeElement mapperElement) { this.processingEnvironment = processingEnvironment; this.messager = new DelegatingMessager( processingEnvironment.getMessager(), options.isVerbose() ); this.accessorNaming = roundContext.getAnnotationProcessorContext().getAccessorNaming(); + this.nullabilityResolver = roundContext.getAnnotationProcessorContext().getNullabilityResolver(); this.versionInformation = DefaultVersionInformation.fromProcessingEnvironment( processingEnvironment ); this.delegatingTypes = TypeUtils.create( processingEnvironment, versionInformation ); - this.delegatingElements = ElementUtils.create( processingEnvironment, versionInformation ); + this.delegatingElements = ElementUtils.create( processingEnvironment, versionInformation, mapperElement ); this.roundContext = roundContext; this.typeFactory = new TypeFactory( delegatingElements, @@ -61,7 +65,8 @@ public DefaultModelElementProcessorContext(ProcessingEnvironment processingEnvir messager, roundContext, notToBeImported, - options.isVerbose() + options.isVerbose(), + versionInformation ); this.options = options; } @@ -96,6 +101,11 @@ public AccessorNamingUtils getAccessorNaming() { return accessorNaming; } + @Override + public NullabilityResolver getNullabilityResolver() { + return nullabilityResolver; + } + @Override public Map getEnumTransformationStrategies() { return roundContext.getAnnotationProcessorContext().getEnumTransformationStrategies(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java index c4baf1bf5f..f8053c92ab 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java @@ -11,7 +11,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.jar.Manifest; - import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.SourceVersion; @@ -41,11 +40,14 @@ public class DefaultVersionInformation implements VersionInformation { private final String runtimeVendor; private final String compiler; private final boolean sourceVersionAtLeast9; + private final boolean sourceVersionAtLeast11; + private final boolean sourceVersionAtLeast14; + private final boolean sourceVersionAtLeast19; private final boolean eclipseJDT; private final boolean javac; DefaultVersionInformation(String runtimeVersion, String runtimeVendor, String compiler, - SourceVersion sourceVersion) { + SourceVersion sourceVersion) { this.runtimeVersion = runtimeVersion; this.runtimeVendor = runtimeVendor; this.compiler = compiler; @@ -53,6 +55,9 @@ public class DefaultVersionInformation implements VersionInformation { this.javac = compiler.startsWith( COMPILER_NAME_JAVAC ); // If the difference between the source version and RELEASE_6 is more that 2 than we are at least on 9 this.sourceVersionAtLeast9 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 2; + this.sourceVersionAtLeast11 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 4; + this.sourceVersionAtLeast14 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 7; + this.sourceVersionAtLeast19 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 12; } @Override @@ -80,6 +85,21 @@ public boolean isSourceVersionAtLeast9() { return sourceVersionAtLeast9; } + @Override + public boolean isSourceVersionAtLeast11() { + return sourceVersionAtLeast11; + } + + @Override + public boolean isSourceVersionAtLeast14() { + return sourceVersionAtLeast14; + } + + @Override + public boolean isSourceVersionAtLeast19() { + return sourceVersionAtLeast19; + } + @Override public boolean isEclipseJDTCompiler() { return eclipseJDT; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/JakartaCdiComponentProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/JakartaCdiComponentProcessor.java new file mode 100644 index 0000000000..11c765668e --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/JakartaCdiComponentProcessor.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.processor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.mapstruct.ap.internal.gem.MappingConstantsGem; +import org.mapstruct.ap.internal.model.Annotation; +import org.mapstruct.ap.internal.model.Mapper; + +/** + * A {@link ModelElementProcessor} which converts the given {@link Mapper} + * object into an application-scoped Jakarta CDI bean in case Jakarta CDI + * is configured as the target component model for this mapper. + * + * @author Filip Hrisafov + */ +public class JakartaCdiComponentProcessor extends AnnotationBasedComponentModelProcessor { + + @Override + protected String getComponentModelIdentifier() { + return MappingConstantsGem.ComponentModelGem.JAKARTA_CDI; + } + + @Override + protected List getTypeAnnotations(Mapper mapper) { + return Collections.singletonList( + new Annotation( getTypeFactory().getType( "jakarta.enterprise.context.ApplicationScoped" ) ) + ); + } + + @Override + protected List getMapperReferenceAnnotations() { + return Arrays.asList( new Annotation( getTypeFactory().getType( "jakarta.inject.Inject" ) ) ); + } + + @Override + protected boolean requiresGenerationOfDecoratorClass() { + return false; + } + + @Override + protected boolean additionalPublicEmptyConstructor() { + return true; + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/JakartaComponentProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/JakartaComponentProcessor.java new file mode 100644 index 0000000000..86c95d612b --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/JakartaComponentProcessor.java @@ -0,0 +1,84 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.processor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.mapstruct.ap.internal.gem.MappingConstantsGem; +import org.mapstruct.ap.internal.model.Annotation; +import org.mapstruct.ap.internal.model.Decorator; +import org.mapstruct.ap.internal.model.Mapper; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement.AnnotationElementType; + +/** + * A {@link ModelElementProcessor} which converts the given {@link Mapper} + * object into a Jakarta Inject style bean in case "jakarta" is configured as the + * target component model for this mapper. + * + * @author Filip Hrisafov + */ +public class JakartaComponentProcessor extends AnnotationBasedComponentModelProcessor { + @Override + protected String getComponentModelIdentifier() { + return MappingConstantsGem.ComponentModelGem.JAKARTA; + } + + @Override + protected List getTypeAnnotations(Mapper mapper) { + if ( mapper.getDecorator() == null ) { + return Arrays.asList( singleton(), named() ); + } + else { + return Arrays.asList( singleton(), namedDelegate( mapper ) ); + } + } + + @Override + protected List getDecoratorAnnotations(Decorator decorator) { + return Arrays.asList( singleton(), named() ); + } + + @Override + protected List getDelegatorReferenceAnnotations(Mapper mapper) { + return Arrays.asList( inject(), namedDelegate( mapper ) ); + } + + @Override + protected List getMapperReferenceAnnotations() { + return Collections.singletonList( inject() ); + } + + @Override + protected boolean requiresGenerationOfDecoratorClass() { + return true; + } + + private Annotation singleton() { + return new Annotation( getTypeFactory().getType( "jakarta.inject.Singleton" ) ); + } + + private Annotation named() { + return new Annotation( getTypeFactory().getType( "jakarta.inject.Named" ) ); + } + + private Annotation namedDelegate(Mapper mapper) { + return new Annotation( + getTypeFactory().getType( "jakarta.inject.Named" ), + Collections.singletonList( + new AnnotationElement( AnnotationElementType.STRING, + Collections.singletonList( mapper.getPackageName() + "." + mapper.getName() ) + ) ) + ); + } + + private Annotation inject() { + return new Annotation( getTypeFactory().getType( "jakarta.inject.Inject" ) ); + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/Jsr330ComponentProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/Jsr330ComponentProcessor.java index 18a6e4f69d..7770eff16c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/Jsr330ComponentProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/Jsr330ComponentProcessor.java @@ -11,7 +11,12 @@ import org.mapstruct.ap.internal.gem.MappingConstantsGem; import org.mapstruct.ap.internal.model.Annotation; +import org.mapstruct.ap.internal.model.Decorator; import org.mapstruct.ap.internal.model.Mapper; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement.AnnotationElementType; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.AnnotationProcessingException; /** * A {@link ModelElementProcessor} which converts the given {@link Mapper} @@ -38,7 +43,7 @@ protected List getTypeAnnotations(Mapper mapper) { } @Override - protected List getDecoratorAnnotations() { + protected List getDecoratorAnnotations(Decorator decorator) { return Arrays.asList( singleton(), named() ); } @@ -58,21 +63,39 @@ protected boolean requiresGenerationOfDecoratorClass() { } private Annotation singleton() { - return new Annotation( getTypeFactory().getType( "javax.inject.Singleton" ) ); + return new Annotation( getType( "Singleton" ) ); } private Annotation named() { - return new Annotation( getTypeFactory().getType( "javax.inject.Named" ) ); + return new Annotation( getType( "Named" ) ); } private Annotation namedDelegate(Mapper mapper) { return new Annotation( - getTypeFactory().getType( "javax.inject.Named" ), - Collections.singletonList( '"' + mapper.getPackageName() + "." + mapper.getName() + '"' ) + getType( "Named" ), + Collections.singletonList( + new AnnotationElement( + AnnotationElementType.STRING, + Collections.singletonList( mapper.getPackageName() + "." + mapper.getName() ) + ) ) ); } private Annotation inject() { - return new Annotation( getTypeFactory().getType( "javax.inject.Inject" ) ); + return new Annotation( getType( "Inject" ) ); + } + + private Type getType(String simpleName) { + if ( getTypeFactory().isTypeAvailable( "javax.inject." + simpleName ) ) { + return getTypeFactory().getType( "javax.inject." + simpleName ); + } + + if ( getTypeFactory().isTypeAvailable( "jakarta.inject." + simpleName ) ) { + return getTypeFactory().getType( "jakarta.inject." + simpleName ); + } + + throw new AnnotationProcessingException( + "Couldn't find any of the JSR330 or Jakarta Dependency Inject types." + + " Are you missing a dependency on your classpath?" ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperAnnotatedFormattingMessenger.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperAnnotatedFormattingMessenger.java new file mode 100644 index 0000000000..d3384f0b4c --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperAnnotatedFormattingMessenger.java @@ -0,0 +1,143 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.processor; + +import java.util.Objects; +import java.util.stream.Collectors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic.Kind; + +import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.TypeUtils; + +/** + * Handles redirection of errors/warnings so that they're shown on the mapper instead of hidden on a superclass. + * + * @author Ben Zegveld + */ +public class MapperAnnotatedFormattingMessenger implements FormattingMessager { + + private FormattingMessager delegateMessager; + private TypeElement mapperTypeElement; + private TypeUtils typeUtils; + + public MapperAnnotatedFormattingMessenger(FormattingMessager delegateMessager, TypeElement mapperTypeElement, + TypeUtils typeUtils) { + this.delegateMessager = delegateMessager; + this.mapperTypeElement = mapperTypeElement; + this.typeUtils = typeUtils; + } + + @Override + public void printMessage(Message msg, Object... args) { + delegateMessager.printMessage( msg, args ); + } + + @Override + public void printMessage(Element e, Message msg, Object... args) { + delegateMessager + .printMessage( + determineDelegationElement( e ), + determineDelegationMessage( e, msg ), + determineDelegationArguments( e, msg, args ) ); + } + + @Override + public void printMessage(Element e, AnnotationMirror a, Message msg, Object... args) { + delegateMessager + .printMessage( + determineDelegationElement( e ), + a, + determineDelegationMessage( e, msg ), + determineDelegationArguments( e, msg, args ) ); + } + + @Override + public void printMessage(Element e, AnnotationMirror a, AnnotationValue v, Message msg, Object... args) { + delegateMessager + .printMessage( + determineDelegationElement( e ), + a, + v, + determineDelegationMessage( e, msg ), + determineDelegationArguments( e, msg, args ) ); + } + + @Override + public void note(int level, Message log, Object... args) { + delegateMessager.note( level, log, args ); + } + + @Override + public boolean isErroneous() { + return delegateMessager.isErroneous(); + } + + private Object[] determineDelegationArguments(Element e, Message msg, Object[] args) { + if ( methodInMapperClass( e ) ) { + return args; + } + String originalMessage = String.format( msg.getDescription(), args ); + return new Object[] { originalMessage, constructMethod( e ), e.getEnclosingElement().getSimpleName() }; + } + + /** + * ExecutableElement.toString() has different values depending on the compiler. Constructing the method itself + * manually will ensure that the message is always traceable to it's source. + */ + private String constructMethod(Element e) { + if ( e instanceof ExecutableElement ) { + ExecutableElement ee = (ExecutableElement) e; + StringBuilder method = new StringBuilder(); + method.append( typeMirrorToString( ee.getReturnType() ) ); + method.append( " " ); + method.append( ee.getSimpleName() ); + method.append( "(" ); + method.append( ee.getParameters() + .stream() + .map( this::parameterToString ) + .collect( Collectors.joining( ", " ) ) ); + method.append( ")" ); + return method.toString(); + } + return e.toString(); + } + + private String parameterToString(VariableElement element) { + return typeMirrorToString( element.asType() ) + " " + element.getSimpleName(); + } + + private String typeMirrorToString(TypeMirror type) { + Element element = typeUtils.asElement( type ); + return element != null ? element.getSimpleName().toString() : Objects.toString( type ); + } + + private Message determineDelegationMessage(Element e, Message msg) { + if ( methodInMapperClass( e ) ) { + return msg; + } + if ( msg.getDiagnosticKind() == Kind.ERROR ) { + return Message.MESSAGE_MOVED_TO_MAPPER_ERROR; + } + return Message.MESSAGE_MOVED_TO_MAPPER_WARNING; + } + + private Element determineDelegationElement(Element e) { + return methodInMapperClass( e ) ? e : mapperTypeElement; + } + + private boolean methodInMapperClass(Element e) { + return mapperTypeElement == null || e.equals( mapperTypeElement ) + || e.getEnclosingElement().equals( mapperTypeElement ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java index 13e0010171..db16d52106 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java @@ -14,6 +14,7 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -23,13 +24,15 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; -import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.gem.DecoratedWithGem; import org.mapstruct.ap.internal.gem.InheritConfigurationGem; import org.mapstruct.ap.internal.gem.InheritInverseConfigurationGem; +import org.mapstruct.ap.internal.gem.JavadocGem; import org.mapstruct.ap.internal.gem.MapperGem; import org.mapstruct.ap.internal.gem.MappingInheritanceStrategyGem; import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem; +import org.mapstruct.ap.internal.model.AdditionalAnnotationsBuilder; +import org.mapstruct.ap.internal.model.Annotation; import org.mapstruct.ap.internal.model.BeanMappingMethod; import org.mapstruct.ap.internal.model.ContainerMappingMethod; import org.mapstruct.ap.internal.model.ContainerMappingMethodBuilder; @@ -38,6 +41,7 @@ import org.mapstruct.ap.internal.model.DelegatingMethod; import org.mapstruct.ap.internal.model.Field; import org.mapstruct.ap.internal.model.IterableMappingMethod; +import org.mapstruct.ap.internal.model.Javadoc; import org.mapstruct.ap.internal.model.MapMappingMethod; import org.mapstruct.ap.internal.model.Mapper; import org.mapstruct.ap.internal.model.MapperReference; @@ -92,15 +96,20 @@ public class MapperCreationProcessor implements ModelElementProcessor sourceModel) { this.elementUtils = context.getElementUtils(); this.typeUtils = context.getTypeUtils(); - this.messager = context.getMessager(); + this.messager = + new MapperAnnotatedFormattingMessenger( context.getMessager(), mapperTypeElement, context.getTypeUtils() ); this.options = context.getOptions(); this.versionInformation = context.getVersionInformation(); this.typeFactory = context.getTypeFactory(); this.accessorNaming = context.getAccessorNaming(); + additionalAnnotationsBuilder = + new AdditionalAnnotationsBuilder( elementUtils, typeFactory, messager ); MapperOptions mapperOptions = MapperOptions.getInstanceOn( mapperTypeElement, context.getOptions() ); List mapperReferences = initReferencedMappers( mapperTypeElement, mapperOptions ); @@ -110,7 +119,9 @@ public Mapper process(ProcessorContext context, TypeElement mapperTypeElement, L elementUtils, typeUtils, messager, + versionInformation, accessorNaming, + context.getNullabilityResolver(), context.getEnumMappingStrategy(), context.getEnumTransformationStrategies(), options, @@ -186,7 +197,7 @@ private Mapper getMapper(TypeElement element, MapperOptions mapperOptions, List< addAllFieldsIn( mappingContext.getUsedSupportedMappings(), supportingFieldSet ); fields.addAll( supportingFieldSet ); - // handle constructorfragments + // handle constructor fragments Set constructorFragments = new LinkedHashSet<>(); addAllFragmentsIn( mappingContext.getUsedSupportedMappings(), constructorFragments ); @@ -197,13 +208,16 @@ private Mapper getMapper(TypeElement element, MapperOptions mapperOptions, List< .constructorFragments( constructorFragments ) .options( options ) .versionInformation( versionInformation ) - .decorator( getDecorator( element, methods, mapperOptions.implementationName(), - mapperOptions.implementationPackage(), getExtraImports( element, mapperOptions ) ) ) + .decorator( getDecorator( element, methods, mapperOptions ) ) .typeFactory( typeFactory ) .elementUtils( elementUtils ) .extraImports( getExtraImports( element, mapperOptions ) ) .implName( mapperOptions.implementationName() ) .implPackage( mapperOptions.implementationPackage() ) + .suppressGeneratorTimestamp( mapperOptions.suppressTimestampInGenerated() ) + .additionalAnnotations( additionalAnnotationsBuilder.getProcessedAnnotations( element ) ) + .javadoc( getJavadoc( element ) ) + .classAccessibility( mapperOptions.accessibility() ) .build(); if ( !mappingContext.getForgedMethodsUnderCreation().isEmpty() ) { @@ -225,8 +239,7 @@ private Mapper getMapper(TypeElement element, MapperOptions mapperOptions, List< return mapper; } - private Decorator getDecorator(TypeElement element, List methods, String implName, - String implPackage, SortedSet extraImports) { + private Decorator getDecorator(TypeElement element, List methods, MapperOptions mapperOptions) { DecoratedWithGem decoratedWith = DecoratedWithGem.instanceOn( element ); if ( decoratedWith == null ) { @@ -277,6 +290,9 @@ else if ( constructor.getParameters().size() == 1 ) { messager.printMessage( element, decoratedWith.mirror(), Message.DECORATOR_CONSTRUCTOR ); } + // Get annotations from the decorator class + Set decoratorAnnotations = additionalAnnotationsBuilder.getProcessedAnnotations( decoratorElement ); + Decorator decorator = new Decorator.Builder() .elementUtils( elementUtils ) .typeFactory( typeFactory ) @@ -286,9 +302,11 @@ else if ( constructor.getParameters().size() == 1 ) { .hasDelegateConstructor( hasDelegateConstructor ) .options( options ) .versionInformation( versionInformation ) - .implName( implName ) - .implPackage( implPackage ) - .extraImports( extraImports ) + .implName( mapperOptions.implementationName() ) + .implPackage( mapperOptions.implementationPackage() ) + .extraImports( getExtraImports( element, mapperOptions ) ) + .suppressGeneratorTimestamp( mapperOptions.suppressTimestampInGenerated() ) + .additionalAnnotations( decoratorAnnotations ) .build(); return decorator; @@ -299,7 +317,7 @@ private SortedSet getExtraImports(TypeElement element, MapperOptions mapp for ( TypeMirror extraImport : mapperOptions.imports() ) { - Type type = typeFactory.getType( extraImport ); + Type type = typeFactory.getAlwaysImportedType( extraImport ); extraImports.add( type ); } @@ -319,7 +337,7 @@ private List getMappingMethods(MapperOptions mapperAnnotation, Li continue; } - mergeInheritedOptions( method, mapperAnnotation, methods, new ArrayList<>() ); + mergeInheritedOptions( method, mapperAnnotation, methods, new ArrayList<>(), null ); MappingMethodOptions mappingOptions = method.getOptions(); @@ -404,15 +422,10 @@ else if ( method.isStreamMapping() ) { } else { this.messager.note( 1, Message.BEANMAPPING_CREATE_NOTE, method ); - BuilderGem builder = method.getOptions().getBeanMapping().getBuilder(); - Type userDefinedReturnType = getUserDesiredReturnType( method ); - Type builderBaseType = userDefinedReturnType != null ? userDefinedReturnType : method.getReturnType(); BeanMappingMethod.Builder beanMappingBuilder = new BeanMappingMethod.Builder(); BeanMappingMethod beanMappingMethod = beanMappingBuilder .mappingContext( mappingContext ) .sourceMethod( method ) - .userDefinedReturnType( userDefinedReturnType ) - .returnTypeBuilder( typeFactory.builderTypeFor( builderBaseType, builder ) ) .build(); // We can consider that the bean mapping method can always be constructed. If there is a problem @@ -433,12 +446,21 @@ else if ( method.isStreamMapping() ) { return mappingMethods; } - private Type getUserDesiredReturnType(SourceMethod method) { - SelectionParameters selectionParameters = method.getOptions().getBeanMapping().getSelectionParameters(); - if ( selectionParameters != null && selectionParameters.getResultType() != null ) { - return typeFactory.getType( selectionParameters.getResultType() ); + private Javadoc getJavadoc(TypeElement element) { + JavadocGem javadocGem = JavadocGem.instanceOn( element ); + + if ( javadocGem == null || !isConsistent( javadocGem, element, messager ) ) { + return null; } - return null; + + Javadoc javadoc = new Javadoc.Builder() + .value( javadocGem.value().getValue() ) + .authors( javadocGem.authors().getValue() ) + .deprecated( javadocGem.deprecated().getValue() ) + .since( javadocGem.since().getValue() ) + .build(); + + return javadoc; } private M createWithElementMappingMethod(SourceMethod method, @@ -461,7 +483,8 @@ private M createWithElementMappingMethod(Sour } private void mergeInheritedOptions(SourceMethod method, MapperOptions mapperConfig, - List availableMethods, List initializingMethods) { + List availableMethods, List initializingMethods, + AnnotationMirror annotationMirror) { if ( initializingMethods.contains( method ) ) { // cycle detected @@ -496,10 +519,10 @@ private void mergeInheritedOptions(SourceMethod method, MapperOptions mapperConf // apply defined (@InheritConfiguration, @InheritInverseConfiguration) mappings if ( forwardTemplateMethod != null ) { - mappingOptions.applyInheritedOptions( forwardTemplateMethod, false ); + mappingOptions.applyInheritedOptions( method, forwardTemplateMethod, false, annotationMirror ); } if ( inverseTemplateMethod != null ) { - mappingOptions.applyInheritedOptions( inverseTemplateMethod, true ); + mappingOptions.applyInheritedOptions( method, inverseTemplateMethod, true, annotationMirror ); } // apply auto inherited options @@ -509,7 +532,8 @@ private void mergeInheritedOptions(SourceMethod method, MapperOptions mapperConf // but.. there should not be an @InheritedConfiguration if ( forwardTemplateMethod == null && inheritanceStrategy.isApplyForward() ) { if ( applicablePrototypeMethods.size() == 1 ) { - mappingOptions.applyInheritedOptions( first( applicablePrototypeMethods ), false ); + mappingOptions.applyInheritedOptions( method, first( applicablePrototypeMethods ), false, + annotationMirror ); } else if ( applicablePrototypeMethods.size() > 1 ) { messager.printMessage( @@ -522,7 +546,8 @@ else if ( applicablePrototypeMethods.size() > 1 ) { // or no @InheritInverseConfiguration if ( inverseTemplateMethod == null && inheritanceStrategy.isApplyReverse() ) { if ( applicableReversePrototypeMethods.size() == 1 ) { - mappingOptions.applyInheritedOptions( first( applicableReversePrototypeMethods ), true ); + mappingOptions.applyInheritedOptions( method, first( applicableReversePrototypeMethods ), true, + annotationMirror ); } else if ( applicableReversePrototypeMethods.size() > 1 ) { messager.printMessage( @@ -534,7 +559,7 @@ else if ( applicableReversePrototypeMethods.size() > 1 ) { } // @BeanMapping( ignoreByDefault = true ) - if ( mappingOptions.getBeanMapping() != null && mappingOptions.getBeanMapping().isignoreByDefault() ) { + if ( mappingOptions.getBeanMapping() != null && mappingOptions.getBeanMapping().isIgnoredByDefault() ) { mappingOptions.applyIgnoreAll( method, typeFactory, mappingContext.getMessager() ); } @@ -601,21 +626,28 @@ else if ( nameFilteredcandidates.size() > 1 ) { reportErrorWhenSeveralNamesMatch( nameFilteredcandidates, method, inverseConfiguration ); } else { - reportErrorWhenAmbigousReverseMapping( candidates, method, inverseConfiguration ); + reportErrorWhenAmbiguousReverseMapping( candidates, method, inverseConfiguration ); } } } - return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods ); + return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods, + getAnnotationMirror( inverseConfiguration ) ); + } + + private AnnotationMirror getAnnotationMirror(InheritInverseConfigurationGem inverseConfiguration) { + return inverseConfiguration == null ? null : inverseConfiguration.mirror(); } private SourceMethod extractInitializedOptions(SourceMethod resultMethod, List rawMethods, MapperOptions mapperConfig, - List initializingMethods) { + List initializingMethods, + AnnotationMirror annotationMirror) { if ( resultMethod != null ) { if ( !resultMethod.getOptions().isFullyInitialized() ) { - mergeInheritedOptions( resultMethod, mapperConfig, rawMethods, initializingMethods ); + mergeInheritedOptions( resultMethod, mapperConfig, rawMethods, initializingMethods, + annotationMirror ); } return resultMethod; @@ -664,30 +696,35 @@ else if ( sourceMethod.getName().equals( name ) ) { else if ( candidates.size() > 1 ) { // ambiguity: find a matching method that matches configuredBy - List nameFilteredcandidates = new ArrayList<>(); + List nameFilteredCandidates = new ArrayList<>(); for ( SourceMethod candidate : candidates ) { if ( candidate.getName().equals( name ) ) { - nameFilteredcandidates.add( candidate ); + nameFilteredCandidates.add( candidate ); } } - if ( nameFilteredcandidates.size() == 1 ) { - resultMethod = first( nameFilteredcandidates ); + if ( nameFilteredCandidates.size() == 1 ) { + resultMethod = first( nameFilteredCandidates ); } - else if ( nameFilteredcandidates.size() > 1 ) { - reportErrorWhenSeveralNamesMatch( nameFilteredcandidates, method, inheritConfiguration ); + else if ( nameFilteredCandidates.size() > 1 ) { + reportErrorWhenSeveralNamesMatch( nameFilteredCandidates, method, inheritConfiguration ); } else { - reportErrorWhenAmbigousMapping( candidates, method, inheritConfiguration ); + reportErrorWhenAmbiguousMapping( candidates, method, inheritConfiguration ); } } } - return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods ); + return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods, + getAnnotationMirror( inheritConfiguration ) ); } - private void reportErrorWhenAmbigousReverseMapping(List candidates, SourceMethod method, - InheritInverseConfigurationGem inverseGem) { + private AnnotationMirror getAnnotationMirror(InheritConfigurationGem inheritConfiguration) { + return inheritConfiguration == null ? null : inheritConfiguration.mirror(); + } + + private void reportErrorWhenAmbiguousReverseMapping(List candidates, SourceMethod method, + InheritInverseConfigurationGem inverseGem) { List candidateNames = new ArrayList<>(); for ( SourceMethod candidate : candidates ) { @@ -737,8 +774,8 @@ private void reportErrorWhenNonMatchingName(SourceMethod onlyCandidate, SourceMe ); } - private void reportErrorWhenAmbigousMapping(List candidates, SourceMethod method, - InheritConfigurationGem gem) { + private void reportErrorWhenAmbiguousMapping(List candidates, SourceMethod method, + InheritConfigurationGem gem) { List candidateNames = new ArrayList<>(); for ( SourceMethod candidate : candidates ) { @@ -787,4 +824,15 @@ private void reportErrorWhenNonMatchingName(SourceMethod onlyCandidate, SourceMe onlyCandidate.getName() ); } + + private boolean isConsistent( JavadocGem gem, TypeElement element, FormattingMessager messager ) { + if ( !gem.value().hasValue() + && !gem.authors().hasValue() + && !gem.deprecated().hasValue() + && !gem.since().hasValue() ) { + messager.printMessage( element, gem.mirror(), Message.JAVADOC_NO_ELEMENTS ); + return false; + } + return true; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperRenderingProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperRenderingProcessor.java index a827a60736..bab6af7b09 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperRenderingProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperRenderingProcessor.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.internal.processor; import java.io.IOException; - import javax.annotation.processing.Filer; import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java index c6e510d315..d7a0329aeb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MethodRetrievalProcessor.java @@ -8,13 +8,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -24,17 +21,24 @@ import org.mapstruct.ap.internal.gem.BeanMappingGem; import org.mapstruct.ap.internal.gem.ConditionGem; +import org.mapstruct.ap.internal.gem.IgnoredGem; +import org.mapstruct.ap.internal.gem.IgnoredListGem; import org.mapstruct.ap.internal.gem.IterableMappingGem; import org.mapstruct.ap.internal.gem.MapMappingGem; import org.mapstruct.ap.internal.gem.MappingGem; import org.mapstruct.ap.internal.gem.MappingsGem; import org.mapstruct.ap.internal.gem.ObjectFactoryGem; +import org.mapstruct.ap.internal.gem.SourcePropertyNameGem; +import org.mapstruct.ap.internal.gem.SubclassMappingGem; +import org.mapstruct.ap.internal.gem.SubclassMappingsGem; +import org.mapstruct.ap.internal.gem.TargetPropertyNameGem; import org.mapstruct.ap.internal.gem.ValueMappingGem; import org.mapstruct.ap.internal.gem.ValueMappingsGem; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.source.BeanMappingOptions; +import org.mapstruct.ap.internal.model.source.ConditionOptions; import org.mapstruct.ap.internal.model.source.EnumMappingOptions; import org.mapstruct.ap.internal.model.source.IterableMappingOptions; import org.mapstruct.ap.internal.model.source.MapMappingOptions; @@ -42,6 +46,8 @@ import org.mapstruct.ap.internal.model.source.MappingOptions; import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods; import org.mapstruct.ap.internal.model.source.SourceMethod; +import org.mapstruct.ap.internal.model.source.SubclassMappingOptions; +import org.mapstruct.ap.internal.model.source.SubclassValidator; import org.mapstruct.ap.internal.model.source.ValueMappingOptions; import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.util.AccessorNamingUtils; @@ -50,6 +56,8 @@ import org.mapstruct.ap.internal.util.Executables; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.MetaAnnotations; +import org.mapstruct.ap.internal.util.RepeatableAnnotations; import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.spi.EnumTransformationStrategy; @@ -63,11 +71,15 @@ */ public class MethodRetrievalProcessor implements ModelElementProcessor> { - private static final String JAVA_LANG_ANNOTATION_PGK = "java.lang.annotation"; - private static final String ORG_MAPSTRUCT_PKG = "org.mapstruct"; private static final String MAPPING_FQN = "org.mapstruct.Mapping"; private static final String MAPPINGS_FQN = "org.mapstruct.Mappings"; - + private static final String SUB_CLASS_MAPPING_FQN = "org.mapstruct.SubclassMapping"; + private static final String SUB_CLASS_MAPPINGS_FQN = "org.mapstruct.SubclassMappings"; + private static final String VALUE_MAPPING_FQN = "org.mapstruct.ValueMapping"; + private static final String VALUE_MAPPINGS_FQN = "org.mapstruct.ValueMappings"; + private static final String CONDITION_FQN = "org.mapstruct.Condition"; + private static final String IGNORED_FQN = "org.mapstruct.Ignored"; + private static final String IGNORED_LIST_FQN = "org.mapstruct.IgnoredList"; private FormattingMessager messager; private TypeFactory typeFactory; private AccessorNamingUtils accessorNaming; @@ -102,7 +114,12 @@ public List process(ProcessorContext context, TypeElement mapperTy } List prototypeMethods = retrievePrototypeMethods( mapperTypeElement, mapperOptions ); - return retrieveMethods( mapperTypeElement, mapperTypeElement, mapperOptions, prototypeMethods ); + return retrieveMethods( + typeFactory.getType( mapperTypeElement.asType() ), + mapperTypeElement, + mapperOptions, + prototypeMethods + ); } @Override @@ -120,6 +137,11 @@ private List retrievePrototypeMethods(TypeElement mapperTypeElemen List methods = new ArrayList<>(); for ( ExecutableElement executable : elementUtils.getAllEnclosedExecutableElements( typeElement ) ) { + if ( executable.isDefault() || executable.getModifiers().contains( Modifier.STATIC ) ) { + // skip the default and static methods because these are not prototypes. + continue; + } + ExecutableType methodType = typeFactory.getMethodType( mapperAnnotation.mapperConfigType(), executable ); List parameters = typeFactory.getParameters( methodType, executable ); boolean containsTargetTypeParameter = SourceMethod.containsTargetTypeParameter( parameters ); @@ -149,19 +171,21 @@ private List retrievePrototypeMethods(TypeElement mapperTypeElemen /** * Retrieves the mapping methods declared by the given mapper type. * - * @param usedMapper The type of interest (either the mapper to implement or a used mapper via @uses annotation) + * @param usedMapperType The type of interest (either the mapper to implement, a used mapper via @uses annotation, + * or a parameter type annotated with @Context) * @param mapperToImplement the top level type (mapper) that requires implementation * @param mapperOptions the mapper config * @param prototypeMethods prototype methods defined in mapper config type * @return All mapping methods declared by the given type */ - private List retrieveMethods(TypeElement usedMapper, TypeElement mapperToImplement, + private List retrieveMethods(Type usedMapperType, TypeElement mapperToImplement, MapperOptions mapperOptions, List prototypeMethods) { List methods = new ArrayList<>(); + TypeElement usedMapper = usedMapperType.getTypeElement(); for ( ExecutableElement executable : elementUtils.getAllEnclosedExecutableElements( usedMapper ) ) { SourceMethod method = getMethod( - usedMapper, + usedMapperType, executable, mapperToImplement, mapperOptions, @@ -178,7 +202,7 @@ private List retrieveMethods(TypeElement usedMapper, TypeElement m TypeElement usesMapperElement = asTypeElement( mapper ); if ( !mapperToImplement.equals( usesMapperElement ) ) { methods.addAll( retrieveMethods( - usesMapperElement, + typeFactory.getType( mapper ), mapperToImplement, mapperOptions, prototypeMethods ) ); @@ -201,13 +225,13 @@ private TypeElement asTypeElement(DeclaredType type) { return (TypeElement) type.asElement(); } - private SourceMethod getMethod(TypeElement usedMapper, + private SourceMethod getMethod(Type usedMapperType, ExecutableElement method, TypeElement mapperToImplement, MapperOptions mapperOptions, List prototypeMethods) { - - ExecutableType methodType = typeFactory.getMethodType( (DeclaredType) usedMapper.asType(), method ); + TypeElement usedMapper = usedMapperType.getTypeElement(); + ExecutableType methodType = typeFactory.getMethodType( (DeclaredType) usedMapperType.getTypeMirror(), method ); List parameters = typeFactory.getParameters( methodType, method ); Type returnType = typeFactory.getReturnType( methodType ); @@ -227,7 +251,7 @@ private SourceMethod getMethod(TypeElement usedMapper, // otherwise add reference to existing mapper method else if ( isValidReferencedMethod( parameters ) || isValidFactoryMethod( method, parameters, returnType ) || isValidLifecycleCallbackMethod( method ) - || isValidPresenceCheckMethod( method, returnType ) ) { + || isValidPresenceCheckMethod( method, parameters, returnType ) ) { return getReferencedMethod( usedMapper, methodType, method, mapperToImplement, parameters ); } else { @@ -273,8 +297,7 @@ private SourceMethod getMethodRequiringImplementation(ExecutableType methodType, typeUtils, typeFactory ); - Set mappingOptions = - getMappings( method, method, beanMappingOptions, new LinkedHashSet<>(), new HashSet<>() ); + Set mappingOptions = getMappings( method, beanMappingOptions ); IterableMappingOptions iterableMappingOptions = IterableMappingOptions.fromGem( IterableMappingGem.instanceOn( method ), @@ -299,6 +322,17 @@ private SourceMethod getMethodRequiringImplementation(ExecutableType methodType, messager ); + // We want to get as much error reporting as possible. + // If targetParameter is not null it means we have an update method + SubclassValidator subclassValidator = new SubclassValidator( messager, typeUtils ); + Set subclassMappingOptions = getSubclassMappings( + sourceParameters, + targetParameter != null ? null : resultType, + method, + beanMappingOptions, + subclassValidator + ); + return new SourceMethod.Builder() .setExecutable( method ) .setParameters( parameters ) @@ -311,6 +345,8 @@ private SourceMethod getMethodRequiringImplementation(ExecutableType methodType, .setMapMappingOptions( mapMappingOptions ) .setValueMappingOptionss( getValueMappings( method ) ) .setEnumMappingOptions( enumMappingOptions ) + .setSubclassMappings( subclassMappingOptions ) + .setSubclassValidator( subclassValidator ) .setTypeUtils( typeUtils ) .setTypeFactory( typeFactory ) .setPrototypeMethods( prototypeMethods ) @@ -328,7 +364,7 @@ private ParameterProvidedMethods retrieveContextProvidedMethods( continue; } List contextParamMethods = retrieveMethods( - contextParam.getType().getTypeElement(), + contextParam.getType(), mapperToImplement, mapperConfig, Collections.emptyList() ); @@ -336,7 +372,7 @@ private ParameterProvidedMethods retrieveContextProvidedMethods( List contextProvidedMethods = new ArrayList<>( contextParamMethods.size() ); for ( SourceMethod sourceMethod : contextParamMethods ) { if ( sourceMethod.isLifecycleCallbackMethod() || sourceMethod.isObjectFactory() - || sourceMethod.isPresenceCheck() ) { + || sourceMethod.getConditionOptions().isAnyStrategyApplicable() ) { contextProvidedMethods.add( sourceMethod ); } } @@ -364,13 +400,14 @@ private SourceMethod getReferencedMethod(TypeElement usedMapper, ExecutableType return new SourceMethod.Builder() .setDeclaringMapper( usedMapper.equals( mapperToImplement ) ? null : usedMapperAsType ) - .setDefininingType( definingType ) + .setDefiningType( definingType ) .setExecutable( method ) .setParameters( parameters ) .setReturnType( returnType ) .setExceptionTypes( exceptionTypes ) .setTypeUtils( typeUtils ) .setTypeFactory( typeFactory ) + .setConditionOptions( getConditionOptions( method, parameters ) ) .setVerboseLogging( options.isVerbose() ) .build(); } @@ -392,7 +429,27 @@ private boolean hasFactoryAnnotation(ExecutableElement method) { return ObjectFactoryGem.instanceOn( method ) != null; } - private boolean isValidPresenceCheckMethod(ExecutableElement method, Type returnType) { + private boolean isValidPresenceCheckMethod(ExecutableElement method, List parameters, Type returnType) { + for ( Parameter param : parameters ) { + + if ( param.isSourcePropertyName() && !param.getType().isString() ) { + messager.printMessage( + param.getElement(), + SourcePropertyNameGem.instanceOn( param.getElement() ).mirror(), + Message.RETRIEVAL_SOURCE_PROPERTY_NAME_WRONG_TYPE + ); + return false; + } + + if ( param.isTargetPropertyName() && !param.getType().isString() ) { + messager.printMessage( + param.getElement(), + TargetPropertyNameGem.instanceOn( param.getElement() ).mirror(), + Message.RETRIEVAL_TARGET_PROPERTY_NAME_WRONG_TYPE + ); + return false; + } + } return isBoolean( returnType ) && hasConditionAnnotation( method ); } @@ -496,25 +553,44 @@ private boolean checkParameterAndReturnType(ExecutableElement method, List getMappings(ExecutableElement method, Element element, - BeanMappingOptions beanMapping, Set mappingOptions, - Set handledElements) { - - for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) { - Element lElement = annotationMirror.getAnnotationType().asElement(); - if ( isAnnotation( lElement, MAPPING_FQN ) ) { - // although getInstanceOn does a search on annotation mirrors, the order is preserved - MappingGem mapping = MappingGem.instanceOn( element ); - MappingOptions.addInstance( mapping, method, beanMapping, messager, typeUtils, mappingOptions ); - } - else if ( isAnnotation( lElement, MAPPINGS_FQN ) ) { - // although getInstanceOn does a search on annotation mirrors, the order is preserved - MappingsGem mappings = MappingsGem.instanceOn( element ); - MappingOptions.addInstances( mappings, method, beanMapping, messager, typeUtils, mappingOptions ); - } - else if ( !isAnnotationInPackage( lElement, JAVA_LANG_ANNOTATION_PGK ) - && !isAnnotationInPackage( lElement, ORG_MAPSTRUCT_PKG ) - && !handledElements.contains( lElement ) - ) { - // recur over annotation mirrors - handledElements.add( lElement ); - getMappings( method, lElement, beanMapping, mappingOptions, handledElements ); - } - } - return mappingOptions; + private Set getMappings(ExecutableElement method, BeanMappingOptions beanMapping) { + Set processedAnnotations = new RepeatableMappings( beanMapping ) + .getProcessedAnnotations( method ); + processedAnnotations.addAll( new IgnoredConditions( processedAnnotations ) + .getProcessedAnnotations( method ) ); + return processedAnnotations; } - private boolean isAnnotationInPackage(Element element, String packageFQN ) { - if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) { - return packageFQN.equals( elementUtils.getPackageOf( element ).getQualifiedName().toString() ); + /** + * Retrieves the subclass mappings configured via {@code @SubclassMapping} from the given method. + * + * @param method The method of interest + * @param beanMapping options coming from bean mapping method + * + * @return The subclass mappings for the given method + */ + private Set getSubclassMappings(List sourceParameters, Type resultType, + ExecutableElement method, BeanMappingOptions beanMapping, + SubclassValidator validator) { + return new RepeatableSubclassMappings( beanMapping, sourceParameters, resultType, validator ) + .getProcessedAnnotations( method ); + } + + /** + * Retrieves the conditions configured via {@code @Condition} from the given method. + * + * @param method The method of interest + * @param parameters + * @return The condition options for the given method + */ + + private Set getConditionOptions(ExecutableElement method, List parameters) { + return new MetaConditions( parameters ).getProcessedAnnotations( method ); + } + + private class RepeatableMappings extends RepeatableAnnotations { + private BeanMappingOptions beanMappingOptions; + + RepeatableMappings(BeanMappingOptions beanMappingOptions) { + super( elementUtils, MAPPING_FQN, MAPPINGS_FQN ); + this.beanMappingOptions = beanMappingOptions; + } + + @Override + protected MappingGem singularInstanceOn(Element element) { + return MappingGem.instanceOn( element ); + } + + @Override + protected MappingsGem multipleInstanceOn(Element element) { + return MappingsGem.instanceOn( element ); + } + + @Override + protected void addInstance(MappingGem gem, Element method, Set mappings) { + MappingOptions.addInstance( + gem, + (ExecutableElement) method, + beanMappingOptions, + messager, + typeUtils, + mappings ); + } + + @Override + protected void addInstances(MappingsGem gem, Element method, Set mappings) { + MappingOptions.addInstances( + gem, + (ExecutableElement) method, + beanMappingOptions, + messager, + typeUtils, + mappings ); } - return false; } - private boolean isAnnotation(Element element, String annotationFQN) { - if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) { - return annotationFQN.equals( ( (TypeElement) element ).getQualifiedName().toString() ); + private class RepeatableSubclassMappings + extends RepeatableAnnotations { + private final List sourceParameters; + private final Type resultType; + private SubclassValidator validator; + private BeanMappingOptions beanMappingOptions; + + RepeatableSubclassMappings(BeanMappingOptions beanMappingOptions, List sourceParameters, + Type resultType, SubclassValidator validator) { + super( elementUtils, SUB_CLASS_MAPPING_FQN, SUB_CLASS_MAPPINGS_FQN ); + this.beanMappingOptions = beanMappingOptions; + this.sourceParameters = sourceParameters; + this.resultType = resultType; + this.validator = validator; + } + + @Override + protected SubclassMappingGem singularInstanceOn(Element element) { + return SubclassMappingGem.instanceOn( element ); + } + + @Override + protected SubclassMappingsGem multipleInstanceOn(Element element) { + return SubclassMappingsGem.instanceOn( element ); + } + + @Override + protected void addInstance(SubclassMappingGem gem, + Element method, + Set mappings) { + SubclassMappingOptions.addInstance( + gem, + (ExecutableElement) method, + beanMappingOptions, + messager, + typeUtils, + mappings, + sourceParameters, + resultType, + validator ); + } + + @Override + protected void addInstances(SubclassMappingsGem gem, + Element method, + Set mappings) { + SubclassMappingOptions.addInstances( + gem, + (ExecutableElement) method, + beanMappingOptions, + messager, + typeUtils, + mappings, + sourceParameters, + resultType, + validator ); } - return false; } /** @@ -615,22 +778,115 @@ private boolean isAnnotation(Element element, String annotationFQN) { * @return The mappings for the given method, keyed by target property name */ private List getValueMappings(ExecutableElement method) { - List valueMappings = new ArrayList<>(); + Set processedAnnotations = new RepeatValueMappings().getProcessedAnnotations( method ); + return new ArrayList<>(processedAnnotations); + } - ValueMappingGem mappingAnnotation = ValueMappingGem.instanceOn( method ); - ValueMappingsGem mappingsAnnotation = ValueMappingsGem.instanceOn( method ); + private class RepeatValueMappings + extends RepeatableAnnotations { - if ( mappingAnnotation != null ) { - ValueMappingOptions valueMapping = ValueMappingOptions.fromMappingGem( mappingAnnotation ); - if ( valueMapping != null ) { - valueMappings.add( valueMapping ); + protected RepeatValueMappings() { + super( elementUtils, VALUE_MAPPING_FQN, VALUE_MAPPINGS_FQN ); + } + + @Override + protected ValueMappingGem singularInstanceOn(Element element) { + return ValueMappingGem.instanceOn( element ); + } + + @Override + protected ValueMappingsGem multipleInstanceOn(Element element) { + return ValueMappingsGem.instanceOn( element ); + } + + @Override + protected void addInstance(ValueMappingGem gem, Element source, Set mappings) { + ValueMappingOptions valueMappingOptions = ValueMappingOptions.fromMappingGem( gem ); + mappings.add( valueMappingOptions ); + } + + @Override + protected void addInstances(ValueMappingsGem gems, Element source, Set mappings) { + ValueMappingOptions.fromMappingsGem( gems, (ExecutableElement) source, messager, mappings ); + } + } + + private class MetaConditions extends MetaAnnotations { + + protected final List parameters; + + protected MetaConditions(List parameters) { + super( elementUtils, CONDITION_FQN ); + this.parameters = parameters; + } + + @Override + protected ConditionGem instanceOn(Element element) { + return ConditionGem.instanceOn( element ); + } + + @Override + protected void addInstance(ConditionGem gem, Element source, Set values) { + ConditionOptions options = ConditionOptions.getInstanceOn( + gem, + (ExecutableElement) source, + parameters, + messager + ); + if ( options != null ) { + values.add( options ); } } + } - if ( mappingsAnnotation != null ) { - ValueMappingOptions.fromMappingsGem( mappingsAnnotation, method, messager, valueMappings ); + private class IgnoredConditions extends RepeatableAnnotations { + + protected final Set processedAnnotations; + + protected IgnoredConditions( Set processedAnnotations ) { + super( elementUtils, IGNORED_FQN, IGNORED_LIST_FQN ); + this.processedAnnotations = processedAnnotations; } - return valueMappings; + @Override + protected IgnoredGem singularInstanceOn(Element element) { + return IgnoredGem.instanceOn( element ); + } + + @Override + protected IgnoredListGem multipleInstanceOn(Element element) { + return IgnoredListGem.instanceOn( element ); + } + + @Override + protected void addInstance(IgnoredGem gem, Element method, Set mappings) { + IgnoredGem ignoredGem = IgnoredGem.instanceOn( method ); + if ( ignoredGem == null ) { + ignoredGem = gem; + } + String prefix = ignoredGem.prefix().get(); + for ( String target : ignoredGem.targets().get() ) { + String realTarget = target; + if ( !prefix.isEmpty() ) { + realTarget = prefix + "." + target; + } + MappingOptions mappingOptions = MappingOptions.forIgnore( realTarget ); + if ( processedAnnotations.contains( mappingOptions ) || mappings.contains( mappingOptions ) ) { + messager.printMessage( method, Message.PROPERTYMAPPING_DUPLICATE_TARGETS, realTarget ); + } + else { + mappings.add( mappingOptions ); + } + } + } + + @Override + protected void addInstances(IgnoredListGem gem, Element method, Set mappings) { + IgnoredListGem ignoredListGem = IgnoredListGem.instanceOn( method ); + for ( IgnoredGem ignoredGem : ignoredListGem.value().get() ) { + addInstance( ignoredGem, method, mappings ); + } + } } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/ModelElementProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/ModelElementProcessor.java index d43b52953d..10328c4437 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/ModelElementProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/ModelElementProcessor.java @@ -8,14 +8,15 @@ import java.util.Map; import javax.annotation.processing.Filer; import javax.lang.model.element.TypeElement; -import org.mapstruct.ap.internal.util.ElementUtils; -import org.mapstruct.ap.internal.util.TypeUtils; import javax.tools.Diagnostic.Kind; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.util.AccessorNamingUtils; +import org.mapstruct.ap.internal.util.ElementUtils; import org.mapstruct.ap.internal.util.FormattingMessager; +import org.mapstruct.ap.internal.util.NullabilityResolver; +import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.version.VersionInformation; import org.mapstruct.ap.spi.EnumMappingStrategy; import org.mapstruct.ap.spi.EnumTransformationStrategy; @@ -40,7 +41,7 @@ public interface ModelElementProcessor { * * @author Gunnar Morling */ - public interface ProcessorContext { + interface ProcessorContext { Filer getFiler(); @@ -54,6 +55,8 @@ public interface ProcessorContext { AccessorNamingUtils getAccessorNaming(); + NullabilityResolver getNullabilityResolver(); + Map getEnumTransformationStrategies(); EnumMappingStrategy getEnumMappingStrategy(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java index f7aa9b4fea..c31ac51f06 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/SpringComponentProcessor.java @@ -8,11 +8,24 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; import org.mapstruct.ap.internal.gem.MappingConstantsGem; import org.mapstruct.ap.internal.model.Annotation; +import org.mapstruct.ap.internal.model.Decorator; import org.mapstruct.ap.internal.model.Mapper; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement; +import org.mapstruct.ap.internal.model.annotation.AnnotationElement.AnnotationElementType; + +import static javax.lang.model.element.ElementKind.PACKAGE; /** * A {@link ModelElementProcessor} which converts the given {@link Mapper} @@ -23,6 +36,10 @@ * @author Andreas Gudian */ public class SpringComponentProcessor extends AnnotationBasedComponentModelProcessor { + + private static final String SPRING_COMPONENT_ANNOTATION = "org.springframework.stereotype.Component"; + private static final String SPRING_PRIMARY_ANNOTATION = "org.springframework.context.annotation.Primary"; + @Override protected String getComponentModelIdentifier() { return MappingConstantsGem.ComponentModelGem.SPRING; @@ -31,7 +48,9 @@ protected String getComponentModelIdentifier() { @Override protected List getTypeAnnotations(Mapper mapper) { List typeAnnotations = new ArrayList<>(); - typeAnnotations.add( component() ); + if ( !isAlreadyAnnotatedAsSpringStereotype( mapper ) ) { + typeAnnotations.add( component() ); + } if ( mapper.getDecorator() != null ) { typeAnnotations.add( qualifierDelegate() ); @@ -40,12 +59,37 @@ protected List getTypeAnnotations(Mapper mapper) { return typeAnnotations; } + /** + * Returns the annotations that need to be added to the generated decorator, filtering out any annotations + * that are already present or represented as meta-annotations. + * + * @param decorator the decorator to process + * @return A list of annotations that should be added to the generated decorator. + */ @Override - protected List getDecoratorAnnotations() { - return Arrays.asList( - component(), - primary() - ); + protected List getDecoratorAnnotations(Decorator decorator) { + Set desiredAnnotationNames = new LinkedHashSet<>(); + desiredAnnotationNames.add( SPRING_COMPONENT_ANNOTATION ); + desiredAnnotationNames.add( SPRING_PRIMARY_ANNOTATION ); + List decoratorAnnotations = decorator.getAnnotations(); + if ( !decoratorAnnotations.isEmpty() ) { + Set handledElements = new HashSet<>(); + for ( Annotation annotation : decoratorAnnotations ) { + removeAnnotationsPresentOnElement( + annotation.getType().getTypeElement(), + desiredAnnotationNames, + handledElements + ); + if ( desiredAnnotationNames.isEmpty() ) { + // If all annotations are removed, we can stop further processing + return Collections.emptyList(); + } + } + } + + return desiredAnnotationNames.stream() + .map( this::createAnnotation ) + .collect( Collectors.toList() ); } @Override @@ -68,21 +112,98 @@ protected boolean requiresGenerationOfDecoratorClass() { return true; } + private Annotation createAnnotation(String canonicalName) { + return new Annotation( getTypeFactory().getType( canonicalName ) ); + } + private Annotation autowired() { - return new Annotation( getTypeFactory().getType( "org.springframework.beans.factory.annotation.Autowired" ) ); + return createAnnotation( "org.springframework.beans.factory.annotation.Autowired" ); } private Annotation qualifierDelegate() { return new Annotation( getTypeFactory().getType( "org.springframework.beans.factory.annotation.Qualifier" ), - Collections.singletonList( "\"delegate\"" ) ); + Collections.singletonList( + new AnnotationElement( + AnnotationElementType.STRING, + Collections.singletonList( "delegate" ) + ) ) ); } - private Annotation primary() { - return new Annotation( getTypeFactory().getType( "org.springframework.context.annotation.Primary" ) ); + private Annotation component() { + return createAnnotation( SPRING_COMPONENT_ANNOTATION ); } - private Annotation component() { - return new Annotation( getTypeFactory().getType( "org.springframework.stereotype.Component" ) ); + private boolean isAlreadyAnnotatedAsSpringStereotype(Mapper mapper) { + Set desiredAnnotationNames = new LinkedHashSet<>(); + desiredAnnotationNames.add( SPRING_COMPONENT_ANNOTATION ); + + List mapperAnnotations = mapper.getAnnotations(); + if ( !mapperAnnotations.isEmpty() ) { + Set handledElements = new HashSet<>(); + for ( Annotation annotation : mapperAnnotations ) { + removeAnnotationsPresentOnElement( + annotation.getType().getTypeElement(), + desiredAnnotationNames, + handledElements + ); + if ( desiredAnnotationNames.isEmpty() ) { + // If all annotations are removed, we can stop further processing + return true; + } + } + } + + return false; + } + + /** + * Removes all the annotations and meta-annotations from {@code annotations} which are on the given element. + * + * @param element the element to check + * @param annotations the annotations to check for + * @param handledElements set of already handled elements to avoid infinite recursion + */ + private void removeAnnotationsPresentOnElement(Element element, Set annotations, + Set handledElements) { + if ( annotations.isEmpty() ) { + return; + } + if ( element instanceof TypeElement && + annotations.remove( ( (TypeElement) element ).getQualifiedName().toString() ) ) { + if ( annotations.isEmpty() ) { + // If all annotations are removed, we can stop further processing + return; + } + } + + for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) { + Element annotationMirrorElement = annotationMirror.getAnnotationType().asElement(); + // Bypass java lang annotations to improve performance avoiding unnecessary checks + if ( !isAnnotationInPackage( annotationMirrorElement, "java.lang.annotation" ) && + !handledElements.contains( annotationMirrorElement ) ) { + handledElements.add( annotationMirrorElement ); + if ( annotations.remove( ( (TypeElement) annotationMirrorElement ).getQualifiedName().toString() ) ) { + if ( annotations.isEmpty() ) { + // If all annotations are removed, we can stop further processing + return; + } + } + + removeAnnotationsPresentOnElement( element, annotations, handledElements ); + } + } + } + + private PackageElement getPackageOf( Element element ) { + while ( element.getKind() != PACKAGE ) { + element = element.getEnclosingElement(); + } + + return (PackageElement) element; + } + + private boolean isAnnotationInPackage(Element element, String packageFQN) { + return packageFQN.equals( getPackageOf( element ).getQualifiedName().toString() ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java index b971341fc9..fb380dc146 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java @@ -5,15 +5,11 @@ */ package org.mapstruct.ap.internal.processor.creation; -import static java.util.Collections.singletonList; -import static org.mapstruct.ap.internal.util.Collections.first; -import static org.mapstruct.ap.internal.util.Collections.firstKey; -import static org.mapstruct.ap.internal.util.Collections.firstValue; - import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -21,7 +17,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; - import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -57,6 +52,7 @@ import org.mapstruct.ap.internal.model.source.builtin.BuiltInMethod; import org.mapstruct.ap.internal.model.source.selector.MethodSelectors; import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; +import org.mapstruct.ap.internal.model.source.selector.SelectionContext; import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; import org.mapstruct.ap.internal.util.Collections; import org.mapstruct.ap.internal.util.ElementUtils; @@ -67,6 +63,10 @@ import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.TypeUtils; +import static org.mapstruct.ap.internal.util.Collections.first; +import static org.mapstruct.ap.internal.util.Collections.firstKey; +import static org.mapstruct.ap.internal.util.Collections.firstValue; + /** * The one and only implementation of {@link MappingResolver}. The class has been split into an interface an * implementation for the sake of avoiding package dependencies. Specifically, this implementation refers to classes @@ -97,12 +97,12 @@ public class MappingResolverImpl implements MappingResolver { * Private methods which are not present in the original mapper interface and are added to map certain property * types. */ - private final Set usedSupportedMappings = new HashSet<>(); + private final Set usedSupportedMappings = new LinkedHashSet<>(); /** * Private fields which are added to map certain property types. */ - private final Set usedSupportedFields = new HashSet<>(); + private final Set usedSupportedFields = new LinkedHashSet<>(); public MappingResolverImpl(FormattingMessager messager, ElementUtils elementUtils, TypeUtils typeUtils, TypeFactory typeFactory, List sourceModel, @@ -116,7 +116,7 @@ public MappingResolverImpl(FormattingMessager messager, ElementUtils elementUtil this.conversions = new Conversions( typeFactory ); this.builtInMethods = new BuiltInMappingMethods( typeFactory ); - this.methodSelectors = new MethodSelectors( typeUtils, elementUtils, typeFactory, messager ); + this.methodSelectors = new MethodSelectors( typeUtils, elementUtils, messager, null ); this.verboseLogging = verboseLogging; } @@ -196,24 +196,24 @@ private ResolvingAttempt(List sourceModel, Method mappingMethod, ForgedM this.mappingMethod = mappingMethod; this.description = description; - this.methods = filterPossibleCandidateMethods( sourceModel ); this.formattingParameters = formattingParameters == null ? FormattingParameters.EMPTY : formattingParameters; this.sourceRHS = sourceRHS; - this.supportingMethodCandidates = new HashSet<>(); + this.supportingMethodCandidates = new LinkedHashSet<>(); this.selectionCriteria = criteria; this.positionHint = positionHint; this.forger = forger; this.builtIns = builtIns; this.messager = messager; this.reportingLimitAmbiguous = verboseLogging ? Integer.MAX_VALUE : LIMIT_REPORTING_AMBIGUOUS; + this.methods = filterPossibleCandidateMethods( sourceModel, mappingMethod ); } // CHECKSTYLE:ON - private List filterPossibleCandidateMethods(List candidateMethods) { + private List filterPossibleCandidateMethods(List candidateMethods, T mappingMethod) { List result = new ArrayList<>( candidateMethods.size() ); for ( T candidate : candidateMethods ) { - if ( isCandidateForMapping( candidate ) ) { + if ( isCandidateForMapping( candidate ) && isNotSelfOrSelfAllowed( mappingMethod, candidate ) ) { result.add( candidate ); } } @@ -221,6 +221,10 @@ private List filterPossibleCandidateMethods(List candid return result; } + private boolean isNotSelfOrSelfAllowed(T mappingMethod, T candidate) { + return selectionCriteria == null || selectionCriteria.isSelfAllowed() || !candidate.equals( mappingMethod ); + } + private Assignment getTargetAssignment(Type sourceType, Type targetType) { Assignment assignment; @@ -291,25 +295,29 @@ && allowDirect( sourceType, targetType ) ) { } // 2 step method, then: method(conversion(source)) - assignment = ConversionMethod.getBestMatch( this, sourceType, targetType ); - if ( assignment != null ) { - usedSupportedMappings.addAll( supportingMethodCandidates ); - return assignment; + if ( allowConversion() ) { + assignment = ConversionMethod.getBestMatch( this, sourceType, targetType ); + if ( assignment != null ) { + usedSupportedMappings.addAll( supportingMethodCandidates ); + return assignment; + } } // stop here when looking for update methods. selectionCriteria.setPreferUpdateMapping( false ); // 2 step method, finally: conversion(method(source)) - assignment = MethodConversion.getBestMatch( this, sourceType, targetType ); - if ( assignment != null ) { - usedSupportedMappings.addAll( supportingMethodCandidates ); - return assignment; + if ( allowConversion() ) { + assignment = MethodConversion.getBestMatch( this, sourceType, targetType ); + if ( assignment != null ) { + usedSupportedMappings.addAll( supportingMethodCandidates ); + return assignment; + } } } if ( hasQualfiers() ) { - if ((sourceType.isCollectionType() || sourceType.isArrayType()) && targetType.isIterableType()) { + if ( (sourceType.isCollectionType() || sourceType.isArrayType()) && targetType.isIterableType() ) { // Allow forging iterable mapping when no iterable mapping already found return forger.get(); } @@ -426,7 +434,6 @@ private boolean allow2Steps() { private ConversionAssignment resolveViaConversion(Type sourceType, Type targetType) { ConversionProvider conversionProvider = conversions.getConversion( sourceType, targetType ); - if ( conversionProvider == null ) { return null; } @@ -442,7 +449,7 @@ private ConversionAssignment resolveViaConversion(Type sourceType, Type targetTy Set allUsedFields = new HashSet<>( mapperReferences ); SupportingField.addAllFieldsIn( supportingMethodCandidates, allUsedFields ); - for ( FieldReference helperField : conversionProvider.getRequiredHelperFields( ctx )) { + for ( FieldReference helperField : conversionProvider.getRequiredHelperFields( ctx ) ) { Field field = SupportingField.getSafeField( null, helperField, allUsedFields ); allUsedFields.add( field ); usedSupportedFields.add( field ); @@ -457,12 +464,15 @@ private ConversionAssignment resolveViaConversion(Type sourceType, Type targetTy Assignment conversion = conversionProvider.to( ctx ); if ( conversion != null ) { - return new ConversionAssignment( sourceType, targetType, conversionProvider.to( ctx ) ); + return new ConversionAssignment( sourceType, targetType, conversion ); } return null; } private boolean isCandidateForMapping(Method methodCandidate) { + if ( methodCandidate.getConditionOptions().isAnyStrategyApplicable() ) { + return false; + } return isCreateMethodForMapping( methodCandidate ) || isUpdateMethodForMapping( methodCandidate ); } @@ -483,12 +493,8 @@ private boolean isUpdateMethodForMapping(Method methodCandidate) { private List> getBestMatch(List methods, Type source, Type target) { return methodSelectors.getMatchingMethods( - mappingMethod, methods, - singletonList( source ), - target, - target, - selectionCriteria + SelectionContext.forMappingMethods( mappingMethod, source, target, selectionCriteria, typeFactory ) ); } @@ -667,7 +673,16 @@ Assignment getAssignment() { void reportMessageWhenNarrowing(FormattingMessager messager, ResolvingAttempt attempt) { - if ( NativeTypes.isNarrowing( sourceType.getFullyQualifiedName(), targetType.getFullyQualifiedName() ) ) { + Type source = sourceType; + if ( sourceType.isOptionalType() ) { + source = sourceType.getOptionalBaseType(); + } + + Type target = targetType; + if ( targetType.isOptionalType() ) { + target = targetType.getOptionalBaseType(); + } + if ( NativeTypes.isNarrowing( source.getFullyQualifiedName(), target.getFullyQualifiedName() ) ) { ReportingPolicyGem policy = attempt.mappingMethod.getOptions().getMapper().typeConversionPolicy(); if ( policy == ReportingPolicyGem.WARN ) { report( messager, attempt, Message.CONVERSION_LOSSY_WARNING ); @@ -712,6 +727,11 @@ public int hashCode() { } } + private enum BestMatchType { + IGNORE_QUALIFIERS_BEFORE_Y_CANDIDATES, + IGNORE_QUALIFIERS_AFTER_Y_CANDIDATES, + } + /** * Suppose mapping required from A to C and: *
        @@ -743,22 +763,37 @@ static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targe if ( mmAttempt.hasResult ) { return mmAttempt.result; } - MethodMethod mbAttempt = - new MethodMethod<>( att, att.methods, att.builtIns, att::toMethodRef, att::toBuildInRef ) - .getBestMatch( sourceType, targetType ); - if ( mbAttempt.hasResult ) { - return mbAttempt.result; + if ( att.hasQualfiers() ) { + mmAttempt = mmAttempt.getBestMatchIgnoringQualifiersBeforeY( sourceType, targetType ); + if ( mmAttempt.hasResult ) { + return mmAttempt.result; + } + + mmAttempt = mmAttempt.getBestMatchIgnoringQualifiersAfterY( sourceType, targetType ); + if ( mmAttempt.hasResult ) { + return mmAttempt.result; + } } - MethodMethod bmAttempt = - new MethodMethod<>( att, att.builtIns, att.methods, att::toBuildInRef, att::toMethodRef ) - .getBestMatch( sourceType, targetType ); - if ( bmAttempt.hasResult ) { - return bmAttempt.result; + if ( att.allowConversion() ) { + MethodMethod mbAttempt = + new MethodMethod<>( att, att.methods, att.builtIns, att::toMethodRef, att::toBuildInRef ) + .getBestMatch( sourceType, targetType ); + if ( mbAttempt.hasResult ) { + return mbAttempt.result; + } + MethodMethod bmAttempt = + new MethodMethod<>( att, att.builtIns, att.methods, att::toBuildInRef, att::toMethodRef ) + .getBestMatch( sourceType, targetType ); + if ( bmAttempt.hasResult ) { + return bmAttempt.result; + } + MethodMethod bbAttempt = + new MethodMethod<>( att, att.builtIns, att.builtIns, att::toBuildInRef, att::toBuildInRef ) + .getBestMatch( sourceType, targetType ); + return bbAttempt.result; } - MethodMethod bbAttempt = - new MethodMethod<>( att, att.builtIns, att.builtIns, att::toBuildInRef, att::toBuildInRef ) - .getBestMatch( sourceType, targetType ); - return bbAttempt.result; + + return null; } MethodMethod(ResolvingAttempt attempt, List xMethods, List yMethods, @@ -772,6 +807,18 @@ static Assignment getBestMatch(ResolvingAttempt att, Type sourceType, Type targe } private MethodMethod getBestMatch(Type sourceType, Type targetType) { + return getBestMatch( sourceType, targetType, null ); + } + + private MethodMethod getBestMatchIgnoringQualifiersBeforeY(Type sourceType, Type targetType) { + return getBestMatch( sourceType, targetType, BestMatchType.IGNORE_QUALIFIERS_BEFORE_Y_CANDIDATES ); + } + + private MethodMethod getBestMatchIgnoringQualifiersAfterY(Type sourceType, Type targetType) { + return getBestMatch( sourceType, targetType, BestMatchType.IGNORE_QUALIFIERS_AFTER_Y_CANDIDATES ); + } + + private MethodMethod getBestMatch(Type sourceType, Type targetType, BestMatchType matchType) { Set yCandidates = new HashSet<>(); Map, List>> xCandidates = new LinkedHashMap<>(); @@ -784,9 +831,12 @@ private MethodMethod getBestMatch(Type sourceType, Type targetType) { // sourceMethod or builtIn that fits the signature B to C. Only then there is a match. If we have a match // a nested method call can be called. so C = methodY( methodX (A) ) attempt.selectionCriteria.setPreferUpdateMapping( false ); + attempt.selectionCriteria.setIgnoreQualifiers( + matchType == BestMatchType.IGNORE_QUALIFIERS_BEFORE_Y_CANDIDATES ); + for ( T2 yCandidate : yMethods ) { Type ySourceType = yCandidate.getMappingSourceType(); - ySourceType = ySourceType.resolveParameterToType( targetType, yCandidate.getResultType() ).getMatch(); + ySourceType = ySourceType.resolveGenericTypeParameters( targetType, yCandidate.getResultType() ); Type yTargetType = yCandidate.getResultType(); if ( ySourceType == null || !yTargetType.isRawAssignableTo( targetType ) @@ -796,13 +846,16 @@ private MethodMethod getBestMatch(Type sourceType, Type targetType) { } List> xMatches = attempt.getBestMatch( xMethods, sourceType, ySourceType ); if ( !xMatches.isEmpty() ) { - xMatches.stream().forEach( x -> xCandidates.put( x, new ArrayList<>() ) ); - final Type typeInTheMiddle = ySourceType; - xMatches.stream().forEach( x -> typesInTheMiddle.put( x, typeInTheMiddle ) ); + for ( SelectedMethod x : xMatches ) { + xCandidates.put( x, new ArrayList<>() ); + typesInTheMiddle.put( x, ySourceType ); + } yCandidates.add( yCandidate ); } } attempt.selectionCriteria.setPreferUpdateMapping( true ); + attempt.selectionCriteria.setIgnoreQualifiers( + matchType == BestMatchType.IGNORE_QUALIFIERS_AFTER_Y_CANDIDATES ); // collect all results List yCandidatesList = new ArrayList<>( yCandidates ); @@ -816,6 +869,7 @@ private MethodMethod getBestMatch(Type sourceType, Type targetType) { } } + attempt.selectionCriteria.setIgnoreQualifiers( false ); // no results left if ( xCandidates.isEmpty() ) { return this; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/AbstractElementUtilsDecorator.java b/processor/src/main/java/org/mapstruct/ap/internal/util/AbstractElementUtilsDecorator.java index 14734b9c8e..67ce6e10ff 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/AbstractElementUtilsDecorator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/AbstractElementUtilsDecorator.java @@ -13,11 +13,13 @@ import java.util.ListIterator; import java.util.Map; import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; import javax.lang.model.element.Name; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; @@ -32,22 +34,52 @@ import static javax.lang.model.util.ElementFilter.fieldsIn; import static javax.lang.model.util.ElementFilter.methodsIn; +/** + * MapStruct specific abstract implementation of {@link ElementUtils}. + * This allows us to provide different implementations for different compilers and / or + * to allow us for easier implementation of using the module system. + */ public abstract class AbstractElementUtilsDecorator implements ElementUtils { private final Elements delegate; + /** + * The module element when running with the module system, + * {@code null} otherwise. + */ + private final Element moduleElement; - AbstractElementUtilsDecorator(ProcessingEnvironment processingEnv) { + @IgnoreJRERequirement + AbstractElementUtilsDecorator(ProcessingEnvironment processingEnv, TypeElement mapperElement) { this.delegate = processingEnv.getElementUtils(); + if ( SourceVersion.RELEASE_8.compareTo( processingEnv.getSourceVersion() ) >= 0 ) { + // We are running with Java 8 or lower + this.moduleElement = null; + } + else { + this.moduleElement = this.delegate.getModuleOf( mapperElement ); + } } @Override + @IgnoreJRERequirement public PackageElement getPackageElement(CharSequence name) { - return delegate.getPackageElement( name ); + if ( this.moduleElement == null ) { + return this.delegate.getPackageElement( name ); + } + + // If the module element is not null then we must be running on Java 8+ + return this.delegate.getPackageElement( (ModuleElement) moduleElement, name ); } @Override + @IgnoreJRERequirement public TypeElement getTypeElement(CharSequence name) { - return delegate.getTypeElement( name ); + if ( this.moduleElement == null ) { + return this.delegate.getTypeElement( name ); + } + + // If the module element is not null then we must be running on Java 8+ + return this.delegate.getTypeElement( (ModuleElement) moduleElement, name ); } @Override @@ -285,6 +317,8 @@ private TypeElement asTypeElement(TypeMirror mirror) { } /** + * Checks whether the {@code executable} does not have a private modifier. + * * @param executable the executable to check * @return {@code true}, iff the executable does not have a private modifier */ diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java b/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java index 0f17946f72..8025acfeb1 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java @@ -10,8 +10,8 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleElementVisitor6; -import javax.lang.model.util.SimpleTypeVisitor6; +import javax.lang.model.util.SimpleElementVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; @@ -84,7 +84,7 @@ public String getElementNameForAdder(Accessor adderMethod) { private static String getQualifiedName(TypeMirror type) { DeclaredType declaredType = type.accept( - new SimpleTypeVisitor6() { + new SimpleTypeVisitor8() { @Override public DeclaredType visitDeclared(DeclaredType t, Void p) { return t; @@ -98,7 +98,7 @@ public DeclaredType visitDeclared(DeclaredType t, Void p) { } TypeElement typeElement = declaredType.asElement().accept( - new SimpleElementVisitor6() { + new SimpleElementVisitor8() { @Override public TypeElement visitType(TypeElement e, Void p) { return e; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java b/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java index 86fb7e3092..9f8523efa0 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/AnnotationProcessorContext.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.ServiceLoader; - import javax.annotation.processing.Messager; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -31,6 +30,7 @@ import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy; import org.mapstruct.ap.spi.ImmutablesBuilderProvider; import org.mapstruct.ap.spi.MapStructProcessingEnvironment; +import org.mapstruct.ap.spi.NoOpBuilderProvider; /** * Keeps contextual data in the scope of the entire annotation processor ("application scope"). @@ -48,18 +48,27 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen private Map enumTransformationStrategies; private AccessorNamingUtils accessorNaming; + private NullabilityResolver nullabilityResolver; private Elements elementUtils; private Types typeUtils; private Messager messager; + private boolean disableBuilder; + private boolean disableJSpecify; private boolean verbose; - public AnnotationProcessorContext(Elements elementUtils, Types typeUtils, Messager messager, boolean verbose) { + private Map options; + + public AnnotationProcessorContext(Elements elementUtils, Types typeUtils, Messager messager, boolean disableBuilder, + boolean disableJSpecify, boolean verbose, Map options) { astModifyingAnnotationProcessors = java.util.Collections.unmodifiableList( findAstModifyingAnnotationProcessors( messager ) ); this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.messager = messager; + this.disableBuilder = disableBuilder; + this.disableJSpecify = disableJSpecify; this.verbose = verbose; + this.options = java.util.Collections.unmodifiableMap( options ); } /** @@ -103,7 +112,9 @@ else if ( elementUtils.getTypeElement( FreeBuilderConstants.FREE_BUILDER_FQN ) ! + this.accessorNamingStrategy.getClass().getCanonicalName() ); } - this.builderProvider = Services.get( BuilderProvider.class, defaultBuilderProvider ); + this.builderProvider = this.disableBuilder ? + new NoOpBuilderProvider() : + Services.get( BuilderProvider.class, defaultBuilderProvider ); this.builderProvider.init( this ); if ( verbose ) { messager.printMessage( @@ -112,6 +123,7 @@ else if ( elementUtils.getTypeElement( FreeBuilderConstants.FREE_BUILDER_FQN ) ! ); } this.accessorNaming = new AccessorNamingUtils( this.accessorNamingStrategy ); + this.nullabilityResolver = new NullabilityResolver( !this.disableJSpecify ); this.enumMappingStrategy = Services.get( EnumMappingStrategy.class, new DefaultEnumMappingStrategy() ); this.enumMappingStrategy.init( this ); @@ -245,6 +257,11 @@ public AccessorNamingUtils getAccessorNaming() { return accessorNaming; } + public NullabilityResolver getNullabilityResolver() { + initialize(); + return nullabilityResolver; + } + public AccessorNamingStrategy getAccessorNamingStrategy() { initialize(); return accessorNamingStrategy; @@ -264,4 +281,8 @@ public Map getEnumTransformationStrategies() initialize(); return enumTransformationStrategies; } + + public Map getOptions() { + return this.options; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/EclipseElementUtilsDecorator.java b/processor/src/main/java/org/mapstruct/ap/internal/util/EclipseElementUtilsDecorator.java index aaf1bb7e20..3c1dc84d36 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/EclipseElementUtilsDecorator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/EclipseElementUtilsDecorator.java @@ -13,8 +13,8 @@ public class EclipseElementUtilsDecorator extends AbstractElementUtilsDecorator private final Elements delegate; - EclipseElementUtilsDecorator(ProcessingEnvironment processingEnv) { - super( processingEnv ); + EclipseElementUtilsDecorator(ProcessingEnvironment processingEnv, TypeElement mapperElement) { + super( processingEnv, mapperElement ); this.delegate = processingEnv.getElementUtils(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/ElementUtils.java b/processor/src/main/java/org/mapstruct/ap/internal/util/ElementUtils.java index fdd221c1a6..3e026cde80 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/ElementUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/ElementUtils.java @@ -16,12 +16,13 @@ public interface ElementUtils extends Elements { - static ElementUtils create(ProcessingEnvironment processingEnvironment, VersionInformation info ) { + static ElementUtils create(ProcessingEnvironment processingEnvironment, VersionInformation info, + TypeElement mapperElement) { if ( info.isEclipseJDTCompiler() ) { - return new EclipseElementUtilsDecorator( processingEnvironment ); + return new EclipseElementUtilsDecorator( processingEnvironment, mapperElement ); } else { - return new JavacElementUtilsDecorator( processingEnvironment ); + return new JavacElementUtilsDecorator( processingEnvironment, mapperElement ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java index 1592a39fda..c845450e13 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.internal.util; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; @@ -21,19 +19,6 @@ */ public class Executables { - private static final Method DEFAULT_METHOD; - - static { - Method method; - try { - method = ExecutableElement.class.getMethod( "isDefault" ); - } - catch ( NoSuchMethodException e ) { - method = null; - } - DEFAULT_METHOD = method; - } - private Executables() { } @@ -54,12 +39,7 @@ public static boolean isFinal(Accessor accessor) { } public static boolean isDefaultMethod(ExecutableElement method) { - try { - return DEFAULT_METHOD != null && Boolean.TRUE.equals( DEFAULT_METHOD.invoke( method ) ); - } - catch ( IllegalAccessException | InvocationTargetException e ) { - return false; - } + return method.isDefault(); } /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Fields.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Fields.java index 8e16a95274..b2f8ac1442 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Fields.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Fields.java @@ -6,13 +6,7 @@ package org.mapstruct.ap.internal.util; import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; - -import org.mapstruct.ap.spi.TypeHierarchyErroneousException; /** * Provides functionality around {@link VariableElement}s. @@ -35,17 +29,4 @@ static boolean isPublic(VariableElement method) { private static boolean isNotStatic(VariableElement method) { return !method.getModifiers().contains( Modifier.STATIC ); } - - private static TypeElement asTypeElement(TypeMirror mirror) { - return (TypeElement) ( (DeclaredType) mirror ).asElement(); - } - - private static boolean hasNonObjectSuperclass(TypeElement element) { - if ( element.getSuperclass().getKind() == TypeKind.ERROR ) { - throw new TypeHierarchyErroneousException( element ); - } - - return element.getSuperclass().getKind() == TypeKind.DECLARED - && !asTypeElement( element.getSuperclass() ).getQualifiedName().toString().equals( "java.lang.Object" ); - } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java index 9cb923b3c8..6492270705 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java @@ -12,6 +12,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -22,13 +23,11 @@ import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.util.accessor.Accessor; -import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor; -import org.mapstruct.ap.internal.util.accessor.FieldElementAccessor; +import org.mapstruct.ap.internal.util.accessor.ElementAccessor; +import org.mapstruct.ap.internal.util.accessor.ReadAccessor; import static org.mapstruct.ap.internal.util.Collections.first; import static org.mapstruct.ap.internal.util.accessor.AccessorType.ADDER; -import static org.mapstruct.ap.internal.util.accessor.AccessorType.GETTER; -import static org.mapstruct.ap.internal.util.accessor.AccessorType.PRESENCE_CHECKER; import static org.mapstruct.ap.internal.util.accessor.AccessorType.SETTER; /** @@ -40,22 +39,16 @@ public class Filters { private static final Method RECORD_COMPONENTS_METHOD; - private static final Method RECORD_COMPONENT_ACCESSOR_METHOD; static { Method recordComponentsMethod; - Method recordComponentAccessorMethod; try { recordComponentsMethod = TypeElement.class.getMethod( "getRecordComponents" ); - recordComponentAccessorMethod = Class.forName( "javax.lang.model.element.RecordComponentElement" ) - .getMethod( "getAccessor" ); } - catch ( NoSuchMethodException | ClassNotFoundException e ) { + catch ( NoSuchMethodException e ) { recordComponentsMethod = null; - recordComponentAccessorMethod = null; } RECORD_COMPONENTS_METHOD = recordComponentsMethod; - RECORD_COMPONENT_ACCESSOR_METHOD = recordComponentAccessorMethod; } private final AccessorNamingUtils accessorNaming; @@ -68,10 +61,10 @@ public Filters(AccessorNamingUtils accessorNaming, TypeUtils typeUtils, TypeMirr this.typeMirror = typeMirror; } - public List getterMethodsIn(List elements) { + public List getterMethodsIn(List elements) { return elements.stream() .filter( accessorNaming::isGetterMethod ) - .map( method -> new ExecutableElementAccessor( method, getReturnType( method ), GETTER ) ) + .map( method -> ReadAccessor.fromGetter( method, getReturnType( method ) ) ) .collect( Collectors.toCollection( LinkedList::new ) ); } @@ -89,53 +82,45 @@ public List recordComponentsIn(TypeElement typeElement) { } } - public Map recordAccessorsIn(Collection recordComponents) { - if ( RECORD_COMPONENT_ACCESSOR_METHOD == null ) { + public Map recordAccessorsIn(Collection recordComponents) { + if ( recordComponents.isEmpty() ) { return java.util.Collections.emptyMap(); } - try { - Map recordAccessors = new LinkedHashMap<>(); - for ( Element recordComponent : recordComponents ) { - ExecutableElement recordExecutableElement = - (ExecutableElement) RECORD_COMPONENT_ACCESSOR_METHOD.invoke( recordComponent ); - recordAccessors.put( - recordComponent.getSimpleName().toString(), - new ExecutableElementAccessor( recordExecutableElement, - getReturnType( recordExecutableElement ), - GETTER - ) - ); - } - - return recordAccessors; - } - catch ( IllegalAccessException | InvocationTargetException e ) { - return java.util.Collections.emptyMap(); + Map recordAccessors = new LinkedHashMap<>(); + for ( Element recordComponent : recordComponents ) { + recordAccessors.put( + recordComponent.getSimpleName().toString(), + ReadAccessor.fromRecordComponent( + recordComponent, + typeUtils.asMemberOf( (DeclaredType) typeMirror, recordComponent ) + ) + ); } + + return recordAccessors; } private TypeMirror getReturnType(ExecutableElement executableElement) { return getWithinContext( executableElement ).getReturnType(); } - public List fieldsIn(List accessors) { + public List fieldsIn(List accessors, BiFunction creator) { return accessors.stream() .filter( Fields::isFieldAccessor ) - .map( FieldElementAccessor::new ) + .map( variableElement -> creator.apply( variableElement, getWithinContext( variableElement ) ) ) .collect( Collectors.toCollection( LinkedList::new ) ); } - public List presenceCheckMethodsIn(List elements) { + public List presenceCheckMethodsIn(List elements) { return elements.stream() .filter( accessorNaming::isPresenceCheckMethod ) - .map( method -> new ExecutableElementAccessor( method, getReturnType( method ), PRESENCE_CHECKER ) ) .collect( Collectors.toCollection( LinkedList::new ) ); } public List setterMethodsIn(List elements) { return elements.stream() .filter( accessorNaming::isSetterMethod ) - .map( method -> new ExecutableElementAccessor( method, getFirstParameter( method ), SETTER ) ) + .map( method -> new ElementAccessor( method, getFirstParameter( method ), SETTER ) ) .collect( Collectors.toCollection( LinkedList::new ) ); } @@ -147,10 +132,14 @@ private ExecutableType getWithinContext( ExecutableElement executableElement ) { return (ExecutableType) typeUtils.asMemberOf( (DeclaredType) typeMirror, executableElement ); } + private TypeMirror getWithinContext( VariableElement variableElement ) { + return typeUtils.asMemberOf( (DeclaredType) typeMirror, variableElement ); + } + public List adderMethodsIn(List elements) { return elements.stream() .filter( accessorNaming::isAdderMethod ) - .map( method -> new ExecutableElementAccessor( method, getFirstParameter( method ), ADDER ) ) + .map( method -> new ElementAccessor( method, getFirstParameter( method ), ADDER ) ) .collect( Collectors.toCollection( LinkedList::new ) ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/IgnoreJRERequirement.java b/processor/src/main/java/org/mapstruct/ap/internal/util/IgnoreJRERequirement.java new file mode 100644 index 0000000000..565ebcce12 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/IgnoreJRERequirement.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE }) +public @interface IgnoreJRERequirement { +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/JSpecifyConstants.java b/processor/src/main/java/org/mapstruct/ap/internal/util/JSpecifyConstants.java new file mode 100644 index 0000000000..7836e0deba --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/JSpecifyConstants.java @@ -0,0 +1,41 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util; + +/** + * Helper holding constants for working with JSpecify null annotations. + * + * @author Filip Hrisafov + */ +public final class JSpecifyConstants { + + /** + * Fully qualified name of {@code org.jspecify.annotations.Nullable}. + */ + public static final String NULLABLE_FQN = "org.jspecify.annotations.Nullable"; + + /** + * Fully qualified name of {@code org.jspecify.annotations.NonNull}. + */ + public static final String NON_NULL_FQN = "org.jspecify.annotations.NonNull"; + + /** + * Fully qualified name of {@code org.jspecify.annotations.NullMarked}. Its presence on an + * element (or an enclosing element closer than any {@code @NullUnmarked} one) means unannotated + * types in that scope are effectively {@code @NonNull}. + */ + public static final String NULL_MARKED_FQN = "org.jspecify.annotations.NullMarked"; + + /** + * Fully qualified name of {@code org.jspecify.annotations.NullUnmarked}. Its presence on an + * element (or an enclosing element closer than any {@code @NullMarked} one) reverts the scope + * back to unknown nullability. + */ + public static final String NULL_UNMARKED_FQN = "org.jspecify.annotations.NullUnmarked"; + + private JSpecifyConstants() { + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/JavaCollectionConstants.java b/processor/src/main/java/org/mapstruct/ap/internal/util/JavaCollectionConstants.java new file mode 100644 index 0000000000..68d556c542 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/JavaCollectionConstants.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util; + +/** + * Helper holding Java collections full qualified class names for conversion registration, + * to achieve Java compatibility. + * + * @author Cause Chung + */ +public final class JavaCollectionConstants { + public static final String SEQUENCED_MAP_FQN = "java.util.SequencedMap"; + public static final String SEQUENCED_SET_FQN = "java.util.SequencedSet"; + + private JavaCollectionConstants() { + + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/JavacElementUtilsDecorator.java b/processor/src/main/java/org/mapstruct/ap/internal/util/JavacElementUtilsDecorator.java index ad035743cf..fa0e46171b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/JavacElementUtilsDecorator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/JavacElementUtilsDecorator.java @@ -10,8 +10,8 @@ public class JavacElementUtilsDecorator extends AbstractElementUtilsDecorator { - JavacElementUtilsDecorator(ProcessingEnvironment processingEnv) { - super( processingEnv ); + JavacElementUtilsDecorator(ProcessingEnvironment processingEnv, TypeElement mapperElement) { + super( processingEnv, mapperElement ); } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/JaxbConstants.java b/processor/src/main/java/org/mapstruct/ap/internal/util/JaxbConstants.java index db18673ba2..c89877062e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/JaxbConstants.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/JaxbConstants.java @@ -10,7 +10,8 @@ */ public final class JaxbConstants { - public static final String JAXB_ELEMENT_FQN = "javax.xml.bind.JAXBElement"; + public static final String JAVAX_JAXB_ELEMENT_FQN = "javax.xml.bind.JAXBElement"; + public static final String JAKARTA_JAXB_ELEMENT_FQN = "jakarta.xml.bind.JAXBElement"; private JaxbConstants() { } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index f2226ab224..acac07b4b3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -20,6 +20,8 @@ public enum Message { // CHECKSTYLE:OFF PROCESSING_NOTE( "processing: %s.", Diagnostic.Kind.NOTE ), CONFIG_NOTE( "applying mapper configuration: %s.", Diagnostic.Kind.NOTE ), + MESSAGE_MOVED_TO_MAPPER_ERROR( "%s Occured at '%s' in '%s'." ), + MESSAGE_MOVED_TO_MAPPER_WARNING( "%s Occured at '%s' in '%s'.", Diagnostic.Kind.WARNING ), BEANMAPPING_CREATE_NOTE( "creating bean mapping method implementation for %s.", Diagnostic.Kind.NOTE ), BEANMAPPING_NO_ELEMENTS( "'nullValueMappingStrategy', 'nullValuePropertyMappingStrategy', 'resultType' and 'qualifiedBy' are undefined in @BeanMapping, define at least one of them." ), @@ -37,10 +39,19 @@ public enum Message { BEANMAPPING_UNMAPPED_FORGED_TARGETS_ERROR( "Unmapped target %s. Mapping from %s to %s." ), BEANMAPPING_UNMAPPED_SOURCES_WARNING( "Unmapped source %s.", Diagnostic.Kind.WARNING ), BEANMAPPING_UNMAPPED_SOURCES_ERROR( "Unmapped source %s." ), + BEANMAPPING_UNMAPPED_FORGED_SOURCES_WARNING( "Unmapped source %s. Mapping from %s to %s.", Diagnostic.Kind.WARNING ), + BEANMAPPING_UNMAPPED_FORGED_SOURCES_ERROR( "Unmapped source %s. Mapping from %s to %s." ), BEANMAPPING_MISSING_IGNORED_SOURCES_ERROR( "Ignored unknown source %s." ), + BEANMAPPING_REDUNDANT_IGNORED_SOURCES_ERROR( "Source %s mapped despite being listed in ignoreUnmappedSourceProperties." ), + BEANMAPPING_REDUNDANT_IGNORED_SOURCES_WARNING( "Source %s mapped despite being listed in ignoreUnmappedSourceProperties.", Diagnostic.Kind.WARNING ), BEANMAPPING_CYCLE_BETWEEN_PROPERTIES( "Cycle(s) between properties given via dependsOn(): %s." ), BEANMAPPING_UNKNOWN_PROPERTY_IN_DEPENDS_ON( "\"%s\" is no property of the method return type." ), BEANMAPPING_IGNORE_BY_DEFAULT_WITH_MAPPING_TARGET_THIS( "Using @BeanMapping( ignoreByDefault = true ) with @Mapping( target = \".\", ... ) is not allowed. You'll need to explicitly ignore the target properties that should be ignored instead." ), + BEANMAPPING_UNKNOWN_PROPERTY_IN_IGNORED("No property named \"%s\" exists in @Ignored for target type \"%s\". Did you mean \"%s\"?"), + + CONDITION_MISSING_APPLIES_TO_STRATEGY("'appliesTo' has to have at least one value in @Condition" ), + CONDITION_SOURCE_PARAMETERS_INVALID_PARAMETER("Parameter \"%s\" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS. Only source and @Context parameters are allowed for conditions applicable to source parameters." ), + CONDITION_PROPERTIES_INVALID_PARAMETER("Parameter \"%s\" cannot be used with the ConditionStrategy#PROPERTIES. Only source, @Context, @MappingTarget, @TargetType, @TargetPropertyName and @SourcePropertyName parameters are allowed for conditions applicable to properties." ), PROPERTYMAPPING_MAPPING_NOTE( "mapping property: %s to: %s.", Diagnostic.Kind.NOTE ), PROPERTYMAPPING_CREATE_NOTE( "creating property mapping: %s.", Diagnostic.Kind.NOTE ), @@ -57,13 +68,17 @@ public enum Message { PROPERTYMAPPING_EXPRESSION_AND_DEFAULT_VALUE_BOTH_DEFINED( "Expression and default value are both defined in @Mapping, either define a defaultValue or an expression." ), PROPERTYMAPPING_CONSTANT_AND_DEFAULT_VALUE_BOTH_DEFINED( "Constant and default value are both defined in @Mapping, either define a defaultValue or a constant." ), PROPERTYMAPPING_EXPRESSION_AND_DEFAULT_EXPRESSION_BOTH_DEFINED( "Expression and default expression are both defined in @Mapping, either define an expression or a default expression." ), + PROPERTYMAPPING_EXPRESSION_AND_CONDITION_EXPRESSION_BOTH_DEFINED( "Expression and condition expression are both defined in @Mapping, either define an expression or a condition expression." ), PROPERTYMAPPING_CONSTANT_AND_DEFAULT_EXPRESSION_BOTH_DEFINED( "Constant and default expression are both defined in @Mapping, either define a constant or a default expression." ), + PROPERTYMAPPING_CONSTANT_AND_CONDITION_EXPRESSION_BOTH_DEFINED( "Constant and condition expression are both defined in @Mapping, either define a constant or a condition expression." ), PROPERTYMAPPING_DEFAULT_VALUE_AND_DEFAULT_EXPRESSION_BOTH_DEFINED( "Default value and default expression are both defined in @Mapping, either define a default value or a default expression." ), PROPERTYMAPPING_DEFAULT_VALUE_AND_NVPMS( "Default value and nullValuePropertyMappingStrategy are both defined in @Mapping, either define a defaultValue or an nullValuePropertyMappingStrategy." ), PROPERTYMAPPING_EXPRESSION_VALUE_AND_NVPMS( "Expression and nullValuePropertyMappingStrategy are both defined in @Mapping, either define an expression or an nullValuePropertyMappingStrategy." ), PROPERTYMAPPING_CONSTANT_VALUE_AND_NVPMS( "Constant and nullValuePropertyMappingStrategy are both defined in @Mapping, either define a constant or an nullValuePropertyMappingStrategy." ), PROPERTYMAPPING_DEFAULT_EXPERSSION_AND_NVPMS( "DefaultExpression and nullValuePropertyMappingStrategy are both defined in @Mapping, either define a defaultExpression or an nullValuePropertyMappingStrategy." ), PROPERTYMAPPING_IGNORE_AND_NVPMS( "Ignore and nullValuePropertyMappingStrategy are both defined in @Mapping, either define ignore or an nullValuePropertyMappingStrategy." ), + PROPERTYMAPPING_TARGET_THIS_AND_IGNORE( "Using @Mapping( target = \".\", ignore = true ) is not allowed. You need to use @BeanMapping( ignoreByDefault = true ) if you would like to ignore all non explicitly mapped target properties." ), + PROPERTYMAPPING_TARGET_THIS_NO_SOURCE( "Using @Mapping( target = \".\") requires a source property. Expression or constant cannot be used as a source."), PROPERTYMAPPING_EXPRESSION_AND_QUALIFIER_BOTH_DEFINED("Expression and a qualifier both defined in @Mapping, either define an expression or a qualifier."), PROPERTYMAPPING_INVALID_EXPRESSION( "Value for expression must be given in the form \"java()\"." ), PROPERTYMAPPING_INVALID_DEFAULT_EXPRESSION( "Value for default expression must be given in the form \"java()\"." ), @@ -78,6 +93,16 @@ public enum Message { PROPERTYMAPPING_WHITESPACE_TRIMMED( "The property named \"%s\" has whitespaces, using trimmed property \"%s\" instead.", Diagnostic.Kind.WARNING ), PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PROPERTY_FROM_TARGET("The type of parameter \"%s\" has no property named \"%s\". Please define the source property explicitly."), PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PARAMETER_FROM_TARGET("No property named \"%s\" exists in source parameter(s). Please define the source explicitly."), + PROPERTYMAPPING_NO_SUITABLE_COLLECTION_OR_MAP_CONSTRUCTOR( "%s does not have an accessible copy or no-args constructor." ), + PROPERTYMAPPING_NO_ACCESSIBLE_PARAMETERLESS_CONSTRUCTOR( "%s does not have an accessible parameterless constructor. Either change the nullValuePropertyMappingStrategy or define a defaultValue or a defaultExpression." ), + PROPERTYMAPPING_EXPRESSION_AND_CONDITION_QUALIFIED_BY_NAME_BOTH_DEFINED( "Expression and condition qualified by name are both defined in @Mapping, either define an expression or a condition qualified by name." ), + PROPERTYMAPPING_TARGET_HAS_NO_TARGET_PROPERTIES( "No target property found for target \"%s\".", Diagnostic.Kind.WARNING ), + PROPERTYMAPPING_NULLABLE_SOURCE_TO_NON_NULL_CONSTRUCTOR_PARAM( "Can't map potentially nullable source property \"%s\" to @NonNull constructor parameter \"%s\". Consider adding a defaultValue or defaultExpression." ), + PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK_NON_NULL_SOURCE( "JSpecify skipping null check for property \"%s\": source is @NonNull.", Diagnostic.Kind.NOTE ), + PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK( "JSpecify adding null check for property \"%s\": source=%s, target=%s.", Diagnostic.Kind.NOTE ), + PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK( "JSpecify skipping null check for property \"%s\": source=%s, target=%s.", Diagnostic.Kind.NOTE ), + PROPERTYMAPPING_JSPECIFY_SKIP_METHOD_GUARD_NON_NULL_PARAM( "JSpecify skipping method-level null guard for property \"%s\": parameter is @NonNull.", Diagnostic.Kind.NOTE ), + MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT( "JSpecify forcing NullValueMappingStrategy.RETURN_DEFAULT for method \"%s\": return type is @NonNull.", Diagnostic.Kind.NOTE ), CONVERSION_LOSSY_WARNING( "%s has a possibly lossy conversion from %s to %s.", Diagnostic.Kind.WARNING ), CONVERSION_LOSSY_ERROR( "Can't map %s. It has a possibly lossy conversion from %s to %s." ), @@ -114,18 +139,27 @@ public enum Message { ENUMMAPPING_NO_ELEMENTS( "'nameTransformationStrategy', 'configuration' and 'unexpectedValueMappingException' are undefined in @EnumMapping, define at least one of them." ), ENUMMAPPING_ILLEGAL_TRANSFORMATION( "Illegal transformation for '%s' EnumTransformationStrategy. Error: '%s'." ), + SUBCLASSMAPPING_DOUBLE_SOURCE_SUBCLASS( "Subclass '%s' is already defined as a source." ), + SUBCLASSMAPPING_ILLEGAL_SUBCLASS( "Class '%s' is not a subclass of '%s'." ), + SUBCLASSMAPPING_NO_VALID_SUPERCLASS( "Could not find a parameter that is a superclass for '%s'." ), + SUBCLASSMAPPING_UPDATE_METHODS_NOT_SUPPORTED( "SubclassMapping annotation can not be used for update methods." ), + SUBCLASSMAPPING_ILLOGICAL_ORDER( "SubclassMapping annotation for '%s' found after '%s', but all '%s' objects are also instances of '%s'.", Diagnostic.Kind.WARNING ), + LIFECYCLEMETHOD_AMBIGUOUS_PARAMETERS( "Lifecycle method has multiple matching parameters (e. g. same type), in this case please ensure to name the parameters in the lifecycle and mapping method identical. This lifecycle method will not be used for the mapping method '%s'.", Diagnostic.Kind.WARNING), DECORATOR_NO_SUBTYPE( "Specified decorator type is no subtype of the annotated mapper type." ), DECORATOR_CONSTRUCTOR( "Specified decorator type has no default constructor nor a constructor with a single parameter accepting the decorated mapper type." ), + JAVADOC_NO_ELEMENTS( "'value', 'authors', 'deprecated' and 'since' are undefined in @Javadoc, define at least one of them." ), + GENERAL_CANNOT_IMPLEMENT_PRIVATE_MAPPER("Cannot create an implementation for mapper %s, because it is a private %s."), GENERAL_NO_IMPLEMENTATION( "No implementation type is registered for return type %s." ), GENERAL_ABSTRACT_RETURN_TYPE( "The return type %s is an abstract class or interface. Provide a non abstract / non interface result type or a factory method." ), GENERAL_AMBIGUOUS_MAPPING_METHOD( "Ambiguous mapping methods found for mapping %s to %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ), GENERAL_AMBIGUOUS_FACTORY_METHOD( "Ambiguous factory methods found for creating %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ), GENERAL_AMBIGUOUS_PRESENCE_CHECK_METHOD( "Ambiguous presence check methods found for checking %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ), - GENERAL_AMBIGUOUS_CONSTRUCTORS( "Ambiguous constructors found for creating %s. Either declare parameterless constructor or annotate the default constructor with an annotation named @Default." ), + GENERAL_AMBIGUOUS_SOURCE_PARAMETER_CHECK_METHOD( "Ambiguous source parameter check methods found for checking %s: %s. See " + FAQ_AMBIGUOUS_URL + " for more info." ), + GENERAL_AMBIGUOUS_CONSTRUCTORS( "Ambiguous constructors found for creating %s: %s. Either declare parameterless constructor or annotate the default constructor with an annotation named @Default." ), GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS( "Incorrect @ConstructorProperties for %s. The size of the @ConstructorProperties does not match the number of constructor parameters" ), GENERAL_UNSUPPORTED_DATE_FORMAT_CHECK( "No dateFormat check is supported for types %s, %s" ), GENERAL_VALID_DATE( "Given date format \"%s\" is valid.", Diagnostic.Kind.NOTE ), @@ -148,10 +182,11 @@ public enum Message { RETRIEVAL_NO_INPUT_ARGS( "Can't generate mapping method with no input arguments." ), RETRIEVAL_DUPLICATE_MAPPING_TARGETS( "Can't generate mapping method with more than one @MappingTarget parameter." ), RETRIEVAL_VOID_MAPPING_METHOD( "Can't generate mapping method with return type void." ), - RETRIEVAL_NON_ASSIGNABLE_RESULTTYPE( "The result type is not assignable to the the return type." ), + RETRIEVAL_NON_ASSIGNABLE_RESULTTYPE( "The result type is not assignable to the return type." ), RETRIEVAL_ITERABLE_TO_NON_ITERABLE( "Can't generate mapping method from iterable type from java stdlib to non-iterable type." ), RETRIEVAL_MAPPING_HAS_TARGET_TYPE_PARAMETER( "Can't generate mapping method that has a parameter annotated with @TargetType." ), RETRIEVAL_NON_ITERABLE_TO_ITERABLE( "Can't generate mapping method from non-iterable type to iterable type from java stdlib." ), + RETRIEVAL_NON_ITERABLE_TO_ARRAY( "Can't generate mapping method from non-iterable type to array." ), RETRIEVAL_PRIMITIVE_PARAMETER( "Can't generate mapping method with primitive parameter type." ), RETRIEVAL_PRIMITIVE_RETURN( "Can't generate mapping method with primitive return type." ), RETRIEVAL_TYPE_VAR_SOURCE( "Can't generate mapping method for a generic type variable source." ), @@ -160,6 +195,10 @@ public enum Message { RETRIEVAL_WILDCARD_EXTENDS_BOUND_RESULT( "Can't generate mapping method for a wildcard extends bound result." ), RETRIEVAL_CONTEXT_PARAMS_WITH_SAME_TYPE( "The types of @Context parameters must be unique." ), RETRIEVAL_MAPPER_USES_CYCLE( "The mapper %s is referenced itself in Mapper#uses.", Diagnostic.Kind.WARNING ), + RETRIEVAL_AFTER_METHOD_NOT_IMPLEMENTED( "@AfterMapping can only be applied to an implemented method." ), + RETRIEVAL_BEFORE_METHOD_NOT_IMPLEMENTED( "@BeforeMapping can only be applied to an implemented method." ), + RETRIEVAL_SOURCE_PROPERTY_NAME_WRONG_TYPE( "@SourcePropertyName can only by applied to a String parameter." ), + RETRIEVAL_TARGET_PROPERTY_NAME_WRONG_TYPE( "@TargetPropertyName can only by applied to a String parameter." ), INHERITINVERSECONFIGURATION_DUPLICATES( "Several matching inverse methods exist: %s(). Specify a name explicitly." ), INHERITINVERSECONFIGURATION_INVALID_NAME( "None of the candidates %s() matches given name: \"%s\"." ), @@ -185,10 +224,23 @@ public enum Message { MAPTOBEANMAPPING_WRONG_KEY_TYPE( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map but it was typed with %s.", Diagnostic.Kind.WARNING ), MAPTOBEANMAPPING_RAW_MAP( "The Map parameter \"%s\" cannot be used for property mapping. It must be typed with Map but it was raw.", Diagnostic.Kind.WARNING ), + + ANNOTATE_WITH_MISSING_REQUIRED_PARAMETER( "Parameter \"%s\" is required for annotation \"%s\"." ), + ANNOTATE_WITH_UNKNOWN_PARAMETER( "Unknown parameter \"%s\" for annotation \"%s\". Did you mean \"%s\"?" ), + ANNOTATE_WITH_DUPLICATE_PARAMETER( "Parameter \"%s\" must not be defined more than once." ), + ANNOTATE_WITH_WRONG_PARAMETER( "Parameter \"%s\" is not of type \"%s\" but of type \"%s\" for annotation \"%s\"." ), + ANNOTATE_WITH_TOO_MANY_VALUE_TYPES( "Parameter \"%s\" has too many value types supplied, type \"%s\" is expected for annotation \"%s\"." ), + ANNOTATE_WITH_PARAMETER_ARRAY_NOT_EXPECTED( "Parameter \"%s\" does not accept multiple values for annotation \"%s\"." ), + ANNOTATE_WITH_NOT_ALLOWED_ON_CLASS( "Annotation \"%s\" is not allowed on classes." ), + ANNOTATE_WITH_NOT_ALLOWED_ON_METHODS( "Annotation \"%s\" is not allowed on methods." ), + ANNOTATE_WITH_ENUM_VALUE_DOES_NOT_EXIST( "Enum \"%s\" does not have value \"%s\"." ), + ANNOTATE_WITH_ENUM_CLASS_NOT_DEFINED( "enumClass needs to be defined when using enums." ), + ANNOTATE_WITH_ENUMS_NOT_DEFINED( "enums needs to be defined when using enumClass." ), + ANNOTATE_WITH_ANNOTATION_IS_NOT_REPEATABLE( "Annotation \"%s\" is not repeatable." ), + ANNOTATE_WITH_DUPLICATE( "Annotation \"%s\" is already present with the same elements configuration.", Diagnostic.Kind.WARNING ), ; // CHECKSTYLE:ON - private final String description; private final Diagnostic.Kind kind; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java b/processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java new file mode 100644 index 0000000000..96768daf56 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/MetaAnnotations.java @@ -0,0 +1,90 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; + +import org.mapstruct.tools.gem.Gem; + +/** + * A base helper class that provides utility methods for working with meta annotations. + * + * @param The type of the processed annotation + * @param The type of the underlying holder for the processed annotation + * + * @author Filip Hrisafov + */ +public abstract class MetaAnnotations { + + private static final String JAVA_LANG_ANNOTATION_PGK = "java.lang.annotation"; + + private final ElementUtils elementUtils; + private final String annotationFqn; + + protected MetaAnnotations(ElementUtils elementUtils, String annotationFqn) { + this.elementUtils = elementUtils; + this.annotationFqn = annotationFqn; + } + + /** + * Retrieves the processed annotations. + * + * @param source The source element of interest + * @return The processed annotations for the given element + */ + public Set getProcessedAnnotations(Element source) { + return getValues( source, source, new LinkedHashSet<>(), new HashSet<>() ); + } + + protected abstract G instanceOn(Element element); + + protected abstract void addInstance(G gem, Element source, Set values); + + /** + * Retrieves the processed annotations. + * + * @param source The source element of interest + * @param element Element of interest: method, or (meta) annotation + * @param values the set of values found so far + * @param handledElements The collection of already handled elements to handle recursion correctly. + * @return The processed annotations for the given element + */ + private Set getValues(Element source, Element element, Set values, Set handledElements) { + for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) { + Element annotationElement = annotationMirror.getAnnotationType().asElement(); + if ( isAnnotation( annotationElement, annotationFqn ) ) { + G gem = instanceOn( element ); + addInstance( gem, source, values ); + } + else if ( isNotJavaAnnotation( element ) && !handledElements.contains( annotationElement ) ) { + handledElements.add( annotationElement ); + getValues( source, annotationElement, values, handledElements ); + } + } + return values; + } + + private boolean isNotJavaAnnotation(Element element) { + if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) { + return !elementUtils.getPackageOf( element ).getQualifiedName().contentEquals( JAVA_LANG_ANNOTATION_PGK ); + } + return true; + } + + private boolean isAnnotation(Element element, String annotationFqn) { + if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) { + return ( (TypeElement) element ).getQualifiedName().contentEquals( annotationFqn ); + } + + return false; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/NativeTypes.java b/processor/src/main/java/org/mapstruct/ap/internal/util/NativeTypes.java index 769f7b83f0..ff01ae0ef3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/NativeTypes.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/NativeTypes.java @@ -174,7 +174,7 @@ void removeAndValidateFloatingPointLiteralSuffix() { boolean endsWithDSuffix = PTRN_DOUBLE.matcher( val ).find(); // error handling if ( isFloat && endsWithDSuffix ) { - throw new NumberFormatException( "Assiging double to a float" ); + throw new NumberFormatException( "Assigning double to a float" ); } // remove suffix if ( endsWithLSuffix || endsWithFSuffix || endsWithDSuffix ) { @@ -270,11 +270,11 @@ public void validate(String s) { @Override void parse(String val, int radix) { - Double d = Double.parseDouble( radix == 16 ? "0x" + val : val ); + double d = Double.parseDouble( radix == 16 ? "0x" + val : val ); if ( doubleHasBecomeZero( d ) ) { throw new NumberFormatException( "floating point number too small" ); } - if ( d.isInfinite() ) { + if ( Double.isInfinite( d ) ) { throw new NumberFormatException( "infinitive is not allowed" ); } } @@ -284,7 +284,7 @@ void parse(String val, int radix) { @Override public Class getLiteral() { - return float.class; + return double.class; } } @@ -297,11 +297,11 @@ public void validate(String s) { NumberRepresentation br = new NumberRepresentation( s, false, false, true ) { @Override void parse(String val, int radix) { - Float f = Float.parseFloat( radix == 16 ? "0x" + val : val ); + float f = Float.parseFloat( radix == 16 ? "0x" + val : val ); if ( doubleHasBecomeZero( f ) ) { throw new NumberFormatException( "floating point number too small" ); } - if ( f.isInfinite() ) { + if ( Float.isInfinite( f ) ) { throw new NumberFormatException( "infinitive is not allowed" ); } } @@ -363,7 +363,7 @@ else if ( new BigInteger( val, radix ).bitLength() > 64 ) { @Override public Class getLiteral() { - return int.class; + return long.class; } } @@ -474,6 +474,7 @@ private NativeTypes() { tmp3.put( Double.class.getName(), 6 ); tmp3.put( BigInteger.class.getName(), 50 ); tmp3.put( BigDecimal.class.getName(), 51 ); + tmp3.put( String.class.getName(), 51 ); NARROWING_LUT = Collections.unmodifiableMap( tmp3 ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java new file mode 100644 index 0000000000..768886cb95 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java @@ -0,0 +1,257 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util; + +import java.util.List; +import java.util.function.BooleanSupplier; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * Resolver for JSpecify nullness annotations on elements, used to decide whether a null + * check is required for a source-to-target property mapping. + *

        + * A single instance is created per annotation-processor run and carries the + * {@link #enabled} flag derived from the {@code mapstruct.disableJSpecify} option. When + * disabled, all public entry points short-circuit to {@link Nullability#UNKNOWN} / + * {@code null}, which causes downstream callers to fall back to the pre-JSpecify + * {@code NullValueCheckStrategy}-based behavior. + * + * @author Filip Hrisafov + */ +public class NullabilityResolver { + + /** + * Represents the effective nullability of a source or target element. + */ + public enum Nullability { + /** + * The element is effectively non-null — either directly annotated {@code @NonNull}, + * or within a {@code @NullMarked} scope without a closer {@code @NullUnmarked} or + * {@code @Nullable} override. + */ + NON_NULL, + /** + * The element is explicitly annotated {@code @Nullable}. + */ + NULLABLE, + /** + * Nullability is unspecified — no annotation applies and the element is not within + * an applicable {@code @NullMarked} scope (or JSpecify support is disabled). + */ + UNKNOWN + } + + private final boolean enabled; + + public NullabilityResolver(boolean enabled) { + this.enabled = enabled; + } + + /** + * Determines the nullability of an accessor element based on JSpecify annotations. + *

        + * For getter methods ({@link ExecutableElement}), this checks the return type's annotations + * since JSpecify annotations are {@code TYPE_USE} annotations placed on the return type. + * For setter parameters ({@link VariableElement}), this checks the parameter type's annotations. + * For fields ({@link VariableElement}), this checks the field type's annotations. + *

        + * If no direct annotation is found, the method walks the enclosing element chain looking + * for a method-level {@code @NullMarked} / {@code @NullUnmarked}. If nothing is found there + * either, {@code enclosingTypeNullMarked} is consulted — when it returns {@code true}, + * unannotated types are effectively {@code @NonNull}. The supplier is invoked at most once. + *

        + * When this resolver is disabled, {@link Nullability#UNKNOWN} is returned without any + * inspection — the supplier is not invoked. + * + * @param element the accessor element to inspect (getter method, setter parameter, or field); + * may be {@code null} in which case {@link Nullability#UNKNOWN} is returned + * @param enclosingTypeNullMarked supplier for whether the enclosing bean type is in a + * {@code @NullMarked} scope; must be non-{@code null} + * @return the nullability state + */ + public Nullability getNullability(Element element, BooleanSupplier enclosingTypeNullMarked) { + if ( !enabled || element == null ) { + return Nullability.UNKNOWN; + } + + // JSpecify annotations are TYPE_USE annotations. + // For getters: annotation is on the return type + // For setter parameters / fields: annotation is on the variable type + if ( element instanceof ExecutableElement ) { + // Getter method — check the return type annotations + TypeMirror returnType = ( (ExecutableElement) element ).getReturnType(); + Nullability result = getNullabilityFromTypeMirror( returnType ); + if ( result != Nullability.UNKNOWN ) { + return result; + } + } + else if ( element instanceof VariableElement ) { + // Setter parameter or field — check the variable type annotations + TypeMirror type = element.asType(); + Nullability result = getNullabilityFromTypeMirror( type ); + if ( result != Nullability.UNKNOWN ) { + return result; + } + } + + // Fallback: check declaration-level annotation mirrors. Some compilers (notably ECJ) + // surface JSpecify TYPE_USE annotations on the element rather than the type mirror. + Nullability fromElement = getNullabilityFromAnnotationMirrors( element.getAnnotationMirrors() ); + if ( fromElement != Nullability.UNKNOWN ) { + return fromElement; + } + + // Walk enclosing elements up to the declaring type to honor method-level + // @NullMarked / @NullUnmarked (e.g. a @NullUnmarked method inside a @NullMarked class + // must revert unannotated types back to unknown nullability). + Boolean elementScope = resolveElementScope( element ); + if ( elementScope != null ) { + return elementScope ? Nullability.NON_NULL : Nullability.UNKNOWN; + } + + // No element-level scope — consult the enclosing bean type's @NullMarked scope. + if ( enclosingTypeNullMarked.getAsBoolean() ) { + return Nullability.NON_NULL; + } + + return Nullability.UNKNOWN; + } + + /** + * Determines the nullability of a write-accessor element — either a setter method or a field. + *

        + * For setter methods ({@link ExecutableElement}) the nullability annotation lives on the + * parameter type, not on the method itself, so the first parameter's nullability is returned. + * For fields (or any other element type) the element's own type nullability is returned. + * + * @param element the write-accessor element (setter or field); may be + * {@code null} in which case {@link Nullability#UNKNOWN} is returned + * @param enclosingTypeNullMarked supplier for whether the enclosing bean type is in a + * {@code @NullMarked} scope; must be non-{@code null} + * @return the nullability state of the setter's parameter or of the field + */ + public Nullability getSetterNullability(Element element, BooleanSupplier enclosingTypeNullMarked) { + if ( !enabled ) { + return Nullability.UNKNOWN; + } + if ( element instanceof ExecutableElement ) { + List parameters = ( (ExecutableElement) element ).getParameters(); + if ( parameters.isEmpty() ) { + // A zero-parameter method is not a valid write accessor. Falling through to + // getNullability would inspect the return type, which is meaningless here. + return Nullability.UNKNOWN; + } + return getNullability( parameters.get( 0 ), enclosingTypeNullMarked ); + } + // Field or other write accessor: consult the element's own type nullability. + return getNullability( element, enclosingTypeNullMarked ); + } + + /** + * Determines whether a null check is required for a property mapping based on JSpecify annotations + * on the source and target elements. + *

        + * Only returns a non-null decision for the clear-cut cases: + * source {@code @NonNull} (skip check) or target {@code @NonNull} (always check). + * All other cases return {@code null} to defer to the existing {@code NullValueCheckStrategy}. + * + * @param sourceNullability the nullability of the source (getter return type / parameter) + * @param targetNullability the nullability of the target (setter parameter / field) + * @return {@code Boolean.TRUE} if a null check is needed, {@code Boolean.FALSE} if it should be skipped, + * or {@code null} if JSpecify annotations are not present and the existing strategy should be used + */ + public Boolean requiresNullCheck(Nullability sourceNullability, Nullability targetNullability) { + if ( !enabled ) { + return null; + } + if ( sourceNullability == Nullability.NON_NULL ) { + // Source is guaranteed non-null, no null check needed + return Boolean.FALSE; + } + if ( targetNullability == Nullability.NON_NULL ) { + // Target requires non-null: always check (regardless of source annotation) + return Boolean.TRUE; + } + // All other cases: defer to existing NullValueCheckStrategy + return null; + } + + /** + * Walks from {@code element} up to (but not including) its declaring {@link TypeElement}, + * checking for {@code @NullMarked} / {@code @NullUnmarked} on intermediate elements + * (e.g. the enclosing method of a parameter, or the element itself for a field / getter). + * + * @return {@code TRUE} when a closer {@code @NullMarked} is found, {@code FALSE} when a + * closer {@code @NullUnmarked} is found, or {@code null} when neither is present before + * the declaring type is reached (leaving the bean-type scope to decide). + */ + private static Boolean resolveElementScope(Element element) { + Element current = element; + while ( current != null && !isTypeElement( current ) ) { + Boolean scope = findScopeAnnotation( current ); + if ( scope != null ) { + return scope; + } + current = current.getEnclosingElement(); + } + return null; + } + + private static boolean isTypeElement(Element element) { + ElementKind kind = element.getKind(); + return kind.isClass() || kind.isInterface(); + } + + private static Boolean findScopeAnnotation(Element element) { + for ( AnnotationMirror mirror : element.getAnnotationMirrors() ) { + Element annotationElement = mirror.getAnnotationType().asElement(); + if ( !( annotationElement instanceof TypeElement ) ) { + continue; + } + String fqn = ( (TypeElement) annotationElement ).getQualifiedName().toString(); + if ( JSpecifyConstants.NULL_MARKED_FQN.equals( fqn ) ) { + return Boolean.TRUE; + } + if ( JSpecifyConstants.NULL_UNMARKED_FQN.equals( fqn ) ) { + return Boolean.FALSE; + } + } + return null; + } + + private static Nullability getNullabilityFromTypeMirror(TypeMirror typeMirror) { + if ( typeMirror == null ) { + return Nullability.UNKNOWN; + } + return getNullabilityFromAnnotationMirrors( typeMirror.getAnnotationMirrors() ); + } + + private static Nullability getNullabilityFromAnnotationMirrors( + List annotationMirrors) { + for ( AnnotationMirror mirror : annotationMirrors ) { + Element annotationElement = mirror.getAnnotationType().asElement(); + if ( !( annotationElement instanceof TypeElement ) ) { + // Defensive: during incremental builds the annotation may be an ErrorType + // whose element is not a TypeElement. Skip instead of failing with a CCE. + continue; + } + String fqn = ( (TypeElement) annotationElement ).getQualifiedName().toString(); + if ( JSpecifyConstants.NON_NULL_FQN.equals( fqn ) ) { + return Nullability.NON_NULL; + } + if ( JSpecifyConstants.NULLABLE_FQN.equals( fqn ) ) { + return Nullability.NULLABLE; + } + } + return Nullability.UNKNOWN; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/RepeatableAnnotations.java b/processor/src/main/java/org/mapstruct/ap/internal/util/RepeatableAnnotations.java new file mode 100644 index 0000000000..76126bf488 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/RepeatableAnnotations.java @@ -0,0 +1,126 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; + +import org.mapstruct.tools.gem.Gem; + +/** + * A base helper class that provides utility methods for working with repeatable annotations. + * + * @param The singular annotation type + * @param The multiple annotation type + * @param The underlying holder for the processed annotations + * + * @author Ben Zegveld + */ +public abstract class RepeatableAnnotations { + private static final String JAVA_LANG_ANNOTATION_PGK = "java.lang.annotation"; + private static final String ORG_MAPSTRUCT_PKG = "org.mapstruct"; + + private ElementUtils elementUtils; + private final String singularFqn; + private final String multipleFqn; + + protected RepeatableAnnotations(ElementUtils elementUtils, String singularFqn, String multipleFqn) { + this.elementUtils = elementUtils; + this.singularFqn = singularFqn; + this.multipleFqn = multipleFqn; + } + + /** + * @param element the element on which the Gem needs to be found + * @return the Gem found on the element. + */ + protected abstract SINGULAR singularInstanceOn(Element element); + + /** + * @param element the element on which the Gems needs to be found + * @return the Gems found on the element. + */ + protected abstract MULTIPLE multipleInstanceOn(Element element); + + /** + * @param gem the annotation gem to be processed + * @param source the source element where the request originated from + * @param mappings the collection of completed processing + */ + protected abstract void addInstance(SINGULAR gem, Element source, Set mappings); + + /** + * @param gems the annotation gems to be processed + * @param source the source element where the request originated from + * @param mappings the collection of completed processing + */ + protected abstract void addInstances(MULTIPLE gems, Element source, Set mappings); + + /** + * Retrieves the processed annotations. + * + * @param source The source element of interest + * @return The processed annotations for the given element + */ + public Set getProcessedAnnotations(Element source) { + return getMappings( source, source, new LinkedHashSet<>(), new HashSet<>() ); + } + + /** + * Retrieves the processed annotations. + * + * @param source The source element of interest + * @param element Element of interest: method, or (meta) annotation + * @param mappingOptions LinkedSet of mappings found so far + * @param handledElements The collection of already handled elements to handle recursion correctly. + * @return The processed annotations for the given element + */ + private Set getMappings(Element source, Element element, + LinkedHashSet mappingOptions, + Set handledElements) { + + for ( AnnotationMirror annotationMirror : element.getAnnotationMirrors() ) { + Element lElement = annotationMirror.getAnnotationType().asElement(); + if ( isAnnotation( lElement, singularFqn ) ) { + // although getInstanceOn does a search on annotation mirrors, the order is preserved + SINGULAR mapping = singularInstanceOn( element ); + addInstance( mapping, source, mappingOptions ); + } + else if ( isAnnotation( lElement, multipleFqn ) ) { + // although getInstanceOn does a search on annotation mirrors, the order is preserved + MULTIPLE mappings = multipleInstanceOn( element ); + addInstances( mappings, source, mappingOptions ); + } + else if ( !isAnnotationInPackage( lElement, JAVA_LANG_ANNOTATION_PGK ) + && !isAnnotationInPackage( lElement, ORG_MAPSTRUCT_PKG ) + && !handledElements.contains( lElement ) ) { + // recur over annotation mirrors + handledElements.add( lElement ); + getMappings( source, lElement, mappingOptions, handledElements ); + } + } + return mappingOptions; + } + + private boolean isAnnotationInPackage(Element element, String packageFQN) { + if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) { + return packageFQN.equals( elementUtils.getPackageOf( element ).getQualifiedName().toString() ); + } + return false; + } + + private boolean isAnnotation(Element element, String annotationFQN) { + if ( ElementKind.ANNOTATION_TYPE == element.getKind() ) { + return annotationFQN.equals( ( (TypeElement) element ).getQualifiedName().toString() ); + } + return false; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/RoundContext.java b/processor/src/main/java/org/mapstruct/ap/internal/util/RoundContext.java index 78e61fbf29..6106fdc1b0 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/RoundContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/RoundContext.java @@ -7,7 +7,6 @@ import java.util.HashSet; import java.util.Set; - import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.spi.AstModifyingAnnotationProcessor; @@ -41,7 +40,7 @@ public void addTypeReadyForProcessing(TypeMirror type) { /** * Whether the given type has been found to be ready for further processing or not. This is the case if the type's - * hierarchy is complete (no super-types need to be generated by other processors) an no processors have signaled + * hierarchy is complete (no super-types need to be generated by other processors) and no processors have signaled * the intention to amend the given type. * * @param type the typed to be checked for its readiness diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Services.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Services.java index 2d37c46db8..292512ff80 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Services.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Services.java @@ -18,6 +18,10 @@ public class Services { private Services() { } + public static Iterable all(Class serviceType) { + return ServiceLoader.load( serviceType, Services.class.getClassLoader() ); + } + public static T get(Class serviceType, T defaultValue) { Iterator services = ServiceLoader.load( serviceType, Services.class.getClassLoader() ).iterator(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java index 42727ed816..61ca397b8d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java @@ -5,14 +5,14 @@ */ package org.mapstruct.ap.internal.util; -import static org.mapstruct.ap.internal.util.Collections.asSet; - import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Locale; import java.util.Set; +import static org.mapstruct.ap.internal.util.Collections.asSet; + /** * Helper class for dealing with strings. * @@ -181,9 +181,21 @@ public static String sanitizeIdentifierName(String identifier) { firstAlphabeticIndex++; } - if ( firstAlphabeticIndex < identifier.length()) { + if ( firstAlphabeticIndex < identifier.length() ) { // If it is not consisted of only underscores - return identifier.substring( firstAlphabeticIndex ).replace( "[]", "Array" ); + String firstAlphaString = identifier.substring( firstAlphabeticIndex ).replace( "[]", "Array" ); + + StringBuilder sb = new StringBuilder( firstAlphaString.length() ); + for ( int i = 0; i < firstAlphaString.length(); i++ ) { + int codePoint = firstAlphaString.codePointAt( i ); + if ( Character.isJavaIdentifierPart( codePoint ) || codePoint == '.' ) { + sb.appendCodePoint( codePoint ); + } + else { + sb.append( '_' ); + } + } + return sb.toString(); } return identifier.replace( "[]", "Array" ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java b/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java deleted file mode 100644 index 16abd074dd..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.util; - -import org.mapstruct.ap.internal.util.accessor.Accessor; -import org.mapstruct.ap.internal.util.accessor.AccessorType; - -/** - * This a wrapper class which provides the value that needs to be used in the models. - * - * It is used to provide the read value for a difference kind of {@link Accessor}. - * - * @author Filip Hrisafov - */ -public class ValueProvider { - - private final String value; - - private ValueProvider(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - @Override - public String toString() { - return value; - } - - /** - * Creates a {@link ValueProvider} from the provided {@code accessor}. The base value is - * {@link Accessor#getSimpleName()}. If the {@code accessor} is for an executable, then {@code ()} is - * appended. - * - * @param accessor that provides the value - * - * @return a {@link ValueProvider} tha provides a read value for the {@code accessor} - */ - public static ValueProvider of(Accessor accessor) { - if ( accessor == null ) { - return null; - } - String value = accessor.getSimpleName(); - if (accessor.getAccessorType() == AccessorType.MAP_GET ) { - value = "get( \"" + value + "\" )"; - return new ValueProvider( value ); - } - if ( !accessor.getAccessorType().isFieldAssignment() ) { - value += "()"; - } - return new ValueProvider( value ); - } -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AbstractAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AbstractAccessor.java deleted file mode 100644 index 04c4c99992..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AbstractAccessor.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.util.accessor; - -import java.util.Set; - -import javax.lang.model.element.Element; -import javax.lang.model.element.Modifier; - -/** - * This is an abstract implementation of an {@link Accessor} that provides the common implementation. - * - * @author Filip Hrisafov - */ -abstract class AbstractAccessor implements Accessor { - - protected final T element; - - AbstractAccessor(T element) { - this.element = element; - } - - @Override - public String getSimpleName() { - return element.getSimpleName().toString(); - } - - @Override - public Set getModifiers() { - return element.getModifiers(); - } - - @Override - public T getElement() { - return element; - } - -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java index 4e3b7a4776..112c1c512d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java @@ -11,10 +11,7 @@ public enum AccessorType { FIELD, GETTER, SETTER, - ADDER, - MAP_GET, - MAP_CONTAINS, - PRESENCE_CHECKER; + ADDER; public boolean isFieldAssignment() { return this == FIELD || this == PARAMETER; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/DelegateAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/DelegateAccessor.java new file mode 100644 index 0000000000..5ebc835ba1 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/DelegateAccessor.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util.accessor; + +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * An {@link Accessor} which delegates all calls to another {@link Accessor}. + * + * @author Filip Hrisafov + */ +public abstract class DelegateAccessor implements Accessor { + + protected final Accessor delegate; + + protected DelegateAccessor(Accessor delegate) { + this.delegate = delegate; + } + + @Override + public TypeMirror getAccessedType() { + return delegate.getAccessedType(); + } + + @Override + public String getSimpleName() { + return delegate.getSimpleName(); + } + + @Override + public Set getModifiers() { + return delegate.getModifiers(); + } + + @Override + public Element getElement() { + return delegate.getElement(); + } + + @Override + public AccessorType getAccessorType() { + return delegate.getAccessorType(); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ElementAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ElementAccessor.java new file mode 100644 index 0000000000..4b363815b7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ElementAccessor.java @@ -0,0 +1,75 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util.accessor; + +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * An {@link Accessor} that wraps a {@link Element}. + * Used for getter, setter, filed, constructor, record-class, etc. + * @author Filip Hrisafov + * @author Tang Yang + */ +public class ElementAccessor implements Accessor { + + private final Element element; + private final String name; + private final AccessorType accessorType; + private final TypeMirror accessedType; + + public ElementAccessor(VariableElement variableElement, TypeMirror accessedType) { + this( variableElement, accessedType, AccessorType.FIELD ); + } + + public ElementAccessor(Element element, TypeMirror accessedType, String name) { + this.element = element; + this.name = name; + this.accessedType = accessedType; + this.accessorType = AccessorType.PARAMETER; + } + + public ElementAccessor(Element element, TypeMirror accessedType, AccessorType accessorType) { + this.element = element; + this.accessedType = accessedType; + this.accessorType = accessorType; + this.name = null; + } + + @Override + public TypeMirror getAccessedType() { + return accessedType != null ? accessedType : element.asType(); + } + + @Override + public String getSimpleName() { + return name != null ? name : element.getSimpleName().toString(); + } + + @Override + public Set getModifiers() { + return element.getModifiers(); + } + + @Override + public Element getElement() { + return element; + } + + @Override + public String toString() { + return element.toString(); + } + + @Override + public AccessorType getAccessorType() { + return accessorType; + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ExecutableElementAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ExecutableElementAccessor.java deleted file mode 100644 index af37acbce1..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ExecutableElementAccessor.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.util.accessor; - -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeMirror; - -/** - * An {@link Accessor} that wraps an {@link ExecutableElement}. - * - * @author Filip Hrisafov - */ -public class ExecutableElementAccessor extends AbstractAccessor { - - private final TypeMirror accessedType; - private final AccessorType accessorType; - - public ExecutableElementAccessor(ExecutableElement element, TypeMirror accessedType, AccessorType accessorType) { - super( element ); - this.accessedType = accessedType; - this.accessorType = accessorType; - } - - @Override - public TypeMirror getAccessedType() { - return accessedType; - } - - @Override - public String toString() { - return element.toString(); - } - - @Override - public AccessorType getAccessorType() { - return accessorType; - } -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/FieldElementAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/FieldElementAccessor.java deleted file mode 100644 index 5174900532..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/FieldElementAccessor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.util.accessor; - -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; - -/** - * An {@link Accessor} that wraps a {@link VariableElement}. - * - * @author Filip Hrisafov - */ -public class FieldElementAccessor extends AbstractAccessor { - - public FieldElementAccessor(VariableElement element) { - super( element ); - } - - @Override - public TypeMirror getAccessedType() { - return element.asType(); - } - - @Override - public String toString() { - return element.toString(); - } - - @Override - public AccessorType getAccessorType() { - return AccessorType.FIELD; - } - -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java index ab086d9b0e..7a708995cd 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValueAccessor.java @@ -16,7 +16,7 @@ * * @author Christian Kosmowski */ -public class MapValueAccessor implements Accessor { +public class MapValueAccessor implements ReadAccessor { private final TypeMirror valueTypeMirror; private final String simpleName; @@ -50,6 +50,11 @@ public Element getElement() { @Override public AccessorType getAccessorType() { - return AccessorType.MAP_GET; + return AccessorType.GETTER; + } + + @Override + public String getReadValueSource() { + return "get( \"" + getSimpleName() + "\" )"; } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValuePresenceChecker.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValuePresenceChecker.java deleted file mode 100644 index 3f72ed86c1..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/MapValuePresenceChecker.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.util.accessor; - -import java.util.Collections; -import java.util.Set; -import javax.lang.model.element.Element; -import javax.lang.model.element.Modifier; -import javax.lang.model.type.TypeMirror; - -/** - * An {@link Accessor} that wraps a Map value. - * - * @author Christian Kosmowski - */ -public class MapValuePresenceChecker implements Accessor { - - private final Element element; - private final TypeMirror valueTypeMirror; - private final String simpleName; - - public MapValuePresenceChecker(Element element, TypeMirror valueTypeMirror, String simpleName) { - this.element = element; - this.valueTypeMirror = valueTypeMirror; - this.simpleName = simpleName; - } - - @Override - public TypeMirror getAccessedType() { - return valueTypeMirror; - } - - @Override - public String getSimpleName() { - return this.simpleName; - } - - @Override - public Set getModifiers() { - return Collections.emptySet(); - } - - @Override - public Element getElement() { - return this.element; - } - - @Override - public AccessorType getAccessorType() { - return AccessorType.MAP_CONTAINS; - } -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ParameterElementAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ParameterElementAccessor.java deleted file mode 100644 index 9991370059..0000000000 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ParameterElementAccessor.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright MapStruct Authors. - * - * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 - */ -package org.mapstruct.ap.internal.util.accessor; - -import javax.lang.model.element.Element; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; - -/** - * An {@link Accessor} that wraps a {@link VariableElement}. - * - * @author Filip Hrisafov - */ -public class ParameterElementAccessor extends AbstractAccessor { - - protected final String name; - protected final TypeMirror accessedType; - - public ParameterElementAccessor(Element element, TypeMirror accessedType, String name) { - super( element ); - this.name = name; - this.accessedType = accessedType; - } - - @Override - public String getSimpleName() { - return name != null ? name : super.getSimpleName(); - } - - @Override - public TypeMirror getAccessedType() { - return accessedType; - } - - @Override - public String toString() { - return element.toString(); - } - - @Override - public AccessorType getAccessorType() { - return AccessorType.PARAMETER; - } - -} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/PresenceCheckAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/PresenceCheckAccessor.java new file mode 100644 index 0000000000..d96788b7f7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/PresenceCheckAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util.accessor; + +import javax.lang.model.element.ExecutableElement; + +/** + * Accessor for presence checks. + * + * @author Filip Hrisafov + */ +public interface PresenceCheckAccessor { + + String getPresenceCheckSuffix(); + + static PresenceCheckAccessor methodInvocation(ExecutableElement element) { + return suffix( "." + element.getSimpleName() + "()" ); + } + + static PresenceCheckAccessor mapContainsKey(String propertyName) { + return suffix( ".containsKey( \"" + propertyName + "\" )" ); + } + + static PresenceCheckAccessor suffix(String suffix) { + return () -> suffix; + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ReadAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ReadAccessor.java new file mode 100644 index 0000000000..4be3c26ff9 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ReadAccessor.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util.accessor; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * An {@link Accessor} that can be used for reading a property from a bean. + * + * @author Filip Hrisafov + */ +public interface ReadAccessor extends Accessor { + + String getReadValueSource(); + + static ReadAccessor fromField(VariableElement variableElement, TypeMirror accessedType) { + return new ReadDelegateAccessor( new ElementAccessor( variableElement, accessedType ) ) { + @Override + public String getReadValueSource() { + return getSimpleName(); + } + }; + } + + static ReadAccessor fromRecordComponent(Element element, TypeMirror accessedType) { + return new ReadDelegateAccessor( new ElementAccessor( element, accessedType, AccessorType.GETTER ) ) { + @Override + public String getReadValueSource() { + return getSimpleName() + "()"; + } + }; + } + + static ReadAccessor fromGetter(ExecutableElement element, TypeMirror accessedType) { + return new ReadDelegateAccessor( new ElementAccessor( element, accessedType, AccessorType.GETTER ) ) { + @Override + public String getReadValueSource() { + return getSimpleName() + "()"; + } + }; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ReadDelegateAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ReadDelegateAccessor.java new file mode 100644 index 0000000000..aa93936662 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ReadDelegateAccessor.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util.accessor; + +/** + * {@link ReadAccessor} that delegates to another {@link Accessor} and requires an implementation of + * {@link #getSimpleName()} + * + * @author Filip Hrisafov + */ +public abstract class ReadDelegateAccessor extends DelegateAccessor implements ReadAccessor { + + protected ReadDelegateAccessor(Accessor delegate) { + super( delegate ); + } + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/kotlin/KotlinMetadata.java b/processor/src/main/java/org/mapstruct/ap/internal/util/kotlin/KotlinMetadata.java new file mode 100644 index 0000000000..66a6fafc02 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/kotlin/KotlinMetadata.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.internal.util.kotlin; + +import java.util.List; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; + +/** + * Information about a type in case it's a Kotlin type. + * + * @author Filip Hrisafov + */ +public interface KotlinMetadata { + + boolean isDataClass(); + + boolean isSealedClass(); + + ExecutableElement determinePrimaryConstructor(List constructors); + + List getPermittedSubclasses(); +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java b/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java index 94e3520ad0..a30a383c0c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java @@ -21,6 +21,12 @@ public interface VersionInformation { boolean isSourceVersionAtLeast9(); + boolean isSourceVersionAtLeast11(); + + boolean isSourceVersionAtLeast14(); + + boolean isSourceVersionAtLeast19(); + boolean isEclipseJDTCompiler(); boolean isJavacCompiler(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/writer/FreeMarkerModelElementWriter.java b/processor/src/main/java/org/mapstruct/ap/internal/writer/FreeMarkerModelElementWriter.java index 6ed6fe24e6..d3991ce1da 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/writer/FreeMarkerModelElementWriter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/writer/FreeMarkerModelElementWriter.java @@ -8,8 +8,6 @@ import java.io.Writer; import java.util.Map; -import org.mapstruct.ap.internal.writer.Writable.Context; - import freemarker.ext.beans.BeanModel; import freemarker.ext.beans.BeansWrapper; import freemarker.ext.beans.SimpleMapModel; @@ -18,6 +16,7 @@ import freemarker.template.TemplateHashModel; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; +import org.mapstruct.ap.internal.writer.Writable.Context; /** * Delegate for writing given {@link Writable}s into a {@link Writer} using diff --git a/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelIncludeDirective.java b/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelIncludeDirective.java index b58fb79205..ea2448ca59 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelIncludeDirective.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelIncludeDirective.java @@ -9,8 +9,6 @@ import java.util.HashMap; import java.util.Map; -import org.mapstruct.ap.internal.writer.ModelWriter.DefaultModelElementWriterContext; - import freemarker.core.Environment; import freemarker.ext.beans.BeanModel; import freemarker.template.Configuration; @@ -18,6 +16,7 @@ import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; +import org.mapstruct.ap.internal.writer.ModelWriter.DefaultModelElementWriterContext; /** * A {@link TemplateDirectiveModel} which allows to recursively write a graph of diff --git a/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelWriter.java b/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelWriter.java index 2c3f762c0d..aeb2593e86 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelWriter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/writer/ModelWriter.java @@ -15,16 +15,14 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; - import javax.tools.FileObject; -import org.mapstruct.ap.internal.writer.Writable.Context; - import freemarker.cache.StrongCacheStorage; import freemarker.cache.TemplateLoader; import freemarker.log.Logger; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; +import org.mapstruct.ap.internal.writer.Writable.Context; /** * Writes Java source files based on given mapper models, using a FreeMarker diff --git a/processor/src/main/java/org/mapstruct/ap/spi/AccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/AccessorNamingStrategy.java index c9d0734753..b4dde64324 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/AccessorNamingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/AccessorNamingStrategy.java @@ -68,7 +68,7 @@ default void init(MapStructProcessingEnvironment processingEnvironment) { * * @return getter name for collection properties * - * @deprecated MapStuct will not call this method anymore. Use {@link #getMethodType(ExecutableElement)} to + * @deprecated MapStruct will not call this method anymore. Use {@link #getMethodType(ExecutableElement)} to * determine the {@link MethodType}. When collections somehow need to be treated special, it should be done in * {@link #getMethodType(ExecutableElement) } as well. In the future, this method will be removed. */ diff --git a/processor/src/main/java/org/mapstruct/ap/spi/AdditionalSupportedOptionsProvider.java b/processor/src/main/java/org/mapstruct/ap/spi/AdditionalSupportedOptionsProvider.java new file mode 100644 index 0000000000..3638f70320 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/AdditionalSupportedOptionsProvider.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.spi; + +import java.util.Set; + +/** + * Provider for any additional supported options required for custom SPI implementations. + * The resolved values are retrieved from {@link MapStructProcessingEnvironment#getOptions()}. + */ +public interface AdditionalSupportedOptionsProvider { + + /** + * Returns the supported options required for custom SPI implementations. + * + * @return the additional supported options. + */ + Set getAdditionalSupportedOptions(); + +} diff --git a/processor/src/main/java/org/mapstruct/ap/spi/AstModifyingAnnotationProcessor.java b/processor/src/main/java/org/mapstruct/ap/spi/AstModifyingAnnotationProcessor.java index 8ac41395bf..6b9e079523 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/AstModifyingAnnotationProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/AstModifyingAnnotationProcessor.java @@ -15,7 +15,7 @@ *

        * This contract will be queried by MapStruct when examining types referenced by mappers to be generated, most notably * the source and target types of mapping methods. If at least one AST-modifying processor announces further changes to - * such type, the generation of the affected mapper(s) will be deferred to a future round in the annnotation processing + * such type, the generation of the affected mapper(s) will be deferred to a future round in the annotation processing * cycle. *

        * Implementations are discovered via the service loader, i.e. a JAR providing an AST-modifying processor needs to diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java index 82ecb4e17b..20cb1c76c1 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java @@ -6,17 +6,20 @@ package org.mapstruct.ap.spi; import java.util.regex.Pattern; - +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; -import javax.lang.model.util.SimpleElementVisitor6; -import javax.lang.model.util.SimpleTypeVisitor6; +import javax.lang.model.util.SimpleElementVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; import javax.lang.model.util.Types; +import kotlin.Metadata; +import kotlin.metadata.Attributes; +import kotlin.metadata.jvm.KotlinClassMetadata; import org.mapstruct.ap.spi.util.IntrospectorUtils; /** @@ -28,6 +31,24 @@ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy { private static final Pattern JAVA_JAVAX_PACKAGE = Pattern.compile( "^javax?\\..*" ); + private static final boolean KOTLIN_METADATA_JVM_PRESENT; + + static { + boolean kotlinMetadataJvmPresent; + try { + Class.forName( + "kotlin.metadata.jvm.KotlinClassMetadata", + false, + AccessorNamingStrategy.class.getClassLoader() + ); + kotlinMetadataJvmPresent = true; + } + catch ( ClassNotFoundException e ) { + kotlinMetadataJvmPresent = false; + } + KOTLIN_METADATA_JVM_PRESENT = kotlinMetadataJvmPresent; + } + protected Elements elementUtils; protected Types typeUtils; @@ -102,10 +123,48 @@ public boolean isSetterMethod(ExecutableElement method) { } protected boolean isFluentSetter(ExecutableElement method) { - return method.getParameters().size() == 1 && - !JAVA_JAVAX_PACKAGE.matcher( method.getEnclosingElement().asType().toString() ).matches() && - !isAdderWithUpperCase4thCharacter( method ) && - typeUtils.isAssignable( method.getReturnType(), method.getEnclosingElement().asType() ); + if ( method.getParameters().size() != 1 ) { + return false; + } + Element methodOwnerElement = method.getEnclosingElement(); + if ( !canMethodOwnerBeUsedInFluentSetter( methodOwnerElement ) ) { + return false; + } + return !isAdderWithUpperCase4thCharacter( method ) && + typeUtils.isAssignable( method.getReturnType(), methodOwnerElement.asType() ); + } + + private boolean canMethodOwnerBeUsedInFluentSetter(Element methodOwnerElement) { + if ( !( methodOwnerElement instanceof TypeElement ) ) { + return false; + } + + TypeElement typeElement = (TypeElement) methodOwnerElement; + if ( JAVA_JAVAX_PACKAGE.matcher( typeElement.getQualifiedName().toString() ).matches() ) { + return false; + } + if ( isKotlinDataClass( typeElement ) ) { + return false; + } + + return true; + } + + private boolean isKotlinDataClass(TypeElement typeElement) { + if ( !KOTLIN_METADATA_JVM_PRESENT ) { + return false; + } + + Metadata kotlinMetadata = typeElement.getAnnotation( Metadata.class ); + if ( kotlinMetadata == null ) { + return false; + } + KotlinClassMetadata classMetadata = KotlinClassMetadata.readLenient( kotlinMetadata ); + if ( classMetadata instanceof KotlinClassMetadata.Class ) { + return Attributes.isData( ( (KotlinClassMetadata.Class) classMetadata ).getKmClass() ); + } + + return false; } /** @@ -128,7 +187,6 @@ private boolean isAdderWithUpperCase4thCharacter(ExecutableElement method) { * {@link #getElementName(ExecutableElement) }. *

        * The calling MapStruct code guarantees there's only one argument. - *

        * * @param method to be analyzed * @@ -214,7 +272,7 @@ public String getElementName(ExecutableElement adderMethod) { */ protected static String getQualifiedName(TypeMirror type) { DeclaredType declaredType = type.accept( - new SimpleTypeVisitor6() { + new SimpleTypeVisitor8() { @Override public DeclaredType visitDeclared(DeclaredType t, Void p) { return t; @@ -228,7 +286,7 @@ public DeclaredType visitDeclared(DeclaredType t, Void p) { } TypeElement typeElement = declaredType.asElement().accept( - new SimpleElementVisitor6() { + new SimpleElementVisitor8() { @Override public TypeElement visitType(TypeElement e, Void p) { return e; diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java index f89e99cd17..c1126fa9fb 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java @@ -10,16 +10,19 @@ import java.util.Collections; import java.util.List; import java.util.regex.Pattern; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; -import javax.lang.model.util.SimpleElementVisitor6; -import javax.lang.model.util.SimpleTypeVisitor6; +import javax.lang.model.util.SimpleElementVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; import javax.lang.model.util.Types; /** @@ -107,25 +110,23 @@ public BuilderInfo findBuilderInfo(TypeMirror type) { * @throws TypeHierarchyErroneousException if the {@link TypeMirror} is of kind {@link TypeKind#ERROR} */ protected TypeElement getTypeElement(TypeMirror type) { - if ( type.getKind() == TypeKind.ERROR ) { - throw new TypeHierarchyErroneousException( type ); - } - DeclaredType declaredType = type.accept( - new SimpleTypeVisitor6() { - @Override - public DeclaredType visitDeclared(DeclaredType t, Void p) { - return t; - } - }, - null - ); + DeclaredType declaredType = getDeclaredType( type ); + return getTypeElement( declaredType ); + } + /** + * Find the {@link TypeElement} for the given {@link DeclaredType}. + * + * @param declaredType for which the {@link TypeElement} needs to be found. + * @return the type element or {@code null} if the declared type element is not {@link TypeElement} + */ + private TypeElement getTypeElement(DeclaredType declaredType) { if ( declaredType == null ) { return null; } return declaredType.asElement().accept( - new SimpleElementVisitor6() { + new SimpleElementVisitor8() { @Override public TypeElement visitType(TypeElement e, Void p) { return e; @@ -135,6 +136,28 @@ public TypeElement visitType(TypeElement e, Void p) { ); } + /** + * Find the {@link DeclaredType} for the given {@link TypeMirror}. + * + * @param type for which the {@link DeclaredType} needs to be found. + * @return the declared or {@code null} if the {@link TypeMirror} is not a {@link DeclaredType} + * @throws TypeHierarchyErroneousException if the {@link TypeMirror} is of kind {@link TypeKind#ERROR} + */ + private DeclaredType getDeclaredType(TypeMirror type) { + if ( type.getKind() == TypeKind.ERROR ) { + throw new TypeHierarchyErroneousException( type ); + } + return type.accept( + new SimpleTypeVisitor8() { + @Override + public DeclaredType visitDeclared(DeclaredType t, Void p) { + return t; + } + }, + null + ); + } + /** * Find the {@link BuilderInfo} for the given {@code typeElement}. *

        @@ -155,34 +178,105 @@ public TypeElement visitType(TypeElement e, Void p) { * @throws MoreThanOneBuilderCreationMethodException if there are multiple builder creation methods */ protected BuilderInfo findBuilderInfo(TypeElement typeElement) { + return findBuilderInfo( typeElement, true ); + } + + protected BuilderInfo findBuilderInfo(TypeElement typeElement, boolean checkParent) { if ( shouldIgnore( typeElement ) ) { return null; } - List methods = ElementFilter.methodsIn( typeElement.getEnclosedElements() ); - List builderInfo = new ArrayList<>(); - for ( ExecutableElement method : methods ) { - if ( isPossibleBuilderCreationMethod( method, typeElement ) ) { - TypeElement builderElement = getTypeElement( method.getReturnType() ); - Collection buildMethods = findBuildMethods( builderElement, typeElement ); - if ( !buildMethods.isEmpty() ) { - builderInfo.add( new BuilderInfo.Builder() - .builderCreationMethod( method ) - .buildMethod( buildMethods ) - .build() - ); + // Builder infos which are determined by a static method on the type itself + List methodBuilderInfos = new ArrayList<>(); + // Builder infos which are determined by an inner builder class in the type itself + List innerClassBuilderInfos = new ArrayList<>(); + + for ( Element enclosedElement : typeElement.getEnclosedElements() ) { + if ( ElementKind.METHOD == enclosedElement.getKind() ) { + ExecutableElement method = (ExecutableElement) enclosedElement; + BuilderInfo builderInfo = determineMethodBuilderInfo( method, typeElement ); + if ( builderInfo != null ) { + methodBuilderInfos.add( builderInfo ); } } + else if ( ElementKind.CLASS == enclosedElement.getKind() ) { + if ( !methodBuilderInfos.isEmpty() ) { + // Small optimization to not check the inner classes + // if we already have at least one builder through a method + continue; + } + TypeElement classElement = (TypeElement) enclosedElement; + BuilderInfo builderInfo = determineInnerClassBuilderInfo( classElement, typeElement ); + if ( builderInfo != null ) { + innerClassBuilderInfos.add( builderInfo ); + } + } + } - if ( builderInfo.size() == 1 ) { - return builderInfo.get( 0 ); + if ( methodBuilderInfos.size() == 1 ) { + return methodBuilderInfos.get( 0 ); + } + else if ( methodBuilderInfos.size() > 1 ) { + throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), methodBuilderInfos ); + } + else if ( innerClassBuilderInfos.size() == 1 ) { + return innerClassBuilderInfos.get( 0 ); } - else if ( builderInfo.size() > 1 ) { - throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), builderInfo ); + else if ( innerClassBuilderInfos.size() > 1 ) { + throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), innerClassBuilderInfos ); } - return findBuilderInfo( typeElement.getSuperclass() ); + if ( checkParent ) { + return findBuilderInfo( typeElement.getSuperclass() ); + } + + return null; + } + + private BuilderInfo determineMethodBuilderInfo(ExecutableElement method, + TypeElement typeElement) { + if ( isPossibleBuilderCreationMethod( method, typeElement ) ) { + TypeElement builderElement = getTypeElement( method.getReturnType() ); + Collection buildMethods = findBuildMethods( builderElement, typeElement ); + if ( !buildMethods.isEmpty() ) { + return new BuilderInfo.Builder() + .builderCreationMethod( method ) + .buildMethod( buildMethods ) + .build(); + } + } + + return null; + } + + private BuilderInfo determineInnerClassBuilderInfo(TypeElement innerClassElement, + TypeElement typeElement) { + if ( innerClassElement.getModifiers().contains( Modifier.PUBLIC ) + && innerClassElement.getModifiers().contains( Modifier.STATIC ) + && innerClassElement.getSimpleName().toString().endsWith( "Builder" ) ) { + for ( Element element : innerClassElement.getEnclosedElements() ) { + if ( ElementKind.CONSTRUCTOR == element.getKind() ) { + ExecutableElement constructor = (ExecutableElement) element; + if ( constructor.getParameters().isEmpty() ) { + // We have a no-arg constructor + // Now check if we have build methods + Collection buildMethods = findBuildMethods( innerClassElement, typeElement ); + if ( !buildMethods.isEmpty() ) { + return new BuilderInfo.Builder() + .builderCreationMethod( constructor ) + .buildMethod( buildMethods ) + .build(); + } + // If we don't have any build methods + // then we can stop since we are only interested in the no-arg constructor + return null; + } + } + } + } + + return null; } /** @@ -218,21 +312,32 @@ protected boolean isPossibleBuilderCreationMethod(ExecutableElement method, Type * Searches for a build method for {@code typeElement} within the {@code builderElement}. *

        * The default implementation iterates over each method in {@code builderElement} and uses - * {@link DefaultBuilderProvider#isBuildMethod(ExecutableElement, TypeElement)} to check if the method is a - * build method for {@code typeElement}. + * {@link DefaultBuilderProvider#isBuildMethod(ExecutableElement, DeclaredType, TypeElement)} + * to check if the method is a build method for {@code typeElement}. *

        * The default implementation uses {@link DefaultBuilderProvider#shouldIgnore(TypeElement)} to check if the * {@code builderElement} should be ignored, i.e. not checked for build elements. - *

        - * If there are multiple methods that satisfy - * {@link DefaultBuilderProvider#isBuildMethod(ExecutableElement, TypeElement)} and one of those methods - * is names {@code build} that that method would be considered as a build method. + * * @param builderElement the element for the builder * @param typeElement the element for the type that is being built * @return the build method for the {@code typeElement} if it exists, or {@code null} if it does not * {@code build} */ protected Collection findBuildMethods(TypeElement builderElement, TypeElement typeElement) { + if ( shouldIgnore( builderElement ) || typeElement == null ) { + return Collections.emptyList(); + } + DeclaredType builderType = getDeclaredType( builderElement.asType() ); + + if ( builderType == null ) { + return Collections.emptyList(); + } + + return findBuildMethods( builderElement, builderType, typeElement ); + } + + private Collection findBuildMethods(TypeElement builderElement, DeclaredType builderType, + TypeElement typeElement) { if ( shouldIgnore( builderElement ) ) { return Collections.emptyList(); } @@ -240,23 +345,57 @@ protected Collection findBuildMethods(TypeElement builderElem List builderMethods = ElementFilter.methodsIn( builderElement.getEnclosedElements() ); List buildMethods = new ArrayList<>(); for ( ExecutableElement buildMethod : builderMethods ) { - if ( isBuildMethod( buildMethod, typeElement ) ) { + if ( isBuildMethod( buildMethod, builderType, typeElement ) ) { buildMethods.add( buildMethod ); } } - if ( buildMethods.isEmpty() ) { - return findBuildMethods( - getTypeElement( builderElement.getSuperclass() ), + if ( !buildMethods.isEmpty() ) { + return buildMethods; + } + + Collection parentClassBuildMethods = findBuildMethods( + getTypeElement( builderElement.getSuperclass() ), + builderType, + typeElement + ); + + if ( !parentClassBuildMethods.isEmpty() ) { + return parentClassBuildMethods; + } + + List interfaces = builderElement.getInterfaces(); + if ( interfaces.isEmpty() ) { + return Collections.emptyList(); + } + + Collection interfaceBuildMethods = new ArrayList<>(); + + for ( TypeMirror builderInterface : interfaces ) { + interfaceBuildMethods.addAll( findBuildMethods( + getTypeElement( builderInterface ), + builderType, typeElement - ); + ) ); } - return buildMethods; + return interfaceBuildMethods; } /** - * Checks if the {@code buildMethod} is a method that creates {@code typeElement}. + * @see #isBuildMethod(ExecutableElement, DeclaredType, TypeElement) + * @deprecated use {@link #isBuildMethod(ExecutableElement, DeclaredType, TypeElement)} instead + */ + @Deprecated + protected boolean isBuildMethod(ExecutableElement buildMethod, TypeElement typeElement) { + return buildMethod.getParameters().isEmpty() && + buildMethod.getModifiers().contains( Modifier.PUBLIC ) + && typeUtils.isAssignable( buildMethod.getReturnType(), typeElement.asType() ); + } + + /** + * Checks if the {@code buildMethod} is a method that creates the {@code typeElement} + * as a member of the {@code builderType}. *

        * The default implementation considers a method to be a build method if the following is satisfied: *

          @@ -266,14 +405,23 @@ protected Collection findBuildMethods(TypeElement builderElem *
        * * @param buildMethod the method that should be checked + * @param builderType the type of the builder in which the {@code buildMethod} is located in * @param typeElement the type element that needs to be built * @return {@code true} if the {@code buildMethod} is a build method for {@code typeElement}, {@code false} * otherwise */ - protected boolean isBuildMethod(ExecutableElement buildMethod, TypeElement typeElement) { - return buildMethod.getParameters().isEmpty() && - buildMethod.getModifiers().contains( Modifier.PUBLIC ) - && typeUtils.isAssignable( buildMethod.getReturnType(), typeElement.asType() ); + protected boolean isBuildMethod(ExecutableElement buildMethod, DeclaredType builderType, TypeElement typeElement) { + if ( !buildMethod.getParameters().isEmpty() ) { + return false; + } + if ( !buildMethod.getModifiers().contains( Modifier.PUBLIC ) ) { + return false; + } + TypeMirror buildMethodType = typeUtils.asMemberOf( builderType, buildMethod ); + if ( buildMethodType instanceof ExecutableType ) { + return typeUtils.isAssignable( ( (ExecutableType) buildMethodType ).getReturnType(), typeElement.asType() ); + } + return false; } /** diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumMappingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumMappingStrategy.java index 62d6c32809..11e1257be1 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumMappingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultEnumMappingStrategy.java @@ -10,6 +10,8 @@ import javax.lang.model.util.Types; /** + * The default implementation of the {@link EnumMappingStrategy} service provider interface. + * * @author Filip Hrisafov * * @since 1.4 diff --git a/processor/src/main/java/org/mapstruct/ap/spi/EnumMappingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/EnumMappingStrategy.java index 97088cd8c6..8ad6737c63 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/EnumMappingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/EnumMappingStrategy.java @@ -17,7 +17,7 @@ * * @since 1.4 */ -@Experimental("This SPI can have it's signature changed in subsequent releases") +@Experimental("This SPI can have its signature changed in subsequent releases") public interface EnumMappingStrategy { /** diff --git a/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java index 2c498fc3a8..796bdb4954 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/EnumTransformationStrategy.java @@ -13,7 +13,7 @@ * @author Filip Hrisafov * @since 1.4 */ -@Experimental("This SPI can have it's signature changed in subsequent releases") +@Experimental("This SPI can have its signature changed in subsequent releases") public interface EnumTransformationStrategy { /** diff --git a/processor/src/main/java/org/mapstruct/ap/spi/FreeBuilderAccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/FreeBuilderAccessorNamingStrategy.java index 92fef3a1be..dc14e20de6 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/FreeBuilderAccessorNamingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/FreeBuilderAccessorNamingStrategy.java @@ -20,8 +20,8 @@ *
      • {@code mergeFrom(Target.Builder)}
      • *
      *

      - * When the JavaBean convention is not used with FreeBuilder then the getters are non standard and MapStruct - * won't recognize them. Therefore one needs to use the JavaBean convention in which the fluent setters + * When the JavaBean convention is not used with FreeBuilder then the getters are non-standard and MapStruct + * won't recognize them. Therefore, one needs to use the JavaBean convention in which the fluent setters * start with {@code set}. * * @author Filip Hrisafov diff --git a/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java index 69c66cdd28..1df6ebc1b6 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java @@ -10,9 +10,9 @@ import org.mapstruct.util.Experimental; /** - * Accesor naming strategy for Immutables. + * Accessor naming strategy for Immutables. * The generated Immutables also have a from that works as a copy. Our default strategy considers this method - * as a setter with a name {@code from}. Therefore we are ignoring it. + * as a setter with a name {@code from}. Therefore, we are ignoring it. * * @author Filip Hrisafov */ @@ -21,7 +21,18 @@ public class ImmutablesAccessorNamingStrategy extends DefaultAccessorNamingStrat @Override protected boolean isFluentSetter(ExecutableElement method) { - return super.isFluentSetter( method ) && !method.getSimpleName().toString().equals( "from" ); + return super.isFluentSetter( method ) && + !method.getSimpleName().toString().equals( "from" ) && + !isPutterWithUpperCase4thCharacter( method ); + } + + private boolean isPutterWithUpperCase4thCharacter(ExecutableElement method) { + return isPutterMethod( method ) && Character.isUpperCase( method.getSimpleName().toString().charAt( 3 ) ); + } + + public boolean isPutterMethod(ExecutableElement method) { + String methodName = method.getSimpleName().toString(); + return methodName.startsWith( "put" ) && methodName.length() > 3; } } diff --git a/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesBuilderProvider.java b/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesBuilderProvider.java index d4ccd029ef..3431808418 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesBuilderProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesBuilderProvider.java @@ -35,18 +35,31 @@ protected BuilderInfo findBuilderInfo(TypeElement typeElement) { if ( name.length() == 0 || JAVA_JAVAX_PACKAGE.matcher( name ).matches() ) { return null; } + + // First look if there is a builder defined in my own type + BuilderInfo info = findBuilderInfo( typeElement, false ); + if ( info != null ) { + return info; + } + + // Check for a builder in the generated immutable type + BuilderInfo immutableInfo = findBuilderInfoForImmutables( typeElement ); + if ( immutableInfo != null ) { + return immutableInfo; + } + + return super.findBuilderInfo( typeElement.getSuperclass() ); + } + + protected BuilderInfo findBuilderInfoForImmutables(TypeElement typeElement) { TypeElement immutableAnnotation = elementUtils.getTypeElement( IMMUTABLE_FQN ); if ( immutableAnnotation != null ) { - BuilderInfo info = findBuilderInfoForImmutables( + return findBuilderInfoForImmutables( typeElement, immutableAnnotation ); - if ( info != null ) { - return info; - } } - - return super.findBuilderInfo( typeElement ); + return null; } protected BuilderInfo findBuilderInfoForImmutables(TypeElement typeElement, @@ -55,7 +68,7 @@ protected BuilderInfo findBuilderInfoForImmutables(TypeElement typeElement, if ( typeUtils.isSameType( annotationMirror.getAnnotationType(), immutableAnnotation.asType() ) ) { TypeElement immutableElement = asImmutableElement( typeElement ); if ( immutableElement != null ) { - return super.findBuilderInfo( immutableElement ); + return super.findBuilderInfo( immutableElement, false ); } else { // Immutables processor has not run yet. Trigger a postpone to the next round for MapStruct diff --git a/processor/src/main/java/org/mapstruct/ap/spi/MapStructProcessingEnvironment.java b/processor/src/main/java/org/mapstruct/ap/spi/MapStructProcessingEnvironment.java index 0471108a51..18086e8591 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/MapStructProcessingEnvironment.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/MapStructProcessingEnvironment.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.spi; +import java.util.Map; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -36,4 +37,12 @@ public interface MapStructProcessingEnvironment { */ Types getTypeUtils(); + /** + * Returns the resolved options specified by the impl of + * {@link AdditionalSupportedOptionsProvider}. + * + * @return resolved options + */ + Map getOptions(); + } diff --git a/processor/src/main/java/org/mapstruct/ap/spi/MappingExclusionProvider.java b/processor/src/main/java/org/mapstruct/ap/spi/MappingExclusionProvider.java index afd7e69511..2a1cacfb1f 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/MappingExclusionProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/MappingExclusionProvider.java @@ -12,10 +12,9 @@ /** * A service provider interface that is used to control if MapStruct is allowed to generate automatic sub-mapping for * a given {@link TypeElement}. - * + *

      * When generating the implementation of a mapping method, MapStruct will apply the following routine for each * attribute pair in the source and target object: - * *

        *
      • If source and target attribute have the same type, the value will be simply copied from source to target. * If the attribute is a collection (e.g. a `List`) a copy of the collection will be set into the target @@ -30,14 +29,14 @@ *
      • If MapStruct could not create a name based mapping method an error will be raised at build time, * indicating the non-mappable attribute and its path.
      • *
      - * + *

      * With this SPI the last step before raising an error can be controlled. i.e. A user can control whether MapStruct * is allowed to generate such automatic sub-mapping method (for the source or target type) or not. * * @author Filip Hrisafov * @since 1.2 */ -@Experimental("This SPI can have it's signature changed in subsequent releases") +@Experimental("This SPI can have its signature changed in subsequent releases") public interface MappingExclusionProvider { /** @@ -46,7 +45,6 @@ public interface MappingExclusionProvider { * The given {@code typeElement} will be excluded from the automatic sub-mapping generation * * @param typeElement that needs to be checked - * * @return {@code true} if MapStruct should exclude the provided {@link TypeElement} from an automatic sub-mapping */ boolean isExcluded(TypeElement typeElement); diff --git a/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java index abb6e9cb4f..226251dfab 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/PrefixEnumTransformationStrategy.java @@ -6,6 +6,8 @@ package org.mapstruct.ap.spi; /** + * An {@link EnumTransformationStrategy} that prepends a prefix to the enum value. + * * @author Filip Hrisafov * * @since 1.4 diff --git a/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java index d3f0c515b3..dec7af6de4 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/StripPrefixEnumTransformationStrategy.java @@ -6,6 +6,8 @@ package org.mapstruct.ap.spi; /** + * An {@link EnumTransformationStrategy} that strips a prefix from the enum value. + * * @author Filip Hrisafov * * @since 1.4 diff --git a/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java index cf239600d4..3b76354fa7 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/StripSuffixEnumTransformationStrategy.java @@ -6,6 +6,8 @@ package org.mapstruct.ap.spi; /** + * An {@link EnumTransformationStrategy} that strips a suffix from the enum value. + * * @author Filip Hrisafov * * @since 1.4 diff --git a/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java index 4cd92b9af8..1f4eb89217 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/SuffixEnumTransformationStrategy.java @@ -6,6 +6,8 @@ package org.mapstruct.ap.spi; /** + * An {@link EnumTransformationStrategy} that appends a suffix to the enum value. + * * @author Filip Hrisafov * * @since 1.4 diff --git a/processor/src/main/java/org/mapstruct/ap/spi/util/IntrospectorUtils.java b/processor/src/main/java/org/mapstruct/ap/spi/util/IntrospectorUtils.java index fccc22b38b..95dcd7a92d 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/util/IntrospectorUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/util/IntrospectorUtils.java @@ -32,7 +32,7 @@ private IntrospectorUtils() { * @return The decapitalized version of the string. */ public static String decapitalize(String name) { - if ( name == null || name.length() == 0 ) { + if ( name == null || name.isEmpty() ) { return name; } if ( name.length() > 1 && Character.isUpperCase( name.charAt( 1 ) ) && diff --git a/processor/src/main/java/org/mapstruct/ap/spi/util/package-info.java b/processor/src/main/java/org/mapstruct/ap/spi/util/package-info.java index 7b8f374cf7..93bccf51d0 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/util/package-info.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/util/package-info.java @@ -4,4 +4,7 @@ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ +/** + * Utility classes for the SPI package. + */ package org.mapstruct.ap.spi.util; diff --git a/processor/src/main/resources/META-INF/services/org.mapstruct.ap.internal.processor.ModelElementProcessor b/processor/src/main/resources/META-INF/services/org.mapstruct.ap.internal.processor.ModelElementProcessor index 7b27e54faf..d5234fca5b 100644 --- a/processor/src/main/resources/META-INF/services/org.mapstruct.ap.internal.processor.ModelElementProcessor +++ b/processor/src/main/resources/META-INF/services/org.mapstruct.ap.internal.processor.ModelElementProcessor @@ -3,7 +3,9 @@ # Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 org.mapstruct.ap.internal.processor.CdiComponentProcessor +org.mapstruct.ap.internal.processor.JakartaCdiComponentProcessor org.mapstruct.ap.internal.processor.Jsr330ComponentProcessor +org.mapstruct.ap.internal.processor.JakartaComponentProcessor org.mapstruct.ap.internal.processor.MapperCreationProcessor org.mapstruct.ap.internal.processor.MapperRenderingProcessor org.mapstruct.ap.internal.processor.MethodRetrievalProcessor diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.ftl index ce0f605f11..6753a73d6f 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/conversion/CreateDecimalFormat.ftl @@ -5,9 +5,10 @@ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 --> -private DecimalFormat ${name}( String numberFormat ) { +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.SupportingMappingMethod" --> +private DecimalFormat ${name}( <#list parameters as param><@includeModel object=param/><#if param_has_next>, ) { - DecimalFormat df = new DecimalFormat( numberFormat ); + DecimalFormat df = new DecimalFormat( numberFormat<#if parameters.size() > 1>, DecimalFormatSymbols.getInstance( locale ) ); df.setParseBigDecimal( true ); return df; -} \ No newline at end of file +} diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/AnnotatedSetter.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/AnnotatedSetter.ftl new file mode 100644 index 0000000000..74d4241fa5 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/AnnotatedSetter.ftl @@ -0,0 +1,14 @@ + <#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> + <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.AnnotatedSetter" --> + <#list methodAnnotations as annotation> + <#nt><@includeModel object=annotation/> + +public void set${fieldName?cap_first}(<#list parameterAnnotations as annotation><#nt><@includeModel object=annotation/> <@includeModel object=type/> ${fieldName}) { + this.${fieldName} = ${fieldName}; +} diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/Annotation.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/Annotation.ftl index 4391bcb975..0b6737fbe4 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/Annotation.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/Annotation.ftl @@ -6,4 +6,15 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.Annotation" --> -@<@includeModel object=type/><#if (properties?size > 0) >(<#list properties as property>${property}<#if property_has_next>, ) \ No newline at end of file +<#switch properties?size> + <#on 0> + @<@includeModel object=type/><#rt> + <#on 1> + @<@includeModel object=type/>(<@includeModel object=properties[0]/>)<#rt> + <#default> + @<@includeModel object=type/>( + <#list properties as property> + <#nt><@includeModel object=property/><#if property_has_next>, + + )<#rt> + \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl index f5d6e799e1..fd101f0d95 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl @@ -6,7 +6,9 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.BeanMappingMethod" --> -<#if overridden>@Override +<#list annotations as annotation> + <#nt><@includeModel object=annotation/> + <#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#assign targetType = resultType /> <#if !existingInstanceMapping> @@ -18,28 +20,58 @@ - <#if !mapNullToDefault> - if ( <#list sourceParametersExcludingPrimitives as sourceParam>${sourceParam.name} == null<#if sourceParam_has_next> && ) { - return<#if returnType.name != "void"> null; + <#list beforeMappingReferencesWithFinalizedReturnType as callback> + <@includeModel object=callback targetBeanName=finalizedResultName targetType=finalizedReturnType/> + <#if !callback_has_next> + + + + <#if !mapNullToDefault && !sourcePresenceChecks.empty> + if ( <#list sourcePresenceChecks as sourcePresenceCheck><@includeModel object=sourcePresenceCheck.negate() /><#if sourcePresenceCheck_has_next> && ) { + <#if returnType.name == "void"> + return; + <#else> + <#if existingInstanceMapping> + <@createReturn applyOptionalAfterMapping=false>${resultName}<#if finalizerMethod??>.<@includeModel object=finalizerMethod /> + <#else> + return ${returnType.null}; + + } + <#if hasSubclassMappings()> + <#list subclassMappings as subclass> + <#if subclass_index > 0>else if (${subclass.sourceArgument} instanceof <@includeModel object=subclass.sourceType/>) { + <@includeModel object=subclass.assignment existingInstanceMapping=existingInstanceMapping/> + } + + else { + + <#if isAbstractReturnType()> + throw new <@includeModel object=subclassExhaustiveException />("Not all subclasses are supported for this mapping. Missing for " + ${subclassMappings[0].sourceArgument}.getClass()); + <#else> <#if !existingInstanceMapping> <#if hasConstructorMappings()> <#if (sourceParameters?size > 1)> - <#list sourceParametersNeedingNullCheck as sourceParam> + <#list sourceParametersNeedingPresenceCheck as sourceParam> <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; - if ( ${sourceParam.name} != null ) { + if ( <@includeModel object=getPresenceCheckByParameter(sourceParam) /> ) { + <#assign sourceParamReassignment = getSourceParameterReassignment(sourceParam)!'' /> + <#if sourceParamReassignment?has_content> + <@includeModel object=sourceParamReassignment.type /> ${sourceParamReassignment.name} = ${sourceParam.name}.get(); + + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> } - <#list sourceParametersNotNeedingNullCheck as sourceParam> + <#list sourceParametersNotNeedingPresenceCheck as sourceParam> <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; @@ -51,7 +83,12 @@ <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; - <#if mapNullToDefault>if ( ${sourceParameters[0].name} != null ) { + <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) { + <#assign sourceParamReassignment = getSourceParameterReassignment(sourceParameters[0])!'' /> + <#if sourceParamReassignment?has_content> + <@includeModel object=sourceParamReassignment.type /> ${sourceParamReassignment.name} = ${sourceParameters[0].name}.get(); + + <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> @@ -69,7 +106,7 @@ <@includeModel object=returnTypeToConstruct/> ${resultName} = <@includeModel object=factoryMethod targetType=returnTypeToConstruct/>; <#else > - <@includeModel object=returnTypeToConstruct/> ${resultName} = <#if factoryMethod??><@includeModel object=factoryMethod targetType=returnTypeToConstruct/><#else>new <@includeModel object=returnTypeToConstruct/>(); + <@includeModel object=returnTypeToConstruct/> ${resultName} = <#if factoryMethod??><@includeModel object=factoryMethod targetType=returnTypeToConstruct/><#else><@includeModel object=newInstance/>(); @@ -80,24 +117,34 @@ <#if (sourceParameters?size > 1)> - <#list sourceParametersNeedingNullCheck as sourceParam> + <#list sourceParametersNeedingPresenceCheck as sourceParam> <#if (propertyMappingsByParameter(sourceParam)?size > 0)> - if ( ${sourceParam.name} != null ) { + if ( <@includeModel object=getPresenceCheckByParameter(sourceParam) /> ) { + <#assign sourceParamReassignment = getSourceParameterReassignment(sourceParam)!'' /> + <#if sourceParamReassignment?has_content> + <@includeModel object=sourceParamReassignment.type /> ${sourceParamReassignment.name} = ${sourceParam.name}.get(); + + <#list propertyMappingsByParameter(sourceParam) as propertyMapping> <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> } - <#list sourceParametersNotNeedingNullCheck as sourceParam> + <#list sourceParametersNotNeedingPresenceCheck as sourceParam> <#if (propertyMappingsByParameter(sourceParam)?size > 0)> <#list propertyMappingsByParameter(sourceParam) as propertyMapping> <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - <#else> - <#if mapNullToDefault>if ( ${sourceParameters[0].name} != null ) { + <#elseif !propertyMappingsByParameter(sourceParameters[0]).empty> + <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) { + <#assign sourceParamReassignment = getSourceParameterReassignment(sourceParameters[0])!'' /> + <#if sourceParamReassignment?has_content> + <@includeModel object=sourceParamReassignment.type /> ${sourceParamReassignment.name} = ${sourceParameters[0].name}.get(); + + <#list propertyMappingsByParameter(sourceParameters[0]) as propertyMapping> <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> @@ -114,11 +161,28 @@ <#if returnType.name != "void"> - <#if finalizerMethod??> - return ${resultName}.<@includeModel object=finalizerMethod />; - <#else> - return ${resultName}; + <#if finalizerMethod??> + <#if (afterMappingReferencesWithFinalizedReturnType?size > 0)> + <@includeModel object=finalizedReturnType /> ${finalizedResultName} = ${resultName}.<@includeModel object=finalizerMethod />; + + <#list afterMappingReferencesWithFinalizedReturnType as callback> + <#if callback_index = 0> + + + <@includeModel object=callback targetBeanName=finalizedResultName targetType=finalizedReturnType/> + + + <@createReturn>${finalizedResultName} + <#else> + <@createReturn>${resultName}.<@includeModel object=finalizerMethod /> + + <#else> + <@createReturn>${resultName} + + + <#if hasSubclassMappings()> + } } <#macro throws> @@ -128,4 +192,27 @@ <#if exceptionType_has_next>, <#t> + +<#macro createReturn applyOptionalAfterMapping=true> +<#-- <@compress single_line=true>--> + <#if returnType.optionalType> + <#if (afterMappingReferencesWithOptionalReturnType?size > 0)> + <@includeModel object=returnType /> ${optionalResultName} = <@includeModel object=returnType.asRawType()/>.of( <#nested/> ); + + <#list afterMappingReferencesWithOptionalReturnType as callback> + <#if callback_index = 0> + + + <@includeModel object=callback targetBeanName=optionalResultName targetType=returnType/> + + + return ${optionalResultName}; + <#else> + return <@includeModel object=returnType.asRawType()/>.of( <#nested/> ); + + <#else> + return <#nested/>; + +<#-- --> + \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/FromOptionalTypeConversion.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/FromOptionalTypeConversion.ftl new file mode 100644 index 0000000000..50ab97e5b4 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/FromOptionalTypeConversion.ftl @@ -0,0 +1,16 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.FromOptionalTypeConversion" --> +<@includeModel object=assignment + targetBeanName=ext.targetBeanName + existingInstanceMapping=ext.existingInstanceMapping + targetReadAccessorName=ext.targetReadAccessorName + targetWriteAccessorName=ext.targetWriteAccessorName + sourcePropertyName=ext.sourcePropertyName + targetPropertyName=ext.targetPropertyName + targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/GeneratedType.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/GeneratedType.ftl index 4981882403..c65b3e5f85 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/GeneratedType.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/GeneratedType.ftl @@ -14,6 +14,7 @@ package ${packageName}; import ${importedType}; +<#if javadoc??><#nt><@includeModel object=javadoc/> <#if !generatedTypeAvailable>/* @Generated( value = "org.mapstruct.ap.MappingProcessor"<#if suppressGeneratorTimestamp == false>, diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableCreation.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableCreation.ftl index d49397a985..aff9641d60 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableCreation.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableCreation.ftl @@ -11,13 +11,15 @@ <@includeModel object=factoryMethod targetType=resultType/> <#elseif enumSet> EnumSet.noneOf( <@includeModel object=enumSetElementType raw=true/>.class ) - <#else> - new - <#if resultType.implementationType??> - <@includeModel object=resultType.implementationType/><#if ext.useSizeIfPossible?? && ext.useSizeIfPossible && canUseSize>( <@sizeForCreation /> )<#else>() + <#elseif resultType.implementation??> + <#if resultType.implementation.factoryMethodName?? && ext.useSizeIfPossible?? && ext.useSizeIfPossible && canUseSize> + <@includeModel object=resultType.implementationType raw=true />.${resultType.implementation.factoryMethodName}( <@sizeForCreation /> ) <#else> - <@includeModel object=resultType/>() + <@includeModel object=newInstance/><#if ext.useSizeIfPossible?? && ext.useSizeIfPossible && canUseSize>( <@sizeForCreation /> )<#else>() + <#else> + <@includeModel object=newInstance/>() + <#macro sizeForCreation> <@compress single_line=true> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl index 3af75079b9..6268366f7b 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl @@ -6,7 +6,10 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.IterableMappingMethod" --> -<#if overridden>@Override +<#import "macro/CommonMacros.ftl" as lib> +<#list annotations as annotation> + <#nt><@includeModel object=annotation/> + <#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> @@ -14,10 +17,10 @@ - if ( ${sourceParameter.name} == null ) { + <#if sourceParameterPresenceCheck??> + if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) { <#if !mapNullToDefault> - <#-- returned target type starts to miss-align here with target handed via param, TODO is this right? --> - return<#if returnType.name != "void"> null; + return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#else>null; <#else> <#if resultType.arrayType> <#if existingInstanceMapping> @@ -27,7 +30,7 @@ } return<#if returnType.name != "void"> ${resultName}; <#else> - return new <@includeModel object=resultElementType/>[0]; + return <@lib.constructArrayType targetType=resultType targetSize=0/>; <#else> <#if existingInstanceMapping> @@ -39,11 +42,11 @@ } + <#if resultType.arrayType> <#if !existingInstanceMapping> - <#assign elementTypeString><@includeModel object=resultElementType/> - ${elementTypeString}[] ${resultName} = new ${elementTypeString?keep_before('[]')}[<@iterableSize/>]${elementTypeString?replace('[^\\[\\]]+', '', 'r')}; + <@includeModel object=resultElementType/>[] ${resultName} = <@lib.constructArrayType targetType=resultType targetSize=iterableSize()/>; <#else> <#if existingInstanceMapping> @@ -63,7 +66,7 @@ int ${index1Name} = 0; for ( <@includeModel object=sourceElementType/> ${loopVariableName} : ${sourceParameter.name} ) { <#if existingInstanceMapping> - if ( ( ${index1Name} >= ${resultName}.length ) || ( ${index1Name} >= <@iterableSize/> ) ) { + if ( ( ${index1Name} >= ${resultName}.length ) || ( ${index1Name} >= ${iterableSize()} ) ) { break; } @@ -94,15 +97,13 @@ -<#macro iterableSize> - <@compress single_line=true> - <#if sourceParameter.type.arrayType> - ${sourceParameter.name}.length - <#else> - ${sourceParameter.name}.size() - - - +<#function iterableSize> + <#if sourceParameter.type.arrayType> + <#return sourceParameter.name + ".length"> + <#else> + <#return sourceParameter.name + ".size()"> + + <#macro iterableLocalVarDef> <@compress single_line=true> <#if resultType.fullyQualifiedName == "java.lang.Iterable"> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/Javadoc.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/Javadoc.ftl new file mode 100644 index 0000000000..89ac57b9ef --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/Javadoc.ftl @@ -0,0 +1,25 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.Javadoc" --> +/** +<#list value?split("\n") as line><#nt>*<#if line?has_content> ${line?trim} + +<#if !authors.isEmpty()> +* +<#list authors as author> <#nt>* @author ${author?trim} + + +<#if deprecated?has_content> +* +<#nt>* @deprecated ${deprecated?trim} + +<#if since?has_content> +* +<#nt>* @since ${since?trim} + +<#nt> */ \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl index 1c6e6bad35..e04d1b9475 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl @@ -6,7 +6,9 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.MapMappingMethod" --> -<#if overridden>@Override +<#list annotations as annotation> + <#nt><@includeModel object=annotation/> + <#lt>${accessibility.keyword} <@includeModel object=returnType /> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> @@ -14,9 +16,10 @@ - if ( ${sourceParameter.name} == null ) { + <#if sourceParameterPresenceCheck??> + if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) { <#if !mapNullToDefault> - return<#if returnType.name != "void"> null; + return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#else>null; <#else> <#if existingInstanceMapping> ${resultName}.clear(); @@ -26,6 +29,7 @@ } + <#if existingInstanceMapping> ${resultName}.clear(); diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl index d0ee7bef3b..63f983df46 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl @@ -15,9 +15,11 @@ <#if static><@includeModel object=providingParameter.type/><#else>${providingParameter.name}.<@methodCall/> <#-- method is referenced java8 static method in the mapper to implement (interface) --> <#elseif static> - <@includeModel object=definingType/>.<@methodCall/> + <@includeModel object=definingType raw=true/>.<@methodCall/> <#elseif constructor> new <@includeModel object=definingType/><#if (parameterBindings?size > 0)>( <@arguments/> )<#else>() + <#elseif methodChaining> + <#list methodsToChain as methodToChain><@includeModel object=methodToChain /><#if methodToChain_has_next>. <#else> <@methodCall/> @@ -39,9 +41,13 @@ <#-- a class is passed on for casting, see @TargetType --> <@includeModel object=inferTypeWhenEnum( ext.targetType ) raw=true/>.class<#t> <#elseif param.mappingTarget> - ${ext.targetBeanName}<#if ext.targetReadAccessorName??>.${ext.targetReadAccessorName}<#t> + <#if ext.targetBeanName??>${ext.targetBeanName}<#else>${param.variableName}<#if ext.targetReadAccessorName??>.${ext.targetReadAccessorName}<#t> <#elseif param.mappingContext> ${param.variableName}<#t> + <#elseif param.sourcePropertyName> + "${ext.sourcePropertyName}"<#t> + <#elseif param.targetPropertyName> + "${ext.targetPropertyName}"<#t> <#elseif param.sourceRHS??> <@_assignment assignmentToUse=param.sourceRHS/><#t> <#elseif assignment??> @@ -56,13 +62,16 @@ <#-- macro: assignment - purpose: note: takes its targetyType from the singleSourceParameterType + purpose: note: takes its targetType from the singleSourceParameterType --> <#macro _assignment assignmentToUse> <@includeModel object=assignmentToUse + presenceCheck=ext.presenceCheck targetBeanName=ext.targetBeanName existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + sourcePropertyName=ext.sourcePropertyName + targetPropertyName=ext.targetPropertyName targetType=singleSourceParameterType/> - \ No newline at end of file + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl index 44ec296736..68b05d84ed 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReferencePresenceCheck.ftl @@ -6,4 +6,8 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.MethodReferencePresenceCheck" --> -<@includeModel object=methodReference/> \ No newline at end of file +<#if isNegate()>!<@includeModel object=methodReference + presenceCheck=true + sourcePropertyName=ext.sourcePropertyName + targetPropertyName=ext.targetPropertyName + targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl index ce6dab0714..04d385a9dc 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl @@ -7,25 +7,16 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.NestedPropertyMappingMethod" --> <#lt>private <@includeModel object=returnType.typeBound/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { - if ( ${sourceParameter.name} == null ) { - return ${returnType.null}; - } <#list propertyEntries as entry> <#if entry.presenceChecker?? > - if ( <#if entry_index != 0>${entry.previousPropertyName} == null || !<@includeModel object=entry.presenceChecker /> ) { - return ${returnType.null}; - } - - <@includeModel object=entry.type.typeBound/> ${entry.name} = ${entry.previousPropertyName}.${entry.accessorName}; - <#if !entry.presenceChecker?? > - <#if !entry.type.primitive> - if ( ${entry.name} == null ) { + if ( <@includeModel object=entry.presenceChecker /> ) { return ${returnType.null}; } - - <#if !entry_has_next> - return ${entry.name}; + <#if entry_has_next> + <@includeModel object=entry.type.typeBound/> ${entry.name} = ${entry.source}; + <#else> + return ${entry.source}; } diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl index 20e6f1734c..f45659cb5d 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/PropertyMapping.ftl @@ -11,5 +11,7 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=targetReadAccessorName targetWriteAccessorName=targetWriteAccessorName + sourcePropertyName=sourcePropertyName + targetPropertyName=name targetType=targetType defaultValueAssignment=defaultValueAssignment /> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl index f9a7309b5d..0f335f9c54 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl @@ -6,7 +6,10 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.StreamMappingMethod" --> -<#if overridden>@Override +<#import "macro/CommonMacros.ftl" as lib> +<#list annotations as annotation> + <#nt><@includeModel object=annotation/> + <#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#--TODO does it even make sense to do a callback if the result is a Stream, as they are immutable--> <#list beforeMappingReferencesWithoutMappingTarget as callback> @@ -15,10 +18,10 @@ - if ( ${sourceParameter.name} == null ) { + <#if sourceParameterPresenceCheck??> + if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) { <#if !mapNullToDefault> - <#-- returned target type starts to miss-align here with target handed via param, TODO is this right? --> - return<#if returnType.name != "void"> null; + return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#else>null; <#else> <#if resultType.arrayType> <#if existingInstanceMapping> @@ -28,7 +31,7 @@ } return<#if returnType.name != "void"> ${resultName}; <#else> - return new <@includeModel object=resultElementType/>[0]; + return <@lib.constructArrayType targetType=resultType targetSize=0/>; <#elseif resultType.iterableType> <#if existingInstanceMapping> @@ -47,6 +50,7 @@ } + <#-- A variable needs to be defined if there are before or after mappings and this is not exisitingInstanceMapping --> <#assign needVarDefine = (beforeMappingReferencesWithMappingTarget?has_content || afterMappingReferences?has_content) && !existingInstanceMapping /> @@ -55,7 +59,7 @@ <#if needVarDefine> <#assign needVarDefine = false /> <#-- We create a null array which later will be directly assigned from the stream--> - ${resultElementType}[] ${resultName} = null; + <@includeModel object=resultElementType/>[] ${resultName} = null; <#elseif resultType.iterableType> <#if existingInstanceMapping> @@ -87,7 +91,7 @@ <#if resultType.arrayType> <#if existingInstanceMapping> int ${index1Name} = 0; - for ( <@includeModel object=resultElementType/> ${loopVariableName} : ${sourceParameter.name}.limit( ${resultName}.length )<@streamMapSupplier />.toArray( ${resultElementType}[]::new ) ) { + for ( <@includeModel object=resultElementType/> ${loopVariableName} : ${sourceParameter.name}.limit( ${resultName}.length )<@streamMapSupplier />.toArray( <@includeModel object=resultElementType raw=true/>[]::new ) ) { if ( ( ${index1Name} >= ${resultName}.length ) ) { break; } @@ -95,7 +99,7 @@ } <#else> <#if canReturnImmediatelly><#if returnType.name != "void">return <#else> <#if needVarDefine>${resultElementType}[] <#else>${resultName} = ${sourceParameter.name}<@streamMapSupplier /> - .toArray( <@includeModel object=resultElementType/>[]::new ); + .toArray( <@includeModel object=resultElementType raw=true/>[]::new ); <#elseif resultType.iterableType> <#if existingInstanceMapping || !canReturnImmediatelly> @@ -112,7 +116,7 @@ <#else> <#-- Streams are immutable so we can't update them --> <#if !existingInstanceMapping> - <#--TODO fhr: after the the result is no longer the same instance, how does it affect the + <#--TODO fhr: after the result is no longer the same instance, how does it affect the Before mapping methods. Does it even make sense to have before mapping on a stream? --> <#if sourceParameter.type.arrayType> <@returnLocalVarDefOrUpdate>Stream.of( ${sourceParameter.name} )<@streamMapSupplier />; diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/ToOptionalTypeConversion.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/ToOptionalTypeConversion.ftl new file mode 100644 index 0000000000..48e20e7017 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/ToOptionalTypeConversion.ftl @@ -0,0 +1,21 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.ToOptionalTypeConversion" --> +<@compress single_line=true> +<@includeModel object=targetType.asRawType() />.of( <@_assignment/> ) +<#macro _assignment> + <@includeModel object=assignment + targetBeanName=ext.targetBeanName + existingInstanceMapping=ext.existingInstanceMapping + targetReadAccessorName=ext.targetReadAccessorName + targetWriteAccessorName=ext.targetWriteAccessorName + sourcePropertyName=ext.sourcePropertyName + targetPropertyName=ext.targetPropertyName + targetType=ext.targetType/> + + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl index 370fe7c978..a5e5798d1c 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/TypeConversion.ftl @@ -14,6 +14,8 @@ ${openExpression}<@_assignment/>${closeExpression} existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + sourcePropertyName=ext.sourcePropertyName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl index 4d961c40af..224b6c3dfa 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl @@ -6,8 +6,11 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.ValueMappingMethod" --> +<#list annotations as annotation> + <#nt><@includeModel object=annotation/> + <#if overridden>@Override -<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, ) { +<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> <#if !callback_has_next> @@ -18,15 +21,35 @@ <#if nullTarget.targetAsException>throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>return <@writeTarget target=nullTarget.target/>; } - <@includeModel object=resultType/> ${resultName}; + <#if versionInformation.isSourceVersionAtLeast14()> + <#if valueMappings.empty> + <#if defaultTarget.targetAsException> + <@includeModel object=resultType/> ${resultName}; + throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} ); + <#else> + <@includeModel object=resultType/> ${resultName} = <@writeTarget target=defaultTarget.target/>; + + <#else> + <@includeModel object=resultType/> ${resultName} = switch ( ${sourceParameter.name} ) { + <#list valueMappings as valueMapping> + case <@writeSource source=valueMapping.source/> -> <#if valueMapping.targetAsException > throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else><@writeTarget target=valueMapping.target/>; + + <#if isDefaultTargetRequired()> + default -> <#if defaultTarget.targetAsException>throw new <@includeModel object=unexpectedValueMappingException/>( "Unexpected enum constant: " + ${sourceParameter.name} )<#else><@writeTarget target=defaultTarget.target/>; + + }; + + <#else> + <@includeModel object=resultType/> ${resultName}; - switch ( ${sourceParameter.name} ) { - <#list valueMappings as valueMapping> - case <@writeSource source=valueMapping.source/>: <#if valueMapping.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>${resultName} = <@writeTarget target=valueMapping.target/>; - break; - - default: <#if defaultTarget.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} )<#else>${resultName} = <@writeTarget target=defaultTarget.target/>; - } + switch ( ${sourceParameter.name} ) { + <#list valueMappings as valueMapping> + case <@writeSource source=valueMapping.source/>: <#if valueMapping.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>${resultName} = <@writeTarget target=valueMapping.target/>; + break; + + default: <#if defaultTarget.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} )<#else>${resultName} = <@writeTarget target=defaultTarget.target/>; + } + <#list beforeMappingReferencesWithMappingTarget as callback> <#if callback_index = 0> @@ -45,24 +68,28 @@ } <#macro writeSource source=""> - <@compress single_line=true> - <#if sourceParameter.type.enumType> - ${source} - <#elseif sourceParameter.type.string> - "${source}" - - + <#if sourceParameter.type.enumType> + ${source}<#t> + <#elseif sourceParameter.type.string> + "${source}"<#t> + <#macro writeTarget target=""> - <@compress single_line=true> - <#if target?has_content> - <#if returnType.enumType> - <@includeModel object=returnType/>.${target} - <#elseif returnType.string> - "${target}" - - <#else> - null + <#if target?has_content> + <#if returnType.enumType> + <@includeModel object=returnType/>.${target}<#t> + <#elseif returnType.string> + "${target}"<#t> + <#else> + null<#t> + + +<#macro throws> + <#if (thrownTypes?size > 0)><#lt> throws <@compress single_line=true> + <#list thrownTypes as exceptionType> + <@includeModel object=exceptionType/> + <#if exceptionType_has_next>, <#t> + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/annotation/AnnotationElement.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/annotation/AnnotationElement.ftl new file mode 100644 index 0000000000..21c1977f58 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/annotation/AnnotationElement.ftl @@ -0,0 +1,48 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.annotation.AnnotationElement" --> +<@compress single_line=true> + <#if elementName??> + ${elementName} = + + <#if (values?size > 1) > + { + + <#-- rt and lt tags below are for formatting the arrays so that there are no spaces before ',' --> + <#list values as value> + <#if boolean> + ${value?c}<#rt> + <#elseif byte> + ${value}<#rt> + <#elseif character> + '${value}'<#rt> + <#elseif class> + <@includeModel object=value raw=true/>.class<#rt> + <#elseif double> + ${value?c}<#rt> + <#elseif enum> + <@includeModel object=value/><#rt> + <#elseif float> + ${value?c}f<#rt> + <#elseif integer> + ${value?c}<#rt> + <#elseif long> + ${value?c}L<#rt> + <#elseif short> + ${value?c}<#rt> + <#elseif string> + "${value}"<#rt> + + <#if value_has_next> + , <#lt> + + + <#if (values?size > 1) > + } + + \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/annotation/EnumAnnotationElementHolder.ftl similarity index 67% rename from processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.ftl rename to processor/src/main/resources/org/mapstruct/ap/internal/model/annotation/EnumAnnotationElementHolder.ftl index 5cffc481a1..145b933156 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceContainsKeyPresenceCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/annotation/EnumAnnotationElementHolder.ftl @@ -5,5 +5,5 @@ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 --> -<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.SourceReferenceContainsKeyPresenceCheck" --> -${sourceReference}.containsKey( "${propertyName}" ) \ No newline at end of file +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.annotation.EnumAnnotationElementHolder" --> +<@includeModel object=enumClass raw=true/>.${name}<#rt> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.ftl index 7364f7922e..449a273614 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/EnumConstantWrapper.ftl @@ -6,4 +6,4 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.EnumConstantWrapper" --> -${ext.targetType.name}.${assignment} \ No newline at end of file +${ext.targetType.createReferenceName()}.${assignment} \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.ftl index cee722e2d3..02852eec88 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ExistingInstanceSetterWrapperForCollectionsAndMaps.ftl @@ -15,8 +15,12 @@ ${ext.targetBeanName}.${ext.targetReadAccessorName}.<#if ext.targetType.collectionType>addAll<#else>putAll( <@lib.handleWithAssignmentOrNullCheckVar/> ); <#if !ext.defaultValueAssignment?? && !sourcePresenceCheckerReference?? && includeElseBranch>else {<#-- the opposite (defaultValueAssignment) case is handeld inside lib.handleLocalVarNullCheck --> - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if mapNullToDefault><@lib.initTargetObject/><#else>null; - } + <#if mapNullToClear> + ${ext.targetBeanName}.${ext.targetReadAccessorName}.clear(); + <#else > + ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if mapNullToDefault><@lib.initTargetObject/><#else>null; + + } } else { @@ -30,6 +34,10 @@ <@lib.handleLocalVarNullCheck needs_explicit_local_var=directAssignment> ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if directAssignment><@wrapLocalVarInCollectionInitializer/><#else><@lib.handleWithAssignmentOrNullCheckVar/>; + <#if !ext.defaultValueAssignment?? && !sourcePresenceCheckerReference?? && mapNullToDefault>else { + ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.initTargetObject/>; + } + <#-- wraps the local variable in a collection initializer (new collection, or EnumSet.copyOf) @@ -38,6 +46,6 @@ <#if enumSet> EnumSet.copyOf( ${nullCheckLocalVarName} ) <#else> - new <#if ext.targetType.implementationType??><@includeModel object=ext.targetType.implementationType/><#else><@includeModel object=ext.targetType/>( ${nullCheckLocalVarName} ) + <@includeModel object=newInstance/>( ${nullCheckLocalVarName} ) \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.ftl index 4ef55f3b40..f47a37106e 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/GetterWrapperForCollectionsAndMaps.ftl @@ -10,10 +10,13 @@ <@lib.sourceLocalVarAssignment/> if ( ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWriteAccesing /> != null ) { <@lib.handleExceptions> - <#if ext.existingInstanceMapping> + <#if ext.existingInstanceMapping && !ignoreMapNull> ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWriteAccesing />.clear(); <@lib.handleLocalVarNullCheck needs_explicit_local_var=false> + <#if ext.existingInstanceMapping && ignoreMapNull> + ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWriteAccesing />.clear(); + ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWriteAccesing />.<#if ext.targetType.collectionType>addAll<#else>putAll( <@lib.handleWithAssignmentOrNullCheckVar/> ); diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl index 2795dac7bc..4c3b6d4804 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/Java8FunctionWrapper.ftl @@ -6,6 +6,7 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.Java8FunctionWrapper" --> +<#import "../macro/CommonMacros.ftl" as lib> <#assign sourceVarName><#if assignment.sourceLocalVarName?? >${assignment.sourceLocalVarName}<#else>${assignment.sourceReference} <#if (thrownTypes?size == 0) > <#compress> @@ -17,14 +18,9 @@ <#else> <#compress> ${sourceVarName} -> { - try { + <@lib.handleExceptions> return <@_assignment/>; - } - <#list thrownTypes as exceptionType> - catch ( <@includeModel object=exceptionType/> e ) { - throw new RuntimeException( e ); - } - + } @@ -34,5 +30,6 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl index 53e987e476..163a52896a 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl @@ -6,18 +6,14 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.LocalVarWrapper" --> +<#import "../macro/CommonMacros.ftl" as lib> <#if (thrownTypes?size == 0) > <#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName} = <@_assignment/>; <#else> <#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName}; - try { + <@lib.handleExceptions> ${ext.targetWriteAccessorName} = <@_assignment/>; - } - <#list thrownTypes as exceptionType> - catch ( <@includeModel object=exceptionType/> e ) { - throw new RuntimeException( e ); - } - + <#macro _assignment> <@includeModel object=assignment @@ -25,5 +21,6 @@ existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/NewInstanceSetterWrapperForCollectionsAndMaps.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/NewInstanceSetterWrapperForCollectionsAndMaps.ftl new file mode 100644 index 0000000000..7b9d746767 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/NewInstanceSetterWrapperForCollectionsAndMaps.ftl @@ -0,0 +1,23 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.SetterWrapperForCollectionsAndMapsWithNullCheck" --> +<#import "../macro/CommonMacros.ftl" as lib> +<@lib.sourceLocalVarAssignment/> +<@lib.handleExceptions> + <@callTargetWriteAccessor/> + +<#-- + assigns the target via the regular target write accessor (usually the setter) +--> +<#macro callTargetWriteAccessor> + <@lib.handleLocalVarNullCheck needs_explicit_local_var=directAssignment> + <#if ext.targetType.implementationType??><@includeModel object=ext.targetType.implementationType/><#else><@includeModel object=ext.targetType/> ${instanceVar} = <@includeModel object=newInstance/>(); + ${instanceVar}.<#if ext.targetType.collectionType>addAll<#else>putAll( ${nullCheckLocalVarName} ); + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite>${instanceVar}; + + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/OptionalGetWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/OptionalGetWrapper.ftl new file mode 100644 index 0000000000..7bbc089d7c --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/OptionalGetWrapper.ftl @@ -0,0 +1,15 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.OptionalGetWrapper" --> +<@compress single_line=true> +<#if optionalType.optionalBaseType.isPrimitive()> +${assignment}.getAs${optionalType.optionalBaseType.name?cap_first}() +<#else> +${assignment}.get() + + \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl new file mode 100644 index 0000000000..9917dbacf8 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ReturnWrapper.ftl @@ -0,0 +1,18 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.ReturnWrapper" --> +return <@_assignment/>; +<#macro _assignment> + <@includeModel object=assignment + targetBeanName=ext.targetBeanName + existingInstanceMapping=ext.existingInstanceMapping + targetReadAccessorName=ext.targetReadAccessorName + targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName + targetType=ext.targetType/> + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl index 3d6172b49d..7fad115bc4 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl @@ -26,6 +26,6 @@ <#if enumSet> EnumSet.copyOf( ${nullCheckLocalVarName} ) <#else> - new <#if ext.targetType.implementationType??><@includeModel object=ext.targetType.implementationType/><#else><@includeModel object=ext.targetType/>( ${nullCheckLocalVarName} ) + <@includeModel object=newInstance/>( ${nullCheckLocalVarName} ) \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.ftl index 58416fc63b..933612f579 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/UpdateWrapper.ftl @@ -6,17 +6,19 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.UpdateWrapper" --> +<#-- @ftlvariable name="ext" type="java.util.Map" --> +<#-- @ftlvariable name="ext.targetType" type="org.mapstruct.ap.internal.model.common.Type" --> <#import '../macro/CommonMacros.ftl' as lib > <@lib.handleExceptions> <#if includeSourceNullCheck> <@lib.sourceLocalVarAssignment/> - if ( <#if sourcePresenceCheckerReference?? >${sourcePresenceCheckerReference}<#else><#if sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference} != null ) { + if ( <@handleSourceReferenceNullCheck/> ) { <@assignToExistingTarget/> <@lib.handleAssignment/>; } <#if setExplicitlyToDefault || setExplicitlyToNull> else { - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if setExplicitlyToDefault><@lib.initTargetObject/><#else>null; + ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if setExplicitlyToDefault><@lib.initTargetObject/><#else><#if mustCastForNull>(<@includeModel object=ext.targetType/>) ${ext.targetType.null}; } <#else> @@ -32,3 +34,16 @@ ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.initTargetObject/>; } + +<#macro handleSourceReferenceNullCheck> + <@compress single_line=true> + <#if sourcePresenceCheckerReference?? > + <@includeModel object=sourcePresenceCheckerReference + targetPropertyName=ext.targetPropertyName + sourcePropertyName=ext.sourcePropertyName + targetType=ext.targetType/> + <#else> + <#if sourceLocalVarName??> ${sourceLocalVarName} <#else> ${sourceReference} != null + + + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceMethodPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/NegatePresenceCheck.ftl similarity index 72% rename from processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceMethodPresenceCheck.ftl rename to processor/src/main/resources/org/mapstruct/ap/internal/model/common/NegatePresenceCheck.ftl index 930d5e24bc..6951952487 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SourceReferenceMethodPresenceCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/NegatePresenceCheck.ftl @@ -5,5 +5,5 @@ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 --> -<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.SourceReferenceMethodPresenceCheck" --> -${sourceReference}.${methodName}() \ No newline at end of file +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.common.NegatePresenceCheck" --> +!( <@includeModel object=presenceCheck /> ) \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/NewInstanceCreation.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/NewInstanceCreation.ftl new file mode 100644 index 0000000000..9b78977245 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/NewInstanceCreation.ftl @@ -0,0 +1,11 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.common.NewInstanceCreation" --> +<@compress single_line=true> +new <@includeModel object=rawType/><#if generic><> + diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/SourceRHS.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/SourceRHS.ftl index 0b60bd39ae..aaf1eb8df4 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/SourceRHS.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/SourceRHS.ftl @@ -6,4 +6,4 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.common.SourceRHS" --> -<#if sourceLoopVarName??>${sourceLoopVarName}<#elseif sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference} \ No newline at end of file +<#if sourceLoopVarName?? && !ext.presenceCheck??>${sourceLoopVarName}<#elseif sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference} \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Type.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Type.ftl index ee7d0b106f..c039dda504 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Type.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Type.ftl @@ -12,6 +12,7 @@ <#elseif hasSuperBound()> ? super <@includeModel object=typeBound /> <#else> - <#if ext.asVarArgs!false>${createReferenceName()?remove_ending("[]")}...<#else>${createReferenceName()}<#if (!ext.raw?? && typeParameters?size > 0) ><<#list typeParameters as typeParameter><@includeModel object=typeParameter /><#if typeParameter_has_next>, > + <#assign createReferenceName = createReferenceName() /> + ${createReferenceName?keep_before("[]")}<#if (!ext.raw?? && typeParameters?size > 0) ><<#list typeParameters as typeParameter><@includeModel object=typeParameter /><#if typeParameter_has_next>, >${createReferenceName?keep_after("[]")}<#if ext.asVarArgs!false>...<#elseif isArrayType()>[] \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl index 048402401b..0599e8c0e4 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl @@ -13,14 +13,19 @@ requires: caller to implement boolean:getIncludeSourceNullCheck() --> +<#-- @ftlvariable name="ext" type="java.util.Map" --> +<#-- @ftlvariable name="ext.targetType" type="org.mapstruct.ap.internal.model.common.Type" --> <#macro handleSourceReferenceNullCheck> <#if sourcePresenceCheckerReference??> - if ( <@includeModel object=sourcePresenceCheckerReference /> ) { + if ( <@includeModel object=sourcePresenceCheckerReference + targetPropertyName=ext.targetPropertyName + sourcePropertyName=ext.sourcePropertyName + targetType=ext.targetType/> ) { <#nested> } <@elseDefaultAssignment/> <#elseif includeSourceNullCheck || ext.defaultValueAssignment??> - if ( <#if sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference} != null ) { + if ( <#if sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference}<#if sourceType.optionalType>.isPresent()<#else> != null ) { <#nested> } <@elseDefaultAssignment/> @@ -40,7 +45,7 @@ } <#elseif setExplicitlyToDefault || setExplicitlyToNull> else { - <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if setExplicitlyToDefault><@lib.initTargetObject/><#else>null; + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if setExplicitlyToDefault><@lib.initTargetObject/><#else><#if mustCastForNull!false>(<@includeModel object=ext.targetType/>) ${ext.targetType.null}; } @@ -57,7 +62,10 @@ --> <#macro handleLocalVarNullCheck needs_explicit_local_var> <#if sourcePresenceCheckerReference??> - if ( <@includeModel object=sourcePresenceCheckerReference /> ) { + if ( <@includeModel object=sourcePresenceCheckerReference + targetType=ext.targetType + sourcePropertyName=ext.sourcePropertyName + targetPropertyName=ext.targetPropertyName /> ) { <#if needs_explicit_local_var> <@includeModel object=nullCheckLocalVarType/> ${nullCheckLocalVarName} = <@lib.handleAssignment/>; <#nested> @@ -96,11 +104,16 @@ try { <#nested> } - <#list thrownTypes as exceptionType> - catch ( <@includeModel object=exceptionType/> e ) { + <@compress single_line=true>catch ( + <#list thrownTypes as exceptionType> + <#if exceptionType_index > 0> | + <@includeModel object=exceptionType/> + + e ) { + + throw new RuntimeException( e ); } - <#-- @@ -112,6 +125,7 @@ Performs a standard assignment. existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> <#-- @@ -123,6 +137,7 @@ Performs a default assignment with a default value. existingInstanceMapping=ext.existingInstanceMapping targetReadAccessorName=ext.targetReadAccessorName targetWriteAccessorName=ext.targetWriteAccessorName + targetPropertyName=ext.targetPropertyName targetType=ext.targetType defaultValue=ext.defaultValue/> @@ -148,23 +163,34 @@ Performs a default assignment with a default value. <#if factoryMethod??> <@includeModel object=factoryMethod targetType=ext.targetType/> <#else> - <@constructTargetObject/> + <@constructTargetObject targetType=ext.targetType/> +<#-- +--> +<#macro constructArrayType targetType targetSize><@compress single_line=true> + <#assign targetTypeString><@includeModel object=targetType raw=true/> + new ${targetTypeString?keep_before("[")}[${targetSize}]${targetTypeString?keep_after("]")} + <#-- macro: constructTargetObject purpose: Either call the constructor of the target object directly or of the implementing type. + Emits the diamond operator for generic types so type-arguments are inferred from the + assignment context. --> -<#macro constructTargetObject><@compress single_line=true> - <#if ext.targetType.implementationType??> - new <@includeModel object=ext.targetType.implementationType/>() - <#elseif ext.targetType.arrayType> - new <@includeModel object=ext.targetType.componentType/>[0] - <#elseif ext.targetType.sensibleDefault??> - ${ext.targetType.sensibleDefault} +<#-- @ftlvariable name="targetType" type="org.mapstruct.ap.internal.model.common.Type" --> +<#macro constructTargetObject targetType><@compress single_line=true> + <#if targetType.implementationType??> + new <@includeModel object=targetType.implementationType raw=true/><#if targetType.implementationType.typeParameters?size != 0><>() + <#elseif targetType.arrayType> + <@constructArrayType targetType=targetType targetSize=0/> + <#elseif targetType.sensibleDefault??> + ${targetType.sensibleDefault} + <#elseif targetType.optionalType> + <@includeModel object=targetType.asRawType()/>.of( <@constructTargetObject targetType=targetType.optionalBaseType/> ) <#else> - new <@includeModel object=ext.targetType/>() + new <@includeModel object=targetType raw=true/><#if targetType.typeParameters?size != 0><>() <#-- @@ -173,6 +199,7 @@ Performs a default assignment with a default value. purpose: assignment for source local variables. The sourceLocalVarName replaces the sourceReference in the assignmentcall. --> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.common.Assignment" --> <#macro sourceLocalVarAssignment> <#if sourceLocalVarName??> <@includeModel object=sourceType/> ${sourceLocalVarName} = ${sourceReference}; diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/AnyPresenceChecksPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/AnyPresenceChecksPresenceCheck.ftl new file mode 100644 index 0000000000..3df10348ad --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/AnyPresenceChecksPresenceCheck.ftl @@ -0,0 +1,16 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.AnyPresenceChecksPresenceCheck" --> +<@compress single_line=true> +<#list presenceChecks as presenceCheck> + <#if presenceCheck_index != 0> + || + + <@includeModel object=presenceCheck /> + + \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.ftl index 6e5494c735..ebd9f12eca 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/NullPresenceCheck.ftl @@ -6,4 +6,4 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.NullPresenceCheck" --> -${sourceReference} != null \ No newline at end of file +${sourceReference} <#if isNegate()>==<#else>!= null \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.ftl new file mode 100644 index 0000000000..c321ce20ec --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.ftl @@ -0,0 +1,19 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.OptionalPresenceCheck" --> +<@compress single_line=true> + <#if isNegate()> + <#if versionInformation.isSourceVersionAtLeast11()> + ${sourceReference}.isEmpty() + <#else> + !${sourceReference}.isPresent() + + <#else> + ${sourceReference}.isPresent() + + \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SuffixPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SuffixPresenceCheck.ftl new file mode 100644 index 0000000000..8eddad3485 --- /dev/null +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/SuffixPresenceCheck.ftl @@ -0,0 +1,9 @@ +<#-- + + Copyright MapStruct Authors. + + Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + +--> +<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.SuffixPresenceCheck" --> +<#if isNegate()>!${sourceReference}${suffix} \ No newline at end of file diff --git a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java index 646f898063..ba890f0e87 100755 --- a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java +++ b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.internal.model.common; -import static org.assertj.core.api.Assertions.assertThat; - import java.lang.annotation.Annotation; import java.time.LocalDate; import java.time.LocalDateTime; @@ -14,7 +12,6 @@ import java.time.ZonedDateTime; import java.util.HashMap; import java.util.List; - import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -24,6 +21,8 @@ import org.mapstruct.ap.internal.util.JodaTimeConstants; import org.mapstruct.ap.testutil.IssueKey; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link org.mapstruct.ap.internal.model.common.DateFormatValidatorFactory}. * diff --git a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java index ce2f895e0f..3d12de6e0b 100755 --- a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java +++ b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java @@ -5,13 +5,10 @@ */ package org.mapstruct.ap.internal.model.common; -import static org.assertj.core.api.Assertions.assertThat; - import java.lang.annotation.Annotation; import java.time.ZonedDateTime; import java.util.HashMap; import java.util.List; - import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -25,6 +22,8 @@ import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.testutil.IssueKey; +import static org.assertj.core.api.Assertions.assertThat; + /** * Testing DefaultConversionContext for dateFormat * @@ -70,7 +69,7 @@ public void testInvalidDateFormatValidation() { statefulMessagerMock, type, type, - new FormattingParameters( "qwertz", null, null, null, null ) + new FormattingParameters( "qwertz", null, null, null, null, null ) ); assertThat( statefulMessagerMock.getLastKindPrinted() ).isEqualTo( Diagnostic.Kind.ERROR ); } @@ -84,7 +83,7 @@ public void testNullDateFormatValidation() { statefulMessagerMock, type, type, - new FormattingParameters( null, null, null, null, null ) + new FormattingParameters( null, null, null, null, null, null ) ); assertThat( statefulMessagerMock.getLastKindPrinted() ).isNull(); } @@ -97,7 +96,7 @@ public void testUnsupportedType() { statefulMessagerMock, type, type, - new FormattingParameters( "qwertz", null, null, null, null ) + new FormattingParameters( "qwertz", null, null, null, null, null ) ); assertThat( statefulMessagerMock.getLastKindPrinted() ).isNull(); } diff --git a/processor/src/test/java/org/mapstruct/ap/internal/util/NativeTypesTest.java b/processor/src/test/java/org/mapstruct/ap/internal/util/NativeTypesTest.java index 9c68a1de45..f3ba8d3a8c 100644 --- a/processor/src/test/java/org/mapstruct/ap/internal/util/NativeTypesTest.java +++ b/processor/src/test/java/org/mapstruct/ap/internal/util/NativeTypesTest.java @@ -5,15 +5,15 @@ */ package org.mapstruct.ap.internal.util; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.math.BigDecimal; import java.math.BigInteger; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * @author Ciaran Liedeman */ @@ -122,7 +122,7 @@ public void testIntegerLiteralFromJLS() { .isNotNull(); // most negative int: dec / octal / int / binary - // NOTE parseInt should be changed to parseUnsignedInt in Java, than the - sign can disssapear (java8) + // NOTE parseInt should be changed to parseUnsignedInt in Java, than the - sign can dissapear (java8) // and the function will be true to what the compiler shows. assertThat( getLiteral( int.class.getCanonicalName(), "-2147483648" ) ).isNotNull(); assertThat( getLiteral( int.class.getCanonicalName(), "0x8000_0000" ) ).isNotNull(); @@ -177,7 +177,7 @@ public void testIntegerLiteralFromJLS() { * The following example shows other ways you can use the underscore in numeric literals: */ @Test - public void testFloatingPoingLiteralFromJLS() { + public void testFloatingPointLiteralFromJLS() { // The largest positive finite literal of type float is 3.4028235e38f. assertThat( getLiteral( float.class.getCanonicalName(), "3.4028235e38f" ) ).isNotNull(); diff --git a/processor/src/test/java/org/mapstruct/ap/internal/util/StringsTest.java b/processor/src/test/java/org/mapstruct/ap/internal/util/StringsTest.java index 687b63f793..20c7a766a6 100644 --- a/processor/src/test/java/org/mapstruct/ap/internal/util/StringsTest.java +++ b/processor/src/test/java/org/mapstruct/ap/internal/util/StringsTest.java @@ -5,14 +5,14 @@ */ package org.mapstruct.ap.internal.util; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Arrays; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.DefaultLocale; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Filip Hrisafov */ @@ -72,6 +72,7 @@ public void testGetSaveVariableNameWithArrayExistingVariables() { assertThat( Strings.getSafeVariableName( "__Test" ) ).isEqualTo( "test" ); assertThat( Strings.getSafeVariableName( "_0Test" ) ).isEqualTo( "test" ); assertThat( Strings.getSafeVariableName( "_0123Test" ) ).isEqualTo( "test" ); + assertThat( Strings.getSafeVariableName( "bad/test" ) ).isEqualTo( "bad_test" ); } @Test @@ -94,6 +95,7 @@ public void testGetSaveVariableNameWithCollection() { assertThat( Strings.getSafeVariableName( "__0Test", Arrays.asList( "test" ) ) ).isEqualTo( "test1" ); assertThat( Strings.getSafeVariableName( "___0", new ArrayList<>() ) ).isEqualTo( "___0" ); assertThat( Strings.getSafeVariableName( "__0123456789Test", Arrays.asList( "test" ) ) ).isEqualTo( "test1" ); + assertThat( Strings.getSafeVariableName( "bad/test", Arrays.asList( "bad_test" ) ) ).isEqualTo( "bad_test1" ); } @Test @@ -110,6 +112,7 @@ public void testSanitizeIdentifierName() { assertThat( Strings.sanitizeIdentifierName( "_0int[]" ) ).isEqualTo( "intArray" ); assertThat( Strings.sanitizeIdentifierName( "__0int[]" ) ).isEqualTo( "intArray" ); assertThat( Strings.sanitizeIdentifierName( "___0" ) ).isEqualTo( "___0" ); + assertThat( Strings.sanitizeIdentifierName( "bad/test" ) ).isEqualTo( "bad_test" ); } @Test diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/AccessibilityTest.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/AccessibilityTest.java index d338cadedd..bab9ca9b71 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/AccessibilityTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/AccessibilityTest.java @@ -41,7 +41,7 @@ public void testGeneratedModifiersFromInterfaceAreCorrect() throws Exception { assertTrue( isDefault( defaultFromIfc.getModifiers() ) ); - assertTrue( isPublic( modifiersFor( defaultFromIfc, "implicitlyPublicSoureToTarget" ) ) ); + assertTrue( isPublic( modifiersFor( defaultFromIfc, "implicitlyPublicSourceToTarget" ) ) ); } private static Class loadForMapper(Class mapper) throws ClassNotFoundException { diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/DefaultSourceTargetMapperIfc.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/DefaultSourceTargetMapperIfc.java index 84e9aebbbc..027d60b9ec 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/DefaultSourceTargetMapperIfc.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/DefaultSourceTargetMapperIfc.java @@ -12,5 +12,5 @@ */ @Mapper interface DefaultSourceTargetMapperIfc { - Target implicitlyPublicSoureToTarget(Source source); + Target implicitlyPublicSourceToTarget(Source source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperPrivate.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperPrivate.java index 507e63a957..2a16c3980f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperPrivate.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperPrivate.java @@ -14,7 +14,7 @@ * @author Sjaak Derksen */ @Mapper -public abstract class AbstractSourceTargetMapperPrivate extends SourceTargetmapperPrivateBase { +public abstract class AbstractSourceTargetMapperPrivate extends SourceTargetMapperPrivateBase { public static final AbstractSourceTargetMapperPrivate INSTANCE = Mappers.getMapper( AbstractSourceTargetMapperPrivate.class ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperProtected.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperProtected.java index ef5df10285..ccdba61514 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperProtected.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/AbstractSourceTargetMapperProtected.java @@ -14,7 +14,7 @@ * @author Sjaak Derksen */ @Mapper -public abstract class AbstractSourceTargetMapperProtected extends SourceTargetmapperProtectedBase { +public abstract class AbstractSourceTargetMapperProtected extends SourceTargetMapperProtectedBase { public static final AbstractSourceTargetMapperProtected INSTANCE = Mappers.getMapper( AbstractSourceTargetMapperProtected.class ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/ReferencedAccessibilityTest.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/ReferencedAccessibilityTest.java index 3cd62a8b4f..dc3d8ea10f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/ReferencedAccessibilityTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/ReferencedAccessibilityTest.java @@ -72,12 +72,12 @@ public void shouldNotBeAbleToAccessDefaultMethodInReferencedInOtherPackage() { @ProcessorTest @IssueKey( "206" ) - @WithClasses( { AbstractSourceTargetMapperProtected.class, SourceTargetmapperProtectedBase.class } ) + @WithClasses( { AbstractSourceTargetMapperProtected.class, SourceTargetMapperProtectedBase.class } ) public void shouldBeAbleToAccessProtectedMethodInBase() { } @ProcessorTest @IssueKey("206") - @WithClasses({ AbstractSourceTargetMapperPrivate.class, SourceTargetmapperPrivateBase.class }) + @WithClasses({ AbstractSourceTargetMapperPrivate.class, SourceTargetMapperPrivateBase.class }) @ExpectedCompilationOutcome( value = CompilationResult.SUCCEEDED, diagnostics = { diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetmapperPrivateBase.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetMapperPrivateBase.java similarity index 91% rename from processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetmapperPrivateBase.java rename to processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetMapperPrivateBase.java index 94f9b2668f..06dbb0cec7 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetmapperPrivateBase.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetMapperPrivateBase.java @@ -8,7 +8,7 @@ /** * @author Sjaak Derksen */ -public class SourceTargetmapperPrivateBase { +public class SourceTargetMapperPrivateBase { @SuppressWarnings("unused") private ReferencedTarget sourceToTarget(ReferencedSource source) { diff --git a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetmapperProtectedBase.java b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetMapperProtectedBase.java similarity index 90% rename from processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetmapperProtectedBase.java rename to processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetMapperProtectedBase.java index 263d74bee6..fe5a50fa37 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetmapperProtectedBase.java +++ b/processor/src/test/java/org/mapstruct/ap/test/accessibility/referenced/SourceTargetMapperProtectedBase.java @@ -9,7 +9,7 @@ * * @author Sjaak Derksen */ -public class SourceTargetmapperProtectedBase { +public class SourceTargetMapperProtectedBase { protected ReferencedTarget sourceToTarget( ReferencedSource source ) { ReferencedTarget target = new ReferencedTarget(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/AdditionalSupportedOptionsProviderTest.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/AdditionalSupportedOptionsProviderTest.java new file mode 100644 index 0000000000..ff2c85139f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/AdditionalSupportedOptionsProviderTest.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.mapstruct.ap.spi.EnumMappingStrategy; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithServiceImplementation; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; +import org.mapstruct.ap.testutil.runner.Compiler; + +import static org.assertj.core.api.Assertions.assertThat; + +@Execution( ExecutionMode.CONCURRENT ) +public class AdditionalSupportedOptionsProviderTest { + + @ProcessorTest + @WithClasses({ + Pet.class, + PetWithMissing.class, + UnknownEnumMappingStrategyMapper.class + }) + @WithServiceImplementation(CustomAdditionalSupportedOptionsProvider.class) + @WithServiceImplementation(value = UnknownEnumMappingStrategy.class, provides = EnumMappingStrategy.class) + @ProcessorOption(name = "myorg.custom.defaultNullEnumConstant", value = "MISSING") + public void shouldUseConfiguredPrefix() { + assertThat( UnknownEnumMappingStrategyMapper.INSTANCE.map( null ) ) + .isEqualTo( PetWithMissing.MISSING ); + } + + @ProcessorTest(Compiler.JDK) // The eclipse compiler does not parse the error message properly + @WithClasses({ + EmptyMapper.class + }) + @WithServiceImplementation(InvalidAdditionalSupportedOptionsProvider.class) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + messageRegExp = "Additional SPI options cannot start with \"mapstruct\". Provider " + + "org.mapstruct.ap.test.additionalsupportedoptions.InvalidAdditionalSupportedOptionsProvider@.*" + + " provided option mapstruct.custom.test" + ) + ) + public void shouldFailWhenOptionsProviderUsesMapstructPrefix() { + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/CustomAdditionalSupportedOptionsProvider.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/CustomAdditionalSupportedOptionsProvider.java new file mode 100644 index 0000000000..87a994bb4e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/CustomAdditionalSupportedOptionsProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +// tag::documentation[] +import java.util.Collections; +import java.util.Set; + +import org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider; + +public class CustomAdditionalSupportedOptionsProvider implements AdditionalSupportedOptionsProvider { + + @Override + public Set getAdditionalSupportedOptions() { + return Collections.singleton( "myorg.custom.defaultNullEnumConstant" ); + } + +} +// end::documentation[] diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/EmptyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/EmptyMapper.java new file mode 100644 index 0000000000..13a6f0aece --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/EmptyMapper.java @@ -0,0 +1,12 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +import org.mapstruct.Mapper; + +@Mapper +public interface EmptyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/InvalidAdditionalSupportedOptionsProvider.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/InvalidAdditionalSupportedOptionsProvider.java new file mode 100644 index 0000000000..1a387abba2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/InvalidAdditionalSupportedOptionsProvider.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +import java.util.Collections; +import java.util.Set; + +import org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider; + +public class InvalidAdditionalSupportedOptionsProvider implements AdditionalSupportedOptionsProvider { + + @Override + public Set getAdditionalSupportedOptions() { + return Collections.singleton( "mapstruct.custom.test" ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/Pet.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/Pet.java new file mode 100644 index 0000000000..8fe9b39558 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/Pet.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +public enum Pet { + + DOG, + CAT, + BEAR + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/PetWithMissing.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/PetWithMissing.java new file mode 100644 index 0000000000..21d5f4a649 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/PetWithMissing.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +public enum PetWithMissing { + + DOG, + CAT, + BEAR, + MISSING + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/UnknownEnumMappingStrategy.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/UnknownEnumMappingStrategy.java new file mode 100644 index 0000000000..ca4403925e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/UnknownEnumMappingStrategy.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +// tag::documentation[] +import javax.lang.model.element.TypeElement; + +import org.mapstruct.ap.spi.DefaultEnumMappingStrategy; +import org.mapstruct.ap.spi.MapStructProcessingEnvironment; + +public class UnknownEnumMappingStrategy extends DefaultEnumMappingStrategy { + + private String defaultNullEnumConstant; + + @Override + public void init(MapStructProcessingEnvironment processingEnvironment) { + super.init( processingEnvironment ); + defaultNullEnumConstant = processingEnvironment.getOptions().get( "myorg.custom.defaultNullEnumConstant" ); + } + + @Override + public String getDefaultNullEnumConstant(TypeElement enumType) { + return defaultNullEnumConstant; + } +} +// end::documentation[] diff --git a/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/UnknownEnumMappingStrategyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/UnknownEnumMappingStrategyMapper.java new file mode 100644 index 0000000000..be8c74e227 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/additionalsupportedoptions/UnknownEnumMappingStrategyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.additionalsupportedoptions; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface UnknownEnumMappingStrategyMapper { + + UnknownEnumMappingStrategyMapper INSTANCE = Mappers.getMapper( UnknownEnumMappingStrategyMapper.class ); + + PetWithMissing map(Pet pet); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateBeanMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateBeanMappingMethodMapper.java new file mode 100644 index 0000000000..cc19c2d282 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateBeanMappingMethodMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author orange add + */ +@Mapper +public interface AnnotateBeanMappingMethodMapper { + + @AnnotateWith(CustomMethodOnlyAnnotation.class) + Target map(Source source); + + class Source { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class Target { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateIterableMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateIterableMappingMethodMapper.java new file mode 100644 index 0000000000..86bef7de45 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateIterableMappingMethodMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.util.List; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author orange add + */ +@Mapper +public interface AnnotateIterableMappingMethodMapper { + + @AnnotateWith(CustomMethodOnlyAnnotation.class) + List toStringList(List integers); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateMapMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateMapMappingMethodMapper.java new file mode 100644 index 0000000000..a972d5e06f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateMapMappingMethodMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.util.Date; +import java.util.Map; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.MapMapping; +import org.mapstruct.Mapper; + +/** + * @author orange add + */ +@Mapper +public interface AnnotateMapMappingMethodMapper { + + @MapMapping(valueDateFormat = "dd.MM.yyyy") + @AnnotateWith(CustomMethodOnlyAnnotation.class) + Map longDateMapToStringStringMap(Map source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateStreamMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateStreamMappingMethodMapper.java new file mode 100644 index 0000000000..6321b5590c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateStreamMappingMethodMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.util.stream.Stream; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author orange add + */ +@Mapper +public interface AnnotateStreamMappingMethodMapper { + + @AnnotateWith(CustomMethodOnlyAnnotation.class) + Stream toStringStream(Stream integerStream); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateValueMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateValueMappingMethodMapper.java new file mode 100644 index 0000000000..a9e7c7b519 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateValueMappingMethodMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; + +/** + * @author orange add + */ +@Mapper +public interface AnnotateValueMappingMethodMapper { + + @ValueMappings({ + @ValueMapping(target = "EXISTING", source = "EXISTING"), + @ValueMapping( source = MappingConstants.ANY_REMAINING, target = "OTHER_EXISTING" ) + }) + @AnnotateWith(CustomMethodOnlyAnnotation.class) + AnnotateWithEnum map(String str); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateWithEnum.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateWithEnum.java new file mode 100644 index 0000000000..b2627fd8ce --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateWithEnum.java @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +public enum AnnotateWithEnum { + EXISTING, OTHER_EXISTING +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateWithTest.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateWithTest.java new file mode 100644 index 0000000000..5dd6a6e8f5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotateWithTest.java @@ -0,0 +1,597 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.WithProperties; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ben Zegveld + */ +@IssueKey("1574") +@WithClasses(AnnotateWithEnum.class) +public class AnnotateWithTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ DeprecateAndCustomMapper.class, CustomAnnotation.class }) + public void mapperBecomesDeprecatedAndGetsCustomAnnotation() { + DeprecateAndCustomMapper mapper = Mappers.getMapper( DeprecateAndCustomMapper.class ); + + assertThat( mapper.getClass() ).hasAnnotations( Deprecated.class, CustomAnnotation.class ); + } + + @ProcessorTest + @WithClasses( { + CustomNamedMapper.class, + CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class, + CustomClassOnlyAnnotation.class, + CustomMethodOnlyAnnotation.class, + } ) + public void annotationWithValue() { + generatedSource.addComparisonToFixtureFor( CustomNamedMapper.class ); + } + + @ProcessorTest + @WithClasses( { MultipleArrayValuesMapper.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void annotationWithMultipleValues() { + generatedSource.addComparisonToFixtureFor( MultipleArrayValuesMapper.class ); + } + + @ProcessorTest + @WithClasses( { CustomNamedGenericClassMapper.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void annotationWithCorrectGenericClassValue() { + CustomNamedGenericClassMapper mapper = Mappers.getMapper( CustomNamedGenericClassMapper.class ); + + CustomAnnotationWithParams annotation = mapper.getClass().getAnnotation( CustomAnnotationWithParams.class ); + assertThat( annotation ).isNotNull(); + assertThat( annotation.stringParam() ).isEqualTo( "test" ); + assertThat( annotation.genericTypedClass() ).isEqualTo( Mapper.class ); + } + + @ProcessorTest + @WithClasses( { AnnotationWithoutElementNameMapper.class, CustomAnnotation.class } ) + public void annotateWithoutElementName() { + generatedSource + .forMapper( AnnotationWithoutElementNameMapper.class ) + .content() + .contains( "@CustomAnnotation(value = \"value\")" ); + } + + @ProcessorTest + @WithClasses({ MetaAnnotatedMapper.class, ClassMetaAnnotation.class, CustomClassOnlyAnnotation.class }) + public void metaAnnotationWorks() { + MetaAnnotatedMapper mapper = Mappers.getMapper( MetaAnnotatedMapper.class ); + + assertThat( mapper.getClass() ).hasAnnotation( CustomClassOnlyAnnotation.class ); + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithMissingParameter.class, + line = 15, + message = "Parameter \"required\" is required for annotation \"AnnotationWithRequiredParameter\"." + ) + } + ) + @WithClasses({ ErroneousMapperWithMissingParameter.class, AnnotationWithRequiredParameter.class }) + public void erroneousMapperWithMissingParameter() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithMethodOnInterface.class, + line = 15, + message = "Annotation \"CustomMethodOnlyAnnotation\" is not allowed on classes." + ) + } + ) + @WithClasses({ ErroneousMapperWithMethodOnInterface.class, CustomMethodOnlyAnnotation.class }) + public void erroneousMapperWithMethodOnInterface() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithMethodOnClass.class, + line = 15, + message = "Annotation \"CustomMethodOnlyAnnotation\" is not allowed on classes." + ) + } + ) + @WithClasses({ ErroneousMapperWithMethodOnClass.class, CustomMethodOnlyAnnotation.class }) + public void erroneousMapperWithMethodOnClass() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithAnnotationOnlyOnInterface.class, + line = 15, + message = "Annotation \"CustomAnnotationOnlyAnnotation\" is not allowed on classes." + ) + } + ) + @WithClasses({ ErroneousMapperWithAnnotationOnlyOnInterface.class, CustomAnnotationOnlyAnnotation.class }) + public void erroneousMapperWithAnnotationOnlyOnInterface() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithAnnotationOnlyOnClass.class, + line = 15, + message = "Annotation \"CustomAnnotationOnlyAnnotation\" is not allowed on classes." + ) + } + ) + @WithClasses({ ErroneousMapperWithAnnotationOnlyOnClass.class, CustomAnnotationOnlyAnnotation.class }) + public void erroneousMapperWithAnnotationOnlyOnClass() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithClassOnMethod.class, + line = 18, + message = "Annotation \"CustomClassOnlyAnnotation\" is not allowed on methods." + ) + } + ) + @WithClasses({ ErroneousMapperWithClassOnMethod.class, CustomClassOnlyAnnotation.class, WithProperties.class }) + public void erroneousMapperWithClassOnMethod() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithUnknownParameter.class, + line = 17, + message = "Unknown parameter \"unknownParameter\" for annotation \"CustomAnnotation\"." + + " Did you mean \"value\"?" + ) + } + ) + @WithClasses({ ErroneousMapperWithUnknownParameter.class, CustomAnnotation.class }) + public void erroneousMapperWithUnknownParameter() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithNonExistantEnum.class, + line = 17, + message = "Enum \"AnnotateWithEnum\" does not have value \"NON_EXISTANT\"." + ) + } + ) + @WithClasses( { ErroneousMapperWithNonExistantEnum.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void erroneousMapperWithNonExistantEnum() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithTooManyParameterValues.class, + line = 17, + message = "Parameter \"stringParam\" has too many value types supplied, type \"String\" is expected" + + " for annotation \"CustomAnnotationWithParams\"." + ) + } + ) + @WithClasses( { ErroneousMapperWithTooManyParameterValues.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void erroneousMapperWithTooManyParameterValues() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 16, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"boolean\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 18, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"byte\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 20, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"char\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 22, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"CustomAnnotationWithParams\"" + + " but of type \"String\" for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 24, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"double\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 26, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"float\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 28, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"int\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 30, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"long\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 32, + alternativeLine = 43, + message = "Parameter \"stringParam\" is not of type \"short\" but of type \"String\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 35, + alternativeLine = 43, + message = "Parameter \"genericTypedClass\" is not of type \"String\" " + + "but of type \"Class\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 36, + alternativeLine = 43, + message = "Parameter \"enumParam\" is not of type \"WrongAnnotateWithEnum\" " + + "but of type \"AnnotateWithEnum\" for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 40, + alternativeLine = 43, + message = "Parameter \"genericTypedClass\" is not of type \"ErroneousMapperWithWrongParameter\" " + + "but of type \"Class\" " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithWrongParameter.class, + line = 42, + alternativeLine = 43, + message = "Parameter \"value\" is not of type \"boolean\" " + + "but of type \"String\" for annotation \"CustomAnnotation\"." + ) + } + ) + @WithClasses({ + ErroneousMapperWithWrongParameter.class, CustomAnnotationWithParams.class, + CustomAnnotationWithParamsContainer.class, WrongAnnotateWithEnum.class, CustomAnnotation.class + }) + public void erroneousMapperWithWrongParameter() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 17, + alternativeLine = 43, + message = "Parameter \"stringParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 18, + alternativeLine = 43, + message = "Parameter \"booleanParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 19, + alternativeLine = 32, + message = "Parameter \"byteParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 20, + alternativeLine = 32, + message = "Parameter \"charParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 21, + alternativeLine = 32, + message = "Parameter \"doubleParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 22, + alternativeLine = 32, + message = "Parameter \"floatParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 23, + alternativeLine = 32, + message = "Parameter \"intParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 24, + alternativeLine = 32, + message = "Parameter \"longParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 25, + alternativeLine = 32, + message = "Parameter \"shortParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 26, + alternativeLine = 32, + message = "Parameter \"genericTypedClass\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMultipleArrayValuesMapper.class, + line = 27, + alternativeLine = 32, + message = "Parameter \"enumParam\" does not accept multiple values " + + "for annotation \"CustomAnnotationWithParams\"." + ) + } + ) + @WithClasses( { ErroneousMultipleArrayValuesMapper.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void erroneousMapperUsingMultipleValuesInsteadOfSingle() { + } + + @ProcessorTest + @WithClasses( { MapperWithMissingAnnotationElementName.class, + CustomAnnotationWithTwoAnnotationElements.class } ) + public void mapperWithMissingAnnotationElementNameShouldCompile() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithMissingEnumClass.class, + line = 17, + message = "enumClass needs to be defined when using enums." + ) + } + ) + @WithClasses( { ErroneousMapperWithMissingEnumClass.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void erroneousMapperWithMissingEnumClass() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithMissingEnums.class, + line = 17, + message = "enums needs to be defined when using enumClass." + ) + } + ) + @WithClasses( { ErroneousMapperWithMissingEnums.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void erroneousMapperWithMissingEnums() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithRepeatOfNotRepeatableAnnotation.class, + line = 16, + alternativeLine = 17, + message = "Annotation \"CustomAnnotation\" is not repeatable." + ) + } + ) + @WithClasses( { ErroneousMapperWithRepeatOfNotRepeatableAnnotation.class, CustomAnnotation.class } ) + public void erroneousMapperWithRepeatOfNotRepeatableAnnotation() { + } + + @ProcessorTest + @WithClasses( { MapperWithRepeatableAnnotation.class, CustomRepeatableAnnotation.class, + CustomRepeatableAnnotationContainer.class } ) + public void mapperWithRepeatableAnnotationShouldCompile() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapperWithParameterRepeat.class, + line = 18, + message = "Parameter \"stringParam\" must not be defined more than once." + ) + } + ) + @WithClasses( { ErroneousMapperWithParameterRepeat.class, CustomAnnotationWithParamsContainer.class, + CustomAnnotationWithParams.class } ) + public void erroneousMapperWithParameterRepeat() { + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.WARNING, + type = MapperWithIdenticalAnnotationRepeated.class, + line = 16, + alternativeLine = 17, + message = "Annotation \"CustomRepeatableAnnotation\" is already present " + + "with the same elements configuration." + ) + } + ) + @WithClasses( { MapperWithIdenticalAnnotationRepeated.class, CustomRepeatableAnnotation.class, + CustomRepeatableAnnotationContainer.class } ) + public void mapperWithIdenticalAnnotationRepeated() { + } + + @ProcessorTest + @WithClasses( {AnnotateBeanMappingMethodMapper.class, CustomMethodOnlyAnnotation.class} ) + public void beanMappingMethodWithCorrectCustomAnnotation() throws NoSuchMethodException { + AnnotateBeanMappingMethodMapper mapper = Mappers.getMapper( AnnotateBeanMappingMethodMapper.class ); + Method method = mapper.getClass().getMethod( "map", AnnotateBeanMappingMethodMapper.Source.class ); + assertThat( method.getAnnotation( CustomMethodOnlyAnnotation.class ) ).isNotNull(); + } + + @ProcessorTest + @WithClasses( {AnnotateIterableMappingMethodMapper.class, CustomMethodOnlyAnnotation.class} ) + public void iterableMappingMethodWithCorrectCustomAnnotation() throws NoSuchMethodException { + AnnotateIterableMappingMethodMapper mapper = Mappers.getMapper( AnnotateIterableMappingMethodMapper.class ); + Method method = mapper.getClass().getMethod( "toStringList", List.class ); + assertThat( method.getAnnotation( CustomMethodOnlyAnnotation.class ) ).isNotNull(); + } + + @ProcessorTest + @WithClasses( {AnnotateMapMappingMethodMapper.class, CustomMethodOnlyAnnotation.class} ) + public void mapMappingMethodWithCorrectCustomAnnotation() throws NoSuchMethodException { + AnnotateMapMappingMethodMapper mapper = Mappers.getMapper( AnnotateMapMappingMethodMapper.class ); + Method method = mapper.getClass().getMethod( "longDateMapToStringStringMap", Map.class ); + assertThat( method.getAnnotation( CustomMethodOnlyAnnotation.class ) ).isNotNull(); + } + + @ProcessorTest + @WithClasses( {AnnotateStreamMappingMethodMapper.class, CustomMethodOnlyAnnotation.class} ) + public void streamMappingMethodWithCorrectCustomAnnotation() throws NoSuchMethodException { + AnnotateStreamMappingMethodMapper mapper = Mappers.getMapper( AnnotateStreamMappingMethodMapper.class ); + Method method = mapper.getClass().getMethod( "toStringStream", Stream.class ); + assertThat( method.getAnnotation( CustomMethodOnlyAnnotation.class ) ).isNotNull(); + } + + @ProcessorTest + @WithClasses( {AnnotateValueMappingMethodMapper.class, AnnotateWithEnum.class, CustomMethodOnlyAnnotation.class} ) + public void valueMappingMethodWithCorrectCustomAnnotation() throws NoSuchMethodException { + AnnotateValueMappingMethodMapper mapper = Mappers.getMapper( AnnotateValueMappingMethodMapper.class ); + Method method = mapper.getClass().getMethod( "map", String.class ); + assertThat( method.getAnnotation( CustomMethodOnlyAnnotation.class ) ).isNotNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotationWithRequiredParameter.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotationWithRequiredParameter.java new file mode 100644 index 0000000000..183b865928 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotationWithRequiredParameter.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE, METHOD } ) +public @interface AnnotationWithRequiredParameter { + String required(); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotationWithoutElementNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotationWithoutElementNameMapper.java new file mode 100644 index 0000000000..8bb83ba562 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/AnnotationWithoutElementNameMapper.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +@Mapper +@AnnotateWith( value = CustomAnnotation.class, elements = @AnnotateWith.Element( strings = "value" ) ) +public interface AnnotationWithoutElementNameMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ClassMetaAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ClassMetaAnnotation.java new file mode 100644 index 0000000000..057cb384fb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ClassMetaAnnotation.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mapstruct.AnnotateWith; + +@Retention( RetentionPolicy.RUNTIME ) +@Target( { ElementType.TYPE } ) +@AnnotateWith( CustomClassOnlyAnnotation.class ) +public @interface ClassMetaAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotation.java new file mode 100644 index 0000000000..71ee6f12e3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotation.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE, METHOD } ) +public @interface CustomAnnotation { + String value() default ""; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationOnlyAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationOnlyAnnotation.java new file mode 100644 index 0000000000..52d3e386a0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationOnlyAnnotation.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({ ANNOTATION_TYPE }) +public @interface CustomAnnotationOnlyAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithParams.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithParams.java new file mode 100644 index 0000000000..bc25302fc1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithParams.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { METHOD, TYPE } ) +@Repeatable( CustomAnnotationWithParamsContainer.class ) +public @interface CustomAnnotationWithParams { + String stringParam(); + + Class genericTypedClass() default CustomAnnotationWithParams.class; + + AnnotateWithEnum enumParam() default AnnotateWithEnum.EXISTING; + + byte byteParam() default 0x00; + + char charParam() default 'a'; + + double doubleParam() default 0.0; + + float floatParam() default 0.0f; + + int intParam() default 0; + + long longParam() default 0L; + + short shortParam() default 0; + + boolean booleanParam() default false; + + short[] shortArray() default {}; + + byte[] byteArray() default {}; + + int[] intArray() default {}; + + long[] longArray() default {}; + + float[] floatArray() default {}; + + double[] doubleArray() default {}; + + char[] charArray() default {}; + + boolean[] booleanArray() default {}; + + String[] stringArray() default {}; + + Class[] classArray() default {}; + + AnnotateWithEnum[] enumArray() default {}; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithParamsContainer.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithParamsContainer.java new file mode 100644 index 0000000000..51ed62885c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithParamsContainer.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE, METHOD } ) +public @interface CustomAnnotationWithParamsContainer { + CustomAnnotationWithParams[] value() default {}; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithTwoAnnotationElements.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithTwoAnnotationElements.java new file mode 100644 index 0000000000..fb8c5c69f8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomAnnotationWithTwoAnnotationElements.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE, METHOD } ) +public @interface CustomAnnotationWithTwoAnnotationElements { + String value() default ""; + boolean namedAnnotationElement() default false; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomClassOnlyAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomClassOnlyAnnotation.java new file mode 100644 index 0000000000..858c8fb52f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomClassOnlyAnnotation.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE } ) +public @interface CustomClassOnlyAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomMethodOnlyAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomMethodOnlyAnnotation.java new file mode 100644 index 0000000000..1dfe8e734e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomMethodOnlyAnnotation.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { METHOD } ) +public @interface CustomMethodOnlyAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomNamedGenericClassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomNamedGenericClassMapper.java new file mode 100644 index 0000000000..a320d56bba --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomNamedGenericClassMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @Element( name = "stringParam", strings = "test" ), + @Element( name = "genericTypedClass", classes = Mapper.class ) +} ) +public interface CustomNamedGenericClassMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomNamedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomNamedMapper.java new file mode 100644 index 0000000000..9f48d8c764 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomNamedMapper.java @@ -0,0 +1,78 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( CustomClassOnlyAnnotation.class ) +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @Element( name = "stringArray", strings = "test" ), + @Element( name = "stringParam", strings = "test" ), + @Element( name = "booleanArray", booleans = true ), + @Element( name = "booleanParam", booleans = true ), + @Element( name = "byteArray", bytes = 0x10 ), + @Element( name = "byteParam", bytes = 0x13 ), + @Element( name = "charArray", chars = 'd' ), + @Element( name = "charParam", chars = 'a' ), + @Element( name = "enumArray", enumClass = AnnotateWithEnum.class, enums = "EXISTING" ), + @Element( name = "enumParam", enumClass = AnnotateWithEnum.class, enums = "EXISTING" ), + @Element( name = "doubleArray", doubles = 0.3 ), + @Element( name = "doubleParam", doubles = 1.2 ), + @Element( name = "floatArray", floats = 0.3f ), + @Element( name = "floatParam", floats = 1.2f ), + @Element( name = "intArray", ints = 3 ), + @Element( name = "intParam", ints = 1 ), + @Element( name = "longArray", longs = 3L ), + @Element( name = "longParam", longs = 1L ), + @Element( name = "shortArray", shorts = 3 ), + @Element( name = "shortParam", shorts = 1 ) +} ) +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @Element(name = "stringParam", strings = "single value") +}) +public interface CustomNamedMapper { + + @AnnotateWith(value = CustomAnnotationWithParams.class, elements = { + @Element(name = "stringParam", strings = "double method value"), + @Element(name = "stringArray", strings = { "first", "second" }), + }) + @AnnotateWith(value = CustomAnnotationWithParams.class, elements = { + @Element(name = "stringParam", strings = "single method value") + }) + @AnnotateWith( CustomMethodOnlyAnnotation.class ) + Target map(Source source); + + class Target { + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomRepeatableAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomRepeatableAnnotation.java new file mode 100644 index 0000000000..d7c0c85107 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomRepeatableAnnotation.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE, METHOD } ) +@Repeatable( CustomRepeatableAnnotationContainer.class ) +public @interface CustomRepeatableAnnotation { + String value() default ""; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomRepeatableAnnotationContainer.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomRepeatableAnnotationContainer.java new file mode 100644 index 0000000000..8de3a8bf0a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/CustomRepeatableAnnotationContainer.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention( RUNTIME ) +@Target( { TYPE, METHOD } ) +public @interface CustomRepeatableAnnotationContainer { + CustomRepeatableAnnotation[] value() default {}; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/DeprecateAndCustomMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/DeprecateAndCustomMapper.java new file mode 100644 index 0000000000..809ad5702a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/DeprecateAndCustomMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( Deprecated.class ) +@AnnotateWith( CustomAnnotation.class ) +public interface DeprecateAndCustomMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithAnnotationOnlyOnClass.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithAnnotationOnlyOnClass.java new file mode 100644 index 0000000000..d617e8d0fc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithAnnotationOnlyOnClass.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper +@AnnotateWith( value = CustomAnnotationOnlyAnnotation.class ) +public abstract class ErroneousMapperWithAnnotationOnlyOnClass { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithAnnotationOnlyOnInterface.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithAnnotationOnlyOnInterface.java new file mode 100644 index 0000000000..4e914b30a3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithAnnotationOnlyOnInterface.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper +@AnnotateWith( value = CustomAnnotationOnlyAnnotation.class ) +public interface ErroneousMapperWithAnnotationOnlyOnInterface { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithClassOnMethod.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithClassOnMethod.java new file mode 100644 index 0000000000..c34bce304f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithClassOnMethod.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.WithProperties; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface ErroneousMapperWithClassOnMethod { + + @AnnotateWith( value = CustomClassOnlyAnnotation.class ) + WithProperties toString(String string); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMethodOnClass.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMethodOnClass.java new file mode 100644 index 0000000000..262880d6f5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMethodOnClass.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomMethodOnlyAnnotation.class ) +public abstract class ErroneousMapperWithMethodOnClass { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMethodOnInterface.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMethodOnInterface.java new file mode 100644 index 0000000000..75f9039093 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMethodOnInterface.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomMethodOnlyAnnotation.class ) +public interface ErroneousMapperWithMethodOnInterface { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingEnumClass.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingEnumClass.java new file mode 100644 index 0000000000..6905a363f2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingEnumClass.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = { @Element( name = "enumParam", enums = "EXISTING" ), + @Element( name = "stringParam", strings = "required" ) } +) +public interface ErroneousMapperWithMissingEnumClass { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingEnums.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingEnums.java new file mode 100644 index 0000000000..fc7096d290 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingEnums.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @Element( name = "stringParam", strings = "required", enumClass = AnnotateWithEnum.class ) +) +public interface ErroneousMapperWithMissingEnums { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingParameter.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingParameter.java new file mode 100644 index 0000000000..40ffa53094 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithMissingParameter.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( AnnotationWithRequiredParameter.class ) +public interface ErroneousMapperWithMissingParameter { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithNonExistantEnum.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithNonExistantEnum.java new file mode 100644 index 0000000000..1ed85b990d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithNonExistantEnum.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = { @Element( name = "enumParam", enumClass = AnnotateWithEnum.class, enums = "NON_EXISTANT" ), + @Element( name = "stringParam", strings = "required" ) } +) +public interface ErroneousMapperWithNonExistantEnum { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithParameterRepeat.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithParameterRepeat.java new file mode 100644 index 0000000000..d75c7a4ee4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithParameterRepeat.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @Element( name = "stringParam", strings = "test" ), + @Element( name = "stringParam", strings = "otherValue" ) +} ) +public interface ErroneousMapperWithParameterRepeat { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithRepeatOfNotRepeatableAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithRepeatOfNotRepeatableAnnotation.java new file mode 100644 index 0000000000..be9596ea46 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithRepeatOfNotRepeatableAnnotation.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotation.class ) +@AnnotateWith( value = CustomAnnotation.class ) +public interface ErroneousMapperWithRepeatOfNotRepeatableAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithTooManyParameterValues.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithTooManyParameterValues.java new file mode 100644 index 0000000000..4b6c20c39b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithTooManyParameterValues.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @Element( name = "stringParam", booleans = true, strings = "test" ) +) +public interface ErroneousMapperWithTooManyParameterValues { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithUnknownParameter.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithUnknownParameter.java new file mode 100644 index 0000000000..c705dc88f7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithUnknownParameter.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotation.class, + elements = @Element( name = "unknownParameter", strings = "unknown" ) +) +public interface ErroneousMapperWithUnknownParameter { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithWrongParameter.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithWrongParameter.java new file mode 100644 index 0000000000..3251122b22 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMapperWithWrongParameter.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", booleans = true ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", bytes = 0x12 ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", chars = 'a' ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", classes = CustomAnnotationWithParams.class ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", doubles = 12.34 ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", floats = 12.34f ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", ints = 1234 ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", longs = 1234L ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, + elements = @AnnotateWith.Element( name = "stringParam", shorts = 12 ) ) +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @AnnotateWith.Element( name = "stringParam", strings = "correctValue" ), + @AnnotateWith.Element( name = "genericTypedClass", strings = "wrong" ), + @AnnotateWith.Element( name = "enumParam", enumClass = WrongAnnotateWithEnum.class, enums = "EXISTING" ) +} ) +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @AnnotateWith.Element( name = "stringParam", strings = "correctValue" ), + @AnnotateWith.Element( name = "genericTypedClass", classes = ErroneousMapperWithWrongParameter.class ) +} ) +@AnnotateWith( value = CustomAnnotation.class, elements = @AnnotateWith.Element( booleans = true ) ) +public interface ErroneousMapperWithWrongParameter { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMultipleArrayValuesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMultipleArrayValuesMapper.java new file mode 100644 index 0000000000..f6bf7629cd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/ErroneousMultipleArrayValuesMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @Element( name = "stringParam", strings = { "test1", "test2" } ), + @Element( name = "booleanParam", booleans = { false, true } ), + @Element( name = "byteParam", bytes = { 0x08, 0x1f } ), + @Element( name = "charParam", chars = { 'b', 'c' } ), + @Element( name = "doubleParam", doubles = { 1.2, 3.4 } ), + @Element( name = "floatParam", floats = { 1.2f, 3.4f } ), + @Element( name = "intParam", ints = { 12, 34 } ), + @Element( name = "longParam", longs = { 12L, 34L } ), + @Element( name = "shortParam", shorts = { 12, 34 } ), + @Element( name = "genericTypedClass", classes = { Mapper.class, CustomAnnotationWithParams.class } ), + @Element( name = "enumParam", enumClass = AnnotateWithEnum.class, enums = { "EXISTING", "OTHER_EXISTING" } ) +} ) +public interface ErroneousMultipleArrayValuesMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithIdenticalAnnotationRepeated.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithIdenticalAnnotationRepeated.java new file mode 100644 index 0000000000..0959ee305a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithIdenticalAnnotationRepeated.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomRepeatableAnnotation.class, elements = @AnnotateWith.Element( strings = "identical" ) ) +@AnnotateWith( value = CustomRepeatableAnnotation.class, elements = @AnnotateWith.Element( strings = "identical" ) ) +public interface MapperWithIdenticalAnnotationRepeated { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithMissingAnnotationElementName.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithMissingAnnotationElementName.java new file mode 100644 index 0000000000..ed9523c545 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithMissingAnnotationElementName.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithTwoAnnotationElements.class, elements = { + @AnnotateWith.Element( strings = "unnamed annotation element" ), + @AnnotateWith.Element( name = "namedAnnotationElement", booleans = false ) +} ) +public abstract class MapperWithMissingAnnotationElementName { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithRepeatableAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithRepeatableAnnotation.java new file mode 100644 index 0000000000..b66ec68261 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MapperWithRepeatableAnnotation.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomRepeatableAnnotation.class ) +@AnnotateWith( value = CustomRepeatableAnnotation.class, elements = @AnnotateWith.Element( strings = "different" ) ) +public interface MapperWithRepeatableAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MetaAnnotatedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MetaAnnotatedMapper.java new file mode 100644 index 0000000000..2dddb17f83 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MetaAnnotatedMapper.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.Mapper; + +@ClassMetaAnnotation +@Mapper +public interface MetaAnnotatedMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MethodMetaAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MethodMetaAnnotation.java new file mode 100644 index 0000000000..a93d54c283 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MethodMetaAnnotation.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mapstruct.AnnotateWith; + +@Retention( RetentionPolicy.RUNTIME ) +@Target( { ElementType.METHOD } ) +@AnnotateWith( CustomMethodOnlyAnnotation.class ) +public @interface MethodMetaAnnotation { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MultipleArrayValuesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MultipleArrayValuesMapper.java new file mode 100644 index 0000000000..2ccdbb0ea2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/MultipleArrayValuesMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.AnnotateWith.Element; +import org.mapstruct.Mapper; + +/** + * @author Ben Zegveld + */ +@Mapper +@AnnotateWith( value = CustomAnnotationWithParams.class, elements = { + @Element( name = "stringArray", strings = { "test1", "test2" } ), + @Element( name = "booleanArray", booleans = { false, true } ), + @Element( name = "byteArray", bytes = { 0x08, 0x1f } ), + @Element( name = "charArray", chars = { 'b', 'c' } ), + @Element( name = "doubleArray", doubles = { 1.2, 3.4 } ), + @Element( name = "floatArray", floats = { 1.2f, 3.4f } ), + @Element( name = "intArray", ints = { 12, 34 } ), + @Element( name = "longArray", longs = { 12L, 34L } ), + @Element( name = "shortArray", shorts = { 12, 34 } ), + @Element( name = "classArray", classes = { Mapper.class, CustomAnnotationWithParams.class } ), + @Element( name = "stringParam", strings = "required parameter" ) +} ) +public interface MultipleArrayValuesMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/WrongAnnotateWithEnum.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/WrongAnnotateWithEnum.java new file mode 100644 index 0000000000..677a6cc6d5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/WrongAnnotateWithEnum.java @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith; + +public enum WrongAnnotateWithEnum { + EXISTING +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedMapperWithClass.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedMapperWithClass.java new file mode 100644 index 0000000000..d0164e8be0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedMapperWithClass.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated; + +import org.mapstruct.Mapper; + +@Mapper +@Deprecated +public class DeprecatedMapperWithClass { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedMapperWithMethod.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedMapperWithMethod.java new file mode 100644 index 0000000000..b6c2c8eb62 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedMapperWithMethod.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.annotatewith.CustomMethodOnlyAnnotation; + +@Mapper +public interface DeprecatedMapperWithMethod { + + @AnnotateWith(CustomMethodOnlyAnnotation.class) + @Deprecated + Target map(Source source); + + class Source { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class Target { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedTest.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedTest.java new file mode 100644 index 0000000000..42bb5d9ce5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/DeprecatedTest.java @@ -0,0 +1,57 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated; + +import java.lang.reflect.Method; + +import org.mapstruct.ap.test.annotatewith.CustomMethodOnlyAnnotation; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author orange add + */ +public class DeprecatedTest { + + @ProcessorTest + @WithClasses( { DeprecatedMapperWithMethod.class, CustomMethodOnlyAnnotation.class} ) + public void deprecatedWithMethodCorrectCopy() throws NoSuchMethodException { + DeprecatedMapperWithMethod mapper = Mappers.getMapper( DeprecatedMapperWithMethod.class ); + Method method = mapper.getClass().getMethod( "map", DeprecatedMapperWithMethod.Source.class ); + Deprecated annotation = method.getAnnotation( Deprecated.class ); + assertThat( annotation ).isNotNull(); + } + + @ProcessorTest + @WithClasses(DeprecatedMapperWithClass.class) + public void deprecatedWithClassCorrectCopy() { + DeprecatedMapperWithClass mapper = Mappers.getMapper( DeprecatedMapperWithClass.class ); + Deprecated annotation = mapper.getClass().getAnnotation( Deprecated.class ); + assertThat( annotation ).isNotNull(); + } + + @ProcessorTest + @WithClasses(RepeatDeprecatedMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.WARNING, + type = RepeatDeprecatedMapper.class, + message = "Annotation \"Deprecated\" is already present with the " + + "same elements configuration." + ) + } + ) + public void deprecatedWithRepeat() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/RepeatDeprecatedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/RepeatDeprecatedMapper.java new file mode 100644 index 0000000000..dd085f101c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/RepeatDeprecatedMapper.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +@Mapper +@Deprecated +@AnnotateWith(Deprecated.class) +public class RepeatDeprecatedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedMapperWithClass.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedMapperWithClass.java new file mode 100644 index 0000000000..2e0507f5ab --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedMapperWithClass.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated.jdk11; + +import org.mapstruct.Mapper; + +@Mapper +@Deprecated(since = "11") +public class DeprecatedMapperWithClass { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedMapperWithMethod.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedMapperWithMethod.java new file mode 100644 index 0000000000..bf898e2043 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedMapperWithMethod.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated.jdk11; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.annotatewith.CustomMethodOnlyAnnotation; + +@Mapper +public interface DeprecatedMapperWithMethod { + + @AnnotateWith(CustomMethodOnlyAnnotation.class) + @Deprecated(since = "18", forRemoval = false) + Target map(Source source); + + class Source { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class Target { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedTest.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedTest.java new file mode 100644 index 0000000000..dc496a22fc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/DeprecatedTest.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated.jdk11; + +import java.lang.reflect.Method; + +import org.mapstruct.ap.test.annotatewith.CustomMethodOnlyAnnotation; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author orange add + */ +public class DeprecatedTest { + @ProcessorTest + @WithClasses({ DeprecatedMapperWithMethod.class, CustomMethodOnlyAnnotation.class}) + public void deprecatedWithMethodCorrectCopyForJdk11() throws NoSuchMethodException { + DeprecatedMapperWithMethod mapper = Mappers.getMapper( DeprecatedMapperWithMethod.class ); + Method method = mapper.getClass().getMethod( "map", DeprecatedMapperWithMethod.Source.class ); + Deprecated annotation = method.getAnnotation( Deprecated.class ); + assertThat( annotation ).isNotNull(); + assertThat( annotation.since() ).isEqualTo( "18" ); + assertThat( annotation.forRemoval() ).isEqualTo( false ); + } + + @ProcessorTest + @WithClasses(DeprecatedMapperWithClass.class) + public void deprecatedWithClassCorrectCopyForJdk11() { + DeprecatedMapperWithClass mapper = Mappers.getMapper( DeprecatedMapperWithClass.class ); + Deprecated annotation = mapper.getClass().getAnnotation( Deprecated.class ); + assertThat( annotation ).isNotNull(); + assertThat( annotation.since() ).isEqualTo( "11" ); + } + + @ProcessorTest + @WithClasses( { RepeatDeprecatedMapperWithParams.class}) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.WARNING, + type = RepeatDeprecatedMapperWithParams.class, + message = "Annotation \"Deprecated\" is already present with the " + + "same elements configuration." + ) + } + ) + public void bothExistPriorityAnnotateWithForJdk11() { + RepeatDeprecatedMapperWithParams mapper = Mappers.getMapper( RepeatDeprecatedMapperWithParams.class ); + Deprecated deprecated = mapper.getClass().getAnnotation( Deprecated.class ); + assertThat( deprecated ).isNotNull(); + assertThat( deprecated.since() ).isEqualTo( "1.5" ); + assertThat( deprecated.forRemoval() ).isEqualTo( false ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/RepeatDeprecatedMapperWithParams.java b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/RepeatDeprecatedMapperWithParams.java new file mode 100644 index 0000000000..971791bfd2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/annotatewith/deprecated/jdk11/RepeatDeprecatedMapperWithParams.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.annotatewith.deprecated.jdk11; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; + +@Mapper +@Deprecated( since = "1.8" ) +@AnnotateWith( + value = Deprecated.class, + elements = { + @AnnotateWith.Element( name = "forRemoval", booleans = false), + @AnnotateWith.Element( name = "since", strings = "1.5") + } +) +public class RepeatDeprecatedMapperWithParams { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/array/ArrayMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/array/ArrayMappingTest.java index 07113f7cf4..fe33893417 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/array/ArrayMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/array/ArrayMappingTest.java @@ -9,7 +9,9 @@ import java.util.List; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.array._target.GenericScientistDto; import org.mapstruct.ap.test.array._target.ScientistDto; +import org.mapstruct.ap.test.array.source.GenericScientist; import org.mapstruct.ap.test.array.source.Scientist; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; @@ -18,7 +20,8 @@ import static org.assertj.core.api.Assertions.assertThat; -@WithClasses( { Scientist.class, ScientistDto.class, ScienceMapper.class } ) +@WithClasses( { Scientist.class, ScientistDto.class, GenericScientist.class, GenericScientistDto.class, + ScienceMapper.class } ) @IssueKey("108") public class ArrayMappingTest { @@ -56,30 +59,134 @@ public void shouldForgeMappingForIntToString() { } @ProcessorTest - public void shouldMapArrayToArray() { + public void shouldMapArrayToArrayAndNullToNull() { ScientistDto[] dtos = ScienceMapper.INSTANCE - .scientistsToDtos( new Scientist[]{ new Scientist( "Bob" ), new Scientist( "Larry" ) } ); + .scientistsToDtosReturnNull( new Scientist[]{ new Scientist( "Bob" ), new Scientist( "Larry" ) } ); assertThat( dtos ).isNotNull(); assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.scientistsToDtosReturnNull( (Scientist[]) null ) ).isNull(); + } + + @ProcessorTest + public void shouldMapArrayToArrayAndNullToDefault() { + ScientistDto[] dtos = ScienceMapper.INSTANCE + .scientistsToDtosReturnDefault( new Scientist[]{ new Scientist( "Bob" ), new Scientist( "Larry" ) } ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.scientistsToDtosReturnDefault( (Scientist[]) null ) ).isEmpty(); } @ProcessorTest - public void shouldMapListToArray() { + public void shouldMapListToArrayAndNullToNull() { ScientistDto[] dtos = ScienceMapper.INSTANCE - .scientistsToDtos( Arrays.asList( new Scientist( "Bob" ), new Scientist( "Larry" ) ) ); + .scientistsToDtosReturnNull( Arrays.asList( new Scientist( "Bob" ), new Scientist( "Larry" ) ) ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.scientistsToDtosReturnNull( (List) null ) ).isNull(); + } + + @ProcessorTest + public void shouldMapListToArrayAndNullToDefault() { + ScientistDto[] dtos = ScienceMapper.INSTANCE + .scientistsToDtosReturnDefault( Arrays.asList( new Scientist( "Bob" ), new Scientist( "Larry" ) ) ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.scientistsToDtosReturnDefault( (List) null ) ).isEmpty(); + } + + @ProcessorTest + public void shouldMapArrayToListAndNullToNull() { + List dtos = ScienceMapper.INSTANCE + .scientistsToDtosAsListReturnNull( new Scientist[]{ new Scientist( "Bob" ), new Scientist( "Larry" ) } ); assertThat( dtos ).isNotNull(); assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.scientistsToDtosAsListReturnNull( null ) ).isNull(); } @ProcessorTest - public void shouldMapArrayToList() { + public void shouldMapArrayToListAndNullToDefault() { List dtos = ScienceMapper.INSTANCE - .scientistsToDtosAsList( new Scientist[]{ new Scientist( "Bob" ), new Scientist( "Larry" ) } ); + .scientistsToDtosAsListReturnDefault( + new Scientist[]{ new Scientist( "Bob" ), new Scientist( "Larry" ) } ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.scientistsToDtosAsListReturnDefault( null ) ).isEmpty(); + } + + @ProcessorTest + @SuppressWarnings("unchecked") + public void shouldMapGenericArrayToGenericArrayAndNullToNull() { + GenericScientistDto[] dtos = ScienceMapper.INSTANCE + .genericScientistToDtosReturnNull( + new GenericScientist[]{ new GenericScientist<>( "Bob" ), new GenericScientist<>( "Larry" ) } ); assertThat( dtos ).isNotNull(); assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.genericScientistToDtosReturnNull( (GenericScientist[]) null ) ) + .isNull(); + } + + @ProcessorTest + @SuppressWarnings("unchecked") + public void shouldMapGenericArrayToGenericArrayAndNullToDefault() { + GenericScientistDto[] dtos = ScienceMapper.INSTANCE.genericScientistToDtosReturnDefault( + new GenericScientist[]{ new GenericScientist<>( "Bob" ), new GenericScientist<>( "Larry" ) } ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.genericScientistToDtosReturnDefault( (GenericScientist[]) null ) ) + .isEmpty(); + } + + @ProcessorTest + public void shouldMapListToGenericArrayAndNullToNull() { + GenericScientistDto[] dtos = ScienceMapper.INSTANCE.genericScientistToDtosReturnNull( + Arrays.asList( new GenericScientist<>( "Bob" ), new GenericScientist<>( "Larry" ) ) ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.genericScientistToDtosReturnNull( (List>) null ) ) + .isNull(); + } + + @ProcessorTest + public void shouldMapListToGenericArrayAndNullToDefault() { + GenericScientistDto[] dtos = ScienceMapper.INSTANCE + .genericScientistToDtosReturnDefault( + Arrays.asList( new GenericScientist<>("Bob"), new GenericScientist<>( "Larry" ) ) ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE + .genericScientistToDtosReturnDefault( (List>) null ) ).isEmpty(); + } + + @ProcessorTest + @SuppressWarnings("unchecked") + public void shouldMapGenericArrayToListAndNullToNull() { + List> dtos = ScienceMapper.INSTANCE.genericScientistToDtosAsList( + new GenericScientist[]{ new GenericScientist<>( "Bob" ), new GenericScientist<>( "Larry" ) } ); + + assertThat( dtos ).isNotNull(); + assertThat( dtos ).extracting( "name" ).containsOnly( "Bob", "Larry" ); + + assertThat( ScienceMapper.INSTANCE.genericScientistToDtosAsList( null ) ).isNull(); } @ProcessorTest @@ -123,15 +230,15 @@ public void shouldMapArrayToArrayExistingLargerSizedTarget() { } @ProcessorTest - public void shouldMapTargetToNullWhenNullSource() { - // TODO: What about existing target? + public void shouldReturnMapTargetWhenNullSource() { ScientistDto[] existingTarget = new ScientistDto[]{ new ScientistDto( "Jim" ) }; ScientistDto[] target = ScienceMapper.INSTANCE.scientistsToDtos( null, existingTarget ); - assertThat( target ).isNull(); + assertThat( target ).isNotNull(); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).extracting( "name" ).containsOnly( "Jim" ); } @@ -143,6 +250,7 @@ public void shouldMapBooleanWhenReturnDefault() { boolean[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( false ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( false ); assertThat( ScienceMapper.INSTANCE.nvmMapping( null ) ).isEmpty(); @@ -154,6 +262,7 @@ public void shouldMapShortWhenReturnDefault() { short[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( new short[] { 0 } ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( new short[] { 0 } ); } @@ -163,6 +272,7 @@ public void shouldMapCharWhenReturnDefault() { char[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( new char[] { 0 } ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( new char[] { 0 } ); } @@ -172,6 +282,7 @@ public void shouldMapIntWhenReturnDefault() { int[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( 0 ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( 0 ); } @@ -181,6 +292,7 @@ public void shouldMapLongWhenReturnDefault() { long[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( 0L ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( 0L ); } @@ -190,6 +302,7 @@ public void shouldMapFloatWhenReturnDefault() { float[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( 0.0f ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( 0.0f ); } @@ -199,6 +312,7 @@ public void shouldMapDoubleWhenReturnDefault() { double[] target = ScienceMapper.INSTANCE.nvmMapping( null, existingTarget ); assertThat( target ).containsOnly( 0.0d ); + assertThat( target ).isEqualTo( existingTarget ); assertThat( existingTarget ).containsOnly( 0.0d ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/array/ScienceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/array/ScienceMapper.java index b0dac0ea98..125dc94c9f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/array/ScienceMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/array/ScienceMapper.java @@ -11,7 +11,9 @@ import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.array._target.GenericScientistDto; import org.mapstruct.ap.test.array._target.ScientistDto; +import org.mapstruct.ap.test.array.source.GenericScientist; import org.mapstruct.ap.test.array.source.Scientist; import org.mapstruct.factory.Mappers; @@ -22,14 +24,37 @@ public interface ScienceMapper { ScientistDto scientistToDto(Scientist scientist); - ScientistDto[] scientistsToDtos(Scientist[] scientists); + ScientistDto[] scientistsToDtosReturnNull(Scientist[] scientists); - ScientistDto[] scientistsToDtos(List scientists); + ScientistDto[] scientistsToDtosReturnNull(List scientists); - List scientistsToDtosAsList(Scientist[] scientists); + List scientistsToDtosAsListReturnNull(Scientist[] scientists); ScientistDto[] scientistsToDtos(Scientist[] scientists, @MappingTarget ScientistDto[] target); + @IterableMapping( nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT ) + ScientistDto[] scientistsToDtosReturnDefault(Scientist[] scientists); + + @IterableMapping( nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT ) + ScientistDto[] scientistsToDtosReturnDefault(List scientists); + + @IterableMapping( nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT ) + List scientistsToDtosAsListReturnDefault(Scientist[] scientists); + + GenericScientistDto genericScientistToDto(GenericScientist scientist); + + GenericScientistDto[] genericScientistToDtosReturnNull(GenericScientist[] genericScientists); + + GenericScientistDto[] genericScientistToDtosReturnNull(List> genericScientists); + + List> genericScientistToDtosAsList(GenericScientist[] genericScientists); + + @IterableMapping( nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT ) + GenericScientistDto[] genericScientistToDtosReturnDefault(GenericScientist[] genericScientists); + + @IterableMapping( nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT ) + GenericScientistDto[] genericScientistToDtosReturnDefault(List> genericScientists); + @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) boolean[] nvmMapping(boolean[] source); diff --git a/processor/src/test/java/org/mapstruct/ap/test/array/_target/GenericScientistDto.java b/processor/src/test/java/org/mapstruct/ap/test/array/_target/GenericScientistDto.java new file mode 100644 index 0000000000..ffab268e61 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/array/_target/GenericScientistDto.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.array._target; + +public class GenericScientistDto { + + //CHECKSTYLE:OFF + public T[] publicPublications; + public int[] publicPublicationYears; + //CHECKSTYLE:ON + + private T name; + private T[] publications; + private int[] publicationYears; + + public GenericScientistDto() { + } + + public GenericScientistDto(T name) { + this.name = name; + } + + public T getName() { + return name; + } + + public void setName(T name) { + this.name = name; + } + + public T[] getPublications() { + return publications; + } + + public void setPublications(T[] publications) { + this.publications = publications; + } + + public int[] getPublicationYears() { + return publicationYears; + } + + public void setPublicationYears(int[] publicationYears) { + this.publicationYears = publicationYears; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/array/source/GenericScientist.java b/processor/src/test/java/org/mapstruct/ap/test/array/source/GenericScientist.java new file mode 100644 index 0000000000..6a6f9a8cda --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/array/source/GenericScientist.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.array.source; + +public class GenericScientist { + + //CHECKSTYLE:OFF + public T[] publicPublications; + public T[] publicPublicationYears; + //CHECKSTYLE:ON + private T name; + private T[] publications; + private T[] publicationYears; + + public GenericScientist(T name) { + this.name = name; + } + + public T getName() { + return name; + } + + public void setName(T name) { + this.name = name; + } + + public T[] getPublications() { + return publications; + } + + public void setPublications(T[] publications) { + this.publications = publications; + } + + public T[] getPublicationYears() { + return publicationYears; + } + + public void setPublicationYears(T[] publicationYears) { + this.publicationYears = publicationYears; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Mapper.java index a5a624e684..322034d590 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Mapper.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.test.bugs._1111; import java.util.List; + import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @@ -24,8 +25,29 @@ public interface Issue1111Mapper { List> listList(List> in); - class Source { } + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } - class Target { } + public String getValue() { + return value; + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Test.java index 5619b64175..f5efb21e3a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1111/Issue1111Test.java @@ -27,7 +27,7 @@ public class Issue1111Test { @ProcessorTest public void shouldCompile() { - List> source = Arrays.asList( Arrays.asList( new Source() ) ); + List> source = Arrays.asList( Arrays.asList( new Source( "test" ) ) ); List> target = Issue1111Mapper.INSTANCE.listList( source ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1130/Issue1130Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1130/Issue1130Mapper.java index 042d5a2f8b..350d95eebc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1130/Issue1130Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1130/Issue1130Mapper.java @@ -30,6 +30,16 @@ public void setB(BEntity b) { } static class BEntity { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } } static class ADto { @@ -46,6 +56,7 @@ public void setB(BDto b) { class BDto { private final String passedViaConstructor; + private String id; BDto(String passedViaConstructor) { this.passedViaConstructor = passedViaConstructor; @@ -54,6 +65,14 @@ class BDto { String getPassedViaConstructor() { return passedViaConstructor; } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } } abstract void mergeA(AEntity source, @MappingTarget ADto target); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1170/PetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1170/PetMapper.java index 96b7e8b72f..10acf7f9c5 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1170/PetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1170/PetMapper.java @@ -9,9 +9,8 @@ import java.util.Map; import java.util.stream.Collectors; -import org.mapstruct.Mapper; - import com.google.common.collect.ImmutableMap; +import org.mapstruct.Mapper; /** * @author Cornelius Dirmeier diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/ErroneousIssue1242MapperMultipleSources.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/ErroneousIssue1242MapperMultipleSources.java index 7259d343b8..7a8ad05542 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/ErroneousIssue1242MapperMultipleSources.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/ErroneousIssue1242MapperMultipleSources.java @@ -7,13 +7,14 @@ import org.mapstruct.Mapper; import org.mapstruct.ObjectFactory; +import org.mapstruct.ReportingPolicy; /** * Results in an ambiguous factory method error, as there are two methods with matching source types available. * * @author Andreas Gudian */ -@Mapper(uses = TargetFactories.class) +@Mapper(uses = TargetFactories.class, unmappedTargetPolicy = ReportingPolicy.IGNORE) public abstract class ErroneousIssue1242MapperMultipleSources { abstract TargetA toTargetA(SourceA source); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Mapper.java index 0491b5ffe0..90ea60a2ee 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Mapper.java @@ -7,13 +7,14 @@ import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; +import org.mapstruct.ReportingPolicy; /** * Test mapper for properly resolving the best fitting factory method * * @author Andreas Gudian */ -@Mapper(uses = TargetFactories.class) +@Mapper(uses = TargetFactories.class, unmappedTargetPolicy = ReportingPolicy.IGNORE) public abstract class Issue1242Mapper { abstract TargetA toTargetA(SourceA source); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java index f27051670c..036a63f312 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java @@ -54,7 +54,7 @@ public void factoryMethodWithSourceParamIsChosen() { diagnostics = { @Diagnostic(type = ErroneousIssue1242MapperMultipleSources.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 20, + line = 21, message = "Ambiguous factory methods found for creating TargetB: " + "TargetB anotherTargetBCreator(SourceB source), " + "TargetB TargetFactories.createTargetB(SourceB source, @TargetType Class clazz), " + diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/TargetB.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/TargetB.java index ab4f639697..6a28adc828 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/TargetB.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/TargetB.java @@ -9,6 +9,7 @@ * @author Andreas Gudian */ class TargetB { + protected String value; private final String passedViaConstructor; TargetB(String passedViaConstructor) { @@ -18,4 +19,12 @@ class TargetB { String getPassedViaConstructor() { return passedViaConstructor; } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1345/Issue1345Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1345/Issue1345Mapper.java index 7699a8f7e9..b2d89c96e6 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1345/Issue1345Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1345/Issue1345Mapper.java @@ -27,8 +27,17 @@ public interface Issue1345Mapper { class A { + private String value; private String readOnlyProperty; + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + public String getReadOnlyProperty() { return readOnlyProperty; } @@ -36,8 +45,17 @@ public String getReadOnlyProperty() { class B { + private String value; private String property; + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + public String getProperty() { return property; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/Issue1395Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Issue1395Mapper.java similarity index 91% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/Issue1395Mapper.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Issue1395Mapper.java index ebc9ae27e0..4e48020c8d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/Issue1395Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Issue1395Mapper.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._1395; +package org.mapstruct.ap.test.bugs._1395.spring; import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/Issue1395Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Issue1395Test.java similarity index 83% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/Issue1395Test.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Issue1395Test.java index 757436a0f4..3417fbd707 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/Issue1395Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Issue1395Test.java @@ -3,11 +3,12 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._1395; +package org.mapstruct.ap.test.bugs._1395.spring; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; /** * @author Filip Hrisafov @@ -18,6 +19,7 @@ Source.class, Target.class } ) +@WithSpring @IssueKey( "1395" ) public class Issue1395Test { diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/NotUsedService.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/NotUsedService.java similarity index 81% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/NotUsedService.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/NotUsedService.java index 765764ef2b..ba47cf179f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/NotUsedService.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/NotUsedService.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._1395; +package org.mapstruct.ap.test.bugs._1395.spring; /** * @author Filip Hrisafov diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Source.java new file mode 100644 index 0000000000..8f2a52d462 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Source.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1395.spring; + +/** + * @author Filip Hrisafov + */ +public class Source { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Target.java new file mode 100644 index 0000000000..1ba393dbe0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1395/spring/Target.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1395.spring; + +/** + * @author Filip Hrisafov + */ +public class Target { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1425/Issue1425Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1425/Issue1425Test.java index 8c5700772f..a71c33c396 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1425/Issue1425Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1425/Issue1425Test.java @@ -9,6 +9,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJoda; import static org.assertj.core.api.Assertions.assertThat; @@ -21,6 +22,7 @@ Target.class }) @IssueKey("1425") +@WithJoda public class Issue1425Test { @ProcessorTest diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1435/Config.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1435/Config.java index 911cbf1f9d..b76e6538f2 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1435/Config.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1435/Config.java @@ -5,12 +5,12 @@ */ package org.mapstruct.ap.test.bugs._1435; +import java.util.Objects; + import org.mapstruct.MapperConfig; import org.mapstruct.Mapping; import org.mapstruct.MappingInheritanceStrategy; -import java.util.Objects; - @MapperConfig(mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG, imports = Objects.class) public interface Config { @Mapping(expression = "java( Objects.equals( source.getName(), \"Rainbow Dash\" ) )", target = "rainbowDash") diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Mapper.java index 90ed83d28a..1d0d4fccbb 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Mapper.java @@ -18,13 +18,13 @@ public abstract class Issue1460Mapper { public abstract Target map(Source source); - public abstract String forceUsageOfIssue1460Enum(Issue1460Enum source); + public abstract Value forceUsageOfIssue1460Enum(Issue1460Enum source); - public abstract String forceUsageOfLocale(Locale source); + public abstract Value forceUsageOfLocale(Locale source); - public abstract String forceUsageOfLocalDate(LocalDate source); + public abstract Value forceUsageOfLocalDate(LocalDate source); - public abstract String forceUsageOfDateTime(DateTime source); + public abstract Value forceUsageOfDateTime(DateTime source); public static class Issue1460Enum { } @@ -37,4 +37,17 @@ public static class LocalDate { public static class DateTime { } + + public static class Value { + + private final T source; + + public Value(T source) { + this.source = source; + } + + public T getSource() { + return source; + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Test.java index 077e66df76..e9c97f39bc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/Issue1460Test.java @@ -12,6 +12,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJoda; import static org.assertj.core.api.Assertions.assertThat; @@ -24,6 +25,7 @@ Target.class }) @IssueKey("1460") +@WithJoda public class Issue1460Test { @ProcessorTest diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/java8/Issue1460JavaTimeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/java8/Issue1460JavaTimeMapper.java index 216cda21fd..b9bedfe451 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/java8/Issue1460JavaTimeMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1460/java8/Issue1460JavaTimeMapper.java @@ -18,8 +18,17 @@ public abstract class Issue1460JavaTimeMapper { public abstract Target map(Source source); - public abstract String forceUsageOfLocalDate(LocalDate source); + public abstract LocalTarget forceUsageOfLocalDate(LocalDate source); public static class LocalDate { } + + public static class LocalTarget { + + private final LocalDate source; + + public LocalTarget(LocalDate source) { + this.source = source; + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1482/TargetSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1482/TargetSourceMapper.java index 401d0c42ae..2ac081f611 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1482/TargetSourceMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1482/TargetSourceMapper.java @@ -21,7 +21,7 @@ public abstract class TargetSourceMapper { abstract Source2 map(Target target); protected > T map(String in, @TargetType Classclz ) { - if ( clz.isAssignableFrom( SourceEnum.class )) { + if ( clz.isAssignableFrom( SourceEnum.class ) ) { return (T) SourceEnum.valueOf( in ); } return null; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1561/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1561/Source.java index 23195b81de..5afe78f12c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1561/Source.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1561/Source.java @@ -13,7 +13,7 @@ */ public class Source { - private List properties = new ArrayList(); + private List properties = new ArrayList<>(); public List getProperties() { return properties; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Book.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Book.java index 53355ee603..bfe79c1772 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Book.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Book.java @@ -6,4 +6,14 @@ package org.mapstruct.ap.test.bugs._1590; public class Book { + + private final String name; + + public Book(String name) { + this.name = name; + } + + public String getName() { + return name; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Issue1590Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Issue1590Test.java index d77add25de..0a9113797c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Issue1590Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1590/Issue1590Test.java @@ -28,7 +28,7 @@ public class Issue1590Test { @ProcessorTest public void shouldTestMappingLocalDates() { BookShelf source = new BookShelf(); - source.setBooks( Arrays.asList( new Book() ) ); + source.setBooks( Arrays.asList( new Book("Test") ) ); BookShelf target = BookShelfMapper.INSTANCE.map( source ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/domain/ImmutableItem.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/domain/ImmutableItem.java index f0ea474287..ed5bb98fa1 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/domain/ImmutableItem.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/domain/ImmutableItem.java @@ -154,7 +154,7 @@ public ImmutableItem build() { } private String formatRequiredAttributesMessage() { - List attributes = new ArrayList(); + List attributes = new ArrayList<>(); if ((initBits & INIT_BIT_ID) != 0) attributes.add("id"); return "Cannot build Item, some of required attributes are not set " + attributes; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/dto/ImmutableItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/dto/ImmutableItemDTO.java index 4d84af3479..699e0f9ded 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/dto/ImmutableItemDTO.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1596/dto/ImmutableItemDTO.java @@ -172,7 +172,7 @@ public ImmutableItemDTO build() { } private String formatRequiredAttributesMessage() { - List attributes = new ArrayList(); + List attributes = new ArrayList<>(); if ( ( initBits & INIT_BIT_ID ) != 0 ) { attributes.add( "id" ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/C.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/C.java index 705f2e2d69..dbbd74ec95 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/C.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/C.java @@ -6,4 +6,14 @@ package org.mapstruct.ap.test.bugs._1650; public class C { + + private final int value; + + public C(int value) { + this.value = value; + } + + public int getValue() { + return value; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/CPrime.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/CPrime.java index 24e4376f88..db504189f8 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/CPrime.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/CPrime.java @@ -6,4 +6,14 @@ package org.mapstruct.ap.test.bugs._1650; public class CPrime { + + private int value; + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/Issue1650Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/Issue1650Test.java index 2d1437eb77..33237e726c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/Issue1650Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1650/Issue1650Test.java @@ -27,7 +27,7 @@ public void shouldCompile() { A a = new A(); a.setB( new B() ); - a.getB().setC( new C() ); + a.getB().setC( new C( 10 ) ); APrime aPrime = new APrime(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1719/Issue1719Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1719/Issue1719Test.java index 182e1849be..7a8665e49f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1719/Issue1719Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1719/Issue1719Test.java @@ -23,8 +23,8 @@ public class Issue1719Test { /** * For adder methods MapStuct cannot generate an update method. MapStruct would cannot know how to remove objects - * from the child-parent relation. It cannot even assume that the the collection can be cleared at forehand. - * Therefore the only sensible choice is for MapStruct to create a create method for the target elements. + * from the child-parent relation. It cannot even assume that the collection can be cleared at forehand. + * Therefore, the only sensible choice is for MapStruct to create a create method for the target elements. */ @ProcessorTest @WithClasses(Issue1719Mapper.class) diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java index 80766d853a..d8b5ccdbb3 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java @@ -55,7 +55,7 @@ protected BuilderInfo findBuilderInfo(TypeElement typeElement) { * @return */ private BuilderInfo findBuilderInfoFromInnerBuilderClass(TypeElement typeElement) { - if (shouldIgnore( typeElement )) { + if ( shouldIgnore( typeElement ) ) { return null; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java index ee6076f823..40b9515305 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java @@ -38,7 +38,7 @@ public class Issue1801Test { @ProcessorTest - public void shouldIncludeBuildeType() { + public void shouldIncludeBuilderType() { ItemDTO item = ImmutableItemDTO.builder().id( "test" ).build(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java index 68231f12ef..d5b39cf4dc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java @@ -124,7 +124,7 @@ public final Builder from(Item instance) { /** * Initializes the value for the {@link Item#getId() id} attribute. - * @param id The value for id + * @param id The value for id * @return {@code this} builder for use in a chained invocation */ public final Builder id(String id) { @@ -146,7 +146,7 @@ public ImmutableItem build() { } private String formatRequiredAttributesMessage() { - List attributes = new ArrayList(); + List attributes = new ArrayList<>(); if ((initBits & INIT_BIT_ID) != 0) attributes.add("id"); return "Cannot build Item, some of required attributes are not set " + attributes; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Mapper.java index 08eccae32e..406fa7b71b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Mapper.java @@ -22,11 +22,31 @@ public interface Issue1821Mapper { ExtendedTarget mapExtended(Source source); class Target { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } } class ExtendedTarget extends Target { } class Source { + + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Test.java index 4481ed43e2..865ef8f6c1 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1821/Issue1821Test.java @@ -15,7 +15,7 @@ public class Issue1821Test { @ProcessorTest public void shouldNotGiveNullPtr() { - Issue1821Mapper.INSTANCE.map( new Issue1821Mapper.Source() ); + Issue1821Mapper.INSTANCE.map( new Issue1821Mapper.Source( "test" ) ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/Car.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/Car.java new file mode 100644 index 0000000000..87722cdb50 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/Car.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1997; + +/** + * @author Filip Hrisafov + */ +public class Car { + private String model; + + private Car(Builder builder) { + this.model = builder.model; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String model; + + public Builder model(String model) { + this.model = model; + return this; + } + + public Car build() { + return new Car( this ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarDetail.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarDetail.java new file mode 100644 index 0000000000..84472d1263 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarDetail.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1997; + +/** + * @author Filip Hrisafov + */ +public class CarDetail { + private String model; + + private CarDetail(Builder builder) { + this.model = builder.model; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String model; + + public Builder model(String model) { + this.model = model; + return this; + } + + public CarDetail build() { + return new CarDetail( this ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarInsurance.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarInsurance.java new file mode 100644 index 0000000000..83fb4350dc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarInsurance.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1997; + +/** + * @author Filip Hrisafov + */ +public class CarInsurance { + private CarDetail detail; + + private CarInsurance(Builder builder) { + this.detail = builder.detail; + } + + public CarDetail getDetail() { + return detail; + } + + public void setDetail(CarDetail detail) { + this.detail = detail; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private CarDetail detail; + + public Builder detail(CarDetail detail) { + this.detail = detail; + return this; + } + + public CarInsurance build() { + return new CarInsurance( this ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarInsuranceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarInsuranceMapper.java new file mode 100644 index 0000000000..79369b9a79 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/CarInsuranceMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1997; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CarInsuranceMapper { + + CarInsuranceMapper INSTANCE = Mappers.getMapper( CarInsuranceMapper.class ); + + @Mapping(source = "model", target = "detail.model") + void update(Car source, @MappingTarget CarInsurance target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/Issue1997Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/Issue1997Test.java new file mode 100644 index 0000000000..5645ca173b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1997/Issue1997Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._1997; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Car.class, + CarDetail.class, + CarInsurance.class, + CarInsuranceMapper.class +}) +class Issue1997Test { + + @ProcessorTest + void shouldCorrectCreateIntermediateObjectsWithBuilder() { + Car source = Car.builder().model( "Model S" ).build(); + CarInsurance target = CarInsurance.builder().build(); + assertThat( target.getDetail() ).isNull(); + + CarInsuranceMapper.INSTANCE.update( source, target ); + + assertThat( target.getDetail() ).isNotNull(); + assertThat( target.getDetail().getModel() ).isEqualTo( "Model S" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java index 1b8969664d..f639f4e0dc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Mapper.java @@ -73,12 +73,12 @@ class ObjectFactory { @XmlElementDecl(namespace = "http://www.test.com/test", name = "Nested") public JAXBElement createNested(Nested value) { - return new JAXBElement( Q_NAME, Nested.class, null, value ); + return new JAXBElement<>( Q_NAME, Nested.class, null, value ); } @XmlElementDecl(namespace = "http://www.test.com/test", name = "nested", scope = Nested.class) public JAXBElement createNestedInNestedTarget(Nested value) { - return new JAXBElement( Q_NAME_NESTED, Nested.class, Target.class, value ); + return new JAXBElement<>( Q_NAME_NESTED, Nested.class, Target.class, value ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java index ec8bb4603c..38aed76003 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2145/Issue2145Test.java @@ -8,11 +8,13 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxJaxb; import static org.assertj.core.api.Assertions.assertThat; @IssueKey("2145") @WithClasses(Issue2145Mapper.class) +@WithJavaxJaxb public class Issue2145Test { @ProcessorTest diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2149/Issue2149Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2149/Issue2149Test.java index 8c19e2e090..39c415f340 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2149/Issue2149Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2149/Issue2149Test.java @@ -30,12 +30,6 @@ public class Issue2149Test { line = 18, message = "Using @BeanMapping( ignoreByDefault = true ) with @Mapping( target = \".\", ... ) is not " + "allowed. You'll need to explicitly ignore the target properties that should be ignored instead." - ), - @Diagnostic( - type = Erroneous2149Mapper.class, - kind = javax.tools.Diagnostic.Kind.WARNING, - line = 20, - message = "Unmapped target property: \"address\"." ) } ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2278/Issue2278Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2278/Issue2278Test.java index 8c8a4cf01f..7289da4ff1 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2278/Issue2278Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2278/Issue2278Test.java @@ -5,12 +5,12 @@ */ package org.mapstruct.ap.test.bugs._2278; -import static org.assertj.core.api.Assertions.assertThat; - import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import static org.assertj.core.api.Assertions.assertThat; + @IssueKey("2278") public class Issue2278Test { diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2530/Issue2530Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2530/Issue2530Mapper.java new file mode 100644 index 0000000000..268237376c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2530/Issue2530Mapper.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2530; + +import java.time.LocalDate; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Issue2530Mapper { + + Issue2530Mapper INSTANCE = Mappers.getMapper( Issue2530Mapper.class ); + + @Mapping(target = "date", source = ".", dateFormat = "yyyy-MM-dd") + Test map(String s); + + class Test { + + LocalDate date; + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate dateTime) { + this.date = dateTime; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2530/Issue2530Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2530/Issue2530Test.java new file mode 100644 index 0000000000..2999939a78 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2530/Issue2530Test.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2530; + +import java.time.Month; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ben Zegveld + */ +@IssueKey("2530") +@WithClasses({ + Issue2530Mapper.class +}) +public class Issue2530Test { + + @ProcessorTest + public void shouldConvert() { + Issue2530Mapper.Test target = Issue2530Mapper.INSTANCE.map( "2021-07-31" ); + + assertThat( target ).isNotNull(); + assertThat( target.getDate().getYear() ).isEqualTo( 2021 ); + assertThat( target.getDate().getMonth() ).isEqualTo( Month.JULY ); + assertThat( target.getDate().getDayOfMonth() ).isEqualTo( 31 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2537/ImplicitSourceTest.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2537/ImplicitSourceTest.java new file mode 100644 index 0000000000..edcbbfd4d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2537/ImplicitSourceTest.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2537; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * implicit source-target mapping should list the source property as being mapped. + * + * @author Ben Zegveld + */ +@WithClasses( { UnmappedSourcePolicyWithImplicitSourceMapper.class } ) +@IssueKey( "2537" ) +public class ImplicitSourceTest { + + @ProcessorTest + public void situationCompilesWithoutErrors() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2537/UnmappedSourcePolicyWithImplicitSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2537/UnmappedSourcePolicyWithImplicitSourceMapper.java new file mode 100644 index 0000000000..21d8ca5332 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2537/UnmappedSourcePolicyWithImplicitSourceMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2537; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +/** + * @author Ben Zegveld + */ +@Mapper( unmappedSourcePolicy = ReportingPolicy.ERROR ) +public interface UnmappedSourcePolicyWithImplicitSourceMapper { + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "property" ) + Target map(Source source); + + class Source { + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + } + + class Target { + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/Group.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/Group.java new file mode 100644 index 0000000000..4451431fee --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/Group.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2538; + +/** + * @author Filip Hrisafov + */ +public class Group { + + private final String id; + + public Group(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/GroupDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/GroupDto.java new file mode 100644 index 0000000000..0d94c90815 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/GroupDto.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2538; + +/** + * @author Filip Hrisafov + */ +public class GroupDto { + + private final String id; + + public GroupDto(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/Issue2538Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/Issue2538Test.java new file mode 100644 index 0000000000..cf99e67f92 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/Issue2538Test.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2538; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Group.class, + GroupDto.class, + TeamMapper.class, + TeamRole.class, + TeamRoleDto.class, +}) +class Issue2538Test { + + @ProcessorTest + void shouldCorrectlyUseQualifiedMethodIn2StepMapping() { + TeamRole role = TeamMapper.INSTANCE.mapUsingFirstLookup( new TeamRoleDto( "test" ) ); + + assertThat( role ).isNotNull(); + assertThat( role.getGroup() ).isNotNull(); + assertThat( role.getGroup().getId() ).isEqualTo( "lookup-test" ); + + role = TeamMapper.INSTANCE.mapUsingSecondLookup( new TeamRoleDto( "test" ) ); + + assertThat( role ).isNotNull(); + assertThat( role.getGroup() ).isNotNull(); + assertThat( role.getGroup().getId() ).isEqualTo( "second-test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamMapper.java new file mode 100644 index 0000000000..5615e7bc43 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamMapper.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2538; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface TeamMapper { + + TeamMapper INSTANCE = Mappers.getMapper( TeamMapper.class ); + + // This method is testing methodX(methodY(...)) where methodY is qualified + @Mapping(target = "group", source = "groupId", qualifiedByName = "firstLookup") + TeamRole mapUsingFirstLookup(TeamRoleDto in); + + // This method is testing methodX(methodY(...)) where methodX is qualified + @Mapping(target = "group", source = "groupId", qualifiedByName = "secondLookup") + TeamRole mapUsingSecondLookup(TeamRoleDto in); + + Group map(GroupDto in); + + @Named("firstLookup") + default GroupDto lookupGroup(String groupId) { + return groupId != null ? new GroupDto( "lookup-" + groupId ) : null; + } + + default GroupDto normalLookup(String groupId) { + return groupId != null ? new GroupDto( groupId ) : null; + } + + @Named("secondLookup") + default Group mapSecondLookup(GroupDto in) { + return in != null ? new Group( "second-" + in.getId() ) : null; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamRole.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamRole.java new file mode 100644 index 0000000000..f73700fa4d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamRole.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2538; + +/** + * @author Filip Hrisafov + */ +public class TeamRole { + + private final Group group; + + public TeamRole(Group group) { + this.group = group; + } + + public Group getGroup() { + return group; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamRoleDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamRoleDto.java new file mode 100644 index 0000000000..fac5a1a9ee --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2538/TeamRoleDto.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2538; + +/** + * @author Filip Hrisafov + */ +public class TeamRoleDto { + + private final String groupId; + + public TeamRoleDto(String groupId) { + this.groupId = groupId; + } + + public String getGroupId() { + return groupId; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Issue2541Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Issue2541Mapper.java new file mode 100644 index 0000000000..0531f5242c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Issue2541Mapper.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2541; + +import java.util.Optional; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue2541Mapper { + + Issue2541Mapper INSTANCE = Mappers.getMapper( Issue2541Mapper.class ); + + Target map(Source source); + + default Optional toOptional(@Nullable T value) { + return Optional.ofNullable( value ); + } + + class Target { + private Optional value; + + public Optional getValue() { + return value; + } + + public void setValue(Optional value) { + this.value = value; + } + } + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Issue2541Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Issue2541Test.java new file mode 100644 index 0000000000..d1b8bd7f52 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Issue2541Test.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2541; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Issue2541Mapper.class, + Nullable.class, +}) +class Issue2541Test { + + @ProcessorTest + void shouldGenerateCorrectCode() { + Issue2541Mapper.Target target = Issue2541Mapper.INSTANCE.map( new Issue2541Mapper.Source( null ) ); + + assertThat( target.getValue() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Nullable.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Nullable.java new file mode 100644 index 0000000000..24c52eb906 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2541/Nullable.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2541; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ + ElementType.METHOD, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Nullable { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2544/Issue2544Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2544/Issue2544Mapper.java new file mode 100644 index 0000000000..6d02d709a6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2544/Issue2544Mapper.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2544; + +import java.math.BigDecimal; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Issue2544Mapper { + + Issue2544Mapper INSTANCE = Mappers.getMapper( Issue2544Mapper.class ); + + @Mapping( target = "bigNumber", source = ".", numberFormat = "##0.#####E0" ) + Target map(String s); + + class Target { + + private BigDecimal bigNumber; + + public BigDecimal getBigNumber() { + return bigNumber; + } + + public void setBigNumber(BigDecimal bigNumber) { + this.bigNumber = bigNumber; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2544/Issue2544Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2544/Issue2544Test.java new file mode 100644 index 0000000000..4f58f81d08 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2544/Issue2544Test.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2544; + +import java.math.BigDecimal; + +import org.junitpioneer.jupiter.DefaultLocale; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ben Zegveld + */ +@IssueKey( "2544" ) +@WithClasses( { Issue2544Mapper.class } ) +public class Issue2544Test { + // Parsing numbers is sensitive to locale settings (e.g. decimal point) + + @ProcessorTest + @DefaultLocale("en") + public void shouldConvertEn() { + Issue2544Mapper.Target target = Issue2544Mapper.INSTANCE.map( "123.45679E6" ); + + assertThat( target ).isNotNull(); + assertThat( target.getBigNumber() ).isEqualTo( new BigDecimal( "1.2345679E+8" ) ); + } + + @ProcessorTest + @DefaultLocale("de") + public void shouldConvertDe() { + Issue2544Mapper.Target target = Issue2544Mapper.INSTANCE.map( "123,45679E6" ); + + assertThat( target ).isNotNull(); + assertThat( target.getBigNumber() ).isEqualTo( new BigDecimal( "1.2345679E+8" ) ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2624/Issue2624Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2624/Issue2624Mapper.java new file mode 100644 index 0000000000..f4d3c3d22d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2624/Issue2624Mapper.java @@ -0,0 +1,76 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2624; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2624Mapper { + + Issue2624Mapper INSTANCE = Mappers.getMapper( Issue2624Mapper.class ); + + @Mapping( target = "department.id", source = "did") + @Mapping( target = "department.name", source = "dname") + Employee fromMap(Map source); + + class Employee { + private String id; + private String name; + private Department department; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } + } + + class Department { + private String id; + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2624/Issue2624Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2624/Issue2624Test.java new file mode 100644 index 0000000000..2fb04ef3a0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2624/Issue2624Test.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2624; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Issue2624Mapper.class +}) +class Issue2624Test { + + @ProcessorTest + void shouldCorrectlyMapNestedTargetFromMap() { + Map map = new HashMap<>(); + map.put( "id", "1234" ); + map.put( "name", "Tester" ); + map.put( "did", "4321" ); //Department Id + map.put( "dname", "Test" ); // Department name + + Issue2624Mapper.Employee employee = Issue2624Mapper.INSTANCE.fromMap( map ); + + assertThat( employee ).isNotNull(); + assertThat( employee.getId() ).isEqualTo( "1234" ); + assertThat( employee.getName() ).isEqualTo( "Tester" ); + + Issue2624Mapper.Department department = employee.getDepartment(); + assertThat( department ).isNotNull(); + assertThat( department.getId() ).isEqualTo( "4321" ); + assertThat( department.getName() ).isEqualTo( "Test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java new file mode 100644 index 0000000000..0ac236372a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper( uses = NullableHelper.class ) +public interface Issue2663Mapper { + + Issue2663Mapper INSTANCE = Mappers.getMapper( Issue2663Mapper.class ); + + Request map(RequestDto dto); + + default JsonNullable mapJsonNullableChildren(JsonNullable dtos) { + if ( dtos.isPresent() ) { + return JsonNullable.of( mapChild( dtos.get() ) ); + } + else { + return JsonNullable.undefined(); + } + } + + Request.ChildRequest mapChild(RequestDto.ChildRequestDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java new file mode 100644 index 0000000000..efd14b1d36 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey( "2663" ) +@WithClasses({ + Issue2663Mapper.class, + JsonNullable.class, + Nullable.class, + NullableHelper.class, + Request.class, + RequestDto.class +}) +public class Issue2663Test { + + @ProcessorTest + public void shouldUnpackGenericsCorrectly() { + RequestDto dto = new RequestDto(); + dto.setName( JsonNullable.of( "Tester" ) ); + + Request request = Issue2663Mapper.INSTANCE.map( dto ); + + assertThat( request.getName() ) + .extracting( Nullable::get ) + .isEqualTo( "Tester" ); + + dto.setName( JsonNullable.undefined() ); + + request = Issue2663Mapper.INSTANCE.map( dto ); + + assertThat( request.getName() ) + .extracting( Nullable::isPresent ) + .isEqualTo( false ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java new file mode 100644 index 0000000000..0c1aeaba93 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +import java.util.NoSuchElementException; + +/** + * @author Filip Hrisafov + */ +public class JsonNullable { + + private static final JsonNullable UNDEFINED = new JsonNullable<>( null, false ); + + private final T value; + private final boolean present; + + private JsonNullable(T value, boolean present) { + this.value = value; + this.present = present; + } + + public T get() { + if ( !present ) { + throw new NoSuchElementException("Value is undefined"); + } + return value; + } + + public boolean isPresent() { + return present; + } + + @SuppressWarnings("unchecked") + public static JsonNullable undefined() { + return (JsonNullable) UNDEFINED; + } + + public static JsonNullable of(T value) { + return new JsonNullable<>( value, true ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java new file mode 100644 index 0000000000..6701793ed9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +import java.util.NoSuchElementException; + +/** + * @author Filip Hrisafov + */ +public class Nullable { + + @SuppressWarnings("rawtypes") + private static final Nullable UNDEFINED = new Nullable<>( null, false ); + + private final T value; + private final boolean present; + + private Nullable(T value, boolean present) { + this.value = value; + this.present = present; + } + + public T get() { + if ( !present ) { + throw new NoSuchElementException("Value is undefined"); + } + return value; + } + + public boolean isPresent() { + return present; + } + + public static Nullable of(T value) { + return new Nullable<>( value, true ); + } + + @SuppressWarnings("unchecked") + public static Nullable undefined() { + return (Nullable) UNDEFINED; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java new file mode 100644 index 0000000000..ae68300e62 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +/** + * @author Filip Hrisafov + */ +public class NullableHelper { + + private NullableHelper() { + // Helper class + } + + public static Nullable jsonNullableToNullable(JsonNullable jsonNullable) { + if ( jsonNullable.isPresent() ) { + return Nullable.of( jsonNullable.get() ); + } + return Nullable.undefined(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java new file mode 100644 index 0000000000..8b58e2e0b5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +/** + * @author Filip Hrisafov + */ +public class Request { + + private Nullable name = Nullable.undefined(); + private Nullable child = Nullable.undefined(); + + public Nullable getName() { + return name; + } + + public void setName(Nullable name) { + this.name = name; + } + + public Nullable getChild() { + return child; + } + + public void setChild(Nullable child) { + this.child = child; + } + + public static class ChildRequest { + + private Nullable name = Nullable.undefined(); + + public Nullable getName() { + return name; + } + + public void setName(Nullable name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java new file mode 100644 index 0000000000..d1a93a9bd4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2663; + +/** + * @author Filip Hrisafov + */ +public class RequestDto { + + private JsonNullable name = JsonNullable.undefined(); + private JsonNullable child = JsonNullable.undefined(); + + public JsonNullable getName() { + return name; + } + + public void setName(JsonNullable name) { + this.name = name; + } + + public JsonNullable getChild() { + return child; + } + + public void setChild(JsonNullable child) { + this.child = child; + } + + public static class ChildRequestDto { + + private JsonNullable name = JsonNullable.undefined(); + + public JsonNullable getName() { + return name; + } + + public void setName(JsonNullable name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Erroneous2668ListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Erroneous2668ListMapper.java new file mode 100644 index 0000000000..beb1d3283a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Erroneous2668ListMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2668; + +import java.util.ArrayList; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Erroneous2668ListMapper { + + Erroneous2668ListMapper INSTANCE = Mappers.getMapper( Erroneous2668ListMapper.class ); + + CollectionTarget map(CollectionSource source); + + class CollectionTarget { + MyArrayList list; + + public MyArrayList getList() { + return list; + } + + public void setList(MyArrayList list) { + this.list = list; + } + } + + class CollectionSource { + MyArrayList list; + + public MyArrayList getList() { + return list; + } + + public void setList(MyArrayList list) { + this.list = list; + } + } + + class MyArrayList extends ArrayList { + private String unusable; + + public MyArrayList(String unusable) { + this.unusable = unusable; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Erroneous2668MapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Erroneous2668MapMapper.java new file mode 100644 index 0000000000..3c9bcd3926 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Erroneous2668MapMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2668; + +import java.util.HashMap; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Erroneous2668MapMapper { + + Erroneous2668MapMapper INSTANCE = Mappers.getMapper( Erroneous2668MapMapper.class ); + + CollectionTarget map(CollectionSource source); + + class CollectionTarget { + MyHashMap map; + + public MyHashMap getMap() { + return map; + } + + public void setMap(MyHashMap map) { + this.map = map; + } + } + + class CollectionSource { + MyHashMap map; + + public MyHashMap getMap() { + return map; + } + + public void setMap(MyHashMap map) { + this.map = map; + } + } + + class MyHashMap extends HashMap { + private String unusable; + + public MyHashMap(String unusable) { + this.unusable = unusable; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Issue2668Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Issue2668Mapper.java new file mode 100644 index 0000000000..1b72e2f636 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Issue2668Mapper.java @@ -0,0 +1,120 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2668; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Issue2668Mapper { + + Issue2668Mapper INSTANCE = Mappers.getMapper( Issue2668Mapper.class ); + + CollectionTarget map(CollectionSource source); + + class CollectionTarget { + MyArrayList list; + MyHashMap map; + MyCopyArrayList copyList; + MyCopyHashMap copyMap; + + public MyArrayList getList() { + return list; + } + + public MyHashMap getMap() { + return map; + } + + public void setList(MyArrayList list) { + this.list = list; + } + + public void setMap(MyHashMap map) { + this.map = map; + } + + public MyCopyArrayList getCopyList() { + return copyList; + } + + public MyCopyHashMap getCopyMap() { + return copyMap; + } + + public void setCopyList(MyCopyArrayList copyList) { + this.copyList = copyList; + } + + public void setCopyMap(MyCopyHashMap copyMap) { + this.copyMap = copyMap; + } + } + + class CollectionSource { + MyArrayList list; + MyHashMap map; + MyCopyArrayList copyList; + MyCopyHashMap copyMap; + + public MyArrayList getList() { + return list; + } + + public MyHashMap getMap() { + return map; + } + + public void setList(MyArrayList list) { + this.list = list; + } + + public void setMap(MyHashMap map) { + this.map = map; + } + + public MyCopyArrayList getCopyList() { + return copyList; + } + + public MyCopyHashMap getCopyMap() { + return copyMap; + } + + public void setCopyList(MyCopyArrayList copyList) { + this.copyList = copyList; + } + + public void setCopyMap(MyCopyHashMap copyMap) { + this.copyMap = copyMap; + } + } + + class MyArrayList extends ArrayList { + } + + class MyHashMap extends HashMap { + } + + class MyCopyArrayList extends ArrayList { + public MyCopyArrayList(Collection copy) { + super( copy ); + } + } + + class MyCopyHashMap extends HashMap { + public MyCopyHashMap(HashMap copy) { + super( copy ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Issue2668Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Issue2668Test.java new file mode 100644 index 0000000000..20b4e445fd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2668/Issue2668Test.java @@ -0,0 +1,59 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2668; + +import javax.tools.Diagnostic.Kind; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * @author Ben Zegveld + */ +@IssueKey( "2668" ) +class Issue2668Test { + + @ProcessorTest + @WithClasses( Issue2668Mapper.class ) + void shouldCompileCorrectlyWithAvailableConstructors() { + } + + @ProcessorTest + @WithClasses( Erroneous2668ListMapper.class ) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = Kind.ERROR, + line = 21, + message = "org.mapstruct.ap.test.bugs._2668.Erroneous2668ListMapper.MyArrayList" + + " does not have an accessible copy or no-args constructor." + ) + } + ) + void errorExpectedBecauseCollectionIsNotUsable() { + } + + @ProcessorTest + @WithClasses( Erroneous2668MapMapper.class ) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = Kind.ERROR, + line = 21, + message = "org.mapstruct.ap.test.bugs._2668.Erroneous2668MapMapper.MyHashMap" + + " does not have an accessible copy or no-args constructor." + ) + } + ) + void errorExpectedBecauseMapIsNotUsable() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2673/Issue2673Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2673/Issue2673Mapper.java new file mode 100644 index 0000000000..221197d18f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2673/Issue2673Mapper.java @@ -0,0 +1,66 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2673; + +import java.util.Optional; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Issue2673Mapper { + + Issue2673Mapper INSTANCE = Mappers.getMapper( Issue2673Mapper.class ); + + Target map(Source source); + + default T map(Optional opt) { + return opt.orElse( null ); + } + + default Optional map(T t) { + return Optional.ofNullable( t ); + } + + class Target { + private final int primitive; + private final String nonPrimitive; + + public Target(int primitive, String nonPrimitive) { + this.primitive = primitive; + this.nonPrimitive = nonPrimitive; + } + + public int getPrimitive() { + return primitive; + } + + public String getNonPrimitive() { + return nonPrimitive; + } + } + + class Source { + private final int primitive; + private final Optional nonPrimitive; + + public Source(int primitive, Optional nonPrimitive) { + this.primitive = primitive; + this.nonPrimitive = nonPrimitive; + } + + public int getPrimitive() { + return primitive; + } + + public Optional getNonPrimitive() { + return nonPrimitive; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2673/Issue2673Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2673/Issue2673Test.java new file mode 100644 index 0000000000..40a5e79276 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2673/Issue2673Test.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2673; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Ben Zegveld + */ +@WithClasses({ + Issue2673Mapper.class +}) +class Issue2673Test { + + @ProcessorTest + @IssueKey( "2673" ) + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/ErroneousSourceTargetMapping.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/ErroneousSourceTargetMapping.java new file mode 100644 index 0000000000..9157a626b4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/ErroneousSourceTargetMapping.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2674; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +interface ErroneousSourceTargetMapping { + + ErroneousSourceTargetMapping INSTANCE = Mappers.getMapper( ErroneousSourceTargetMapping.class ); + + @BeforeMapping + void beforeMappingMethod(Target target, @MappingTarget Source source); + + @AfterMapping + void afterMappingMethod(Source source, @MappingTarget Target target); + + Target toTarget(Source source); + + Source toSource(Target target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Issue2674Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Issue2674Test.java new file mode 100644 index 0000000000..90530051fa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Issue2674Test.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2674; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +import static javax.tools.Diagnostic.Kind; + +/** + * @author Justyna Kubica-Ledzion + */ +@IssueKey("2674") +@WithClasses({ Source.class, Target.class, ErroneousSourceTargetMapping.class }) +public class Issue2674Test { + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousSourceTargetMapping.class, + kind = Kind.ERROR, + line = 20, + message = "@BeforeMapping can only be applied to an implemented method."), + @Diagnostic(type = ErroneousSourceTargetMapping.class, + kind = Kind.ERROR, + line = 23, + message = "@AfterMapping can only be applied to an implemented method.") + } + ) + public void shouldRaiseErrorIfThereIsNoAfterOrBeforeMethodImplementation() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Source.java new file mode 100644 index 0000000000..f9145c08b8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Source.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2674; + +public class Source { + + private int id; + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Target.java new file mode 100644 index 0000000000..244bdeb9a0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2674/Target.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2674; + +public class Target { + + private int id; + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2677/Issue2677Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2677/Issue2677Mapper.java new file mode 100644 index 0000000000..e74ad1e770 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2677/Issue2677Mapper.java @@ -0,0 +1,117 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2677; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2677Mapper { + + Issue2677Mapper INSTANCE = Mappers.getMapper( Issue2677Mapper.class ); + + @Mapping(target = "id", source = "value.id") + Output map(Wrapper in); + + @Mapping(target = ".", source = "value") + Output mapImplicitly(Wrapper in); + + @Mapping(target = "id", source = "value.id") + Output mapFromParent(Wrapper in); + + @Mapping(target = "id", source = "value.id") + Output mapFromChild(Wrapper in); + + @Mapping( target = "value", source = "wrapperValue") + Wrapper mapToWrapper(String wrapperValue, Wrapper wrapper); + + @Mapping(target = "id", source = "value.id") + Output mapWithPresenceCheck(Wrapper in); + + class Wrapper { + private final T value; + private final String status; + + public Wrapper(T value, String status) { + this.value = value; + this.status = status; + } + + public String getStatus() { + return status; + } + + public T getValue() { + return value; + } + } + + class Parent { + private final int id; + + public Parent(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + class Child extends Parent { + private final String whatever; + + public Child(int id, String whatever) { + super( id ); + this.whatever = whatever; + } + + public String getWhatever() { + return whatever; + } + } + + class ParentWithPresenceCheck { + private final int id; + + public ParentWithPresenceCheck(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public boolean hasId() { + return id > 10; + } + } + + class Output { + private int id; + private String status; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2677/Issue2677Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2677/Issue2677Test.java new file mode 100644 index 0000000000..0eb4d6d794 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2677/Issue2677Test.java @@ -0,0 +1,92 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2677; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2677") +@WithClasses({ + Issue2677Mapper.class +}) +class Issue2677Test { + + @ProcessorTest + void shouldCorrectlyUseGenericsWithExtends() { + Issue2677Mapper.Parent parent = new Issue2677Mapper.Parent( 10 ); + Issue2677Mapper.Child child = new Issue2677Mapper.Child( 15, "Test" ); + + Issue2677Mapper.Output output = Issue2677Mapper.INSTANCE.map( new Issue2677Mapper.Wrapper<>( + parent, + "extends" + ) ); + + assertThat( output.getStatus() ).isEqualTo( "extends" ); + assertThat( output.getId() ).isEqualTo( 10 ); + + output = Issue2677Mapper.INSTANCE.mapFromChild( new Issue2677Mapper.Wrapper<>( + child, + "child" + ) ); + + assertThat( output.getStatus() ).isEqualTo( "child" ); + assertThat( output.getId() ).isEqualTo( 15 ); + + output = Issue2677Mapper.INSTANCE.mapFromParent( new Issue2677Mapper.Wrapper<>( + parent, + "parent" + ) ); + + assertThat( output.getStatus() ).isEqualTo( "parent" ); + assertThat( output.getId() ).isEqualTo( 10 ); + + output = Issue2677Mapper.INSTANCE.mapImplicitly( new Issue2677Mapper.Wrapper<>( + child, + "implicit" + ) ); + + assertThat( output.getStatus() ).isEqualTo( "implicit" ); + assertThat( output.getId() ).isEqualTo( 15 ); + + Issue2677Mapper.Wrapper result = Issue2677Mapper.INSTANCE.mapToWrapper( + "test", + new Issue2677Mapper.Wrapper<>( + child, + "super" + ) + ); + + assertThat( result.getStatus() ).isEqualTo( "super" ); + assertThat( result.getValue() ).isEqualTo( "test" ); + + output = Issue2677Mapper.INSTANCE.mapWithPresenceCheck( + new Issue2677Mapper.Wrapper<>( + new Issue2677Mapper.ParentWithPresenceCheck( 8 ), + "presenceCheck" + ) + ); + + assertThat( output.getStatus() ).isEqualTo( "presenceCheck" ); + assertThat( output.getId() ).isEqualTo( 0 ); + + output = Issue2677Mapper.INSTANCE.mapWithPresenceCheck( + new Issue2677Mapper.Wrapper<>( + new Issue2677Mapper.ParentWithPresenceCheck( 15 ), + "presenceCheck" + ) + ); + + assertThat( output.getStatus() ).isEqualTo( "presenceCheck" ); + assertThat( output.getId() ).isEqualTo( 15 ); + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/Issue2704Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/Issue2704Test.java new file mode 100644 index 0000000000..5920a6022b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/Issue2704Test.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2704; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Valentin Kulesh + */ +@IssueKey("2704") +@WithClasses({ TestMapper.class, TopLevel.class }) +public class Issue2704Test { + @ProcessorTest + public void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/TestMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/TestMapper.java new file mode 100644 index 0000000000..846a783437 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/TestMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2704; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.bugs._2704.TopLevel.Target; + +/** + * @author Valentin Kulesh + */ +@Mapper(implementationPackage = "") +public interface TestMapper { + @Mapping(target = "e", constant = "VALUE1") + Target test(Object unused); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/TopLevel.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/TopLevel.java new file mode 100644 index 0000000000..ef30c2cd6e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2704/TopLevel.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2704; + +/** + * @author Valentin Kulesh + */ +public interface TopLevel { + enum InnerEnum { + VALUE1, + VALUE2, + } + + class Target { + public void setE(@SuppressWarnings("unused") InnerEnum e) { + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2743/Issue2743Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2743/Issue2743Mapper.java new file mode 100644 index 0000000000..db3c0e6d49 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2743/Issue2743Mapper.java @@ -0,0 +1,75 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2743; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface Issue2743Mapper { + + @BeanMapping(ignoreUnmappedSourceProperties = { "number" }) + Target map(Source source); + + class Source { + + private final int number = 10; + private final NestedSource nested; + + public Source(NestedSource nested) { + this.nested = nested; + } + + public int getNumber() { + return number; + } + + public NestedSource getNested() { + return nested; + } + } + + class NestedSource { + private final String value; + + public NestedSource(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Target { + + private final NestedTarget nested; + + public Target(NestedTarget nested) { + this.nested = nested; + } + + public NestedTarget getNested() { + return nested; + } + } + + class NestedTarget { + private final String value; + + public NestedTarget(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2743/Issue2743Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2743/Issue2743Test.java new file mode 100644 index 0000000000..5c2bb61279 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2743/Issue2743Test.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2743; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2743") +@WithClasses({ + Issue2743Mapper.class +}) +class Issue2743Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2748/Issue2748Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2748/Issue2748Mapper.java new file mode 100644 index 0000000000..0062667df4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2748/Issue2748Mapper.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2748; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2748Mapper { + + Issue2748Mapper INSTANCE = Mappers.getMapper( Issue2748Mapper.class ); + + @Mapping(target = "specificValue", source = "annotations.specific/value") + Target map(Source source); + + class Target { + private final String specificValue; + + public Target(String specificValue) { + this.specificValue = specificValue; + } + + public String getSpecificValue() { + return specificValue; + } + } + + class Source { + private final Map annotations; + + public Source(Map annotations) { + this.annotations = annotations; + } + + public Map getAnnotations() { + return annotations; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2748/Issue2748Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2748/Issue2748Test.java new file mode 100644 index 0000000000..4bda2cd43d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2748/Issue2748Test.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2748; + +import java.util.Collections; +import java.util.Map; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2748") +@WithClasses( { + Issue2748Mapper.class +} ) +class Issue2748Test { + + @ProcessorTest + void shouldMapNonJavaIdentifier() { + Map annotations = Collections.singletonMap( "specific/value", "value" ); + Issue2748Mapper.Source source = new Issue2748Mapper.Source( annotations ); + + Issue2748Mapper.Target target = Issue2748Mapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getSpecificValue() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2781/Issue2781Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2781/Issue2781Mapper.java new file mode 100644 index 0000000000..9abdf560c4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2781/Issue2781Mapper.java @@ -0,0 +1,53 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2781; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Mengxing Yuan + */ +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface Issue2781Mapper { + + Issue2781Mapper INSTANCE = Mappers.getMapper( Issue2781Mapper.class ); + + @Mapping(target = "nested", source = "source") + Target map(Source source); + + class Target { + private Source nested; + + public Source getNested() { + return nested; + } + + public void setNested(Source nested) { + this.nested = nested; + } + } + + class Source { + private String field1; + private String field2; + + public Source(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } + + public String getField1() { + return field1; + } + + public String getField2() { + return field2; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2781/Issue2781Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2781/Issue2781Test.java new file mode 100644 index 0000000000..80ac4f5ce3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2781/Issue2781Test.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2781; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Mengxing Yuan + */ +@IssueKey("2781") +@WithClasses({ + Issue2781Mapper.class +}) +class Issue2781Test { + + @ProcessorTest + void shouldCompileWithoutErrors() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Issue2795Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Issue2795Mapper.java new file mode 100644 index 0000000000..41f29da920 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Issue2795Mapper.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2795; + +import java.util.Optional; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper +public interface Issue2795Mapper { + + void update(Source update, @MappingTarget Target destination); + + void update(NestedDto update, @MappingTarget Nested destination); + + static T unwrap(Optional optional) { + return optional.orElse( null ); + } + + @Condition + static boolean isNotEmpty(Optional field) { + return field != null && field.isPresent(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Issue2795Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Issue2795Test.java new file mode 100644 index 0000000000..4b7b599527 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Issue2795Test.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2795; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +@IssueKey("2795") +@WithClasses({ + Issue2795Mapper.class, + Nested.class, + NestedDto.class, + Target.class, + Source.class, +}) +public class Issue2795Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Nested.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Nested.java new file mode 100644 index 0000000000..5214c7d076 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Nested.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2795; + +public class Nested { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/NestedDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/NestedDto.java new file mode 100644 index 0000000000..1b27453435 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/NestedDto.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2795; + +public class NestedDto { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Source.java new file mode 100644 index 0000000000..927e2b09f5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Source.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2795; + +import java.util.Optional; + +public class Source { + + private Optional nested = Optional.empty(); + + public Optional getNested() { + return nested; + } + + public void setNested(Optional nested) { + this.nested = nested; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Target.java new file mode 100644 index 0000000000..ed69a6d48f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2795/Target.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2795; + +public class Target { + + private Nested nested; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/ExampleDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/ExampleDto.java new file mode 100644 index 0000000000..eff9f26271 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/ExampleDto.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2797; + +/** + * @author Ben Zegveld + */ +public class ExampleDto { + + private String personFirstName; + private String personLastName; + + public String getPersonFirstName() { + return personFirstName; + } + + public String getPersonLastName() { + return personLastName; + } + + public void setPersonFirstName(String personFirstName) { + this.personFirstName = personFirstName; + } + + public void setPersonLastName(String personLastName) { + this.personLastName = personLastName; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/ExampleMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/ExampleMapper.java new file mode 100644 index 0000000000..eccac27256 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/ExampleMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2797; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.bugs._2797.model.Example.Person; + +import static org.mapstruct.ReportingPolicy.ERROR; + +/** + * @author Ben Zegveld + */ +@Mapper(unmappedTargetPolicy = ERROR) +public interface ExampleMapper { + + @Mapping(target = "personFirstName", source = "names.first") + @Mapping(target = "personLastName", source = "names.last") + ExampleDto map(Person person); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/Issue2797Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/Issue2797Test.java new file mode 100644 index 0000000000..7f3dd5f3de --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/Issue2797Test.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2797; + +import org.mapstruct.ap.test.bugs._2797.model.BasePerson; +import org.mapstruct.ap.test.bugs._2797.model.Example; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Ben Zegveld + */ +@IssueKey( "2797" ) +@WithClasses( { ExampleDto.class, ExampleMapper.class, Example.class, BasePerson.class } ) +public class Issue2797Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/model/BasePerson.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/model/BasePerson.java new file mode 100644 index 0000000000..6b54e6ee31 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/model/BasePerson.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2797.model; + +/** + * @author Ben Zegveld + */ +public class BasePerson { + + private Names names; + + public Names getNames() { + return names; + } + + public void setNames(Names names) { + this.names = names; + } + + public static class Names { + + private String first; + private String last; + + public String getFirst() { + return first; + } + + public String getLast() { + return last; + } + + public void setFirst(String first) { + this.first = first; + } + + public void setLast(String last) { + this.last = last; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/model/Example.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/model/Example.java new file mode 100644 index 0000000000..4dfa40f695 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2797/model/Example.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2797.model; + +/** + * @author Ben Zegveld + */ +public class Example { + + public static class Person extends BasePerson { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/Issue2807Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/Issue2807Test.java new file mode 100644 index 0000000000..102a9d26eb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/Issue2807Test.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2807.spring; + +import org.mapstruct.ap.test.bugs._2807.spring.after.AfterMethod; +import org.mapstruct.ap.test.bugs._2807.spring.before.BeforeMethod; +import org.mapstruct.ap.test.bugs._2807.spring.beforewithtarget.BeforeWithTarget; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; + +/** + * @author Ben Zegveld + */ +@IssueKey( "2807" ) +public class Issue2807Test { + + @ProcessorTest + @WithSpring + @WithClasses( { SpringLifeCycleMapper.class, BeforeMethod.class, BeforeWithTarget.class, AfterMethod.class } ) + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/SpringLifeCycleMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/SpringLifeCycleMapper.java new file mode 100644 index 0000000000..408a2ca3e0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/SpringLifeCycleMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2807.spring; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.bugs._2807.spring.after.AfterMethod; +import org.mapstruct.ap.test.bugs._2807.spring.before.BeforeMethod; +import org.mapstruct.ap.test.bugs._2807.spring.beforewithtarget.BeforeWithTarget; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper( componentModel = "spring", uses = { BeforeMethod.class, AfterMethod.class, + BeforeWithTarget.class }, unmappedTargetPolicy = ReportingPolicy.IGNORE ) +public interface SpringLifeCycleMapper { + SpringLifeCycleMapper INSTANCE = Mappers.getMapper( SpringLifeCycleMapper.class ); + + List map(List list); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/after/AfterMethod.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/after/AfterMethod.java new file mode 100644 index 0000000000..05770c6603 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/after/AfterMethod.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2807.spring.after; + +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.MappingTarget; + +/** + * @author Ben Zegveld + */ +public class AfterMethod { + private AfterMethod() { + } + + @AfterMapping + public static void doNothing(@MappingTarget List source) { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/before/BeforeMethod.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/before/BeforeMethod.java new file mode 100644 index 0000000000..9fb9e7e883 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/before/BeforeMethod.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2807.spring.before; + +import org.mapstruct.BeforeMapping; + +/** + * @author Ben Zegveld + */ +public class BeforeMethod { + private BeforeMethod() { + } + + @BeforeMapping + public static void doNothing(Iterable source) { + return; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/beforewithtarget/BeforeWithTarget.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/beforewithtarget/BeforeWithTarget.java new file mode 100644 index 0000000000..a3ee8b57ea --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2807/spring/beforewithtarget/BeforeWithTarget.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2807.spring.beforewithtarget; + +import java.util.List; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.MappingTarget; + +/** + * @author Ben Zegveld + */ +public class BeforeWithTarget { + private BeforeWithTarget() { + } + + @BeforeMapping + public static void doNothingBeforeWithTarget(Iterable source, @MappingTarget List target) { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Animal.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Animal.java new file mode 100644 index 0000000000..3d60eac9fa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Animal.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2825; + +/** + * @author orange add + */ +public class Animal { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Cat.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Cat.java new file mode 100644 index 0000000000..ec826c0ffb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Cat.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2825; + +/** + * @author orange add + */ +public class Cat extends Animal { + private String race; + + public String getRace() { + return race; + } + + public void setRace(String race) { + this.race = race; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Dog.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Dog.java new file mode 100644 index 0000000000..53b41a98c9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Dog.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2825; + +/** + * @author orange add + */ +public class Dog extends Animal { + private String race; + + public String getRace() { + return race; + } + + public void setRace(String race) { + this.race = race; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Issue2825Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Issue2825Mapper.java new file mode 100644 index 0000000000..c515011b0b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Issue2825Mapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2825; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author orange add + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface Issue2825Mapper { + + Issue2825Mapper INSTANCE = Mappers.getMapper( Issue2825Mapper.class ); + + @SubclassMapping(target = TargetAnimal.class, source = Dog.class) + @SubclassMapping(target = TargetAnimal.class, source = Cat.class) + TargetAnimal map(Animal source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Issue2825Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Issue2825Test.java new file mode 100644 index 0000000000..9c0609b754 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/Issue2825Test.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2825; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author orange add + */ +@IssueKey("2825") +@WithClasses({ + Animal.class, + Cat.class, + Dog.class, + Issue2825Mapper.class, + TargetAnimal.class, +}) +public class Issue2825Test { + + @ProcessorTest + public void mappingMethodShouldNotBeReusedForSubclassMappings() { + Dog dog = new Dog(); + dog.setName( "Lucky" ); + dog.setRace( "Shepherd" ); + TargetAnimal target = Issue2825Mapper.INSTANCE.map( dog ); + assertThat( target.getName() ).isEqualTo( "Lucky" ); + assertThat( target.getRace() ).isEqualTo( "Shepherd" ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/TargetAnimal.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/TargetAnimal.java new file mode 100644 index 0000000000..479741099a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2825/TargetAnimal.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2825; + +/** + * @author orange add + */ +public class TargetAnimal { + private String name; + + private String race; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getRace() { + return race; + } + + public void setRace(String race) { + this.race = race; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Car.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Car.java new file mode 100644 index 0000000000..9419223a42 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Car.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2839; + +import java.util.List; + +/** + * @author Hakan Özkan + */ +public final class Car { + + private final Id id; + private final List seatIds; + private final List tireIds; + + public Car(Id id, List seatIds, List tireIds) { + this.id = id; + this.seatIds = seatIds; + this.tireIds = tireIds; + } + + public Id getId() { + return id; + } + + public List getSeatIds() { + return seatIds; + } + + public List getTireIds() { + return tireIds; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/CarDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/CarDto.java new file mode 100644 index 0000000000..68741ebc92 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/CarDto.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2839; + +import java.util.List; + +/** + * @author Hakan Özkan + */ +public final class CarDto { + + private final String id; + private final List seatIds; + private final List tireIds; + + public CarDto(String id, List seatIds, List tireIds) { + this.id = id; + this.seatIds = seatIds; + this.tireIds = tireIds; + } + + public String getId() { + return id; + } + + public List getSeatIds() { + return seatIds; + } + + public List getTireIds() { + return tireIds; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/CarMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/CarMapper.java new file mode 100644 index 0000000000..e535935d21 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/CarMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2839; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Hakan Özkan + */ +@Mapper +public abstract class CarMapper { + + public static final CarMapper MAPPER = Mappers.getMapper( CarMapper.class ); + + public abstract Car toEntity(CarDto dto); + + protected Id mapId(String id) throws Issue2839Exception { + throw new Issue2839Exception("For id " + id); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Id.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Id.java new file mode 100644 index 0000000000..5bb9a29dd7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Id.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2839; + +import java.util.UUID; + +/** + * @author Hakan Özkan + */ +public class Id { + + private final UUID id; + + public Id() { + this.id = UUID.randomUUID(); + } + + public Id(UUID id) { + this.id = id; + } + + public UUID getId() { + return id; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Issue2839Exception.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Issue2839Exception.java new file mode 100644 index 0000000000..91f02014d6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Issue2839Exception.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2839; + +/** + * @author Hakan Özkan + */ +public class Issue2839Exception extends Exception { + + public Issue2839Exception(String message) { + super( message ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Issue2839Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Issue2839Test.java new file mode 100644 index 0000000000..ed61c39a63 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2839/Issue2839Test.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2839; + +import java.util.Collections; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Hakan Özkan + */ +@IssueKey("2839") +@WithClasses({ + Car.class, + CarDto.class, + CarMapper.class, + Id.class, + Issue2839Exception.class, +}) +public class Issue2839Test { + + @ProcessorTest + void shouldCompile() { + CarDto car1 = new CarDto( + "carId", + Collections.singletonList( "seatId" ), + Collections.singletonList( "tireId" ) + ); + assertThatThrownBy( () -> CarMapper.MAPPER.toEntity( car1 ) ) + .isExactlyInstanceOf( RuntimeException.class ) + .getCause() + .isInstanceOf( Issue2839Exception.class ) + .hasMessage( "For id seatId" ); + + CarDto car2 = new CarDto( "carId", Collections.emptyList(), Collections.singletonList( "tireId" ) ); + assertThatThrownBy( () -> CarMapper.MAPPER.toEntity( car2 ) ) + .isExactlyInstanceOf( RuntimeException.class ) + .getCause() + .isInstanceOf( Issue2839Exception.class ) + .hasMessage( "For id tireId" ); + + CarDto car3 = new CarDto( "carId", Collections.emptyList(), Collections.emptyList() ); + assertThatThrownBy( () -> CarMapper.MAPPER.toEntity( car3 ) ) + .isExactlyInstanceOf( RuntimeException.class ) + .getCause() + .isInstanceOf( Issue2839Exception.class ) + .hasMessage( "For id carId" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2840/Issue2840Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2840/Issue2840Mapper.java new file mode 100644 index 0000000000..d2baac8cac --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2840/Issue2840Mapper.java @@ -0,0 +1,51 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2840; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2840Mapper { + + Issue2840Mapper INSTANCE = + Mappers.getMapper( Issue2840Mapper.class ); + + Issue2840Mapper.Target map(Short shortValue, Integer intValue); + + default int toInt(Number number) { + return number.intValue() + 5; + } + + default short toShort(Number number) { + return (short) (number.shortValue() + 10); + } + + class Target { + + private int intValue; + private short shortValue; + + public int getIntValue() { + return intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + public short getShortValue() { + return shortValue; + } + + public void setShortValue(short shortValue) { + this.shortValue = shortValue; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2840/Issue2840Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2840/Issue2840Test.java new file mode 100644 index 0000000000..1c6b19306f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2840/Issue2840Test.java @@ -0,0 +1,31 @@ + +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2840; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2840") +@WithClasses({ + Issue2840Mapper.class, +}) +class Issue2840Test { + + @ProcessorTest + void shouldUseMethodWithMostSpecificReturnType() { + Issue2840Mapper.Target target = Issue2840Mapper.INSTANCE.map( (short) 10, 50 ); + + assertThat( target.getShortValue() ).isEqualTo( (short) 20 ); + assertThat( target.getIntValue() ).isEqualTo( 55 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867BaseMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867BaseMapper.java new file mode 100644 index 0000000000..8ebe1aa98d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867BaseMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2867; + +import org.mapstruct.MappingTarget; + +/** + * @author Filip Hrisafov + */ +public interface Issue2867BaseMapper { + + void update(@MappingTarget T target, S source); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867Mapper.java new file mode 100644 index 0000000000..77dfda0fc9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867Mapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2867; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2867Mapper extends Issue2867BaseMapper { + + class Target { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Source { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867Test.java new file mode 100644 index 0000000000..5f0e7ad763 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2867/Issue2867Test.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2867; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Issue2867BaseMapper.class, + Issue2867Mapper.class, +}) +@IssueKey("2867") +class Issue2867Test { + + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = @Diagnostic( + type = Issue2867Mapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 14, + message = "Unmapped target property: \"name\"." + + " Occured at 'void update(T target, S source)' in 'Issue2867BaseMapper'." + ) + ) + @ProcessorTest + void shouldCompile() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Issue2880Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Issue2880Mapper.java new file mode 100644 index 0000000000..218f6d384b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Issue2880Mapper.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2880; + +import org.mapstruct.Mapper; + +@Mapper +public interface Issue2880Mapper { + + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Issue2880Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Issue2880Test.java new file mode 100644 index 0000000000..e3d0f810c8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Issue2880Test.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2880; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +@IssueKey("2880") +@WithClasses({ + Issue2880Mapper.class, + Outer.class, + Source.class, + Target.class, + TargetData.class +}) +public class Issue2880Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Outer.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Outer.java new file mode 100644 index 0000000000..90b756137a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Outer.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2880; + +public class Outer { + + public static class SourceData { + + // CHECKSTYLE:OFF + public String value; + // CHECKSTYLE:ON + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Source.java new file mode 100644 index 0000000000..0ab3b81661 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Source.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2880; + +import java.util.List; + +public class Source { + + // CHECKSTYLE:OFF + public Outer.SourceData[] data1; + + public Outer.SourceData[] data2; + + public List data3; + + public List data4; + // CHECKSTYLE:ON +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Target.java new file mode 100644 index 0000000000..28c6b1cca1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/Target.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2880; + +import java.util.List; + +public class Target { + + // CHECKSTYLE:OFF + public TargetData[] data1; + + public List data2; + + public TargetData[] data3; + + public List data4; + // CHECKSTYLE:ON +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/TargetData.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/TargetData.java new file mode 100644 index 0000000000..28cca96601 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2880/TargetData.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2880; + +public class TargetData { + + // CHECKSTYLE:OFF + public String value; + // CHECKSTYLE:ON +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_289/TargetWithoutSetter.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_289/TargetWithoutSetter.java index b3fb312c82..c550662e83 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_289/TargetWithoutSetter.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_289/TargetWithoutSetter.java @@ -14,7 +14,7 @@ */ public class TargetWithoutSetter { - private final Collection collection = new ArrayList(); + private final Collection collection = new ArrayList<>(); public Collection getCollection() { return collection; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2891/Issue2891Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2891/Issue2891Mapper.java new file mode 100644 index 0000000000..205978aeb8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2891/Issue2891Mapper.java @@ -0,0 +1,76 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2891; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; + +/** + * @author Sergei Portnov + */ +@Mapper +public interface Issue2891Mapper { + + @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) + @SubclassMapping(source = Source1.class, target = Target1.class) + @SubclassMapping(source = Source2.class, target = Target2.class) + AbstractTarget map(AbstractSource source); + + abstract class AbstractTarget { + + private final String name; + + protected AbstractTarget(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class Target1 extends AbstractTarget { + + protected Target1(String name) { + super( name ); + } + } + + class Target2 extends AbstractTarget { + + protected Target2(String name) { + super( name ); + } + } + + abstract class AbstractSource { + + private final String name; + + protected AbstractSource(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class Source1 extends AbstractSource { + protected Source1(String name) { + super( name ); + } + } + + class Source2 extends AbstractSource { + + protected Source2(String name) { + super( name ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2891/Issue2891Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2891/Issue2891Test.java new file mode 100644 index 0000000000..c82d5aa63e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2891/Issue2891Test.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2891; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * @author Sergei Portnov + */ +@WithClasses({ + Issue2891Mapper.class +}) +@IssueKey("2891") +class Issue2891Test { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource() + .addComparisonToFixtureFor( Issue2891Mapper.class ); + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Issue2897Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Issue2897Mapper.java new file mode 100644 index 0000000000..e13f2083ee --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Issue2897Mapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2897; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.bugs._2897.util.Util; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(imports = Util.Factory.class) +public interface Issue2897Mapper { + + Issue2897Mapper INSTANCE = Mappers.getMapper( Issue2897Mapper.class ); + + @Mapping( target = "value", expression = "java(Factory.parse( source ))") + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Issue2897Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Issue2897Test.java new file mode 100644 index 0000000000..718ece4a90 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Issue2897Test.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2897; + +import org.mapstruct.ap.test.bugs._2897.util.Util; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2897") +@WithClasses({ + Util.class, + Issue2897Mapper.class, + Source.class, + Target.class, +}) +class Issue2897Test { + + @ProcessorTest + void shouldImportNestedClassInMapperImports() { + Target target = Issue2897Mapper.INSTANCE.map( new Source( "test" ) ); + + assertThat( target.getValue() ).isEqualTo( "parsed(test)" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Source.java new file mode 100644 index 0000000000..5d46ad1ba3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Source.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2897; + +/** + * @author Filip Hrisafov + */ +public class Source { + + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Target.java new file mode 100644 index 0000000000..71fbabbe57 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/Target.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2897; + +/** + * @author Filip Hrisafov + */ +public class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/util/Util.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/util/Util.java new file mode 100644 index 0000000000..5ef0c7e254 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2897/util/Util.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2897.util; + +import org.mapstruct.ap.test.bugs._2897.Source; + +/** + * @author Filip Hrisafov + */ +public class Util { + + public static class Factory { + + public static String parse(Source source) { + return source == null ? null : "parsed(" + source.getValue() + ")"; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/ConditionWithTargetTypeOnCollectionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/ConditionWithTargetTypeOnCollectionMapper.java new file mode 100644 index 0000000000..7e833cfc53 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/ConditionWithTargetTypeOnCollectionMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2901; + +import java.util.List; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.TargetType; + +@Mapper +public interface ConditionWithTargetTypeOnCollectionMapper { + + Target map(Source source); + + @Condition + default boolean check(List test, @TargetType Class type) { + return type.isInstance( test ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Issue2901Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Issue2901Test.java new file mode 100644 index 0000000000..3ae2c3db86 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Issue2901Test.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2901; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Ben Zegveld + */ +@IssueKey( "2901" ) +class Issue2901Test { + + @ProcessorTest + @WithClasses( { Source.class, Target.class, ConditionWithTargetTypeOnCollectionMapper.class } ) + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Source.java new file mode 100644 index 0000000000..4eb50d58fb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Source.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2901; + +import java.util.List; + +public class Source { + + private List field; + + public List getField() { + return field; + } + + public void setField(List field) { + this.field = field; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Target.java new file mode 100644 index 0000000000..b4c5299f35 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2901/Target.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2901; + +import java.util.List; + +public class Target { + + private List field; + + public List getField() { + return field; + } + + public void setField(List field) { + this.field = field; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Issue2907Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Issue2907Test.java new file mode 100644 index 0000000000..a005b6a1ad --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Issue2907Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2907; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.bugs._2907.mapper.Issue2907Mapper; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2907") +@WithClasses({ + Issue2907Mapper.class, + Source.class, + SourceNested.class, + Target.class, +}) +class Issue2907Test { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + void shouldNotGeneratedImportForNestedClass() { + generatedSource.forMapper( Issue2907Mapper.class ) + .containsNoImportFor( Target.TargetNested.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Source.java new file mode 100644 index 0000000000..42d015fde7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Source.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2907; + +import java.util.Set; + +public class Source { + + private Set nested; + + public Set getNested() { + return nested; + } + + public void setNested(Set nested) { + this.nested = nested; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/SourceNested.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/SourceNested.java new file mode 100644 index 0000000000..a58a94f0f3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/SourceNested.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2907; + +public class SourceNested { + + private String prop; + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Target.java new file mode 100644 index 0000000000..80a796d20d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/Target.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2907; + +public class Target { + + private TargetNested[] nested; + + public TargetNested[] getNested() { + return nested; + } + + public void setNested(TargetNested[] nested) { + this.nested = nested; + } + + public static class TargetNested { + private String prop; + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/mapper/Issue2907Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/mapper/Issue2907Mapper.java new file mode 100644 index 0000000000..5244f95bb4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2907/mapper/Issue2907Mapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2907.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.bugs._2907.Source; +import org.mapstruct.ap.test.bugs._2907.Target; + +@Mapper +public interface Issue2907Mapper { + + Target map(Source source); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2913/Issue2913Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2913/Issue2913Mapper.java new file mode 100644 index 0000000000..095f5457fd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2913/Issue2913Mapper.java @@ -0,0 +1,85 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2913; + +import java.math.BigDecimal; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2913Mapper { + + Issue2913Mapper INSTANCE = Mappers.getMapper( Issue2913Mapper.class ); + + @Mapping(target = "doublePrimitiveValue", source = "rounding") + @Mapping(target = "doubleValue", source = "rounding") + @Mapping(target = "longPrimitiveValue", source = "rounding") + @Mapping(target = "longValue", source = "rounding") + Target map(Source source); + + default Long mapAmount(BigDecimal amount) { + return amount != null ? amount.movePointRight( 2 ).longValue() : null; + } + + class Target { + + private double doublePrimitiveValue; + private Double doubleValue; + private long longPrimitiveValue; + private Long longValue; + + public double getDoublePrimitiveValue() { + return doublePrimitiveValue; + } + + public void setDoublePrimitiveValue(double doublePrimitiveValue) { + this.doublePrimitiveValue = doublePrimitiveValue; + } + + public Double getDoubleValue() { + return doubleValue; + } + + public void setDoubleValue(Double doubleValue) { + this.doubleValue = doubleValue; + } + + public long getLongPrimitiveValue() { + return longPrimitiveValue; + } + + public void setLongPrimitiveValue(long longPrimitiveValue) { + this.longPrimitiveValue = longPrimitiveValue; + } + + public Long getLongValue() { + return longValue; + } + + public void setLongValue(Long longValue) { + this.longValue = longValue; + } + } + + class Source { + + private final BigDecimal rounding; + + public Source(BigDecimal rounding) { + this.rounding = rounding; + } + + public BigDecimal getRounding() { + return rounding; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2913/Issue2913Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2913/Issue2913Test.java new file mode 100644 index 0000000000..7257c9ab7e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2913/Issue2913Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2913; + +import java.math.BigDecimal; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2913") +@WithClasses({ + Issue2913Mapper.class, +}) +class Issue2913Test { + + @ProcessorTest + void shouldNotWidenWithUserDefinedMethods() { + Issue2913Mapper.Source source = new Issue2913Mapper.Source( BigDecimal.valueOf( 10.543 ) ); + Issue2913Mapper.Target target = Issue2913Mapper.INSTANCE.map( source ); + + assertThat( target.getDoubleValue() ).isEqualTo( 10.543 ); + assertThat( target.getDoublePrimitiveValue() ).isEqualTo( 10.543 ); + assertThat( target.getLongValue() ).isEqualTo( 1054 ); + assertThat( target.getLongPrimitiveValue() ).isEqualTo( 1054 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2921/Issue2921Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2921/Issue2921Mapper.java new file mode 100644 index 0000000000..d686fcbc16 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2921/Issue2921Mapper.java @@ -0,0 +1,49 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2921; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2921Mapper { + + Issue2921Mapper INSTANCE = Mappers.getMapper( Issue2921Mapper.class ); + + Target map(Source source); + + default Short toShort(Integer value) { + throw new UnsupportedOperationException( "toShort method should not be used" ); + } + + class Source { + private final Integer value; + + public Source(Integer value) { + this.value = value; + } + + public Integer getValue() { + return value; + } + } + + class Target { + private final int value; + + public Target(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2921/Issue2921Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2921/Issue2921Test.java new file mode 100644 index 0000000000..5b8dd0386c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2921/Issue2921Test.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2921; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2921") +@WithClasses({ + Issue2921Mapper.class, +}) +class Issue2921Test { + + @ProcessorTest + void shouldNotUseIntegerToShortForMappingIntegerToInt() { + Issue2921Mapper.Target target = Issue2921Mapper.INSTANCE.map( new Issue2921Mapper.Source( 10 ) ); + assertThat( target.getValue() ).isEqualTo( 10 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Issue2925Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Issue2925Mapper.java new file mode 100644 index 0000000000..d231413820 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Issue2925Mapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2925; + +import java.util.Optional; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue2925Mapper { + + Issue2925Mapper INSTANCE = Mappers.getMapper( Issue2925Mapper.class ); + + Target map(Source source); + + static Optional toOptional(T value) { + return Optional.ofNullable( value ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Issue2925Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Issue2925Test.java new file mode 100644 index 0000000000..73d009d093 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Issue2925Test.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2925; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2925") +@WithClasses({ + Issue2925Mapper.class, + Source.class, + Target.class, +}) +class Issue2925Test { + + @ProcessorTest + void shouldUseOptionalWrappingMethod() { + Target target = Issue2925Mapper.INSTANCE.map( new Source( 10L ) ); + + assertThat( target.getValue() ) + .hasValue( 10L ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Source.java new file mode 100644 index 0000000000..c21ce67775 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Source.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2925; + +public class Source { + + private final long value; + + public Source(long value) { + this.value = value; + } + + public long getValue() { + return value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Target.java new file mode 100644 index 0000000000..4693a83fdb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2925/Target.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2925; + +import java.util.Optional; + +public class Target { + + private Long value; + + public Optional getValue() { + return Optional.ofNullable( value ); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public void setValue(Optional value) { + this.value = value.orElse( null ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2937/Issue2937Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2937/Issue2937Mapper.java new file mode 100644 index 0000000000..95d767acb8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2937/Issue2937Mapper.java @@ -0,0 +1,59 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2937; + +import java.util.ArrayList; +import java.util.Collection; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) +public interface Issue2937Mapper { + + Issue2937Mapper INSTANCE = Mappers.getMapper( Issue2937Mapper.class ); + + Target map(Source source); + + @Condition + default boolean isApplicable(Collection collection) { + return collection == null || collection.size() > 1; + } + + class Source { + private final Collection names; + + public Source(Collection names) { + this.names = names; + } + + public Collection getNames() { + return names; + } + + } + + class Target { + private final Collection names; + + public Target() { + this.names = new ArrayList<>(); + } + + public Collection getNames() { + return names; + } + + public void addName(String name) { + this.names.add( name ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2937/Issue2937Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2937/Issue2937Test.java new file mode 100644 index 0000000000..140575263c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2937/Issue2937Test.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2937; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2937") +@WithClasses({ + Issue2937Mapper.class, +}) +class Issue2937Test { + + @ProcessorTest + void shouldCorrectlyUseConditionalForAdder() { + List sourceNames = new ArrayList<>(); + sourceNames.add( "Tester 1" ); + Issue2937Mapper.Source source = new Issue2937Mapper.Source( sourceNames ); + Issue2937Mapper.Target target = Issue2937Mapper.INSTANCE.map( source ); + + assertThat( target.getNames() ).isEmpty(); + + sourceNames.add( "Tester 2" ); + + target = Issue2937Mapper.INSTANCE.map( source ); + + assertThat( target.getNames() ) + .containsExactly( "Tester 1", "Tester 2" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/Issue2945Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/Issue2945Mapper.java new file mode 100644 index 0000000000..604d546273 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/Issue2945Mapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2945; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.bugs._2945._target.Target; +import org.mapstruct.ap.test.bugs._2945.source.Source; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue2945Mapper { + + Issue2945Mapper INSTANCE = Mappers.getMapper( Issue2945Mapper.class ); + + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/Issue2945Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/Issue2945Test.java new file mode 100644 index 0000000000..11dcf752c0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/Issue2945Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2945; + +import org.mapstruct.ap.test.bugs._2945._target.EnumHolder; +import org.mapstruct.ap.test.bugs._2945._target.Target; +import org.mapstruct.ap.test.bugs._2945.source.Source; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2945") +@WithClasses({ + EnumHolder.class, + Issue2945Mapper.class, + Source.class, + Target.class, +}) +class Issue2945Test { + + @ProcessorTest + void shouldCompile() { + Target target = Issue2945Mapper.INSTANCE.map( new Source( "VALUE_1" ) ); + + assertThat( target.getProperty() ).isEqualTo( EnumHolder.Property.VALUE_1 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/_target/EnumHolder.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/_target/EnumHolder.java new file mode 100644 index 0000000000..0b75e9ce36 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/_target/EnumHolder.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2945._target; + +public class EnumHolder { + public enum Property { + VALUE_1, + VALUE_2; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/_target/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/_target/Target.java new file mode 100644 index 0000000000..25b69eed29 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/_target/Target.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2945._target; + +public class Target { + private EnumHolder.Property property; + + public EnumHolder.Property getProperty() { + return property; + } + + public void setProperty(EnumHolder.Property property) { + this.property = property; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/source/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/source/Source.java new file mode 100644 index 0000000000..8364941ad1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2945/source/Source.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2945.source; + +/** + * @author Filip Hrisafov + */ +public class Source { + + private final String property; + + public Source(String property) { + this.property = property; + } + + public String getProperty() { + return property; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2949/Issue2949Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2949/Issue2949Mapper.java new file mode 100644 index 0000000000..553563066e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2949/Issue2949Mapper.java @@ -0,0 +1,59 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2949; + +import org.mapstruct.BeanMapping; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface Issue2949Mapper { + + Issue2949Mapper INSTANCE = Mappers.getMapper( Issue2949Mapper.class ); + + @Mapping( target = "property1", ignore = true) + @InheritInverseConfiguration + Source toSource(Target target); + + @BeanMapping(ignoreUnmappedSourceProperties = { "property1" }) + Target toTarget(Source source); + + class Target { + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Source { + private final String value; + private final String property1; + + public Source(String value, String property1) { + this.value = value; + this.property1 = property1; + } + + public String getValue() { + return value; + } + + public String getProperty1() { + return property1; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2949/Issue2949Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2949/Issue2949Test.java new file mode 100644 index 0000000000..2a277a4bfd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2949/Issue2949Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2949; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Issue2949Mapper.class +}) +class Issue2949Test { + + @ProcessorTest + void shouldCorrectlyInheritInverseBeanMappingWithIgnoreUnmappedSourceProeprties() { + Issue2949Mapper.Target target = Issue2949Mapper.INSTANCE.toTarget( new Issue2949Mapper.Source( + "test", + "first" + ) ); + + assertThat( target.getValue() ).isEqualTo( "test" ); + + Issue2949Mapper.Source source = Issue2949Mapper.INSTANCE.toSource( target ); + + assertThat( source.getValue() ).isEqualTo( "test" ); + assertThat( source.getProperty1() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2952/Issue2952Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2952/Issue2952Mapper.java new file mode 100644 index 0000000000..885d74448f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2952/Issue2952Mapper.java @@ -0,0 +1,62 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2952; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE) +public interface Issue2952Mapper { + + Issue2952Mapper INSTANCE = Mappers.getMapper( Issue2952Mapper.class ); + + Target map(Source source); + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Target { + private final Map attributes = new HashMap<>(); + private final List values = new ArrayList<>(); + private String value; + + public Map getAttributes() { + return Collections.unmodifiableMap( attributes ); + } + + public List getValues() { + return Collections.unmodifiableList( values ); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2952/Issue2952Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2952/Issue2952Test.java new file mode 100644 index 0000000000..7a517ec979 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2952/Issue2952Test.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._2952; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2952") +@WithClasses({ + Issue2952Mapper.class +}) +class Issue2952Test { + + @ProcessorTest + void shouldCorrectIgnoreImmutableIterable() { + Issue2952Mapper.Target target = Issue2952Mapper.INSTANCE.map( new Issue2952Mapper.Source( "test" ) ); + + assertThat( target.getValue() ).isEqualTo( "test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3015/Issue3015Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3015/Issue3015Mapper.java new file mode 100644 index 0000000000..1683db8f39 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3015/Issue3015Mapper.java @@ -0,0 +1,153 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3015; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.annotatewith.CustomMethodOnlyAnnotation; + +/** + * @author orange add + */ +@Mapper +public interface Issue3015Mapper { + + @AnnotateWith( CustomMethodOnlyAnnotation.class ) + Target map(Source source); + + class Source { + + private NestedSource nested; + private List list; + private Stream stream; + private AnnotateSourceEnum annotateWithEnum; + private Map map; + + public NestedSource getNested() { + return nested; + } + + public void setNested(NestedSource nested) { + this.nested = nested; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public Stream getStream() { + return stream; + } + + public void setStream(Stream stream) { + this.stream = stream; + } + + public AnnotateSourceEnum getAnnotateWithEnum() { + return annotateWithEnum; + } + + public void setAnnotateWithEnum(AnnotateSourceEnum annotateWithEnum) { + this.annotateWithEnum = annotateWithEnum; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + } + + class Target { + private NestedTarget nested; + private List list; + private Stream stream; + private AnnotateTargetEnum annotateWithEnum; + private Map map; + + public NestedTarget getNested() { + return nested; + } + + public void setNested(NestedTarget nested) { + this.nested = nested; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public Stream getStream() { + return stream; + } + + public void setStream(Stream stream) { + this.stream = stream; + } + + public AnnotateTargetEnum getAnnotateWithEnum() { + return annotateWithEnum; + } + + public void setAnnotateWithEnum(AnnotateTargetEnum annotateWithEnum) { + this.annotateWithEnum = annotateWithEnum; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + } + + enum AnnotateSourceEnum { + EXISTING; + } + + enum AnnotateTargetEnum { + EXISTING; + } + + class NestedSource { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class NestedTarget { + private Integer value; + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3015/Issue3015Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3015/Issue3015Test.java new file mode 100644 index 0000000000..96004b8c2d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3015/Issue3015Test.java @@ -0,0 +1,38 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3015; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.ap.test.annotatewith.CustomMethodOnlyAnnotation; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author orange add + */ +@WithClasses({ + Issue3015Mapper.class, + CustomMethodOnlyAnnotation.class +}) +class Issue3015Test { + + @ProcessorTest + void noNeedPassAnnotationToForgeMethod() { + Issue3015Mapper mapper = Mappers.getMapper( Issue3015Mapper.class ); + Method[] declaredMethods = mapper.getClass().getDeclaredMethods(); + List annotationMethods = Arrays.stream( declaredMethods ) + .filter( method -> method.getAnnotation( CustomMethodOnlyAnnotation.class ) != null ) + .collect( Collectors.toList() ); + assertThat( annotationMethods ).hasSize( 1 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3057/Issue3057Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3057/Issue3057Mapper.java new file mode 100644 index 0000000000..d8c444d8c5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3057/Issue3057Mapper.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3057; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface Issue3057Mapper { + + Issue3057Mapper INSTANCE = Mappers.getMapper( Issue3057Mapper.class ); + + class Source { + private Source self; + + public Source getSelf() { + return self; + } + + public void setSelf(Source self) { + this.self = self; + } + } + + class Target { + private Target self; + private String value; + + public Target getSelf() { + return self; + } + + public void setSelf(Target self) { + this.self = self; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + @Mapping( target = "value", constant = "constantValue" ) + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3057/Issue3057MapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3057/Issue3057MapperTest.java new file mode 100644 index 0000000000..7e64f529f3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3057/Issue3057MapperTest.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3057; + +import org.mapstruct.ap.test.bugs._3057.Issue3057Mapper.Source; +import org.mapstruct.ap.test.bugs._3057.Issue3057Mapper.Target; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ben Zegveld + */ +@WithClasses(Issue3057Mapper.class) +@IssueKey("3057") +class Issue3057MapperTest { + + @ProcessorTest + void mapsSelf() { + Source sourceOuter = new Issue3057Mapper.Source(); + Source sourceInner = new Issue3057Mapper.Source(); + sourceOuter.setSelf( sourceInner ); + + Target targetOuter = Issue3057Mapper.INSTANCE.map( sourceOuter ); + + assertThat( targetOuter.getValue() ).isEqualTo( "constantValue" ); + assertThat( targetOuter.getSelf().getValue() ).isEqualTo( "constantValue" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3077/Issue3077Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3077/Issue3077Mapper.java new file mode 100644 index 0000000000..f1a630a444 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3077/Issue3077Mapper.java @@ -0,0 +1,65 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3077; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3077Mapper { + + Issue3077Mapper INSTANCE = Mappers.getMapper( Issue3077Mapper.class ); + + class Source { + private final String source; + private final Source self; + + public Source(String source, Source self) { + this.source = source; + this.self = self; + } + + public String getSource() { + return source; + } + + public Source getSelf() { + return self; + } + } + + class Target { + private String value; + private Target self; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Target getSelf() { + return self; + } + + public void setSelf(Target self) { + this.self = self; + } + + } + + @Named("self") + @Mapping(target = "value", source = "source") + @Mapping(target = "self", qualifiedByName = "self") + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3077/Issue3077MapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3077/Issue3077MapperTest.java new file mode 100644 index 0000000000..bfba7988aa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3077/Issue3077MapperTest.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3077; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3077Mapper.class) +@IssueKey("3057") +class Issue3077MapperTest { + + @ProcessorTest + void mapsSelf() { + Issue3077Mapper.Source sourceInner = new Issue3077Mapper.Source( "inner", null ); + Issue3077Mapper.Source sourceOuter = new Issue3077Mapper.Source( "outer", sourceInner ); + + Issue3077Mapper.Target targetOuter = Issue3077Mapper.INSTANCE.map( sourceOuter ); + + assertThat( targetOuter.getValue() ).isEqualTo( "outer" ); + assertThat( targetOuter.getSelf().getValue() ).isEqualTo( "inner" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/Issue3089BuilderProvider.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/Issue3089BuilderProvider.java new file mode 100644 index 0000000000..52f0f96345 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/Issue3089BuilderProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089; + +import java.util.List; +import java.util.Objects; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; + +import org.mapstruct.ap.spi.BuilderInfo; +import org.mapstruct.ap.spi.BuilderProvider; +import org.mapstruct.ap.spi.ImmutablesBuilderProvider; + +/** + * @author Oliver Erhart + */ +public class Issue3089BuilderProvider extends ImmutablesBuilderProvider implements BuilderProvider { + + @Override + protected BuilderInfo findBuilderInfo(TypeElement typeElement) { + Name name = typeElement.getQualifiedName(); + if ( name.toString().endsWith( ".Item" ) ) { + BuilderInfo info = findBuilderInfoFromInnerBuilderClass( typeElement ); + if ( info != null ) { + return info; + } + } + return super.findBuilderInfo( typeElement ); + } + + /** + * Looks for inner builder class in the Immutable interface / abstract class. + * + * The inner builder class should be be declared with the following line + * + *

      +     *     public static Builder() extends ImmutableItem.Builder { }
      +     * 
      + * + * The Immutable instance should be created with the following line + * + *
      +     *     new Item.Builder().withId("123").build();
      +     * 
      + * + * @see org.mapstruct.ap.test.bugs._3089.domain.Item + * + * @param typeElement + * @return + */ + private BuilderInfo findBuilderInfoFromInnerBuilderClass(TypeElement typeElement) { + if ( shouldIgnore( typeElement ) ) { + return null; + } + + List innerTypes = ElementFilter.typesIn( typeElement.getEnclosedElements() ); + ExecutableElement defaultConstructor = innerTypes.stream() + .filter( this::isBuilderCandidate ) + .map( this::getEmptyArgPublicConstructor ) + .filter( Objects::nonNull ) + .findAny() + .orElse( null ); + + if ( defaultConstructor != null ) { + return new BuilderInfo.Builder() + .builderCreationMethod( defaultConstructor ) + .buildMethod( findBuildMethods( (TypeElement) defaultConstructor.getEnclosingElement(), typeElement ) ) + .build(); + } + return null; + } + + private boolean isBuilderCandidate(TypeElement innerType ) { + TypeElement outerType = (TypeElement) innerType.getEnclosingElement(); + String packageName = this.elementUtils.getPackageOf( outerType ).getQualifiedName().toString(); + Name outerSimpleName = outerType.getSimpleName(); + String builderClassName = packageName + ".Immutable" + outerSimpleName + ".Builder"; + return innerType.getSimpleName().contentEquals( "Builder" ) + && getTypeElement( innerType.getSuperclass() ).getQualifiedName().contentEquals( builderClassName ) + && innerType.getModifiers().contains( Modifier.PUBLIC ); + } + + private ExecutableElement getEmptyArgPublicConstructor(TypeElement builderType) { + return ElementFilter.constructorsIn( builderType.getEnclosedElements() ).stream() + .filter( c -> c.getParameters().isEmpty() ) + .filter( c -> c.getModifiers().contains( Modifier.PUBLIC ) ) + .findAny() + .orElse( null ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/Issue3089Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/Issue3089Test.java new file mode 100644 index 0000000000..f7c138c146 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/Issue3089Test.java @@ -0,0 +1,61 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.ap.spi.AccessorNamingStrategy; +import org.mapstruct.ap.spi.BuilderProvider; +import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy; +import org.mapstruct.ap.test.bugs._3089.domain.ImmutableItem; +import org.mapstruct.ap.test.bugs._3089.domain.Item; +import org.mapstruct.ap.test.bugs._3089.dto.ImmutableItemDTO; +import org.mapstruct.ap.test.bugs._3089.dto.ItemDTO; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithServiceImplementation; +import org.mapstruct.ap.testutil.WithServiceImplementations; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Oliver Erhart + */ +@WithClasses({ + ItemMapper.class, + Item.class, + ImmutableItem.class, + ItemDTO.class, + ImmutableItemDTO.class +}) +@IssueKey("3089") +@WithServiceImplementations({ + @WithServiceImplementation(provides = BuilderProvider.class, value = Issue3089BuilderProvider.class), + @WithServiceImplementation(provides = AccessorNamingStrategy.class, value = ImmutablesAccessorNamingStrategy.class) +}) +public class Issue3089Test { + + @ProcessorTest + public void shouldIgnorePutterOfMap() { + + Map attributesMap = new HashMap<>(); + attributesMap.put( "a", "b" ); + attributesMap.put( "c", "d" ); + + ItemDTO item = ImmutableItemDTO.builder() + .id( "test" ) + .attributes( attributesMap ) + .build(); + + Item target = ItemMapper.INSTANCE.map( item ); + + assertThat( target ).isNotNull(); + assertThat( target.getId() ).isEqualTo( "test" ); + assertThat( target.getAttributes() ).isEqualTo( attributesMap ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/ItemMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/ItemMapper.java new file mode 100644 index 0000000000..ed06115ae2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/ItemMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089; + +import org.mapstruct.Builder; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.bugs._3089.domain.Item; +import org.mapstruct.ap.test.bugs._3089.dto.ItemDTO; +import org.mapstruct.factory.Mappers; + +/** + * @author Oliver Erhart + */ +@Mapper(builder = @Builder) +public abstract class ItemMapper { + + public static final ItemMapper INSTANCE = Mappers.getMapper( ItemMapper.class ); + + public abstract Item map(ItemDTO itemDTO); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/domain/ImmutableItem.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/domain/ImmutableItem.java new file mode 100644 index 0000000000..ab5a6afdc0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/domain/ImmutableItem.java @@ -0,0 +1,296 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Immutable implementation of {@link Item}. + *

      + * Superclass should expose a static subclass of the Builder to create immutable instance + * {@code public static Builder extends ImmutableItem.Builder}. + * + * @author Oliver Erhart + */ +@SuppressWarnings({"all"}) +public final class ImmutableItem extends Item { + private final String id; + private final Map attributes; + + private ImmutableItem(String id, Map attributes) { + this.id = id; + this.attributes = attributes; + } + + /** + * @return The value of the {@code id} attribute + */ + @Override + public String getId() { + return id; + } + + /** + * @return The value of the {@code attributes} attribute + */ + @Override + public Map getAttributes() { + return attributes; + } + + /** + * Copy the current immutable object by setting a value for the {@link Item#getId() id} attribute. + * An equals check used to prevent copying of the same value by returning {@code this}. + * @param value A new value for id + * @return A modified copy of the {@code this} object + */ + public final ImmutableItem withId(String value) { + String newValue = Objects.requireNonNull(value, "id"); + if (this.id.equals(newValue)) return this; + return new ImmutableItem(newValue, this.attributes); + } + + /** + * Copy the current immutable object by replacing the {@link Item#getAttributes() attributes} map with the specified map. + * Nulls are not permitted as keys or values. + * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}. + * @param entries The entries to be added to the attributes map + * @return A modified copy of {@code this} object + */ + public final ImmutableItem withAttributes(Map entries) { + if (this.attributes == entries) return this; + Map newValue = createUnmodifiableMap(true, false, entries); + return new ImmutableItem(this.id, newValue); + } + + /** + * This instance is equal to all instances of {@code ImmutableItem} that have equal attribute values. + * @return {@code true} if {@code this} is equal to {@code another} instance + */ + @Override + public boolean equals(Object another) { + if (this == another) return true; + return another instanceof ImmutableItem + && equalTo((ImmutableItem) another); + } + + private boolean equalTo(ImmutableItem another) { + return id.equals(another.id) + && attributes.equals(another.attributes); + } + + /** + * Computes a hash code from attributes: {@code id}, {@code attributes}. + * @return hashCode value + */ + @Override + public int hashCode() { + int h = 5381; + h += (h << 5) + id.hashCode(); + h += (h << 5) + attributes.hashCode(); + return h; + } + + /** + * Prints the immutable value {@code Item} with attribute values. + * @return A string representation of the value + */ + @Override + public String toString() { + return "Item{" + + "id=" + id + + ", attributes=" + attributes + + "}"; + } + + /** + * Creates an immutable copy of a {@link Item} value. + * Uses accessors to get values to initialize the new immutable instance. + * If an instance is already immutable, it is returned as is. + * @param instance The instance to copy + * @return A copied immutable Item instance + */ + public static ImmutableItem copyOf(Item instance) { + if (instance instanceof ImmutableItem) { + return (ImmutableItem) instance; + } + return ImmutableItem.builder() + .from(instance) + .build(); + } + + /** + * Creates a builder for {@link ImmutableItem ImmutableItem}. + *

      +   * ImmutableItem.builder()
      +   *    .id(String) // required {@link Item#getId() id}
      +   *    .putAttributes|putAllAttributes(String => String) // {@link Item#getAttributes() attributes} mappings
      +   *    .build();
      +   * 
      + * @return A new ImmutableItem builder + */ + public static ImmutableItem.Builder builder() { + return new ImmutableItem.Builder(); + } + + /** + * Builds instances of type {@link ImmutableItem ImmutableItem}. + * Initialize attributes and then invoke the {@link #build()} method to create an + * immutable instance. + *

      {@code Builder} is not thread-safe and generally should not be stored in a field or collection, + * but instead used immediately to create instances. + */ + public static class Builder { + private static final long INIT_BIT_ID = 0x1L; + private long initBits = 0x1L; + + private String id; + private Map attributes = new LinkedHashMap(); + + public Builder() { + } + + /** + * Fill a builder with attribute values from the provided {@code Item} instance. + * Regular attribute values will be replaced with those from the given instance. + * Absent optional values will not replace present values. + * Collection elements and entries will be added, not replaced. + * @param instance The instance from which to copy values + * @return {@code this} builder for use in a chained invocation + */ + public final Builder from(Item instance) { + Objects.requireNonNull(instance, "instance"); + id(instance.getId()); + putAllAttributes(instance.getAttributes()); + return this; + } + + /** + * Initializes the value for the {@link Item#getId() id} attribute. + * @param id The value for id + * @return {@code this} builder for use in a chained invocation + */ + public final Builder id(String id) { + this.id = Objects.requireNonNull(id, "id"); + initBits &= ~INIT_BIT_ID; + return this; + } + + /** + * Put one entry to the {@link Item#getAttributes() attributes} map. + * @param key The key in the attributes map + * @param value The associated value in the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final Builder putAttributes(String key, String value) { + this.attributes.put( + Objects.requireNonNull(key, "attributes key"), + Objects.requireNonNull(value, "attributes value")); + return this; + } + + /** + * Put one entry to the {@link Item#getAttributes() attributes} map. Nulls are not permitted + * @param entry The key and value entry + * @return {@code this} builder for use in a chained invocation + */ + public final Builder putAttributes(Map.Entry entry) { + String k = entry.getKey(); + String v = entry.getValue(); + this.attributes.put( + Objects.requireNonNull(k, "attributes key"), + Objects.requireNonNull(v, "attributes value")); + return this; + } + + /** + * Sets or replaces all mappings from the specified map as entries for the {@link Item#getAttributes() attributes} map. Nulls are not permitted + * @param entries The entries that will be added to the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final Builder attributes(Map entries) { + this.attributes.clear(); + return putAllAttributes(entries); + } + + /** + * Put all mappings from the specified map as entries to {@link Item#getAttributes() attributes} map. Nulls are not permitted + * @param entries The entries that will be added to the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final Builder putAllAttributes(Map entries) { + for (Map.Entry e : entries.entrySet()) { + String k = e.getKey(); + String v = e.getValue(); + this.attributes.put( + Objects.requireNonNull(k, "attributes key"), + Objects.requireNonNull(v, "attributes value")); + } + return this; + } + + /** + * Builds a new {@link ImmutableItem ImmutableItem}. + * @return An immutable instance of Item + * @throws java.lang.IllegalStateException if any required attributes are missing + */ + public ImmutableItem build() { + if (initBits != 0) { + throw new IllegalStateException(formatRequiredAttributesMessage()); + } + return new ImmutableItem(id, createUnmodifiableMap(false, false, attributes)); + } + + private String formatRequiredAttributesMessage() { + List attributes = new ArrayList<>(); + if ((initBits & INIT_BIT_ID) != 0) attributes.add("id"); + return "Cannot build Item, some of required attributes are not set " + attributes; + } + } + + private static Map createUnmodifiableMap(boolean checkNulls, boolean skipNulls, Map map) { + switch (map.size()) { + case 0: return Collections.emptyMap(); + case 1: { + Map.Entry e = map.entrySet().iterator().next(); + K k = e.getKey(); + V v = e.getValue(); + if (checkNulls) { + Objects.requireNonNull(k, "key"); + Objects.requireNonNull(v, "value"); + } + if (skipNulls && (k == null || v == null)) { + return Collections.emptyMap(); + } + return Collections.singletonMap(k, v); + } + default: { + Map linkedMap = new LinkedHashMap<>(map.size()); + if (skipNulls || checkNulls) { + for (Map.Entry e : map.entrySet()) { + K k = e.getKey(); + V v = e.getValue(); + if (skipNulls) { + if (k == null || v == null) continue; + } else if (checkNulls) { + Objects.requireNonNull(k, "key"); + Objects.requireNonNull(v, "value"); + } + linkedMap.put(k, v); + } + } else { + linkedMap.putAll(map); + } + return Collections.unmodifiableMap(linkedMap); + } + } + } +} \ No newline at end of file diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/domain/Item.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/domain/Item.java new file mode 100644 index 0000000000..e193432367 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/domain/Item.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089.domain; + +import java.util.Map; + +/** + * @author Oliver Erhart + */ +public abstract class Item { + public abstract String getId(); + + public abstract Map getAttributes(); + + public static class Builder extends ImmutableItem.Builder { } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/dto/ImmutableItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/dto/ImmutableItemDTO.java new file mode 100644 index 0000000000..de64727bf1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/dto/ImmutableItemDTO.java @@ -0,0 +1,330 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089.dto; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Immutable implementation of {@link ItemDTO}. + *

      + * Use the builder to create immutable instances: + * {@code ImmutableItemDTO.builder()}. + * + * @author Oliver Erhart + */ +public final class ImmutableItemDTO extends ItemDTO { + private final String id; + private final Map attributes; + + private ImmutableItemDTO(String id, Map attributes) { + this.id = id; + this.attributes = attributes; + } + + /** + * @return The value of the {@code id} attribute + */ + @Override + public String getId() { + return id; + } + + /** + * @return The value of the {@code attributes} attribute + */ + @Override + public Map getAttributes() { + return attributes; + } + + /** + * Copy the current immutable object by setting a value for the {@link ItemDTO#getId() id} attribute. + * An equals check used to prevent copying of the same value by returning {@code this}. + * + * @param value A new value for id + * @return A modified copy of the {@code this} object + */ + public ImmutableItemDTO withId(String value) { + String newValue = Objects.requireNonNull( value, "id" ); + if ( this.id.equals( newValue ) ) { + return this; + } + return new ImmutableItemDTO( newValue, this.attributes ); + } + + /** + * Copy the current immutable object by replacing the {@link ItemDTO#getAttributes() attributes} map with + * the specified map. + * Nulls are not permitted as keys or values. + * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}. + * + * @param entries The entries to be added to the attributes map + * @return A modified copy of {@code this} object + */ + public ImmutableItemDTO withAttributes(Map entries) { + if ( this.attributes == entries ) { + return this; + } + Map newValue = createUnmodifiableMap( true, false, entries ); + return new ImmutableItemDTO( this.id, newValue ); + } + + /** + * This instance is equal to all instances of {@code ImmutableItemDTO} that have equal attribute values. + * + * @return {@code true} if {@code this} is equal to {@code another} instance + */ + @Override + public boolean equals(Object another) { + if ( this == another ) { + return true; + } + return another instanceof ImmutableItemDTO + && equalTo( (ImmutableItemDTO) another ); + } + + private boolean equalTo(ImmutableItemDTO another) { + return id.equals( another.id ) + && attributes.equals( another.attributes ); + } + + /** + * Computes a hash code from attributes: {@code id}, {@code attributes}. + * + * @return hashCode value + */ + @Override + public int hashCode() { + int h = 5381; + h += ( h << 5 ) + id.hashCode(); + h += ( h << 5 ) + attributes.hashCode(); + return h; + } + + /** + * Prints the immutable value {@code Item} with attribute values. + * + * @return A string representation of the value + */ + @Override + public String toString() { + return "Item{" + + "id=" + id + + ", attributes=" + attributes + + "}"; + } + + /** + * Creates an immutable copy of a {@link ItemDTO} value. + * Uses accessors to get values to initialize the new immutable instance. + * If an instance is already immutable, it is returned as is. + * + * @param instance The instance to copy + * @return A copied immutable Item instance + */ + public static ImmutableItemDTO copyOf(ItemDTO instance) { + if ( instance instanceof ImmutableItemDTO ) { + return (ImmutableItemDTO) instance; + } + return ImmutableItemDTO.builder() + .from( instance ) + .build(); + } + + /** + * Creates a builder for {@link ImmutableItemDTO ImmutableItemDTO}. + *

      +     * ImmutableItemDTO.builder()
      +     *    .id(String) // required {@link ItemDTO#getId() id}
      +     *    .putAttributes|putAllAttributes(String => String) // {@link ItemDTO#getAttributes() attributes} mappings
      +     *    .build();
      +     * 
      + * + * @return A new ImmutableItemDTO builder + */ + public static ImmutableItemDTO.Builder builder() { + return new ImmutableItemDTO.Builder(); + } + + /** + * Builds instances of type {@link ImmutableItemDTO ImmutableItemDTO}. + * Initialize attributes and then invoke the {@link #build()} method to create an + * immutable instance. + *

      {@code Builder} is not thread-safe and generally should not be stored in a field or collection, + * but instead used immediately to create instances. + */ + public static class Builder { + private static final long INIT_BIT_ID = 0x1L; + private long initBits = 0x1L; + + private String id; + private Map attributes = new LinkedHashMap(); + + public Builder() { + } + + /** + * Fill a builder with attribute values from the provided {@code Item} instance. + * Regular attribute values will be replaced with those from the given instance. + * Absent optional values will not replace present values. + * Collection elements and entries will be added, not replaced. + * + * @param instance The instance from which to copy values + * @return {@code this} builder for use in a chained invocation + */ + public final ImmutableItemDTO.Builder from(ItemDTO instance) { + Objects.requireNonNull( instance, "instance" ); + id( instance.getId() ); + putAllAttributes( instance.getAttributes() ); + return this; + } + + /** + * Initializes the value for the {@link ItemDTO#getId() id} attribute. + * + * @param id The value for id + * @return {@code this} builder for use in a chained invocation + */ + public final ImmutableItemDTO.Builder id(String id) { + this.id = Objects.requireNonNull( id, "id" ); + initBits &= ~INIT_BIT_ID; + return this; + } + + /** + * Put one entry to the {@link ItemDTO#getAttributes() attributes} map. + * + * @param key The key in the attributes map + * @param value The associated value in the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final ImmutableItemDTO.Builder putAttributes(String key, String value) { + this.attributes.put( + Objects.requireNonNull( key, "attributes key" ), + Objects.requireNonNull( value, "attributes value" ) + ); + return this; + } + + /** + * Put one entry to the {@link ItemDTO#getAttributes() attributes} map. Nulls are not permitted + * + * @param entry The key and value entry + * @return {@code this} builder for use in a chained invocation + */ + public final ImmutableItemDTO.Builder putAttributes(Map.Entry entry) { + String k = entry.getKey(); + String v = entry.getValue(); + this.attributes.put( + Objects.requireNonNull( k, "attributes key" ), + Objects.requireNonNull( v, "attributes value" ) + ); + return this; + } + + /** + * Sets or replaces all mappings from the specified map as entries for the {@link ItemDTO#getAttributes() + * attributes} map. Nulls are not permitted + * + * @param entries The entries that will be added to the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final ImmutableItemDTO.Builder attributes(Map entries) { + this.attributes.clear(); + return putAllAttributes( entries ); + } + + /** + * Put all mappings from the specified map as entries to {@link ItemDTO#getAttributes() attributes} map. + * Nulls are not permitted + * + * @param entries The entries that will be added to the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final ImmutableItemDTO.Builder putAllAttributes(Map entries) { + for ( Map.Entry e : entries.entrySet() ) { + String k = e.getKey(); + String v = e.getValue(); + this.attributes.put( + Objects.requireNonNull( k, "attributes key" ), + Objects.requireNonNull( v, "attributes value" ) + ); + } + return this; + } + + /** + * Builds a new {@link ImmutableItemDTO ImmutableItemDTO}. + * + * @return An immutable instance of Item + * @throws java.lang.IllegalStateException if any required attributes are missing + */ + public ImmutableItemDTO build() { + if ( initBits != 0 ) { + throw new IllegalStateException( formatRequiredAttributesMessage() ); + } + return new ImmutableItemDTO( id, createUnmodifiableMap( false, false, attributes ) ); + } + + private String formatRequiredAttributesMessage() { + List attributes = new ArrayList<>(); + if ( ( initBits & INIT_BIT_ID ) != 0 ) { + attributes.add( "id" ); + } + return "Cannot build Item, some of required attributes are not set " + attributes; + } + } + + @SuppressWarnings( "checkstyle:AvoidNestedBlocks" ) + private static Map createUnmodifiableMap(boolean checkNulls, boolean skipNulls, + Map map) { + switch ( map.size() ) { + case 0: + return Collections.emptyMap(); + case 1: { + Map.Entry e = map.entrySet().iterator().next(); + K k = e.getKey(); + V v = e.getValue(); + if ( checkNulls ) { + Objects.requireNonNull( k, "key" ); + Objects.requireNonNull( v, "value" ); + } + if ( skipNulls && ( k == null || v == null ) ) { + return Collections.emptyMap(); + } + return Collections.singletonMap( k, v ); + } + default: { + Map linkedMap = new LinkedHashMap<>( map.size() ); + if ( skipNulls || checkNulls ) { + for ( Map.Entry e : map.entrySet() ) { + K k = e.getKey(); + V v = e.getValue(); + if ( skipNulls ) { + if ( k == null || v == null ) { + continue; + } + } + else if ( checkNulls ) { + Objects.requireNonNull( k, "key" ); + Objects.requireNonNull( v, "value" ); + } + linkedMap.put( k, v ); + } + } + else { + linkedMap.putAll( map ); + } + return Collections.unmodifiableMap( linkedMap ); + } + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/dto/ItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/dto/ItemDTO.java new file mode 100644 index 0000000000..c38e37f08a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3089/dto/ItemDTO.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3089.dto; + +import java.util.Map; + +/** + * @author Oliver Erhart + */ +public abstract class ItemDTO { + public abstract String getId(); + + public abstract Map getAttributes(); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3104/Issue3104Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3104/Issue3104Mapper.java new file mode 100644 index 0000000000..e561f40bab --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3104/Issue3104Mapper.java @@ -0,0 +1,77 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3104; + +import java.util.Collections; +import java.util.List; + +import org.mapstruct.BeanMapping; +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE) +public interface Issue3104Mapper { + + Issue3104Mapper INSTANCE = Mappers.getMapper( Issue3104Mapper.class ); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void update(@MappingTarget Target target, Source source); + + class Target { + private List children = Collections.emptyList(); + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + if ( children == null ) { + throw new IllegalArgumentException( "children is null" ); + } + this.children = Collections.unmodifiableList( children ); + } + } + + class Child { + private String myField; + + public String getMyField() { + return myField; + } + + public void setMyField(String myField) { + this.myField = myField; + } + } + + class Source { + private final List children; + + public Source(List children) { + this.children = children; + } + + public List getChildren() { + return children; + } + + } + + class ChildSource { + private final String myField; + + public ChildSource(String myField) { + this.myField = myField; + } + + public String getMyField() { + return myField; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3104/Issue3104Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3104/Issue3104Test.java new file mode 100644 index 0000000000..5f3a9e160b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3104/Issue3104Test.java @@ -0,0 +1,38 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3104; + +import java.util.Collections; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3104") +@WithClasses(Issue3104Mapper.class) +class Issue3104Test { + + @ProcessorTest + void shouldCorrectlyMapUpdateMappingWithTargetImmutableCollectionStrategy() { + Issue3104Mapper.Target target = new Issue3104Mapper.Target(); + Issue3104Mapper.INSTANCE.update( target, new Issue3104Mapper.Source( null ) ); + + assertThat( target.getChildren() ).isEmpty(); + + Issue3104Mapper.INSTANCE.update( + target, + new Issue3104Mapper.Source( Collections.singletonList( new Issue3104Mapper.ChildSource( "tester" ) ) ) + ); + assertThat( target.getChildren() ) + .extracting( Issue3104Mapper.Child::getMyField ) + .containsExactly( "tester" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3110/Issue3110Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3110/Issue3110Mapper.java new file mode 100644 index 0000000000..12dd823760 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3110/Issue3110Mapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3110; + +import org.mapstruct.EnumMapping; +import org.mapstruct.Mapper; + +@Mapper +public interface Issue3110Mapper { + enum SourceEnum { + FOO, BAR + } + + enum TargetEnum { + FOO, BAR + } + + class CustomCheckedException extends Exception { + public CustomCheckedException(String message) { + super( message ); + } + } + + @EnumMapping(unexpectedValueMappingException = CustomCheckedException.class) + TargetEnum map(SourceEnum sourceEnum) throws CustomCheckedException; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3110/Issue3110MapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3110/Issue3110MapperTest.java new file mode 100644 index 0000000000..3b256ba100 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3110/Issue3110MapperTest.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3110; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +@WithClasses({ + Issue3110Mapper.class +}) +@IssueKey("3110") +class Issue3110MapperTest { + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + void throwsException() { + generatedSource.forMapper( Issue3110Mapper.class ).content() + .contains( "throws CustomCheckedException" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3126/Issue3126Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3126/Issue3126Mapper.java new file mode 100644 index 0000000000..e680db9317 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3126/Issue3126Mapper.java @@ -0,0 +1,92 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3126; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) +public interface Issue3126Mapper { + + Issue3126Mapper INSTANCE = Mappers.getMapper( Issue3126Mapper.class ); + + @SubclassMapping(target = HomeAddressDto.class, source = HomeAddress.class) + @SubclassMapping(target = OfficeAddressDto.class, source = OfficeAddress.class) + @Mapping(target = ".", source = "auditable") + AddressDto map(Address address); + + interface AddressDto { + + } + + class HomeAddressDto implements AddressDto { + private String createdBy; + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + class OfficeAddressDto implements AddressDto { + private String createdBy; + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + class HomeAddress extends Address { + + public HomeAddress(Auditable auditable) { + super( auditable ); + } + } + + class OfficeAddress extends Address { + + public OfficeAddress(Auditable auditable) { + super( auditable ); + } + } + + abstract class Address { + + private final Auditable auditable; + + protected Address(Auditable auditable) { + this.auditable = auditable; + } + + public Auditable getAuditable() { + return auditable; + } + } + + class Auditable { + + private final String createdBy; + + public Auditable(String createdBy) { + this.createdBy = createdBy; + } + + public String getCreatedBy() { + return createdBy; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3126/Issue3126Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3126/Issue3126Test.java new file mode 100644 index 0000000000..35e3eb873c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3126/Issue3126Test.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3126; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3126") +@WithClasses(Issue3126Mapper.class) +class Issue3126Test { + + @ProcessorTest + void shouldCompile() { + Issue3126Mapper.Auditable auditable = new Issue3126Mapper.Auditable( "home-user" ); + Issue3126Mapper.Address address = new Issue3126Mapper.HomeAddress( auditable ); + Issue3126Mapper.AddressDto addressDto = Issue3126Mapper.INSTANCE.map( address ); + + assertThat( addressDto ).isInstanceOfSatisfying( Issue3126Mapper.HomeAddressDto.class, homeAddress -> { + assertThat( homeAddress.getCreatedBy() ).isEqualTo( "home-user" ); + } ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142Exception.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142Exception.java new file mode 100644 index 0000000000..54ed903ee7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142Exception.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3142; + +/** + * @author Filip Hrisafov + */ +public class Issue3142Exception extends Exception { + + public Issue3142Exception(String message) { + super( message ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142Test.java new file mode 100644 index 0000000000..a8b44874bb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142Test.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3142; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Issue3142Exception.class, + Source.class, + Target.class, +}) +@IssueKey("3142") +class Issue3142Test { + + @ProcessorTest + @WithClasses({ + Issue3142WithBeforeMappingExceptionMapper.class + }) + void exceptionThrownInBeforeMapping() { + assertThatThrownBy( () -> Issue3142WithBeforeMappingExceptionMapper.INSTANCE.map( + new Source( new Source.Nested( "throwException" ) ), "test" ) + ) + .isInstanceOf( Issue3142Exception.class ) + .hasMessage( "Source nested exception" ); + } + + @ProcessorTest + @WithClasses({ + Issue3142WithAfterMappingExceptionMapper.class + }) + void exceptionThrownInAfterMapping() { + assertThatThrownBy( () -> Issue3142WithAfterMappingExceptionMapper.INSTANCE.map( + new Source( new Source.Nested( "throwException" ) ), "test" ) + ) + .isInstanceOf( Issue3142Exception.class ) + .hasMessage( "Source nested exception" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142WithAfterMappingExceptionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142WithAfterMappingExceptionMapper.java new file mode 100644 index 0000000000..e188626424 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142WithAfterMappingExceptionMapper.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3142; + +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3142WithAfterMappingExceptionMapper { + + Issue3142WithAfterMappingExceptionMapper INSTANCE = + Mappers.getMapper( Issue3142WithAfterMappingExceptionMapper.class ); + + Target map(Source source, String id) throws Issue3142Exception; + + @AfterMapping + default void afterMappingValidation(Object source) throws Issue3142Exception { + if ( source instanceof Source.Nested ) { + Source.Nested nested = (Source.Nested) source; + if ( "throwException".equals( nested.getValue() ) ) { + throw new Issue3142Exception( "Source nested exception" ); + } + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142WithBeforeMappingExceptionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142WithBeforeMappingExceptionMapper.java new file mode 100644 index 0000000000..ee27d87b7f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Issue3142WithBeforeMappingExceptionMapper.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3142; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3142WithBeforeMappingExceptionMapper { + + Issue3142WithBeforeMappingExceptionMapper INSTANCE = + Mappers.getMapper( Issue3142WithBeforeMappingExceptionMapper.class ); + + Target map(Source source, String id) throws Issue3142Exception; + + @BeforeMapping + default void preMappingValidation(Object source) throws Issue3142Exception { + if ( source instanceof Source.Nested ) { + Source.Nested nested = (Source.Nested) source; + if ( "throwException".equals( nested.getValue() ) ) { + throw new Issue3142Exception( "Source nested exception" ); + } + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Source.java new file mode 100644 index 0000000000..2b78ffa022 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Source.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3142; + +/** + * @author Filip Hrisafov + */ +public class Source { + private final Nested nested; + + public Source(Nested nested) { + this.nested = nested; + } + + public Nested getNested() { + return nested; + } + + public static class Nested { + private final String value; + + public Nested(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Target.java new file mode 100644 index 0000000000..6f832b6ecf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3142/Target.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3142; + +/** + * @author Filip Hrisafov + */ +public class Target { + private String id; + private Nested nested; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + public static class Nested { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3144/Issue3144Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3144/Issue3144Mapper.java new file mode 100644 index 0000000000..df5b767876 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3144/Issue3144Mapper.java @@ -0,0 +1,66 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3144; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface Issue3144Mapper { + + Issue3144Mapper INSTANCE = Mappers.getMapper( Issue3144Mapper.class ); + + @Mapping(target = "map", source = "sourceMap") + Target mapExplicitDefined(Map sourceMap); + + @Mapping(target = "map", ignore = true) + Target map(Map sourceMap); + + Target mapMultiParameters(Source source, Map map); + + @Mapping(target = "value", source = "map.testValue") + Target mapMultiParametersDefinedMapping(Source source, Map map); + + class Source { + private final String sourceValue; + + public Source(String sourceValue) { + this.sourceValue = sourceValue; + } + + public String getSourceValue() { + return sourceValue; + } + } + + class Target { + private String value; + private Map map; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3144/Issue3144Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3144/Issue3144Test.java new file mode 100644 index 0000000000..13f349414a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3144/Issue3144Test.java @@ -0,0 +1,63 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3144; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3144Mapper.class) +@IssueKey("3144") +class Issue3144Test { + + @ProcessorTest + void shouldCorrectlyHandleMapBeanMapping() { + Map map = new HashMap<>(); + map.put( "value", "Map Value" ); + map.put( "testValue", "Map Test Value" ); + + Issue3144Mapper.Target target = Issue3144Mapper.INSTANCE.mapExplicitDefined( map ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "Map Value" ); + assertThat( target.getMap() ) + .containsOnly( + entry( "value", "Map Value" ), + entry( "testValue", "Map Test Value" ) + ); + + target = Issue3144Mapper.INSTANCE.map( map ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "Map Value" ); + assertThat( target.getMap() ).isNull(); + + target = Issue3144Mapper.INSTANCE.mapMultiParameters( null, map ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isNull(); + assertThat( target.getMap() ) + .containsOnly( + entry( "value", "Map Value" ), + entry( "testValue", "Map Test Value" ) + ); + + target = Issue3144Mapper.INSTANCE.mapMultiParametersDefinedMapping( null, map ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "Map Test Value" ); + assertThat( target.getMap() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3153/Issue3153Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3153/Issue3153Mapper.java new file mode 100644 index 0000000000..af54d23a8d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3153/Issue3153Mapper.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3153; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +interface Issue3153Mapper { + + Issue3153Mapper INSTANCE = Mappers.getMapper( Issue3153Mapper.class ); + + @ValueMapping(source = " PR", target = "PR") + @ValueMapping(source = " PR", target = "PR") + @ValueMapping(source = " PR", target = "PR") + @ValueMapping(source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL) + Target mapToEnum(String value); + + @ValueMapping(source = "PR", target = " PR") + String mapFromEnum(Target value); + + enum Target { + PR, + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3153/Issue3153Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3153/Issue3153Test.java new file mode 100644 index 0000000000..658a497086 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3153/Issue3153Test.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3153; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3153Mapper.class) +@IssueKey("3153") +class Issue3153Test { + + @ProcessorTest + void shouldNotTrimStringValueSource() { + assertThat( Issue3153Mapper.INSTANCE.mapToEnum( "PR" ) ).isEqualTo( Issue3153Mapper.Target.PR ); + assertThat( Issue3153Mapper.INSTANCE.mapToEnum( " PR" ) ).isEqualTo( Issue3153Mapper.Target.PR ); + assertThat( Issue3153Mapper.INSTANCE.mapToEnum( " PR" ) ).isEqualTo( Issue3153Mapper.Target.PR ); + assertThat( Issue3153Mapper.INSTANCE.mapToEnum( " PR" ) ).isEqualTo( Issue3153Mapper.Target.PR ); + + assertThat( Issue3153Mapper.INSTANCE.mapFromEnum( Issue3153Mapper.Target.PR ) ).isEqualTo( " PR" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3158/Issue3158Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3158/Issue3158Mapper.java new file mode 100644 index 0000000000..25af6b3d44 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3158/Issue3158Mapper.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3158; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3158Mapper { + + Issue3158Mapper INSTANCE = Mappers.getMapper( Issue3158Mapper.class ); + + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "name") + Target map(Target target); + + class Target { + private final String name; + private final String email; + + public Target(String name, String email) { + this.name = name; + this.email = email; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3158/Issue3158Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3158/Issue3158Test.java new file mode 100644 index 0000000000..e4832edfa9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3158/Issue3158Test.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3158; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3158Mapper.class) +@IssueKey("3158") +class Issue3158Test { + + @ProcessorTest + void beanMappingIgnoreByDefaultShouldBeRespectedForConstructorProperties() { + Issue3158Mapper.Target target = Issue3158Mapper.INSTANCE.map( new Issue3158Mapper.Target( + "tester", + "tester@test.com" + ) ); + + assertThat( target.getName() ).isEqualTo( "tester" ); + assertThat( target.getEmail() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3159/Issue3159Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3159/Issue3159Mapper.java new file mode 100644 index 0000000000..42c77c71dd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3159/Issue3159Mapper.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3159; + +import java.util.Collection; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3159Mapper { + + Issue3159Mapper INSTANCE = Mappers.getMapper( Issue3159Mapper.class ); + + @Mapping(target = "elements", defaultExpression = "java(new ArrayList<>())") + Target map(Source source); + + default String elementName(Element element) { + return element != null ? element.getName() : null; + } + + class Target { + private final Collection elements; + + public Target(Collection elements) { + this.elements = elements; + } + + public Collection getElements() { + return elements; + } + } + + class Source { + private final Collection elements; + + public Source(Collection elements) { + this.elements = elements; + } + + public Collection getElements() { + return elements; + } + } + + class Element { + private final String name; + + public Element(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3159/Issue3159Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3159/Issue3159Test.java new file mode 100644 index 0000000000..77feba151c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3159/Issue3159Test.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3159; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3159") +@WithClasses(Issue3159Mapper.class) +class Issue3159Test { + + @ProcessorTest + void shouldUseDefaultExpressionForCollection() { + Issue3159Mapper.Target target = Issue3159Mapper.INSTANCE.map( new Issue3159Mapper.Source( null ) ); + + assertThat( target.getElements() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Issue3163Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Issue3163Mapper.java new file mode 100644 index 0000000000..bd44a9d386 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Issue3163Mapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3163; + +import java.util.Optional; + +import org.mapstruct.Mapper; + +@Mapper +public interface Issue3163Mapper { + + Target map(Source value); + + Target.Nested map(Source.Nested value); + + default Optional wrapAsOptional(T value) { + return Optional.ofNullable( value ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Issue3163Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Issue3163Test.java new file mode 100644 index 0000000000..5c16a707f3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Issue3163Test.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3163; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +@WithClasses({ + Issue3163Mapper.class, + Source.class, + Target.class +}) +class Issue3163Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Source.java new file mode 100644 index 0000000000..8f5a57f1bf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Source.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3163; + +/** + * @author Filip Hrisafov + */ +public class Source { + + private final Nested nested; + + public Source(Nested nested) { + this.nested = nested; + } + + public Nested getNested() { + return nested; + } + + public static class Nested { + private final String value; + + public Nested(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Target.java new file mode 100644 index 0000000000..301a281d2d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3163/Target.java @@ -0,0 +1,38 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3163; + +import java.util.Optional; + +/** + * @author Filip Hrisafov + */ +public class Target { + + private Nested nested; + + public Optional getNested() { + return Optional.ofNullable( nested ); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public final void setNested(Optional nested) { + this.nested = nested.orElse( null ); + } + + public static class Nested { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3165/Issue3165Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3165/Issue3165Mapper.java new file mode 100644 index 0000000000..b6d5fc1ed7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3165/Issue3165Mapper.java @@ -0,0 +1,65 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3165; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) +public interface Issue3165Mapper { + + Issue3165Mapper INSTANCE = Mappers.getMapper( Issue3165Mapper.class ); + + Target toTarget(Source source); + + class Source { + private String[] pets; + private Iterable cats; + + public Source(String[] pets, Iterable cats) { + this.pets = pets; + this.cats = cats; + } + + public String[] getPets() { + return pets; + } + + public Iterable getCats() { + return cats; + } + } + + class Target { + private List pets; + private List cats; + + Target() { + this.pets = new ArrayList<>(); + this.cats = new ArrayList<>(); + } + + public List getPets() { + return pets; + } + + public void addPet(String pet) { + pets.add( pet ); + } + + public List getCats() { + return cats; + } + + public void addCat(String cat) { + cats.add( cat ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3165/Issue3165MapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3165/Issue3165MapperTest.java new file mode 100644 index 0000000000..4fb9e0ab3a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3165/Issue3165MapperTest.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3165; + +import java.util.Arrays; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + Issue3165Mapper.class +}) +@IssueKey("3165") +class Issue3165MapperTest { + + @ProcessorTest + void supportsAdderWhenMappingArrayAndIterableToCollection() { + Issue3165Mapper.Source src = new Issue3165Mapper.Source( + new String[] { "cat", "dog", "mouse" }, + Arrays.asList( "ivy", "flu", "freya" ) + ); + Issue3165Mapper.Target target = Issue3165Mapper.INSTANCE.toTarget( src ); + assertThat( target.getPets() ).containsExactly( "cat", "dog", "mouse" ); + assertThat( target.getCats() ).containsExactly( "ivy", "flu", "freya" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3238/ErroneousIssue3238Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3238/ErroneousIssue3238Mapper.java new file mode 100644 index 0000000000..fd0d4672a6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3238/ErroneousIssue3238Mapper.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3238; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface ErroneousIssue3238Mapper { + + @Mapping(target = ".", ignore = true) + Target map(Source source); + + class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Source { + + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3238/Issue3238Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3238/Issue3238Test.java new file mode 100644 index 0000000000..6648495a70 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3238/Issue3238Test.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3238; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * @author Filip Hrisafov + */ +@WithClasses(ErroneousIssue3238Mapper.class) +@IssueKey("3238") +class Issue3238Test { + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = @Diagnostic( type = ErroneousIssue3238Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 14, + message = "Using @Mapping( target = \".\", ignore = true ) is not allowed." + + " You need to use @BeanMapping( ignoreByDefault = true ) if you would like to ignore" + + " all non explicitly mapped target properties." + ) + ) + void shouldGenerateValidCompileError() { + + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3248/Issue3248Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3248/Issue3248Mapper.java new file mode 100644 index 0000000000..e0cbba40f7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3248/Issue3248Mapper.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3248; + +import org.mapstruct.BeanMapping; +import org.mapstruct.InheritConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface Issue3248Mapper { + + @BeanMapping(ignoreUnmappedSourceProperties = "otherValue") + Target map(Source source); + + @InheritConfiguration + Target secondMap(Source source); + + class Target { + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public String getOtherValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3248/Issue3248Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3248/Issue3248Test.java new file mode 100644 index 0000000000..ad8cf2e499 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3248/Issue3248Test.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3248; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3248") +@WithClasses({ + Issue3248Mapper.class +}) +class Issue3248Test { + + @ProcessorTest + void shouldCompileCode() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Entity.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Entity.java new file mode 100644 index 0000000000..cfd038b03c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Entity.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +public class Entity { + String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Issue3296Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Issue3296Test.java new file mode 100644 index 0000000000..9bd28d333a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Issue3296Test.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ben Zegveld + */ +@IssueKey( "3296" ) +@WithClasses( { Entity.class, Payload.class } ) +public class Issue3296Test { + + @ProcessorTest + @WithClasses( { MapperExtendingConfig.class, MapperConfigWithPayloadArgument.class } ) + public void shouldNotRaiseErrorForDefaultAfterMappingMethodImplementation() { + Payload payload = new Payload(); + payload.setName( "original" ); + + Entity entity = Mappers.getMapper( MapperExtendingConfig.class ).toEntity( payload ); + + assertThat( entity.getName() ).isEqualTo( "AfterMapping called" ); + } + + @ProcessorTest + @WithClasses( { MapperNotExtendingConfig.class, MapperConfigWithoutPayloadArgument.class } ) + public void shouldNotRaiseErrorRequiringArgumentsForDefaultMethods() { + Payload payload = new Payload(); + payload.setName( "original" ); + + Entity entity = Mappers.getMapper( MapperNotExtendingConfig.class ).toEntity( payload ); + + assertThat( entity.getName() ).isEqualTo( "original" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperConfigWithPayloadArgument.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperConfigWithPayloadArgument.java new file mode 100644 index 0000000000..69826aa10e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperConfigWithPayloadArgument.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +import org.mapstruct.AfterMapping; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingTarget; + +@MapperConfig +public interface MapperConfigWithPayloadArgument { + + @AfterMapping + default void afterMapping(@MappingTarget Entity entity, Payload unused) { + staticMethod( entity ); + } + + static void staticMethod(Entity entity) { + entity.setName( "AfterMapping called" ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperConfigWithoutPayloadArgument.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperConfigWithoutPayloadArgument.java new file mode 100644 index 0000000000..36cc046d12 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperConfigWithoutPayloadArgument.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +import org.mapstruct.AfterMapping; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingTarget; + +@MapperConfig +public interface MapperConfigWithoutPayloadArgument { + + @AfterMapping + default void afterMapping(@MappingTarget Entity entity) { + staticMethod( entity ); + } + + static void staticMethod(Entity entity) { + entity.setName( "AfterMapping called" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperExtendingConfig.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperExtendingConfig.java new file mode 100644 index 0000000000..157bd051cb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperExtendingConfig.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +import org.mapstruct.Mapper; + +@Mapper( config = MapperConfigWithPayloadArgument.class ) +public interface MapperExtendingConfig extends MapperConfigWithPayloadArgument { + Entity toEntity(Payload payload); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperNotExtendingConfig.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperNotExtendingConfig.java new file mode 100644 index 0000000000..2ceb18c253 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/MapperNotExtendingConfig.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +import org.mapstruct.Mapper; + +@Mapper( config = MapperConfigWithoutPayloadArgument.class ) +public interface MapperNotExtendingConfig { + Entity toEntity(Payload payload); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Payload.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Payload.java new file mode 100644 index 0000000000..2502e5e4a2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3296/Payload.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3296; + +public class Payload { + + String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3310/Issue3310Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3310/Issue3310Mapper.java new file mode 100644 index 0000000000..2975a5de8a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3310/Issue3310Mapper.java @@ -0,0 +1,61 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3310; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) +public interface Issue3310Mapper { + + Issue3310Mapper INSTANCE = Mappers.getMapper( Issue3310Mapper.class ); + + Target map(Source source); + + abstract class BaseClass { + + private List items; + + public List getItems() { + return items; + } + + public void setItems(List items) { + throw new UnsupportedOperationException( "adder should be used instead" ); + } + + public void addItem(T item) { + if ( items == null ) { + items = new ArrayList<>(); + } + items.add( item ); + } + } + + class Target extends BaseClass { + + } + + class Source { + + private final List items; + + public Source(List items) { + this.items = items; + } + + public List getItems() { + return items; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3310/Issue3310Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3310/Issue3310Test.java new file mode 100644 index 0000000000..a00526985f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3310/Issue3310Test.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3310; + +import java.util.Collections; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3310") +@WithClasses(Issue3310Mapper.class) +class Issue3310Test { + + @ProcessorTest + void shouldUseAdderWithGenericBaseClass() { + Issue3310Mapper.Source source = new Issue3310Mapper.Source( Collections.singletonList( "test" ) ); + Issue3310Mapper.Target target = Issue3310Mapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getItems() ).containsExactly( "test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3317/Issue3317Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3317/Issue3317Mapper.java new file mode 100644 index 0000000000..33a40b1dfb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3317/Issue3317Mapper.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3317; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3317Mapper { + + Issue3317Mapper INSTANCE = Mappers.getMapper( Issue3317Mapper.class ); + + Target map(int id, long value); + + class Target { + + private final int id; + private final long value; + + public Target(int id, long value) { + this.id = id; + this.value = value; + } + + public int getId() { + return id; + } + + public long getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3317/Issue3317Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3317/Issue3317Test.java new file mode 100644 index 0000000000..218a1f8979 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3317/Issue3317Test.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3317; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3317") +@WithClasses(Issue3317Mapper.class) +class Issue3317Test { + + @ProcessorTest + void shouldGenerateValidCode() { + Issue3317Mapper.Target target = Issue3317Mapper.INSTANCE.map( 10, 42L ); + assertThat( target ).isNotNull(); + assertThat( target.getId() ).isEqualTo( 10 ); + assertThat( target.getValue() ).isEqualTo( 42L ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Issue3331Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Issue3331Mapper.java new file mode 100644 index 0000000000..3011479c65 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Issue3331Mapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3331; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) +public interface Issue3331Mapper { + + Issue3331Mapper INSTANCE = Mappers.getMapper( Issue3331Mapper.class ); + + @SubclassMapping(source = Vehicle.Car.class, target = VehicleDto.Car.class) + @SubclassMapping(source = Vehicle.Motorbike.class, target = VehicleDto.Motorbike.class) + @Mapping(target = "name", constant = "noname") + VehicleDto map(Vehicle vehicle); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Issue3331Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Issue3331Test.java new file mode 100644 index 0000000000..3871868226 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Issue3331Test.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3331; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3331") +@WithClasses({ + Issue3331Mapper.class, + Vehicle.class, + VehicleDto.class, +}) +class Issue3331Test { + + @ProcessorTest + void shouldCorrectCompileAndThrowExceptionOnRuntime() { + VehicleDto target = Issue3331Mapper.INSTANCE.map( new Vehicle.Car( "Test car", 4 ) ); + + assertThat( target.getName() ).isEqualTo( "noname" ); + assertThat( target ) + .isInstanceOfSatisfying( VehicleDto.Car.class, car -> { + assertThat( car.getNumOfDoors() ).isEqualTo( 4 ); + } ); + + target = Issue3331Mapper.INSTANCE.map( new Vehicle.Motorbike( "Test bike", true ) ); + + assertThat( target.getName() ).isEqualTo( "noname" ); + assertThat( target ) + .isInstanceOfSatisfying( VehicleDto.Motorbike.class, bike -> { + assertThat( bike.isAllowedForMinor() ).isTrue(); + } ); + + assertThatThrownBy( () -> Issue3331Mapper.INSTANCE.map( new Vehicle.Truck( "Test truck", 3 ) ) ) + .isInstanceOf( IllegalArgumentException.class ) + .hasMessage( "Not all subclasses are supported for this mapping. Missing for " + Vehicle.Truck.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Vehicle.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Vehicle.java new file mode 100644 index 0000000000..3b68a3acc4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/Vehicle.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3331; + +/** + * @author Filip Hrisafov + */ +public abstract class Vehicle { + + private final String name; + + protected Vehicle(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static class Car extends Vehicle { + + private final int numOfDoors; + + public Car(String name, int numOfDoors) { + super( name ); + this.numOfDoors = numOfDoors; + } + + public int getNumOfDoors() { + return numOfDoors; + } + } + + public static class Motorbike extends Vehicle { + + private final boolean allowedForMinor; + + public Motorbike(String name, boolean allowedForMinor) { + super( name ); + this.allowedForMinor = allowedForMinor; + } + + public boolean isAllowedForMinor() { + return allowedForMinor; + } + } + + public static class Truck extends Vehicle { + + private final int numOfAxis; + + public Truck(String name, int numOfAxis) { + super( name ); + this.numOfAxis = numOfAxis; + } + + public int getNumOfAxis() { + return numOfAxis; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/VehicleDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/VehicleDto.java new file mode 100644 index 0000000000..8c2ef13dd2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3331/VehicleDto.java @@ -0,0 +1,51 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3331; + +/** + * @author Filip Hrisafov + */ +public abstract class VehicleDto { + + private final String name; + + protected VehicleDto(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static class Car extends VehicleDto { + + private final int numOfDoors; + + public Car(String name, int numOfDoors) { + super( name ); + this.numOfDoors = numOfDoors; + } + + public int getNumOfDoors() { + return numOfDoors; + } + } + + public static class Motorbike extends VehicleDto { + + private final boolean allowedForMinor; + + public Motorbike(String name, boolean allowedForMinor) { + super( name ); + this.allowedForMinor = allowedForMinor; + } + + public boolean isAllowedForMinor() { + return allowedForMinor; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Issue3360Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Issue3360Mapper.java new file mode 100644 index 0000000000..812d885489 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Issue3360Mapper.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3360; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper( + subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION, + unmappedTargetPolicy = ReportingPolicy.ERROR, + unmappedSourcePolicy = ReportingPolicy.ERROR +) +public interface Issue3360Mapper { + + Issue3360Mapper INSTANCE = Mappers.getMapper( Issue3360Mapper.class ); + + @SubclassMapping(target = VehicleDto.Car.class, source = Vehicle.Car.class) + VehicleDto map(Vehicle vehicle); + + @Mapping(target = "model", source = "modelName") + @BeanMapping(ignoreUnmappedSourceProperties = "computedName") + VehicleDto.Car map(Vehicle.Car car); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Issue3360Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Issue3360Test.java new file mode 100644 index 0000000000..307c2b265a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Issue3360Test.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3360; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3360") +@WithClasses({ + Issue3360Mapper.class, + Vehicle.class, + VehicleDto.class, +}) +class Issue3360Test { + + @ProcessorTest + void shouldCompileWithoutErrorsAndWarnings() { + + Vehicle vehicle = new Vehicle.Car( "Test", "car", 4 ); + + VehicleDto target = Issue3360Mapper.INSTANCE.map( vehicle ); + + assertThat( target.getName() ).isEqualTo( "Test" ); + assertThat( target.getModel() ).isEqualTo( "car" ); + assertThat( target ).isInstanceOfSatisfying( VehicleDto.Car.class, car -> { + assertThat( car.getNumOfDoors() ).isEqualTo( 4 ); + } ); + + assertThatThrownBy( () -> Issue3360Mapper.INSTANCE.map( new Vehicle.Motorbike( "Test", "bike" ) ) ) + .isInstanceOf( IllegalArgumentException.class ) + .hasMessage( "Not all subclasses are supported for this mapping. Missing for " + Vehicle.Motorbike.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Vehicle.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Vehicle.java new file mode 100644 index 0000000000..0cc2011bc9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/Vehicle.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3360; + +/** + * @author Filip Hrisafov + */ +public abstract class Vehicle { + + private final String name; + private final String modelName; + + protected Vehicle(String name, String modelName) { + this.name = name; + this.modelName = modelName; + } + + public String getName() { + return name; + } + + public String getModelName() { + return modelName; + } + + public String getComputedName() { + return null; + } + + public static class Car extends Vehicle { + + private final int numOfDoors; + + public Car(String name, String modelName, int numOfDoors) { + super( name, modelName ); + this.numOfDoors = numOfDoors; + } + + public int getNumOfDoors() { + return numOfDoors; + } + } + + public static class Motorbike extends Vehicle { + + public Motorbike(String name, String modelName) { + super( name, modelName ); + } + + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/VehicleDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/VehicleDto.java new file mode 100644 index 0000000000..fc93a2fcae --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3360/VehicleDto.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3360; + +/** + * @author Filip Hrisafov + */ +public abstract class VehicleDto { + + private String name; + private String model; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public static class Car extends VehicleDto { + + private int numOfDoors; + + public int getNumOfDoors() { + return numOfDoors; + } + + public void setNumOfDoors(int numOfDoors) { + this.numOfDoors = numOfDoors; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3361/Issue3361Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3361/Issue3361Mapper.java new file mode 100644 index 0000000000..a556b63306 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3361/Issue3361Mapper.java @@ -0,0 +1,80 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3361; + +import org.mapstruct.InheritConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +@Mapper +public abstract class Issue3361Mapper { + + public static final Issue3361Mapper INSTANCE = Mappers.getMapper( Issue3361Mapper.class ); + + @Mapping(target = "someAttribute", source = "source.attribute") + @Mapping(target = "otherAttribute", source = "otherSource.anotherAttribute") + public abstract Target mapFromSource(Source source, OtherSource otherSource); + + @InheritConfiguration(name = "mapFromSource") + @Mapping(target = "otherAttribute", source = "source", qualifiedByName = "otherMapping") + public abstract Target mapInherited(Source source, OtherSource otherSource); + + @Named("otherMapping") + protected Long otherMapping(Source source) { + return source.getAttribute() != null ? 1L : 0L; + } + + public static class Target { + private String someAttribute; + private Long otherAttribute; + + public String getSomeAttribute() { + return someAttribute; + } + + public Target setSomeAttribute(String someAttribute) { + this.someAttribute = someAttribute; + return this; + } + + public Long getOtherAttribute() { + return otherAttribute; + } + + public Target setOtherAttribute(Long otherAttribute) { + this.otherAttribute = otherAttribute; + return this; + } + } + + public static class Source { + private final String attribute; + + public Source(String attribute) { + this.attribute = attribute; + } + + public String getAttribute() { + return attribute; + } + } + + public static class OtherSource { + + private final Long anotherAttribute; + + public OtherSource(Long anotherAttribute) { + this.anotherAttribute = anotherAttribute; + } + + public Long getAnotherAttribute() { + return anotherAttribute; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3361/Issue3361Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3361/Issue3361Test.java new file mode 100644 index 0000000000..3fa16ec1ae --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3361/Issue3361Test.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3361; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3361") +@WithClasses(Issue3361Mapper.class) +class Issue3361Test { + + @ProcessorTest + void multiSourceShouldInherit() { + Issue3361Mapper.Source source = new Issue3361Mapper.Source( "Test" ); + Issue3361Mapper.OtherSource otherSource = new Issue3361Mapper.OtherSource( 10L ); + + Issue3361Mapper.Target target = Issue3361Mapper.INSTANCE.mapFromSource( source, otherSource ); + assertThat( target.getSomeAttribute() ).isEqualTo( "Test" ); + assertThat( target.getOtherAttribute() ).isEqualTo( 10L ); + + target = Issue3361Mapper.INSTANCE.mapInherited( source, otherSource ); + assertThat( target.getSomeAttribute() ).isEqualTo( "Test" ); + assertThat( target.getOtherAttribute() ).isEqualTo( 1L ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/Issue3370BuilderProvider.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/Issue3370BuilderProvider.java new file mode 100644 index 0000000000..55a3f783c3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/Issue3370BuilderProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3370; + +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; + +import org.mapstruct.ap.spi.BuilderInfo; +import org.mapstruct.ap.spi.BuilderProvider; +import org.mapstruct.ap.spi.ImmutablesBuilderProvider; + +public class Issue3370BuilderProvider extends ImmutablesBuilderProvider implements BuilderProvider { + + @Override + protected BuilderInfo findBuilderInfoForImmutables(TypeElement typeElement) { + Name name = typeElement.getQualifiedName(); + if ( name.toString().endsWith( ".Item" ) ) { + return super.findBuilderInfo( asImmutableElement( typeElement ) ); + } + return super.findBuilderInfo( typeElement ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/Issue3370Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/Issue3370Test.java new file mode 100644 index 0000000000..0389de5b46 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/Issue3370Test.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3370; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.ap.spi.AccessorNamingStrategy; +import org.mapstruct.ap.spi.BuilderProvider; +import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy; +import org.mapstruct.ap.test.bugs._3370.domain.ImmutableItem; +import org.mapstruct.ap.test.bugs._3370.domain.Item; +import org.mapstruct.ap.test.bugs._3370.dto.ItemDTO; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithServiceImplementation; +import org.mapstruct.ap.testutil.WithServiceImplementations; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + ItemMapper.class, + Item.class, + ImmutableItem.class, + ItemDTO.class, +}) +@IssueKey("3370") +@WithServiceImplementations({ + @WithServiceImplementation(provides = BuilderProvider.class, value = Issue3370BuilderProvider.class), + @WithServiceImplementation(provides = AccessorNamingStrategy.class, + value = ImmutablesAccessorNamingStrategy.class), +}) +public class Issue3370Test { + + @ProcessorTest + public void shouldUseBuilderOfImmutableSuperClass() { + + Map attributesMap = new HashMap<>(); + attributesMap.put( "a", "b" ); + attributesMap.put( "c", "d" ); + + ItemDTO item = new ItemDTO( "test", attributesMap ); + + Item target = ItemMapper.INSTANCE.map( item ); + + assertThat( target ).isNotNull(); + assertThat( target.getId() ).isEqualTo( "test" ); + assertThat( target.getAttributes() ).isEqualTo( attributesMap ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/ItemMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/ItemMapper.java new file mode 100644 index 0000000000..5583e191b3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/ItemMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3370; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.bugs._3370.domain.Item; +import org.mapstruct.ap.test.bugs._3370.dto.ItemDTO; +import org.mapstruct.factory.Mappers; + +@Mapper +public abstract class ItemMapper { + + public static final ItemMapper INSTANCE = Mappers.getMapper( ItemMapper.class ); + + public abstract Item map(ItemDTO itemDTO); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/domain/ImmutableItem.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/domain/ImmutableItem.java new file mode 100644 index 0000000000..fabae2f0d0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/domain/ImmutableItem.java @@ -0,0 +1,316 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3370.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Immutable implementation of {@link Item}. + *

      + * Use the builder to create immutable instances: + * {@code new Item.Builder()}. + */ +@SuppressWarnings("all") +public final class ImmutableItem extends Item { + private final String id; + private final Map attributes; + + private ImmutableItem(String id, Map attributes) { + this.id = id; + this.attributes = attributes; + } + + /** + * @return The value of the {@code id} attribute + */ + @Override + public String getId() { + return id; + } + + /** + * @return The value of the {@code attributes} attribute + */ + @Override + public Map getAttributes() { + return attributes; + } + + /** + * Copy the current immutable object by setting a value for the {@link Item#getId() id} attribute. + * An equals check used to prevent copying of the same value by returning {@code this}. + * + * @param id A new value for id + * @return A modified copy of the {@code this} object + */ + public final ImmutableItem withId(String id) { + if ( this.id.equals( id ) ) { + return this; + } + String newValue = Objects.requireNonNull( id, "id" ); + return new ImmutableItem( newValue, this.attributes ); + } + + /** + * Copy the current immutable object by replacing the {@link Item#getAttributes() attributes} map with the specified map. + * Nulls are not permitted as keys or values. + * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}. + * + * @param attributes The entries to be added to the attributes map + * @return A modified copy of {@code this} object + */ + public final ImmutableItem withAttributes(Map attributes) { + if ( this.attributes == attributes ) { + return this; + } + Map newValue = createUnmodifiableMap( true, false, attributes ); + return new ImmutableItem( this.id, newValue ); + } + + /** + * This instance is equal to all instances of {@code ImmutableItem} that have equal attribute values. + * + * @return {@code true} if {@code this} is equal to {@code another} instance + */ + @Override + public boolean equals(Object another) { + if ( this == another ) { + return true; + } + return another instanceof ImmutableItem + && equalTo( (ImmutableItem) another ); + } + + private boolean equalTo(ImmutableItem another) { + return id.equals( another.id ) + && attributes.equals( another.attributes ); + } + + /** + * Computes a hash code from attributes: {@code id}, {@code attributes}. + * + * @return hashCode value + */ + @Override + public int hashCode() { + int h = 31; + h = h * 17 + id.hashCode(); + h = h * 17 + attributes.hashCode(); + return h; + } + + /** + * Prints the immutable value {@code Item} with attribute values. + * + * @return A string representation of the value + */ + @Override + public String toString() { + return "Item{" + + "id=" + id + + ", attributes=" + attributes + + "}"; + } + + /** + * Creates an immutable copy of a {@link Item} value. + * Uses accessors to get values to initialize the new immutable instance. + * If an instance is already immutable, it is returned as is. + * + * @param instance The instance to copy + * @return A copied immutable Item instance + */ + public static ImmutableItem copyOf(Item instance) { + if ( instance instanceof ImmutableItem ) { + return (ImmutableItem) instance; + } + return new Item.Builder() + .from( instance ) + .build(); + } + + /** + * Builds instances of type {@link ImmutableItem ImmutableItem}. + * Initialize attributes and then invoke the {@link #build()} method to create an + * immutable instance. + *

      {@code Builder} is not thread-safe and generally should not be stored in a field or collection, + * but instead used immediately to create instances. + */ + public static class Builder { + private static final long INIT_BIT_ID = 0x1L; + private long initBits = 0x1L; + + private String id; + private Map attributes = new LinkedHashMap(); + + /** + * Creates a builder for {@link ImmutableItem ImmutableItem} instances. + */ + public Builder() { + if ( !( this instanceof Item.Builder ) ) { + throw new UnsupportedOperationException( "Use: new Item.Builder()" ); + } + } + + /** + * Fill a builder with attribute values from the provided {@code Item} instance. + * Regular attribute values will be replaced with those from the given instance. + * Absent optional values will not replace present values. + * Collection elements and entries will be added, not replaced. + * + * @param instance The instance from which to copy values + * @return {@code this} builder for use in a chained invocation + */ + public final Item.Builder from(Item instance) { + Objects.requireNonNull( instance, "instance" ); + id( instance.getId() ); + putAllAttributes( instance.getAttributes() ); + return (Item.Builder) this; + } + + /** + * Initializes the value for the {@link Item#getId() id} attribute. + * + * @param id The value for id + * @return {@code this} builder for use in a chained invocation + */ + public final Item.Builder id(String id) { + this.id = Objects.requireNonNull( id, "id" ); + initBits &= ~INIT_BIT_ID; + return (Item.Builder) this; + } + + /** + * Put one entry to the {@link Item#getAttributes() attributes} map. + * + * @param key The key in the attributes map + * @param value The associated value in the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final Item.Builder putAttributes(String key, String value) { + this.attributes.put( + Objects.requireNonNull( key, "attributes key" ), + Objects.requireNonNull( value, "attributes value" ) + ); + return (Item.Builder) this; + } + + /** + * Put one entry to the {@link Item#getAttributes() attributes} map. Nulls are not permitted + * + * @param entry The key and value entry + * @return {@code this} builder for use in a chained invocation + */ + public final Item.Builder putAttributes(Map.Entry entry) { + String k = entry.getKey(); + String v = entry.getValue(); + this.attributes.put( + Objects.requireNonNull( k, "attributes key" ), + Objects.requireNonNull( v, "attributes value" ) + ); + return (Item.Builder) this; + } + + /** + * Sets or replaces all mappings from the specified map as entries for the {@link Item#getAttributes() attributes} map. Nulls are not permitted + * + * @param attributes The entries that will be added to the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final Item.Builder attributes(Map attributes) { + this.attributes.clear(); + return putAllAttributes( attributes ); + } + + /** + * Put all mappings from the specified map as entries to {@link Item#getAttributes() attributes} map. Nulls are not permitted + * + * @param attributes The entries that will be added to the attributes map + * @return {@code this} builder for use in a chained invocation + */ + public final Item.Builder putAllAttributes(Map attributes) { + for ( Map.Entry entry : attributes.entrySet() ) { + String k = entry.getKey(); + String v = entry.getValue(); + this.attributes.put( + Objects.requireNonNull( k, "attributes key" ), + Objects.requireNonNull( v, "attributes value" ) + ); + } + return (Item.Builder) this; + } + + /** + * Builds a new {@link ImmutableItem ImmutableItem}. + * + * @return An immutable instance of Item + * @throws java.lang.IllegalStateException if any required attributes are missing + */ + public ImmutableItem build() { + if ( initBits != 0 ) { + throw new IllegalStateException( formatRequiredAttributesMessage() ); + } + return new ImmutableItem( id, createUnmodifiableMap( false, false, attributes ) ); + } + + private String formatRequiredAttributesMessage() { + List attributes = new ArrayList<>(); + if ( ( initBits & INIT_BIT_ID ) != 0 ) { + attributes.add( "id" ); + } + return "Cannot build Item, some of required attributes are not set " + attributes; + } + } + + private static Map createUnmodifiableMap(boolean checkNulls, boolean skipNulls, + Map map) { + switch ( map.size() ) { + case 0: + return Collections.emptyMap(); + case 1: { + Map.Entry e = map.entrySet().iterator().next(); + K k = e.getKey(); + V v = e.getValue(); + if ( checkNulls ) { + Objects.requireNonNull( k, "key" ); + Objects.requireNonNull( v, "value" ); + } + if ( skipNulls && ( k == null || v == null ) ) { + return Collections.emptyMap(); + } + return Collections.singletonMap( k, v ); + } + default: { + Map linkedMap = new LinkedHashMap( map.size() ); + if ( skipNulls || checkNulls ) { + for ( Map.Entry e : map.entrySet() ) { + K k = e.getKey(); + V v = e.getValue(); + if ( skipNulls ) { + if ( k == null || v == null ) { + continue; + } + } + else if ( checkNulls ) { + Objects.requireNonNull( k, "key" ); + Objects.requireNonNull( v, "value" ); + } + linkedMap.put( k, v ); + } + } + else { + linkedMap.putAll( map ); + } + return Collections.unmodifiableMap( linkedMap ); + } + } + } +} \ No newline at end of file diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/domain/Item.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/domain/Item.java new file mode 100644 index 0000000000..a78c3bc007 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/domain/Item.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3370.domain; + +import java.util.Collections; +import java.util.Map; + +public abstract class Item { + + public abstract String getId(); + + public abstract Map getAttributes(); + + public static Item.Builder builder() { + return new Item.Builder(); + } + + public static class Builder extends ImmutableItem.Builder { + + public ImmutableItem.Builder addSomeData(String key, String data) { + return super.attributes( Collections.singletonMap( key, data ) ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/dto/ItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/dto/ItemDTO.java new file mode 100644 index 0000000000..70dba87fe4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3370/dto/ItemDTO.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3370.dto; + +import java.util.Map; + +public class ItemDTO { + private final String id; + private final Map attributes; + + public ItemDTO(String id, Map attributes) { + this.id = id; + this.attributes = attributes; + } + + public String getId() { + return id; + } + + public Map getAttributes() { + return attributes; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3413/Erroneous3413Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3413/Erroneous3413Mapper.java new file mode 100644 index 0000000000..784e47b8e7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3413/Erroneous3413Mapper.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3413; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Muhammad Usama + */ +@Mapper +public interface Erroneous3413Mapper { + Erroneous3413Mapper INSTANCE = Mappers.getMapper( Erroneous3413Mapper.class ); + + @Mapping(target = "", expression = "", conditionQualifiedByName = "") + ToPOJO map(FromPOJO fromPOJO); + + class FromPOJO { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class ToPOJO { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3413/Issue3413Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3413/Issue3413Test.java new file mode 100644 index 0000000000..f98ed9cdbd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3413/Issue3413Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3413; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * @author Muhammad Usama + */ +@IssueKey("3413") +public class Issue3413Test { + @ProcessorTest + @WithClasses(Erroneous3413Mapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 19, + message = "Expression and condition qualified by name are both defined in @Mapping, " + + "either define an expression or a condition qualified by name." + ) + } + ) + void errorExpectedBecauseExpressionAndConditionQualifiedByNameCannotCoExists() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3462/Issue3462Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3462/Issue3462Mapper.java new file mode 100644 index 0000000000..1bf87672c6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3462/Issue3462Mapper.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3462; + +import java.util.List; +import java.util.stream.Stream; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3462Mapper { + + Issue3462Mapper INSTANCE = Mappers.getMapper( Issue3462Mapper.class ); + + Target map(Source source); + + class Source { + private final List values; + + public Source(List values) { + this.values = values; + } + + public List getValues() { + return values; + } + + public Stream getValuesStream() { + return values != null ? values.stream() : Stream.empty(); + } + } + + class Target { + private List values; + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } + + public Stream getValuesStream() { + return values != null ? values.stream() : Stream.empty(); + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3462/Issue3462Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3462/Issue3462Test.java new file mode 100644 index 0000000000..16be4f441e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3462/Issue3462Test.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3462; + +import java.util.Arrays; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3462") +@WithClasses(Issue3462Mapper.class) +class Issue3462Test { + + @ProcessorTest + void shouldNotTreatStreamGettersAsAlternativeSetter() { + + Issue3462Mapper.Source source = new Issue3462Mapper.Source( Arrays.asList( "first", "second" ) ); + Issue3462Mapper.Target target = Issue3462Mapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getValues() ).containsExactly( "first", "second" ); + assertThat( target.getValuesStream() ).containsExactly( "first", "second" ); + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/EntityBuilder.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/EntityBuilder.java new file mode 100644 index 0000000000..6b0b71993e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/EntityBuilder.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3463; + +/** + * @author Filip Hrisafov + */ +public interface EntityBuilder { + + T build(); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Issue3463Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Issue3463Mapper.java new file mode 100644 index 0000000000..37ead3dfdf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Issue3463Mapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3463; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3463Mapper { + + Issue3463Mapper INSTANCE = Mappers.getMapper( Issue3463Mapper.class ); + + Person map(PersonDto dto); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Issue3463Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Issue3463Test.java new file mode 100644 index 0000000000..e6cfc3ece7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Issue3463Test.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3463; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3463") +@WithClasses({ + EntityBuilder.class, + Issue3463Mapper.class, + Person.class, + PersonDto.class +}) +class Issue3463Test { + + @ProcessorTest + void shouldUseInterfaceBuildMethod() { + Person person = Issue3463Mapper.INSTANCE.map( new PersonDto( "Tester" ) ); + + assertThat( person ).isNotNull(); + assertThat( person.getName() ).isEqualTo( "Tester" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Person.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Person.java new file mode 100644 index 0000000000..977d619474 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/Person.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3463; + +/** + * @author Filip Hrisafov + */ +public class Person { + + private final String name; + + private Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder extends EntityBuilder { + + Builder name(String name); + } + + private static final class BuilderImpl implements Builder { + + private String name; + + private BuilderImpl() { + } + + @Override + public Builder name(String name) { + this.name = name; + return this; + } + + @Override + public Person build() { + return new Person( this.name ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/PersonDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/PersonDto.java new file mode 100644 index 0000000000..447e4813cc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3463/PersonDto.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3463; + +/** + * @author Filip Hrisafov + */ +public class PersonDto { + private final String name; + + public PersonDto(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3485/ErroneousIssue3485Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3485/ErroneousIssue3485Mapper.java new file mode 100644 index 0000000000..dc1558907c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3485/ErroneousIssue3485Mapper.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3485; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author hduelme + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ErroneousIssue3485Mapper { + + ErroneousIssue3485Mapper INSTANCE = Mappers.getMapper( ErroneousIssue3485Mapper.class ); + + class Target { + private final String value; + + public Target( String value ) { + this.value = value; + } + + public String getValue() { + return value; + } + + } + + @Mapping(target = ".") + Target targetFromExpression(String s); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3485/Issue3485Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3485/Issue3485Test.java new file mode 100644 index 0000000000..964de36ec8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3485/Issue3485Test.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3485; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * @author hduelme + */ +@IssueKey("3485") +public class Issue3485Test { + + @ProcessorTest + @WithClasses(ErroneousIssue3485Mapper.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousIssue3485Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 34, + message = "Using @Mapping( target = \".\") requires a source property. Expression or " + + "constant cannot be used as a source.") + }) + void thisMappingWithoutSource() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3561/Issue3561Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3561/Issue3561Mapper.java new file mode 100644 index 0000000000..fd1137137b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3561/Issue3561Mapper.java @@ -0,0 +1,60 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3561; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue3561Mapper { + + Issue3561Mapper INSTANCE = Mappers.getMapper( Issue3561Mapper.class ); + + @Mapping(target = "value", conditionQualifiedByName = "shouldMapValue") + Target map(Source source, @Context boolean shouldMapValue); + + @Condition + @Named("shouldMapValue") + default boolean shouldMapValue(@Context boolean shouldMapValue) { + return shouldMapValue; + } + + class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Source { + + private String value; + private boolean valueInitialized; + + public String getValue() { + if ( valueInitialized ) { + return value; + } + + throw new IllegalStateException( "value is not initialized" ); + } + + public void setValue(String value) { + this.valueInitialized = true; + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3561/Issue3561Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3561/Issue3561Test.java new file mode 100644 index 0000000000..7cd0c26694 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3561/Issue3561Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3561; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3561Mapper.class) +@IssueKey("3561") +class Issue3561Test { + + @ProcessorTest + void shouldCorrectlyUseConditionWithContext() { + + Issue3561Mapper.Source source = new Issue3561Mapper.Source(); + + assertThatThrownBy( () -> Issue3561Mapper.INSTANCE.map( source, true ) ) + .isInstanceOf( IllegalStateException.class ) + .hasMessage( "value is not initialized" ); + + Issue3561Mapper.Target target = Issue3561Mapper.INSTANCE.map( source, false ); + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3565/Issue3565Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3565/Issue3565Mapper.java new file mode 100644 index 0000000000..b86e667e56 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3565/Issue3565Mapper.java @@ -0,0 +1,57 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3565; + +import java.util.Optional; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3565Mapper { + + Issue3565Mapper INSTANCE = Mappers.getMapper( Issue3565Mapper.class ); + + default T mapFromOptional(Optional value) { + return value.orElse( (T) null ); + } + + @Condition + default boolean isOptionalPresent(Optional value) { + return value.isPresent(); + } + + Target map(Source source); + + class Source { + + private final Boolean condition; + + public Source(Boolean condition) { + this.condition = condition; + } + + public Optional getCondition() { + return Optional.ofNullable( this.condition ); + } + } + + class Target { + private String condition; + + public Optional getCondition() { + return Optional.ofNullable( this.condition ); + } + + public void setCondition(String condition) { + this.condition = condition; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3565/Issue3565Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3565/Issue3565Test.java new file mode 100644 index 0000000000..c4cd028d45 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3565/Issue3565Test.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3565; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3565Mapper.class) +@IssueKey("3565") +class Issue3565Test { + + @ProcessorTest + void shouldGenerateValidCode() { + Issue3565Mapper.Target target = Issue3565Mapper.INSTANCE.map( new Issue3565Mapper.Source( null ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getCondition() ).isEmpty(); + + target = Issue3565Mapper.INSTANCE.map( new Issue3565Mapper.Source( false ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getCondition() ).hasValue( "false" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/Bean.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/Bean.java new file mode 100644 index 0000000000..7da423c906 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/Bean.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import java.util.List; + +public class Bean { + private List beans; + private String value; + + public Bean() { + } + + public Bean(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getBeans() { + return beans; + } + + public void setBeans(List beans) { + this.beans = beans; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/BeanDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/BeanDto.java new file mode 100644 index 0000000000..00e70ff228 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/BeanDto.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import java.util.List; + +public class BeanDto { + + private List beans; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getBeans() { + return beans; + } + + public void setBeans(List beans) { + this.beans = beans; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/BeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/BeanMapper.java new file mode 100644 index 0000000000..eeb54b4710 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/BeanMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface BeanMapper { + + BeanMapper INSTANCE = Mappers.getMapper( BeanMapper.class ); + + @Mapping(source = "beans", target = "beans") + BeanDto map(Bean bean, @MappingTarget BeanDto beanDto); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBean.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBean.java new file mode 100644 index 0000000000..b0e68ecfcf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBean.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import java.util.Map; +import java.util.stream.Stream; + +public class ContainerBean { + + private String value; + private Map beanMap; + private Stream beanStream; + + public ContainerBean() { + } + + public ContainerBean(String value) { + this.value = value; + } + + public Map getBeanMap() { + return beanMap; + } + + public void setBeanMap(Map beanMap) { + this.beanMap = beanMap; + } + + public Stream getBeanStream() { + return beanStream; + } + + public void setBeanStream(Stream beanStream) { + this.beanStream = beanStream; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBeanDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBeanDto.java new file mode 100644 index 0000000000..86bb0193ac --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBeanDto.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import java.util.Map; +import java.util.stream.Stream; + +public class ContainerBeanDto { + + private String value; + private Map beanMap; + private Stream beanStream; + + public Map getBeanMap() { + return beanMap; + } + + public void setBeanMap(Map beanMap) { + this.beanMap = beanMap; + } + + public Stream getBeanStream() { + return beanStream; + } + + public void setBeanStream(Stream beanStream) { + this.beanStream = beanStream; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBeanMapper.java new file mode 100644 index 0000000000..0da338aadf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/ContainerBeanMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ContainerBeanMapper { + + ContainerBeanMapper INSTANCE = Mappers.getMapper( ContainerBeanMapper.class ); + + @Mapping(source = "beanMap", target = "beanMap") + @Mapping(source = "beanStream", target = "beanStream") + ContainerBeanDto mapWithMapMapping(ContainerBean containerBean, @MappingTarget ContainerBeanDto containerBeanDto); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/Issue3591Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/Issue3591Test.java new file mode 100644 index 0000000000..15bb191ffc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3591/Issue3591Test.java @@ -0,0 +1,79 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3591; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3591") +class Issue3591Test { + + @RegisterExtension + GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + BeanDto.class, + Bean.class, + BeanMapper.class + }) + void mapNestedBeansWithMappingAnnotation() { + Bean bean = new Bean( "parent" ); + Bean child = new Bean( "child" ); + bean.setBeans( Collections.singletonList( child ) ); + + BeanDto beanDto = BeanMapper.INSTANCE.map( bean, new BeanDto() ); + + assertThat( beanDto ).isNotNull(); + assertThat( beanDto.getValue() ).isEqualTo( "parent" ); + assertThat( beanDto.getBeans() ) + .extracting( BeanDto::getValue ) + .containsExactly( "child" ); + } + + @ProcessorTest + @WithClasses({ + ContainerBean.class, + ContainerBeanDto.class, + ContainerBeanMapper.class, + }) + void shouldMapNestedMapAndStream() { + generatedSource.addComparisonToFixtureFor( ContainerBeanMapper.class ); + + ContainerBean containerBean = new ContainerBean( "parent" ); + Map beanMap = new HashMap<>(); + beanMap.put( "child", new ContainerBean( "mapChild" ) ); + containerBean.setBeanMap( beanMap ); + + Stream streamChild = Stream.of( new ContainerBean( "streamChild" ) ); + containerBean.setBeanStream( streamChild ); + + ContainerBeanDto dto = ContainerBeanMapper.INSTANCE.mapWithMapMapping( containerBean, new ContainerBeanDto() ); + + assertThat( dto ).isNotNull(); + + assertThat( dto.getBeanMap() ) + .extractingByKey( "child" ) + .extracting( ContainerBeanDto::getValue ) + .isEqualTo( "mapChild" ); + + assertThat( dto.getBeanStream() ) + .singleElement() + .extracting( ContainerBeanDto::getValue ) + .isEqualTo( "streamChild" ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3601/Issue3601Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3601/Issue3601Mapper.java new file mode 100644 index 0000000000..851e353de7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3601/Issue3601Mapper.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3601; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourceParameterCondition; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3601Mapper { + + Issue3601Mapper INSTANCE = Mappers.getMapper( Issue3601Mapper.class ); + + @Mapping(target = "currentId", source = "source.uuid") + @Mapping(target = "targetIds", source = "sourceIds") + Target map(Source source, List sourceIds); + + @SourceParameterCondition + default boolean isNotEmpty(List elements) { + return elements != null && !elements.isEmpty(); + } + + class Source { + private final String uuid; + + public Source(String uuid) { + this.uuid = uuid; + } + + public String getUuid() { + return uuid; + } + } + + class Target { + //CHECKSTYLE:OFF + public String currentId; + public List targetIds; + //CHECKSTYLE:ON + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3601/Issue3601Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3601/Issue3601Test.java new file mode 100644 index 0000000000..b9b68fb583 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3601/Issue3601Test.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3601; + +import java.util.Collections; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3601") +class Issue3601Test { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses( Issue3601Mapper.class ) + void shouldUseSourceParameterPresenceCheckCorrectly() { + Issue3601Mapper.Target target = Issue3601Mapper.INSTANCE.map( + new Issue3601Mapper.Source( "test1" ), + Collections.emptyList() + ); + + assertThat( target ).isNotNull(); + assertThat( target.currentId ).isEqualTo( "test1" ); + assertThat( target.targetIds ).isNull(); + + target = Issue3601Mapper.INSTANCE.map( + null, + Collections.emptyList() + ); + + assertThat( target ).isNull(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3609/Issue3609Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3609/Issue3609Mapper.java new file mode 100644 index 0000000000..41b1e2aefa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3609/Issue3609Mapper.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3609; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.SubclassMapping; + +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public abstract class Issue3609Mapper { + + @SubclassMapping(source = CarDto.class, target = Car.class) + @BeanMapping(ignoreUnmappedSourceProperties = "id") + public abstract Vehicle toVehicle(VehicleDto vehicle); + + //CHECKSTYLE:OFF + public static class Vehicle { + public int price; + } + + public static class Car extends Vehicle { + public int seats; + + public Car(int price, int seats) { + this.price = price; + this.seats = seats; + } + } + + public static class VehicleDto { + public int id; + public int price; + } + + public static class CarDto extends VehicleDto { + public int seats; + } + //CHECKSTYLE:ON +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3609/Issue3609Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3609/Issue3609Test.java new file mode 100644 index 0000000000..8a57a1357f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3609/Issue3609Test.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3609; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Roman Obolonskyii + */ +@IssueKey("3609") +@WithClasses(Issue3609Mapper.class) +public class Issue3609Test { + + @ProcessorTest + void shouldCompileWithoutErrors() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Bar.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Bar.java new file mode 100644 index 0000000000..3ac8b595ee --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Bar.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3652; + +public class Bar { + + private int secret; + private int doesNotExistInFoo; + + public int getSecret() { + return secret; + } + + public void setSecret(int secret) { + this.secret = secret; + } + + public int getDoesNotExistInFoo() { + return doesNotExistInFoo; + } + + public void setDoesNotExistInFoo(int doesNotExistInFoo) { + this.doesNotExistInFoo = doesNotExistInFoo; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Foo.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Foo.java new file mode 100644 index 0000000000..02b5b6e8b0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Foo.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3652; + +public class Foo { + + private int secret; + + public int getSecret() { + return secret; + } + + public void setSecret(int secret) { + this.secret = secret; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/FooBarConfig.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/FooBarConfig.java new file mode 100644 index 0000000000..3cf19dfbfb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/FooBarConfig.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3652; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.MapperConfig; +import org.mapstruct.Mapping; +import org.mapstruct.MappingInheritanceStrategy; + +@MapperConfig(mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG) +public interface FooBarConfig { + + @Mapping(target = "doesNotExistInFoo", ignore = true) + @Mapping(target = "secret", ignore = true) + Bar toBar(Foo foo); + + @InheritInverseConfiguration(name = "toBar") + Foo toFoo(Bar bar); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/FooBarMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/FooBarMapper.java new file mode 100644 index 0000000000..d58be74f1e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/FooBarMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3652; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper(config = FooBarConfig.class) +public interface FooBarMapper { + + FooBarMapper INSTANCE = Mappers.getMapper( FooBarMapper.class ); + + Bar toBar(Foo foo); + + Foo toFoo(Bar bar); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Issue3652Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Issue3652Test.java new file mode 100644 index 0000000000..aa8a64ada8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3652/Issue3652Test.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3652; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3652") +public class Issue3652Test { + + @WithClasses({ + Bar.class, + Foo.class, + FooBarConfig.class, + FooBarMapper.class, + }) + @ProcessorTest + void ignoreMappingsWithoutSourceShouldBeInvertible() { + Bar bar = new Bar(); + bar.setSecret( 123 ); + bar.setDoesNotExistInFoo( 6 ); + + Foo foo = FooBarMapper.INSTANCE.toFoo( bar ); + + assertThat( foo.getSecret() ).isEqualTo( 0 ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Issue3667Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Issue3667Mapper.java new file mode 100644 index 0000000000..d4f2dff0af --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Issue3667Mapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3667; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue3667Mapper { + + Issue3667Mapper INSTANCE = Mappers.getMapper( Issue3667Mapper.class ); + + @Mapping(target = "nested.value", source = "nested.nested1.value") + Target mapFirst(Source source); + + @Mapping(target = "nested.value", source = "nested.nested2.value") + Target mapSecond(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Issue3667Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Issue3667Test.java new file mode 100644 index 0000000000..1e91de00d7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Issue3667Test.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3667; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3667") +@WithClasses({ + Issue3667Mapper.class, + Source.class, + Target.class +}) +class Issue3667Test { + + @ProcessorTest + void shouldCorrectlyMapNestedProperty() { + Source source = new Source( + new Source.Nested( + new Source.NestedNested( "value1" ), + new Source.NestedNested( "value2" ) + ) + ); + + Target target1 = Issue3667Mapper.INSTANCE.mapFirst( source ); + Target target2 = Issue3667Mapper.INSTANCE.mapSecond( source ); + + assertThat( target1 ).isNotNull(); + assertThat( target1.getNested() ).isNotNull(); + assertThat( target1.getNested().getValue() ).isEqualTo( "value1" ); + + assertThat( target2 ).isNotNull(); + assertThat( target2.getNested() ).isNotNull(); + assertThat( target2.getNested().getValue() ).isEqualTo( "value2" ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Source.java new file mode 100644 index 0000000000..ede78edf0d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Source.java @@ -0,0 +1,51 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3667; + +public class Source { + + private final Nested nested; + + public Source(Nested nested) { + this.nested = nested; + } + + public Nested getNested() { + return nested; + } + + public static class Nested { + + private final NestedNested nested1; + private final NestedNested nested2; + + public Nested(NestedNested nested1, NestedNested nested2) { + this.nested1 = nested1; + this.nested2 = nested2; + } + + public NestedNested getNested1() { + return nested1; + } + + public NestedNested getNested2() { + return nested2; + } + } + + public static class NestedNested { + + private final String value; + + public NestedNested(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Target.java new file mode 100644 index 0000000000..e0c8d23296 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3667/Target.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3667; + +public class Target { + + private Nested nested; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + public static class Nested { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Child.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Child.java new file mode 100644 index 0000000000..3c10c4d470 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Child.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +public abstract class Child { + + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public static class ChildA extends Child { } + + public static class ChildB extends Child { } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ChildDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ChildDto.java new file mode 100644 index 0000000000..458cc57a9a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ChildDto.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +public abstract class ChildDto { + + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public static class ChildDtoA extends ChildDto { } + + public static class ChildDtoB extends ChildDto { } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ChildMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ChildMapper.java new file mode 100644 index 0000000000..c381a99fcd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ChildMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +import org.mapstruct.Mapper; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; + +@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) +public interface ChildMapper { + + @SubclassMapping(target = Child.ChildA.class, source = ChildDto.ChildDtoA.class) + @SubclassMapping(target = Child.ChildB.class, source = ChildDto.ChildDtoB.class) + Child toEntity(ChildDto childDto); + + @SubclassMapping(target = ChildDto.ChildDtoA.class, source = Child.ChildA.class) + @SubclassMapping(target = ChildDto.ChildDtoB.class, source = Child.ChildB.class) + ChildDto toDto(Child child); + + Child.ChildA toEntity(ChildDto.ChildDtoA childDto); + + ChildDto.ChildDtoA toDto(Child.ChildA child); + + Child.ChildB toEntity(ChildDto.ChildDtoB childDto); + + ChildDto.ChildDtoB toDto(Child.ChildB child); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Issue3668Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Issue3668Test.java new file mode 100644 index 0000000000..a35ac70dce --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Issue3668Test.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3668") +@WithClasses({ + Child.class, + ChildDto.class, + ChildMapper.class, + Parent.class, + ParentDto.class, + ParentMapper.class, +}) +class Issue3668Test { + + @ProcessorTest + void shouldCompile() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Parent.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Parent.java new file mode 100644 index 0000000000..900a7fa68b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/Parent.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +public abstract class Parent { + + private Long id; + + private T child; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public T getChild() { + return child; + } + + public void setChild(T child) { + this.child = child; + } + + public static class ParentA extends Parent { } + + public static class ParentB extends Parent { } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ParentDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ParentDto.java new file mode 100644 index 0000000000..f4736ceef0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ParentDto.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +public abstract class ParentDto { + + private Long id; + + private T child; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public T getChild() { + return child; + } + + public void setChild(T child) { + this.child = child; + } + + public static class ParentDtoA extends ParentDto { } + + public static class ParentDtoB extends ParentDto { } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ParentMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ParentMapper.java new file mode 100644 index 0000000000..484ddbc930 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3668/ParentMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3668; + +import org.mapstruct.Mapper; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; + +@Mapper(uses = { ChildMapper.class }, subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) +public interface ParentMapper { + + @SubclassMapping(target = Parent.ParentA.class, source = ParentDto.ParentDtoA.class) + @SubclassMapping(target = Parent.ParentB.class, source = ParentDto.ParentDtoB.class) + Parent toEntity(ParentDto parentDto); + + @SubclassMapping(target = ParentDto.ParentDtoA.class, source = Parent.ParentA.class) + @SubclassMapping(target = ParentDto.ParentDtoB.class, source = Parent.ParentB.class) + ParentDto toDto(Parent parent); + + Parent.ParentA toEntity(ParentDto.ParentDtoA parentDto); + + ParentDto.ParentDtoA toDto(Parent.ParentA parent); + + Parent.ParentB toEntity(ParentDto.ParentDtoB parentDto); + + ParentDto.ParentDtoB toDto(Parent.ParentB parent); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3670/Issue3670Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3670/Issue3670Mapper.java new file mode 100644 index 0000000000..a014a7d52d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3670/Issue3670Mapper.java @@ -0,0 +1,74 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3670; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3670Mapper { + + @Mapping(target = "name", source = ".", qualifiedByName = "nestedName") + Target map(Source source); + + @InheritInverseConfiguration + @Mapping(target = "nested.nestedName", source = "name") + Source map(Target target); + + @Named("nestedName") + default String mapNestedName(Source source) { + if ( source == null ) { + return null; + } + + Nested nested = source.getNested(); + + return nested != null ? nested.getNestedName() : null; + } + + class Target { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Nested { + private String nestedName; + + public String getNestedName() { + return nestedName; + } + + public void setNestedName(String nestedName) { + this.nestedName = nestedName; + } + } + + class Source { + + private Nested nested; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3670/Issue3670Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3670/Issue3670Test.java new file mode 100644 index 0000000000..fc3929b685 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3670/Issue3670Test.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3670; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3670") +@WithClasses(Issue3670Mapper.class) +class Issue3670Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Animal.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Animal.java new file mode 100644 index 0000000000..9fe97e0e79 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Animal.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +public class Animal { + + private AnimalDetails details; + + public AnimalDetails getDetails() { + return details; + } + + public void setDetails(AnimalDetails details) { + this.details = details; + } + + public enum Type { + CAT, + DOG + } + + public static class AnimalDetails { + private Type type; + private String name; + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Cat.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Cat.java new file mode 100644 index 0000000000..63e5391690 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Cat.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +public class Cat { + + private final Details details; + + public Cat(Details details) { + this.details = details; + } + + public Details getDetails() { + return details; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Details.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Details.java new file mode 100644 index 0000000000..8526793015 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Details.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +public class Details { + + private final String name; + + public Details(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Dog.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Dog.java new file mode 100644 index 0000000000..a021a5d579 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Dog.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +public class Dog { + + private final Details details; + + public Dog(Details details) { + this.details = details; + } + + public Details getDetails() { + return details; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673ConstantMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673ConstantMapper.java new file mode 100644 index 0000000000..d74a954f9f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673ConstantMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue3673ConstantMapper { + + Issue3673ConstantMapper INSTANCE = Mappers.getMapper( Issue3673ConstantMapper.class ); + + @Mapping(target = "details.name", source = "details.name") + @Mapping(target = "details.type", constant = "DOG") + Animal map(Dog dog); + + @Mapping(target = "details.name", source = "details.name") + @Mapping(target = "details.type", constant = "CAT") + Animal map(Cat cat); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673ExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673ExpressionMapper.java new file mode 100644 index 0000000000..3aca966807 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673ExpressionMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue3673ExpressionMapper { + + Issue3673ExpressionMapper INSTANCE = Mappers.getMapper( Issue3673ExpressionMapper.class ); + + @Mapping(target = "details.name", source = "details.name") + @Mapping(target = "details.type", expression = "java(Animal.Type.DOG)") + Animal map(Dog dog); + + @Mapping(target = "details.name", source = "details.name") + @Mapping(target = "details.type", expression = "java(Animal.Type.CAT)") + Animal map(Cat cat); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673Test.java new file mode 100644 index 0000000000..2d796b4670 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3673/Issue3673Test.java @@ -0,0 +1,68 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3673; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3673") +@WithClasses({ + Cat.class, + Dog.class, + Details.class, + Animal.class +}) +class Issue3673Test { + + @ProcessorTest + @WithClasses(Issue3673ConstantMapper.class) + void shouldCorrectlyMapNestedPropertyConstant() { + + Animal cat = Issue3673ConstantMapper.INSTANCE.map( + new Cat( new Details( "cat" ) ) + ); + + Animal dog = Issue3673ConstantMapper.INSTANCE.map( + new Dog( new Details( "dog" ) ) + ); + + assertThat( cat ).isNotNull(); + assertThat( cat.getDetails() ).isNotNull(); + assertThat( cat.getDetails().getName() ).isEqualTo( "cat" ); + assertThat( cat.getDetails().getType() ).isEqualTo( Animal.Type.CAT ); + + assertThat( dog ).isNotNull(); + assertThat( dog.getDetails() ).isNotNull(); + assertThat( dog.getDetails().getName() ).isEqualTo( "dog" ); + assertThat( dog.getDetails().getType() ).isEqualTo( Animal.Type.DOG ); + } + + @ProcessorTest + @WithClasses(Issue3673ExpressionMapper.class) + void shouldCorrectlyMapNestedPropertyExpression() { + + Animal cat = Issue3673ExpressionMapper.INSTANCE.map( + new Cat( new Details( "cat" ) ) + ); + + Animal dog = Issue3673ExpressionMapper.INSTANCE.map( + new Dog( new Details( "dog" ) ) + ); + + assertThat( cat ).isNotNull(); + assertThat( cat.getDetails() ).isNotNull(); + assertThat( cat.getDetails().getName() ).isEqualTo( "cat" ); + assertThat( cat.getDetails().getType() ).isEqualTo( Animal.Type.CAT ); + + assertThat( dog ).isNotNull(); + assertThat( dog.getDetails() ).isNotNull(); + assertThat( dog.getDetails().getName() ).isEqualTo( "dog" ); + assertThat( dog.getDetails().getType() ).isEqualTo( Animal.Type.DOG ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3678/Issue3678Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3678/Issue3678Mapper.java new file mode 100644 index 0000000000..374fbf931a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3678/Issue3678Mapper.java @@ -0,0 +1,128 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3678; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue3678Mapper { + + Issue3678Mapper INSTANCE = Mappers.getMapper( Issue3678Mapper.class ); + + @Mapping(target = "name", source = "sourceA.name") + @Mapping(target = "description", source = "sourceB.description") + Target map(SourceA sourceA, SourceB sourceB, @Context MappingContext context); + + @Mapping(target = "description", constant = "some description") + Target map(SourceA sourceA, @Context MappingContext context); + + class MappingContext { + + private final List invokedMethods = new ArrayList<>(); + + @BeforeMapping + public void beforeMappingSourceA(SourceA sourceA) { + invokedMethods.add( "beforeMappingSourceA" ); + } + + @AfterMapping + public void afterMappingSourceB(SourceA sourceA) { + invokedMethods.add( "afterMappingSourceA" ); + } + + @BeforeMapping + public void beforeMappingSourceB(SourceB sourceB) { + invokedMethods.add( "beforeMappingSourceB" ); + } + + @AfterMapping + public void afterMappingSourceB(SourceB sourceB) { + invokedMethods.add( "afterMappingSourceB" ); + } + + public List getInvokedMethods() { + return invokedMethods; + } + } + + class SourceA { + + private final String name; + + public SourceA(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class SourceB { + + private final String description; + + public SourceB(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + final class Target { + + private final String name; + private final String description; + + private Target(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String name; + private String description; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Target build() { + return new Target( this.name, this.description ); + } + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3678/Issue3678Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3678/Issue3678Test.java new file mode 100644 index 0000000000..25e7e21622 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3678/Issue3678Test.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3678; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3678") +@WithClasses(Issue3678Mapper.class) +public class Issue3678Test { + + @ProcessorTest + void beforeAndAfterMappingOnlyCalledOnceForTwoSources() { + + Issue3678Mapper.MappingContext mappingContext = new Issue3678Mapper.MappingContext(); + Issue3678Mapper.SourceA sourceA = new Issue3678Mapper.SourceA( "name" ); + Issue3678Mapper.SourceB sourceB = new Issue3678Mapper.SourceB( "description" ); + Issue3678Mapper.INSTANCE.map( sourceA, sourceB, mappingContext ); + + assertThat( mappingContext.getInvokedMethods() ) + .containsExactly( + "beforeMappingSourceA", + "beforeMappingSourceB", + "afterMappingSourceA", + "afterMappingSourceB" + ); + } + + @ProcessorTest + void beforeAndAfterMappingOnlyCalledOnceForSingleSource() { + + Issue3678Mapper.MappingContext mappingContext = new Issue3678Mapper.MappingContext(); + Issue3678Mapper.SourceA sourceA = new Issue3678Mapper.SourceA( "name" ); + Issue3678Mapper.INSTANCE.map( sourceA, mappingContext ); + + assertThat( mappingContext.getInvokedMethods() ) + .containsExactly( + "beforeMappingSourceA", + "afterMappingSourceA" + ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/Issue3703Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/Issue3703Mapper.java new file mode 100644 index 0000000000..287e7655fb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/Issue3703Mapper.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3703; + +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.ap.test.bugs._3703.dto.Contact; + +@Mapper +public interface Issue3703Mapper { + + Contact map(org.mapstruct.ap.test.bugs._3703.entity.Contact contact); + + org.mapstruct.ap.test.bugs._3703.entity.Contact map(Contact contact); + + @AfterMapping + default void afterMapping(@MappingTarget Contact target, org.mapstruct.ap.test.bugs._3703.entity.Contact contact) { + } + + @AfterMapping + default void afterMapping(@MappingTarget Contact.Builder targetBuilder, + org.mapstruct.ap.test.bugs._3703.entity.Contact contact) { + } + + @AfterMapping + default void afterMapping(@MappingTarget org.mapstruct.ap.test.bugs._3703.entity.Contact target, Contact contact) { + } + + @AfterMapping + default void afterMapping(@MappingTarget org.mapstruct.ap.test.bugs._3703.entity.Contact.Builder targetBuilder, + Contact contact) { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/Issue3703Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/Issue3703Test.java new file mode 100644 index 0000000000..ac5cae3959 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/Issue3703Test.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3703; + +import org.mapstruct.ap.test.bugs._3703.dto.Contact; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +@IssueKey("3703") +@WithClasses({ + Contact.class, + org.mapstruct.ap.test.bugs._3703.entity.Contact.class, + Issue3703Mapper.class +}) +public class Issue3703Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/dto/Contact.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/dto/Contact.java new file mode 100644 index 0000000000..a949864791 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/dto/Contact.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3703.dto; + +public class Contact { + + private final String name; + + private Contact(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + + private String name; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Contact build() { + return new Contact( name ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/entity/Contact.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/entity/Contact.java new file mode 100644 index 0000000000..31fde373fe --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3703/entity/Contact.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3703.entity; + +public class Contact { + + private final String name; + + private Contact(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + + private String name; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Contact build() { + return new Contact( name ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/BaseMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/BaseMapper.java new file mode 100644 index 0000000000..e2a04fe336 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/BaseMapper.java @@ -0,0 +1,12 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3711; + +import org.mapstruct.Context; + +interface BaseMapper { + E toEntity(T s, @Context JpaContext ctx); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/Issue3711Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/Issue3711Test.java new file mode 100644 index 0000000000..7b141e9e14 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/Issue3711Test.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3711; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + ParentEntity.class, + ParentDto.class, + JpaContext.class, + SourceTargetMapper.class, + BaseMapper.class, +}) +@IssueKey("3711") +class Issue3711Test { + @ProcessorTest + void shouldGenerateContextMethod() { + JpaContext jpaContext = new JpaContext<>(); + SourceTargetMapper.INSTANCE.toEntity( new ParentDto(), jpaContext ); + + assertThat( jpaContext.getInvokedMethods() ) + .containsExactly( "beforeMapping", "afterMapping" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/JpaContext.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/JpaContext.java new file mode 100644 index 0000000000..b21bf639d7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/JpaContext.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3711; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.MappingTarget; + +public class JpaContext { + private final List invokedMethods = new ArrayList<>(); + + @BeforeMapping + void beforeMapping(@MappingTarget T parentEntity) { + invokedMethods.add( "beforeMapping" ); + } + + @AfterMapping + void afterMapping(@MappingTarget T parentEntity) { + invokedMethods.add( "afterMapping" ); + } + + public List getInvokedMethods() { + return invokedMethods; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/ParentDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/ParentDto.java new file mode 100644 index 0000000000..664d6e58ab --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/ParentDto.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3711; + +public class ParentDto { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/ParentEntity.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/ParentEntity.java new file mode 100644 index 0000000000..aaefa949fe --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/ParentEntity.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3711; + +public class ParentEntity { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/SourceTargetMapper.java new file mode 100644 index 0000000000..b09333fd56 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3711/SourceTargetMapper.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3711; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SourceTargetMapper extends BaseMapper { + SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); + + ParentEntity toDTO(ParentDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3732/Issue3732Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3732/Issue3732Mapper.java new file mode 100644 index 0000000000..79b13aa8ce --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3732/Issue3732Mapper.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3732; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3732Mapper { + + Target map(Source source); + + Source map(Target source); + + class Source { + private LocalDateTime value; + + public LocalDateTime getValue() { + return value; + } + + public void setValue(LocalDateTime value) { + this.value = value; + } + } + + class Target { + + private LocalDate value; + + public LocalDate getValue() { + return value; + } + + public void setValue(LocalDate value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3732/Issue3732Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3732/Issue3732Test.java new file mode 100644 index 0000000000..9dbd9c0bb8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3732/Issue3732Test.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3732; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3732") +@WithClasses({ Issue3732Mapper.class }) +class Issue3732Test { + + @ProcessorTest + void shouldGenerateCorrectMapper() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_374/Issue374Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_374/Issue374Test.java index 7f174732ff..2bc6d6a170 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_374/Issue374Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_374/Issue374Test.java @@ -41,7 +41,8 @@ public void shouldMapExistingTargetWithConstantToDefault() { Target target2 = new Target(); Target result2 = Issue374Mapper.INSTANCE.map2( null, target2 ); - assertThat( result2 ).isNull(); + assertThat( result2 ).isNotNull(); + assertThat( result2 ).isEqualTo( target2 ); assertThat( target2.getTest() ).isNull(); assertThat( target2.getConstant() ).isNull(); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3747/Issue3747Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3747/Issue3747Mapper.java new file mode 100644 index 0000000000..04addc43f3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3747/Issue3747Mapper.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3747; + +import org.mapstruct.Mapper; +import org.mapstruct.NullValueMappingStrategy; + +/** + * @author Filip Hrisafov + */ +@Mapper(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) +public interface Issue3747Mapper { + + Target map(Source source); + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Target { + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3747/Issue3747Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3747/Issue3747Test.java new file mode 100644 index 0000000000..57650e4dc0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3747/Issue3747Test.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3747; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3747") +@WithClasses(Issue3747Mapper.class) +class Issue3747Test { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + void shouldNotGenerateObsoleteCode() { + generatedSource.addComparisonToFixtureFor( Issue3747Mapper.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3786/ErroneousByteArrayMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3786/ErroneousByteArrayMapper.java new file mode 100644 index 0000000000..abf84f8228 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3786/ErroneousByteArrayMapper.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3786; + +import org.mapstruct.Mapper; + +@Mapper +public interface ErroneousByteArrayMapper { + byte[] map( String something ); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3786/Issue3786Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3786/Issue3786Test.java new file mode 100644 index 0000000000..bd71392c30 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3786/Issue3786Test.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3786; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * @author Ben Zegveld + */ +@IssueKey( "3786" ) +public class Issue3786Test { + + @WithClasses( ErroneousByteArrayMapper.class ) + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = ErroneousByteArrayMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 12, + message = "Can't generate mapping method from non-iterable type to array." + ) + } + ) + void byteArrayReturnTypeShouldGiveInaccessibleContructorError() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3806/Issue3806Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3806/Issue3806Mapper.java new file mode 100644 index 0000000000..89f325dfbb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3806/Issue3806Mapper.java @@ -0,0 +1,63 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3806; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface Issue3806Mapper { + + Issue3806Mapper INSTANCE = Mappers.getMapper( Issue3806Mapper.class ); + + void update(@MappingTarget Target target, Target source); + + class Target { + + private final Collection authors; + private final Map booksByAuthor; + + protected Collection books; + protected Map booksByPublisher; + + public Target(Collection authors, Map booksByAuthor) { + this.authors = authors != null ? new ArrayList<>( authors ) : null; + this.booksByAuthor = booksByAuthor != null ? new HashMap<>( booksByAuthor ) : null; + } + + public Collection getAuthors() { + return authors; + } + + public Map getBooksByAuthor() { + return booksByAuthor; + } + + public Collection getBooks() { + return books; + } + + public void setBooks(Collection books) { + this.books = books; + } + + public Map getBooksByPublisher() { + return booksByPublisher; + } + + public void setBooksByPublisher(Map booksByPublisher) { + this.booksByPublisher = booksByPublisher; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3806/Issue3806Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3806/Issue3806Test.java new file mode 100644 index 0000000000..1df3318c22 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3806/Issue3806Test.java @@ -0,0 +1,86 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3806; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +@WithClasses(Issue3806Mapper.class) +@IssueKey("3806") +class Issue3806Test { + + @ProcessorTest + void shouldNotClearGetterOnlyCollectionsInUpdateMapping() { + Map booksByAuthor = new HashMap<>(); + booksByAuthor.put( "author1", "book1" ); + booksByAuthor.put( "author2", "book2" ); + List authors = new ArrayList<>(); + authors.add( "author1" ); + authors.add( "author2" ); + + List books = new ArrayList<>(); + books.add( "book1" ); + books.add( "book2" ); + Map booksByPublisher = new HashMap<>(); + booksByPublisher.put( "publisher1", "book1" ); + booksByPublisher.put( "publisher2", "book2" ); + Issue3806Mapper.Target target = new Issue3806Mapper.Target( authors, booksByAuthor ); + target.setBooks( books ); + target.setBooksByPublisher( booksByPublisher ); + + Issue3806Mapper.Target source = new Issue3806Mapper.Target( null, null ); + Issue3806Mapper.INSTANCE.update( target, source ); + + assertThat( target.getAuthors() ).containsExactly( "author1", "author2" ); + assertThat( target.getBooksByAuthor() ) + .containsOnly( + entry( "author1", "book1" ), + entry( "author2", "book2" ) + ); + + assertThat( target.getBooks() ).containsExactly( "book1", "book2" ); + assertThat( target.getBooksByPublisher() ) + .containsOnly( + entry( "publisher1", "book1" ), + entry( "publisher2", "book2" ) + ); + + booksByAuthor = new HashMap<>(); + booksByAuthor.put( "author3", "book3" ); + authors = new ArrayList<>(); + authors.add( "author3" ); + + books = new ArrayList<>(); + books.add( "book3" ); + booksByPublisher = new HashMap<>(); + booksByPublisher.put( "publisher3", "book3" ); + source = new Issue3806Mapper.Target( authors, booksByAuthor ); + source.setBooks( books ); + source.setBooksByPublisher( booksByPublisher ); + Issue3806Mapper.INSTANCE.update( target, source ); + + assertThat( target.getAuthors() ).containsExactly( "author3" ); + assertThat( target.getBooksByAuthor() ) + .containsOnly( + entry( "author3", "book3" ) + ); + + assertThat( target.getBooks() ).containsExactly( "book3" ); + assertThat( target.getBooksByPublisher() ) + .containsOnly( + entry( "publisher3", "book3" ) + ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3807/Issue3807Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3807/Issue3807Mapper.java new file mode 100644 index 0000000000..83ee1f32bd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3807/Issue3807Mapper.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3807; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue3807Mapper { + Issue3807Mapper INSTANCE = Mappers.getMapper( Issue3807Mapper.class ); + + TargetWithoutSetter mapNoSetter(Source target); + + NormalTarget mapNormalSource(Source target); + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + //CHECKSTYLE:OFF + class TargetWithoutSetter { + public T value; + } + //CHECKSTYLE:ON + + class NormalTarget { + private T value; + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3807/Issue3807Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3807/Issue3807Test.java new file mode 100644 index 0000000000..db82472f98 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3807/Issue3807Test.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3807; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses(Issue3807Mapper.class) +@IssueKey("3807") +class Issue3807Test { + + @ProcessorTest + void fieldAndSetterShouldWorkWithGeneric() { + Issue3807Mapper.Source source = new Issue3807Mapper.Source( "value" ); + Issue3807Mapper.TargetWithoutSetter targetWithoutSetter = + Issue3807Mapper.INSTANCE.mapNoSetter( source ); + + assertThat( targetWithoutSetter ).isNotNull(); + assertThat( targetWithoutSetter.value ).isEqualTo( "value" ); + + Issue3807Mapper.NormalTarget normalTarget = Issue3807Mapper.INSTANCE.mapNormalSource( source ); + + assertThat( normalTarget ).isNotNull(); + assertThat( normalTarget.getValue() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3809/Issue3809Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3809/Issue3809Mapper.java new file mode 100644 index 0000000000..10ff9e2628 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3809/Issue3809Mapper.java @@ -0,0 +1,69 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3809; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetPropertyName; + +@Mapper +public interface Issue3809Mapper { + void updateMappingFails(Source source, @MappingTarget Target target); + + @Condition + default boolean canMap(Object source, @TargetPropertyName String propertyName) { + return true; + } + + class Source { + private NestedSource param; + + public NestedSource getParam() { + return param; + } + } + + class NestedSource { + private String param1; + + public String getParam1() { + return param1; + } + + public void setParam1(String param1) { + this.param1 = param1; + } + + } + + class Target { + + private NestedTarget param; + + public NestedTarget getParam() { + return param; + } + + public void setParam(NestedTarget param) { + this.param = param; + } + + } + + class NestedTarget { + private String param1; + + public String getParam1() { + return param1; + } + + public void setParam1(String param1) { + this.param1 = param1; + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3809/Issue3809Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3809/Issue3809Test.java new file mode 100644 index 0000000000..23d97b877c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3809/Issue3809Test.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3809; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +@WithClasses(Issue3809Mapper.class) +@IssueKey("3809") +public class Issue3809Test { + + @ProcessorTest + public void shouldCompileNoError() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Child.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Child.java new file mode 100644 index 0000000000..393cb16970 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Child.java @@ -0,0 +1,12 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +public class Child extends Parent { + public Child() { + super(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/ChildDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/ChildDto.java new file mode 100644 index 0000000000..e8cdae4d17 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/ChildDto.java @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +public class ChildDto extends ParentDto { + public ChildDto(String value) { + super( value ); + } + + public void setValue(String value) { + super.setValue( value ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateBySourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateBySourceMapper.java new file mode 100644 index 0000000000..68450c7d9d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateBySourceMapper.java @@ -0,0 +1,69 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface DeduplicateBySourceMapper { + + DeduplicateBySourceMapper INSTANCE = Mappers.getMapper( DeduplicateBySourceMapper.class ); + List INVOKED_METHODS = new ArrayList<>(); + + ParentDto mapParent(Parent source, @Context MappingContext context); + + ParentDto mapChild(Child source, @Context MappingContext context); + + class MappingContext { + @BeforeMapping + void deduplicateBySourceForBefore(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingParentSourceInOtherClass" ); + } + + @BeforeMapping + void deduplicateBySourceForBefore(Child sourceChild, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingChildSourceInOtherClass" ); + } + + @AfterMapping + void deduplicateBySource(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParentSourceInOtherClass" ); + } + + @AfterMapping + void deduplicateBySource(Child sourceChild, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingChildSourceInOtherClass" ); + } + } + + @BeforeMapping + default void deduplicateBySourceForBefore(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingParentSource" ); + } + + @BeforeMapping + default void deduplicateBySourceForBefore(Child sourceChild, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingChildSource" ); + } + + @AfterMapping + default void deduplicateBySource(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParentSource" ); + } + + @AfterMapping + default void deduplicateBySource(Child sourceChild, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingChildSource" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateByTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateByTargetMapper.java new file mode 100644 index 0000000000..0aed8a2957 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateByTargetMapper.java @@ -0,0 +1,69 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface DeduplicateByTargetMapper { + + DeduplicateByTargetMapper INSTANCE = Mappers.getMapper( DeduplicateByTargetMapper.class ); + List INVOKED_METHODS = new ArrayList<>(); + + ParentDto mapParent(Parent source, @Context MappingContext context); + + ChildDto mapChild(Parent source, @Context MappingContext context); + + class MappingContext { + @BeforeMapping + void deduplicateByTargetForBefore(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingParentTargetInOtherClass" ); + } + + @BeforeMapping + void deduplicateByTargetForBefore(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "beforeMappingChildTargetInOtherClass" ); + } + + @AfterMapping + void deduplicateByTarget(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParentTargetInOtherClass" ); + } + + @AfterMapping + void deduplicateByTarget(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "afterMappingChildTargetInOtherClass" ); + } + } + + @BeforeMapping + default void deduplicateByTargetForBefore(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingParentTarget" ); + } + + @BeforeMapping + default void deduplicateByTargetForBefore(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "beforeMappingChildTarget" ); + } + + @AfterMapping + default void deduplicateByTarget(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParentTarget" ); + } + + @AfterMapping + default void deduplicateByTarget(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "afterMappingChildTarget" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateForCompileArgsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateForCompileArgsMapper.java new file mode 100644 index 0000000000..276a2c4352 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateForCompileArgsMapper.java @@ -0,0 +1,69 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface DeduplicateForCompileArgsMapper { + + DeduplicateForCompileArgsMapper INSTANCE = Mappers.getMapper( DeduplicateForCompileArgsMapper.class ); + List INVOKED_METHODS = new ArrayList<>(); + + ParentDto mapParent(Parent source, @Context MappingContext context); + + ChildDto mapChild(Parent source, @Context MappingContext context); + + class MappingContext { + @BeforeMapping + void deduplicateByTargetForBefore(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingParentTargetInOtherClass" ); + } + + @BeforeMapping + void deduplicateByTargetForBefore(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "beforeMappingChildTargetInOtherClass" ); + } + + @AfterMapping + void deduplicateByTarget(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParentTargetInOtherClass" ); + } + + @AfterMapping + void deduplicateByTarget(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "afterMappingChildTargetInOtherClass" ); + } + } + + @BeforeMapping + default void deduplicateByTargetForBefore(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "beforeMappingParentTarget" ); + } + + @BeforeMapping + default void deduplicateByTargetForBefore(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "beforeMappingChildTarget" ); + } + + @AfterMapping + default void deduplicateByTarget(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParentTarget" ); + } + + @AfterMapping + default void deduplicateByTarget(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "afterMappingChildTarget" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateGenericMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateGenericMapper.java new file mode 100644 index 0000000000..662137f1a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/DeduplicateGenericMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface DeduplicateGenericMapper { + + DeduplicateGenericMapper INSTANCE = Mappers.getMapper( DeduplicateGenericMapper.class ); + List INVOKED_METHODS = new ArrayList<>(); + + ParentDto mapParent(Parent source); + + ChildDto mapChild(Parent source); + + @BeforeMapping + default void deduplicateBefore(Parent source, @MappingTarget T target) { + INVOKED_METHODS.add( "beforeMappingParentGeneric" ); + } + + @BeforeMapping + default void deduplicateBefore(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "beforeMappingChild" ); + } + + @AfterMapping + default void deduplicate(Parent source, @MappingTarget T target) { + INVOKED_METHODS.add( "afterMappingGeneric" ); + } + + @AfterMapping + default void deduplicate(Parent source, @MappingTarget ParentDto target) { + INVOKED_METHODS.add( "afterMappingParent" ); + } + + @AfterMapping + default void deduplicate(Parent source, @MappingTarget ChildDto target) { + INVOKED_METHODS.add( "afterMappingChild" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Issue3849Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Issue3849Test.java new file mode 100644 index 0000000000..a58c52c921 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Issue3849Test.java @@ -0,0 +1,150 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package org.mapstruct.ap.test.bugs._3849; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3849") +@WithClasses({ + Parent.class, + ParentDto.class, + Child.class, + ChildDto.class +}) +public class Issue3849Test { + + @ProcessorOption(name = "mapstruct.disableLifecycleOverloadDeduplicateSelector", value = "true") + @ProcessorTest() + @WithClasses(DeduplicateForCompileArgsMapper.class) + void lifecycleMappingOverloadSelectorDisableCompileArgs() { + Child child = new Child(); + Parent parent = new Parent(); + + DeduplicateForCompileArgsMapper.MappingContext mappingContext = + new DeduplicateForCompileArgsMapper.MappingContext(); + ParentDto parentDto = DeduplicateForCompileArgsMapper.INSTANCE.mapParent( parent, mappingContext ); + assertThat( DeduplicateForCompileArgsMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingParentTargetInOtherClass", + "beforeMappingParentTarget", + "afterMappingParentTargetInOtherClass", + "afterMappingParentTarget" + ); + + DeduplicateForCompileArgsMapper.INVOKED_METHODS.clear(); + + ParentDto childDto = DeduplicateForCompileArgsMapper.INSTANCE.mapChild( child, mappingContext ); + + assertThat( DeduplicateForCompileArgsMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingChildTargetInOtherClass", + "beforeMappingChildTargetInOtherClass", + "beforeMappingChildTarget", + "beforeMappingChildTarget", + "afterMappingChildTargetInOtherClass", + "afterMappingChildTargetInOtherClass", + "afterMappingChildTarget", + "afterMappingChildTarget" + ); + + DeduplicateForCompileArgsMapper.INVOKED_METHODS.clear(); + } + + @ProcessorTest() + @WithClasses( DeduplicateByTargetMapper.class ) + void lifecycleMappingOverloadByTarget() { + Child child = new Child(); + Parent parent = new Parent(); + + DeduplicateByTargetMapper.MappingContext mappingContext = new DeduplicateByTargetMapper.MappingContext(); + ParentDto parentDto = DeduplicateByTargetMapper.INSTANCE.mapParent( parent, mappingContext ); + assertThat( DeduplicateByTargetMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingParentTargetInOtherClass", + "beforeMappingParentTarget", + "afterMappingParentTargetInOtherClass", + "afterMappingParentTarget" + ); + + DeduplicateByTargetMapper.INVOKED_METHODS.clear(); + + ParentDto childDto = DeduplicateByTargetMapper.INSTANCE.mapChild( child, mappingContext ); + + assertThat( DeduplicateByTargetMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingChildTargetInOtherClass", + "beforeMappingChildTarget", + "afterMappingChildTargetInOtherClass", + "afterMappingChildTarget" + ); + + DeduplicateByTargetMapper.INVOKED_METHODS.clear(); + } + + @ProcessorTest + @WithClasses( DeduplicateBySourceMapper.class ) + void lifecycleMappingOverloadBySource() { + Child child = new Child(); + Parent parent = new Parent(); + + DeduplicateBySourceMapper.MappingContext mappingContext = new DeduplicateBySourceMapper.MappingContext(); + ParentDto parentDto = DeduplicateBySourceMapper.INSTANCE.mapParent( parent, mappingContext ); + assertThat( DeduplicateBySourceMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingParentSourceInOtherClass", + "beforeMappingParentSource", + "afterMappingParentSourceInOtherClass", + "afterMappingParentSource" + ); + + DeduplicateBySourceMapper.INVOKED_METHODS.clear(); + + ParentDto childDto = DeduplicateBySourceMapper.INSTANCE.mapChild( child, mappingContext ); + + assertThat( DeduplicateBySourceMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingChildSourceInOtherClass", + "beforeMappingChildSource", + "afterMappingChildSourceInOtherClass", + "afterMappingChildSource" + ); + + DeduplicateBySourceMapper.INVOKED_METHODS.clear(); + } + + @ProcessorTest + @WithClasses(DeduplicateGenericMapper.class) + void lifecycleMappingOverloadForGeneric() { + Child child = new Child(); + Parent parent = new Parent(); + + ParentDto parentDto = DeduplicateGenericMapper.INSTANCE.mapParent( parent ); + assertThat( DeduplicateGenericMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingParentGeneric", + "afterMappingParent" + ); + + DeduplicateGenericMapper.INVOKED_METHODS.clear(); + + ParentDto childDto = DeduplicateGenericMapper.INSTANCE.mapChild( child ); + + assertThat( DeduplicateGenericMapper.INVOKED_METHODS ) + .containsExactly( + "beforeMappingChild", + "afterMappingChild" + ); + + DeduplicateGenericMapper.INVOKED_METHODS.clear(); + } +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Parent.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Parent.java new file mode 100644 index 0000000000..4930677286 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/Parent.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +public class Parent { + private String value; + + public Parent() { + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/ParentDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/ParentDto.java new file mode 100644 index 0000000000..098917f001 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3849/ParentDto.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3849; + +public class ParentDto { + private String value; + + public ParentDto(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/DestinationType.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/DestinationType.java new file mode 100644 index 0000000000..467b58990c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/DestinationType.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3884; + +import java.util.List; +import java.util.Map; + +/** + * Destination type interface for testing null value property mapping strategy with Map properties. + */ +public interface DestinationType { + Map getAttributes(); + + void setAttributes(Map attributes); + + List getItems(); + + void setItems(List items); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/Issue3884Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/Issue3884Mapper.java new file mode 100644 index 0000000000..6f6f4f924d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/Issue3884Mapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3884; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/** + * Mapper for testing null value property mapping strategy with Map properties. + */ +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) +public interface Issue3884Mapper { + Issue3884Mapper INSTANCE = Mappers.getMapper( Issue3884Mapper.class ); + + void update(@MappingTarget DestinationType destination, SourceType source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/Issue3884Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/Issue3884Test.java new file mode 100644 index 0000000000..6c789a0c29 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/Issue3884Test.java @@ -0,0 +1,116 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3884; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Test for issue 3884: NullValuePropertyMappingStrategy.SET_TO_DEFAULT should set target Map/Collection to default + * when source and target are all null. + */ +@IssueKey("3884") +@WithClasses({ + DestinationType.class, + SourceType.class, + Issue3884Mapper.class +}) +public class Issue3884Test { + + @ProcessorTest + public void shouldSetTargetToDefaultWhenBothSourceAndTargetAreNull() { + DestinationType target = new SourceType(); + SourceType source = new SourceType(); + + assertThat( source.getAttributes() ).isNull(); + assertThat( target.getAttributes() ).isNull(); + assertThat( source.getItems() ).isNull(); + assertThat( target.getItems() ).isNull(); + + Issue3884Mapper.INSTANCE.update( target, source ); + + assertThat( target.getAttributes() ).isEmpty(); + assertThat( target.getItems() ).isEmpty(); + } + + @ProcessorTest + public void shouldClearTargetWhenSourceIsNullAndTargetIsInitialized() { + DestinationType target = new SourceType(); + SourceType source = new SourceType(); + + Map targetAttributes = new HashMap<>(); + targetAttributes.put( "targetKey", "targetValue" ); + target.setAttributes( targetAttributes ); + + List targetItems = new ArrayList<>(); + targetItems.add( "targetItem" ); + target.setItems( targetItems ); + + assertThat( source.getAttributes() ).isNull(); + assertThat( target.getAttributes() ).isNotEmpty(); + assertThat( source.getItems() ).isNull(); + assertThat( target.getItems() ).isNotEmpty(); + + Issue3884Mapper.INSTANCE.update( target, source ); + + assertThat( target.getAttributes() ).isEmpty(); + assertThat( target.getItems() ).isEmpty(); + } + + @ProcessorTest + public void shouldCopySourceToTargetWhenSourceIsInitializedAndTargetIsNull() { + DestinationType target = new SourceType(); + SourceType source = new SourceType(); + + source.setAttributes( Map.of( "sourceKey", "sourceValue" ) ); + source.setItems( List.of( "sourceItem" ) ); + + assertThat( source.getAttributes() ).isNotEmpty(); + assertThat( target.getAttributes() ).isNull(); + assertThat( source.getItems() ).isNotEmpty(); + assertThat( target.getItems() ).isNull(); + + Issue3884Mapper.INSTANCE.update( target, source ); + + assertThat( target.getAttributes() ).containsOnly( entry( "sourceKey", "sourceValue" ) ); + assertThat( target.getItems() ).containsExactly( "sourceItem" ); + } + + @ProcessorTest + public void shouldCopySourceToTargetWhenBothSourceAndTargetAreInitialized() { + DestinationType target = new SourceType(); + SourceType source = new SourceType(); + + source.setAttributes( Map.of( "sourceKey", "sourceValue" ) ); + source.setItems( List.of( "sourceItem" ) ); + + Map targetAttributes = new HashMap<>(); + targetAttributes.put( "targetKey", "targetValue" ); + target.setAttributes( targetAttributes ); + List targetItems = new ArrayList<>(); + targetItems.add( "targetItem" ); + target.setItems( targetItems ); + + assertThat( source.getAttributes() ).isNotEmpty(); + assertThat( target.getAttributes() ).isNotEmpty(); + assertThat( source.getItems() ).isNotEmpty(); + assertThat( target.getItems() ).isNotEmpty(); + + Issue3884Mapper.INSTANCE.update( target, source ); + + assertThat( target.getAttributes() ).containsOnly( entry( "sourceKey", "sourceValue" ) ); + assertThat( target.getItems() ).containsExactly( "sourceItem" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/SourceType.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/SourceType.java new file mode 100644 index 0000000000..be5c19e01c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3884/SourceType.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3884; + +import java.util.List; +import java.util.Map; + +/** + * Source type class implementing DestinationType for testing null value property mapping strategy with Map properties. + */ +public class SourceType implements DestinationType { + private Map attributes; + private List items; + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public List getItems() { + return items; + } + + @Override + public void setItems(List items) { + this.items = items; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3886/jdk21/Issue3886Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3886/jdk21/Issue3886Mapper.java new file mode 100644 index 0000000000..aba305a0bf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3886/jdk21/Issue3886Mapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3886.jdk21; + +import java.time.LocalDate; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface Issue3886Mapper { + + RangeRecord map(LocalDate validFrom); + + record RangeRecord(LocalDate validFrom) { + + public RangeRecord restrictTo(RangeRecord other) { + return null; + } + + public void setName(String name) { + // This method is here to ensure that MapStruct won't treat it as a setter + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3886/jdk21/Issue3886Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3886/jdk21/Issue3886Test.java new file mode 100644 index 0000000000..93c068cbc7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3886/jdk21/Issue3886Test.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3886.jdk21; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.Compiler; + +/** + * @author Filip Hrisafov + */ +@WithClasses(Issue3886Mapper.class) +@IssueKey("3886") +class Issue3886Test { + + // The current version of the Eclipse compiler we use does not support records + @ProcessorTest(Compiler.JDK) + void shouldCompile() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/ErroneousIssue3902Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/ErroneousIssue3902Mapper.java new file mode 100644 index 0000000000..b646f1a08e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/ErroneousIssue3902Mapper.java @@ -0,0 +1,72 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3902; + +import org.mapstruct.Ignored; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Mapper for testing bug #3902. + * + * @author znight1020 + */ +@Mapper +public interface ErroneousIssue3902Mapper { + + ErroneousIssue3902Mapper INSTANCE = Mappers.getMapper( ErroneousIssue3902Mapper.class ); + + @Ignored(targets = {"name", "foo", "bar"}) + ZooDto mapWithOneKnownAndMultipleUnknowns(Zoo source); + + @Ignored(targets = {"name", "address", "foo"}) + ZooDto mapWithMultipleKnownAndOneUnknown(Zoo source); + + @Ignored(targets = {"name", "addres"}) + ZooDto mapWithTypo(Zoo source); + + class Zoo { + private String name; + private String address; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + } + + class ZooDto { + private String name; + private String address; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/Issue3902Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/Issue3902Test.java new file mode 100644 index 0000000000..44247d8c27 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/Issue3902Test.java @@ -0,0 +1,66 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3902; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * Verifies that using an unknown property in {@code @Ignored} yields a proper + * compile error instead of an internal processor error. + * + * @author znight1020 + */ +@WithClasses({ErroneousIssue3902Mapper.class}) +@IssueKey("3902") +public class Issue3902Test { + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + // Test case: mapWithOneKnownAndMultipleUnknowns + @Diagnostic( + type = ErroneousIssue3902Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 23, + message = "No property named \"foo\" exists in @Ignored for target type " + + "\"ErroneousIssue3902Mapper.ZooDto\". Did you mean \"name\"?" + ), + @Diagnostic( + type = ErroneousIssue3902Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 23, + message = "No property named \"bar\" exists in @Ignored for target type " + + "\"ErroneousIssue3902Mapper.ZooDto\". Did you mean \"name\"?" + ), + + // Test case: mapWithMultipleKnownAndOneUnknown + @Diagnostic( + type = ErroneousIssue3902Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 26, + message = "No property named \"foo\" exists in @Ignored for target type " + + "\"ErroneousIssue3902Mapper.ZooDto\". Did you mean \"name\"?" + ), + + // Test case: mapWithTypo + @Diagnostic( + type = ErroneousIssue3902Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 29, + message = "No property named \"addres\" exists in @Ignored for target type " + + "\"ErroneousIssue3902Mapper.ZooDto\". Did you mean \"address\"?" + ) + } + ) + public void shouldFailOnUnknownPropertiesInIgnored() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Issue3905Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Issue3905Mapper.java new file mode 100644 index 0000000000..27e3ac9960 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Issue3905Mapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3905; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface Issue3905Mapper { + + OverrideDto map(Override override); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Issue3905Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Issue3905Test.java new file mode 100644 index 0000000000..f15271362d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Issue3905Test.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3905; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author Filip Hrisafov + */ +@IssueKey("3905") +@WithClasses({ + Issue3905Mapper.class, + Override.class, + OverrideDto.class +}) +class Issue3905Test { + + @ProcessorTest + void shouldCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Override.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Override.java new file mode 100644 index 0000000000..dedc6b28dc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/Override.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3905; + +/** + * @author Filip Hrisafov + */ +public class Override { + + private final String name; + + public Override(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/OverrideDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/OverrideDto.java new file mode 100644 index 0000000000..531fab505b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3905/OverrideDto.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3905; + +/** + * @author Filip Hrisafov + */ +public class OverrideDto { + + private final String name; + + public OverrideDto(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java new file mode 100644 index 0000000000..ab053bc44e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java @@ -0,0 +1,93 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3948; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface Issue3948Mapper { + + class ComparableField implements Comparable { + + private final String code; + + public ComparableField(String code) { + this.code = code; + } + + @Override + public int compareTo(ComparableField o) { + return code.compareTo( o.code ); + } + } + + class Id implements Comparable> { + private final long value; + + public Id(long value) { + this.value = value; + } + + @Override + public int compareTo(Id o) { + return Long.compare( value, o.value ); + } + } + + class From { + private long id; + private String code; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + } + + class To { + private Id id; + private ComparableField comparableField; + + public Id getId() { + return id; + } + + public void setId(Id id) { + this.id = id; + } + + public ComparableField getComparableField() { + return comparableField; + } + + public void setComparableField(ComparableField comparableField) { + this.comparableField = comparableField; + } + } + + @Mapping(source = "code", target = "comparableField") + To convert(From from); + + default Id longToId(Long id) { + return id == null ? null : new Id<>(id); + } + + default ComparableField convert(String code) { + return code == null ? null : new ComparableField(code); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java new file mode 100644 index 0000000000..8c80b2e802 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3948; + +import org.junitpioneer.jupiter.Issue; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author hduelme + */ +@Issue("3948") +public class Issue3948Test { + + @ProcessorTest + @WithClasses(Issue3948Mapper.class) + public void shouldRequireExactGenericMatchForDeclaredSameTypes() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/DateSource.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/DateSource.java new file mode 100644 index 0000000000..e542c388d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/DateSource.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import java.time.LocalDate; + +public class DateSource { + public LocalDate getDate() { + return null; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949ClassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949ClassMapper.java new file mode 100644 index 0000000000..135aa79056 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949ClassMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL) +public interface Issue3949ClassMapper { + + Issue3949ClassMapper INSTANCE = Mappers.getMapper( Issue3949ClassMapper.class ); + + void overwriteDate(@MappingTarget TargetDate target, DateSource dateSource); + + void overwriteString(@MappingTarget TargetString target, StringSource stringSource); + + void overwriteDateWithConversion(@MappingTarget TargetDate target, StringSource dateSource); + + void overwriteStringWithConversion(@MappingTarget TargetString target, DateSource stringSource); + + void updateParent(@MappingTarget ParentTarget target, ParentSource source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949InterfaceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949InterfaceMapper.java new file mode 100644 index 0000000000..c25041ad2f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949InterfaceMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL) +public interface Issue3949InterfaceMapper { + + Issue3949InterfaceMapper INSTANCE = Mappers.getMapper( Issue3949InterfaceMapper.class ); + + void overwriteDate(@MappingTarget TargetDateInterface target, DateSource dateSource); + + void overwriteString(@MappingTarget TargetStringInterface target, StringSource stringSource); + + void overwriteDateWithConversion(@MappingTarget TargetDateInterface target, StringSource dateSource); + + void overwriteStringWithConversion(@MappingTarget TargetStringInterface target, DateSource stringSource); + + void updateParent(@MappingTarget ParentTargetInterface target, ParentSource source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949Test.java new file mode 100644 index 0000000000..b6abee819c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/Issue3949Test.java @@ -0,0 +1,94 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests if overloaded targets are correctly cast when set to null + * + * @author hduelme + */ +@IssueKey("3949") +@WithClasses({ + ParentSource.class, + ParentTargetInterface.class, + ParentTarget.class, + StringSource.class, + TargetStringInterface.class, + TargetString.class, + DateSource.class, + TargetDateInterface.class, + TargetDate.class +}) +public class Issue3949Test { + + @ProcessorTest + @WithClasses({ + Issue3949ClassMapper.class + }) + void shouldCompileAndSetCorrectlyToNullForClass() { + TargetDate shouldSetDateToNull = new TargetDate(); + Issue3949ClassMapper.INSTANCE.overwriteDate( shouldSetDateToNull, new DateSource() ); + assertThat( shouldSetDateToNull.getString() ).isNotNull(); + assertThat( shouldSetDateToNull.getDate() ).isNull(); + + shouldSetDateToNull = new TargetDate(); + Issue3949ClassMapper.INSTANCE.overwriteDateWithConversion( shouldSetDateToNull, new StringSource() ); + assertThat( shouldSetDateToNull.getString() ).isNotNull(); + assertThat( shouldSetDateToNull.getDate() ).isNull(); + + TargetString shouldSetStringToNull = new TargetString(); + Issue3949ClassMapper.INSTANCE.overwriteString( shouldSetStringToNull, new StringSource() ); + assertThat( shouldSetStringToNull.getDate() ).isNull(); + assertThat( shouldSetStringToNull.getDateValue() ).isNotNull(); + + shouldSetStringToNull = new TargetString(); + Issue3949ClassMapper.INSTANCE.overwriteStringWithConversion( shouldSetStringToNull, new DateSource() ); + assertThat( shouldSetStringToNull.getDate() ).isNull(); + assertThat( shouldSetStringToNull.getDateValue() ).isNotNull(); + + ParentTarget parentTarget = new ParentTarget(); + parentTarget.setChild( new ParentTarget() ); + Issue3949ClassMapper.INSTANCE.updateParent( parentTarget, new ParentSource() ); + assertThat( parentTarget.getChild() ).isNull(); + } + + @ProcessorTest + @WithClasses({ + Issue3949InterfaceMapper.class + }) + void shouldCompileAndSetCorrectlyToNullForInterface() { + TargetDateInterface shouldSetDateToNull = new TargetDate(); + Issue3949InterfaceMapper.INSTANCE.overwriteDate( shouldSetDateToNull, new DateSource() ); + assertThat( shouldSetDateToNull.getString() ).isNotNull(); + assertThat( shouldSetDateToNull.getDate() ).isNull(); + + shouldSetDateToNull = new TargetDate(); + Issue3949InterfaceMapper.INSTANCE.overwriteDateWithConversion( shouldSetDateToNull, new StringSource() ); + assertThat( shouldSetDateToNull.getString() ).isNotNull(); + assertThat( shouldSetDateToNull.getDate() ).isNull(); + + TargetStringInterface shouldSetStringToNull = new TargetString(); + Issue3949InterfaceMapper.INSTANCE.overwriteString( shouldSetStringToNull, new StringSource() ); + assertThat( shouldSetStringToNull.getDate() ).isNull(); + assertThat( shouldSetStringToNull.getDateValue() ).isNotNull(); + + shouldSetStringToNull = new TargetString(); + Issue3949InterfaceMapper.INSTANCE.overwriteStringWithConversion( shouldSetStringToNull, new DateSource() ); + assertThat( shouldSetStringToNull.getDate() ).isNull(); + assertThat( shouldSetStringToNull.getDateValue() ).isNotNull(); + + ParentTargetInterface parentTarget = new ParentTarget(); + parentTarget.setChild( new ParentTarget() ); + Issue3949InterfaceMapper.INSTANCE.updateParent( parentTarget, new ParentSource() ); + assertThat( parentTarget.getChild() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentSource.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentSource.java new file mode 100644 index 0000000000..551501aa24 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentSource.java @@ -0,0 +1,12 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +public class ParentSource { + public ParentSource getChild() { + return null; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentTarget.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentTarget.java new file mode 100644 index 0000000000..3d8ca3b51b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentTarget.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +public class ParentTarget implements ParentTargetInterface { + private ParentTarget child; + + @Override + public ParentTarget getChild() { + return child; + } + + @Override + public void setChild(String child) { + throw new IllegalArgumentException(); + } + + @Override + public void setChild(ParentTarget child) { + this.child = child; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentTargetInterface.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentTargetInterface.java new file mode 100644 index 0000000000..5311fb4c9b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/ParentTargetInterface.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +public interface ParentTargetInterface { + ParentTarget getChild(); + + void setChild(String child); + + void setChild(ParentTarget child); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/StringSource.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/StringSource.java new file mode 100644 index 0000000000..11ba1419d5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/StringSource.java @@ -0,0 +1,12 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +public class StringSource { + public String getDate() { + return null; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetDate.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetDate.java new file mode 100644 index 0000000000..85b12a618d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetDate.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import java.time.LocalDate; + +public class TargetDate implements TargetDateInterface { + private LocalDate date = LocalDate.now(); + private String string = ""; + + @Override + public void setDate(LocalDate date) { + this.date = date; + } + + @Override + public void setDate(String date) { + this.string = date; + } + + @Override + public LocalDate getDate() { + return date; + } + + @Override + public String getString() { + return string; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetDateInterface.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetDateInterface.java new file mode 100644 index 0000000000..df28936914 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetDateInterface.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import java.time.LocalDate; + +public interface TargetDateInterface { + void setDate(LocalDate date); + + void setDate(String date); + + LocalDate getDate(); + + String getString(); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetString.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetString.java new file mode 100644 index 0000000000..b8cbbb2bbb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetString.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import java.time.LocalDate; + +public class TargetString implements TargetStringInterface { + private LocalDate date = LocalDate.now(); + private String string = ""; + + @Override + public void setDate(LocalDate date) { + this.date = date; + } + + @Override + public void setDate(String date) { + this.string = date; + } + + @Override + public String getDate() { + return string; + } + + @Override + public LocalDate getDateValue() { + return date; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetStringInterface.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetStringInterface.java new file mode 100644 index 0000000000..dd82231a51 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3949/TargetStringInterface.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._3949; + +import java.time.LocalDate; + +public interface TargetStringInterface { + void setDate(LocalDate date); + + void setDate(String date); + + String getDate(); + + LocalDate getDateValue(); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java new file mode 100644 index 0000000000..4ede382781 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java @@ -0,0 +1,100 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._4060; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +@Mapper +public interface ErroneousSetToDefaultMapper { + + class WithLocalDate { + private LocalDate value; + + public LocalDate getValue() { + return value; + } + + public void setValue(LocalDate value) { + this.value = value; + } + } + + class WithBigDecimal { + private BigDecimal value; + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + } + + class WithComparable { + private Comparable value; + + public Comparable getValue() { + return value; + } + + public void setValue(Comparable value) { + this.value = value; + } + } + + abstract class AbstractValue { + } + + class WithAbstractValue { + private AbstractValue value; + + public AbstractValue getValue() { + return value; + } + + public void setValue(AbstractValue value) { + this.value = value; + } + } + + enum EnumValue { + FIRST + } + + class WithEnumValue { + private EnumValue value; + + public EnumValue getValue() { + return value; + } + + public void setValue(EnumValue value) { + this.value = value; + } + } + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateLocalDate(@MappingTarget WithLocalDate target, WithLocalDate source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateBigDecimal(@MappingTarget WithBigDecimal target, WithBigDecimal source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateComparable(@MappingTarget WithComparable target, WithComparable source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateAbstractValue(@MappingTarget WithAbstractValue target, WithAbstractValue source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateEnumValue(@MappingTarget WithEnumValue target, WithEnumValue source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java new file mode 100644 index 0000000000..e0e51dac32 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java @@ -0,0 +1,77 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._4060; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("4060") +public class Issue4060Test { + + @ProcessorTest + @WithClasses(ErroneousSetToDefaultMapper.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 87, + message = "LocalDate does not have an accessible parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 90, + message = "BigDecimal does not have an accessible parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 93, + message = "Comparable does not have an accessible parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 96, + message = "ErroneousSetToDefaultMapper.AbstractValue does not have an accessible " + + "parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 99, + message = "ErroneousSetToDefaultMapper.EnumValue does not have an accessible " + + "parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression.") + }) + void setToDefaultWithoutParameterlessConstructorFails() { + } + + @ProcessorTest + @WithClasses(SetToDefaultMapper.class) + void setToDefaultWithParameterlessConstructorResetsToNewInstance() { + SetToDefaultMapper.Target target = new SetToDefaultMapper.Target(); + SetToDefaultMapper.Nested existing = new SetToDefaultMapper.Nested(); + existing.setName( "existing" ); + target.setNested( existing ); + target.setText( "existing" ); + + SetToDefaultMapper.INSTANCE.update( target, new SetToDefaultMapper.Source() ); + + // source properties are null, SET_TO_DEFAULT resets them to a fresh default + assertThat( target.getNested() ).isNotNull(); + assertThat( target.getNested() ).isNotSameAs( existing ); + assertThat( target.getNested().getName() ).isNull(); + assertThat( target.getText() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java new file mode 100644 index 0000000000..edfe7e9b3b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java @@ -0,0 +1,75 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._4060; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SetToDefaultMapper { + + SetToDefaultMapper INSTANCE = Mappers.getMapper( SetToDefaultMapper.class ); + + class Nested { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Target { + private Nested nested; + private String text; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + class Source { + private Nested nested; + private String text; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void update(@MappingTarget Target target, Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_516/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_516/SourceTargetMapper.java index a55f793ffa..788ed39c89 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_516/SourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_516/SourceTargetMapper.java @@ -9,7 +9,6 @@ import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; - /** * * @author Sjaak Derksen diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/Issue611Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/Issue611Test.java index 79a6e045ec..b77da90ad9 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/Issue611Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/Issue611Test.java @@ -43,8 +43,8 @@ public void mapperNestedInsideNestedClassIsFound() { */ @ProcessorTest public void rightMapperIsFound() { - SomeClass.InnerMapper.Source source1 = new SomeClass.InnerMapper.Source(); - SomeOtherClass.InnerMapper.Source source2 = new SomeOtherClass.InnerMapper.Source(); + SomeClass.InnerMapper.Source source1 = new SomeClass.InnerMapper.Source( "test" ); + SomeOtherClass.InnerMapper.Source source2 = new SomeOtherClass.InnerMapper.Source( "test2" ); SomeClass.InnerMapper.Target target1 = SomeClass.InnerMapper.INSTANCE.toTarget( source1 ); SomeOtherClass.InnerMapper.Target target2 = SomeOtherClass.InnerMapper.INSTANCE.toTarget( source2 ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeClass.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeClass.java index 0d41128711..e0fb59d4d7 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeClass.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeClass.java @@ -19,9 +19,29 @@ public interface InnerMapper { Target toTarget(Source in); class Source { + + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } } @@ -33,9 +53,29 @@ public interface InnerMapper { Target toTarget(Source in); class Source { + + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeOtherClass.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeOtherClass.java index e50187a82a..2ff2d0d831 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeOtherClass.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_611/SomeOtherClass.java @@ -19,9 +19,29 @@ public interface InnerMapper { Target toTarget(Source in); class Source { + + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } class Target { + + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_775/ListContainer.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_775/ListContainer.java index df4229bfe2..77a5303196 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_775/ListContainer.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_775/ListContainer.java @@ -12,7 +12,7 @@ * @author Andreas Gudian */ public class ListContainer { - private List values = new ArrayList(); + private List values = new ArrayList<>(); public List getValues() { return values; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_855/OrderedTarget.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_855/OrderedTarget.java index af9f7c3c3d..10e89795b2 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_855/OrderedTarget.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_855/OrderedTarget.java @@ -12,7 +12,7 @@ * @author Markus Heberling */ class OrderedTarget { - private List order = new LinkedList(); + private List order = new LinkedList<>(); public void setField0(String field0) { order.add( "field0" ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Config.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Config.java similarity index 87% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Config.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Config.java index 32f58384b9..7aef1e9e1f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Config.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Config.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._880; +package org.mapstruct.ap.test.bugs._880.spring; import org.mapstruct.MapperConfig; import org.mapstruct.ReportingPolicy; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/DefaultsToProcessorOptionsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/DefaultsToProcessorOptionsMapper.java similarity index 86% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_880/DefaultsToProcessorOptionsMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/DefaultsToProcessorOptionsMapper.java index c54da3f7cb..5f67b41552 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/DefaultsToProcessorOptionsMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/DefaultsToProcessorOptionsMapper.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._880; +package org.mapstruct.ap.test.bugs._880.spring; import org.mapstruct.Mapper; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Issue880Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Issue880Test.java similarity index 94% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Issue880Test.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Issue880Test.java index d05f01dc91..cd85829147 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Issue880Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Issue880Test.java @@ -3,14 +3,15 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._880; +package org.mapstruct.ap.test.bugs._880.spring; import javax.tools.Diagnostic.Kind; import org.junit.jupiter.api.extension.RegisterExtension; -import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.MappingConstants; +import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; @@ -30,6 +31,7 @@ @ProcessorOptions({ @ProcessorOption(name = "mapstruct.defaultComponentModel", value = MappingConstants.ComponentModel.SPRING), @ProcessorOption(name = "mapstruct.unmappedTargetPolicy", value = "ignore") }) +@WithSpring public class Issue880Test { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Poodle.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Poodle.java similarity index 88% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Poodle.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Poodle.java index e822db1cfe..71166f6d4c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/Poodle.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/Poodle.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._880; +package org.mapstruct.ap.test.bugs._880.spring; /** * @author Andreas Gudian diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/UsesConfigFromAnnotationMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/UsesConfigFromAnnotationMapper.java similarity index 90% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_880/UsesConfigFromAnnotationMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/UsesConfigFromAnnotationMapper.java index d9a1efe33e..bb9a7def9f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/UsesConfigFromAnnotationMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_880/spring/UsesConfigFromAnnotationMapper.java @@ -3,7 +3,7 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.bugs._880; +package org.mapstruct.ap.test.bugs._880.spring; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/Issue895Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/Issue895Test.java index 320c7d1306..6250e139f2 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/Issue895Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/Issue895Test.java @@ -6,7 +6,10 @@ package org.mapstruct.ap.test.bugs._895; import org.mapstruct.ap.test.bugs._895.MultiArrayMapper.WithArrayOfByteArray; +import org.mapstruct.ap.test.bugs._895.MultiArrayMapper.WithArrayOfGenericArray; import org.mapstruct.ap.test.bugs._895.MultiArrayMapper.WithListOfByteArray; +import org.mapstruct.ap.test.bugs._895.MultiArrayMapper.WithListOfGenericArray; +import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; import org.mapstruct.factory.Mappers; @@ -14,10 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Verifies that forged iterable mapping methods for multi-dimensional arrays are generated properly. + * Verifies that forged iterable mapping methods for multidimensional arrays are generated properly. * * @author Andreas Gudian */ +@IssueKey("895") @WithClasses(MultiArrayMapper.class) public class Issue895Test { @ProcessorTest @@ -31,4 +35,42 @@ public void properlyMapsMultiDimensionalArrays() { arrayOfByteArray = Mappers.getMapper( MultiArrayMapper.class ).convert( listOfByteArray ); assertThat( arrayOfByteArray.getBytes() ).isDeepEqualTo( new byte[][] { { 0, 1 }, { 1, 2 } } ); } + + @ProcessorTest + public void properlyMapsGenericMultiDimensionalArrays() { + WithArrayOfGenericArray arrayOfStringArray = new MultiArrayMapper.WithArrayOfGenericArray<>(); + arrayOfStringArray.setData( new String[][] { new String[] { "a", "b" }, new String[] { "b", "c" } } ); + + MultiArrayMapper mapper = Mappers.getMapper( MultiArrayMapper.class ); + WithListOfGenericArray listOfStringArray = mapper.convertGeneric( arrayOfStringArray ); + assertThat( listOfStringArray.getData() ) + .containsExactly( new String[] { "a", "b" }, new String[] { "b", "c" } ); + + arrayOfStringArray = mapper.convertGeneric( listOfStringArray ); + assertThat( arrayOfStringArray.getData() ) + .isDeepEqualTo( new String[][] { new String[] { "a", "b" }, new String[] { "b", "c" } } ); + } + + @ProcessorTest + public void properlyReturnDefaultForMultiArray() { + assertThat( Mappers.getMapper( MultiArrayMapper.class ).copyMultiArray( null ) ).isEmpty(); + } + + @ProcessorTest + @SuppressWarnings("unchecked") + public void properlyMapsGenericMultiArray() { + WithArrayOfGenericArray arrayOfGenericArray = new WithArrayOfGenericArray<>(); + arrayOfGenericArray.setData( new String[][] { new String[] { "a", "b" }, new String[] { "b", "c" } } ); + WithArrayOfGenericArray[][] multiArrayOfGenericArray = + new WithArrayOfGenericArray[][]{ new WithArrayOfGenericArray[]{ arrayOfGenericArray } }; + WithArrayOfGenericArray[][] result = + Mappers.getMapper( MultiArrayMapper.class ).copyGenericMultiArray( multiArrayOfGenericArray ); + assertThat( result ).isNotSameAs( multiArrayOfGenericArray ); + assertThat( result ).isDeepEqualTo( multiArrayOfGenericArray ); + } + + @ProcessorTest + public void properlyReturnDefaultForGenericMultiArray() { + assertThat( Mappers.getMapper( MultiArrayMapper.class ).copyGenericMultiArray( null ) ).isEmpty(); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/MultiArrayMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/MultiArrayMapper.java index 7d01485e38..13a67125b8 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/MultiArrayMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_895/MultiArrayMapper.java @@ -7,7 +7,9 @@ import java.util.List; +import org.mapstruct.IterableMapping; import org.mapstruct.Mapper; +import org.mapstruct.NullValueMappingStrategy; /** * @author Andreas Gudian @@ -30,6 +32,16 @@ public void setBytes(byte[][] bytes) { } } + @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) + int[][] copyMultiArray(int[][] multiArray); + + WithListOfGenericArray convertGeneric(WithArrayOfGenericArray b); + + WithArrayOfGenericArray convertGeneric(WithListOfGenericArray b); + + @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) + WithArrayOfGenericArray[][] copyGenericMultiArray(WithArrayOfGenericArray[][] multiArray); + class WithListOfByteArray { private List bytes; @@ -41,4 +53,28 @@ public void setBytes(List bytes) { this.bytes = bytes; } } + + class WithArrayOfGenericArray { + private T [][] data; + + public T[][] getData() { + return data; + } + + public void setData(T[][] data) { + this.data = data; + } + } + + class WithListOfGenericArray { + private List data; + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_955/CustomMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_955/CustomMapper.java index b4688f6258..a2a456ca59 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_955/CustomMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_955/CustomMapper.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.test.bugs._955; import java.util.Map; + import org.mapstruct.MappingTarget; import org.mapstruct.ap.test.bugs._955.dto.Person; diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/BuilderLifecycleCallbacksTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/BuilderLifecycleCallbacksTest.java index e3892f2154..3d5fb7e533 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/BuilderLifecycleCallbacksTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/BuilderLifecycleCallbacksTest.java @@ -43,12 +43,16 @@ public void lifecycleMethodsShouldBeInvoked() { assertThat( context.getInvokedMethods() ) .contains( "beforeWithoutParameters", + "beforeWithTargetType", "beforeWithBuilderTargetType", "beforeWithBuilderTarget", "afterWithoutParameters", "afterWithBuilderTargetType", "afterWithBuilderTarget", - "afterWithBuilderTargetReturningTarget" + "afterWithBuilderTargetReturningTarget", + "afterWithTargetType", + "afterWithTarget", + "afterWithTargetReturningTarget" ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/MappingContext.java b/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/MappingContext.java index 96b9b30db6..2065bad478 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/MappingContext.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/lifecycle/MappingContext.java @@ -18,7 +18,7 @@ */ public class MappingContext { - private final List invokedMethods = new ArrayList(); + private final List invokedMethods = new ArrayList<>(); @BeforeMapping public void beforeWithoutParameters() { @@ -74,7 +74,15 @@ public void afterWithBuilderTarget(OrderDto source, @MappingTarget Order.Builder public Order afterWithBuilderTargetReturningTarget(@MappingTarget Order.Builder orderBuilder) { invokedMethods.add( "afterWithBuilderTargetReturningTarget" ); - return orderBuilder.create(); + // return null, so that @AfterMapping methods on the finalized object will be called in the tests + return null; + } + + @AfterMapping + public Order afterWithTargetReturningTarget(@MappingTarget Order order) { + invokedMethods.add( "afterWithTargetReturningTarget" ); + + return order; } public List getInvokedMethods() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/BuilderInfoTargetTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/BuilderInfoTargetTest.java index 3682f2ccc5..5d0ff919ec 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/BuilderInfoTargetTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/BuilderInfoTargetTest.java @@ -6,8 +6,12 @@ package org.mapstruct.ap.test.builder.mappingTarget.simple; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; import org.mapstruct.ap.testutil.runner.GeneratedSource; import static org.assertj.core.api.Assertions.assertThat; @@ -36,6 +40,18 @@ public void testSimpleImmutableBuilderHappyPath() { assertThat( targetObject.getName() ).isEqualTo( "Bob" ); } + @ProcessorTest + @IssueKey("1752") + public void testSimpleImmutableBuilderFromNullSource() { + SimpleImmutableTarget targetObject = SimpleBuilderMapper.INSTANCE.toImmutable( + null, + SimpleImmutableTarget.builder().age( 3 ).name( "Bob" ) + ); + assertThat( targetObject ).isNotNull(); + assertThat( targetObject.getAge() ).isEqualTo( 3 ); + assertThat( targetObject.getName() ).isEqualTo( "Bob" ); + } + @ProcessorTest public void testMutableTargetWithBuilder() { SimpleMutableSource source = new SimpleMutableSource(); @@ -67,6 +83,17 @@ public void testUpdateMutableWithBuilder() { } @ProcessorTest + @WithClasses( { + SimpleImmutableUpdateMapper.class + } ) + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = SimpleImmutableUpdateMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 18, + message = "No target property found for target \"SimpleImmutableTarget\"."), + }) + public void updatingTargetWithNoSettersShouldNotFail() { SimpleMutableSource source = new SimpleMutableSource(); @@ -77,7 +104,7 @@ public void updatingTargetWithNoSettersShouldNotFail() { .build(); assertThat( target.getAge() ).isEqualTo( 20 ); - SimpleBuilderMapper.INSTANCE.toImmutable( source, target ); + SimpleImmutableUpdateMapper.INSTANCE.toImmutable( source, target ); assertThat( target.getAge() ).isEqualTo( 20 ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleBuilderMapper.java index 9f7187c634..6185daee97 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleBuilderMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleBuilderMapper.java @@ -21,9 +21,6 @@ public interface SimpleBuilderMapper { @Mapping(target = "builder.name", source = "source.fullName") void updateImmutable(SimpleMutableSource source, @MappingTarget SimpleImmutableTarget.Builder builder); - // This method is fine as if the mapping target has setters it would use them, otherwise it won't - void toImmutable(SimpleMutableSource source, @MappingTarget SimpleImmutableTarget target); - @Mapping(target = "name", source = "fullName") SimpleImmutableTarget toImmutable(SimpleMutableSource source); diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleImmutableUpdateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleImmutableUpdateMapper.java new file mode 100644 index 0000000000..c6eccc96c8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/mappingTarget/simple/SimpleImmutableUpdateMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builder.mappingTarget.simple; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SimpleImmutableUpdateMapper { + + SimpleImmutableUpdateMapper INSTANCE = Mappers.getMapper( SimpleImmutableUpdateMapper.class ); + + // This method is fine as if the mapping target has setters it would use them, otherwise it won't + void toImmutable(SimpleMutableSource source, @MappingTarget SimpleImmutableTarget target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleNotRealyImmutableBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleNotRealyImmutableBuilderTest.java index cc11a8fcdb..30cf5ff12d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleNotRealyImmutableBuilderTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleNotRealyImmutableBuilderTest.java @@ -9,12 +9,13 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; import org.mapstruct.ap.testutil.runner.GeneratedSource; import org.mapstruct.factory.Mappers; import static org.assertj.core.api.Assertions.assertThat; -@IssueKey("1743") +@IssueKey("1661") @WithClasses({ SimpleMutablePerson.class, SimpleNotRealyImmutablePerson.class @@ -36,4 +37,18 @@ public void testSimpleImmutableBuilderHappyPath() { assertThat( targetObject.getName() ).isEqualTo( "Bob" ); } + + @ProcessorTest + @WithClasses({ SimpleWithBuilderMapper.class }) + @ProcessorOption( name = "mapstruct.disableBuilders", value = "true") + public void builderGloballyDisabled() { + SimpleWithBuilderMapper mapper = Mappers.getMapper( SimpleWithBuilderMapper.class ); + SimpleMutablePerson source = new SimpleMutablePerson(); + source.setFullName( "Bob" ); + + SimpleNotRealyImmutablePerson targetObject = mapper.toNotRealyImmutable( source ); + + assertThat( targetObject.getName() ).isEqualTo( "Bob" ); + + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleWithBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleWithBuilderMapper.java new file mode 100644 index 0000000000..6df547db78 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/off/SimpleWithBuilderMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builder.off; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) +public interface SimpleWithBuilderMapper { + + @Mapping(target = "name", source = "fullName") + SimpleNotRealyImmutablePerson toNotRealyImmutable(SimpleMutablePerson source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/parentchild/ParentChildBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/parentchild/ParentChildBuilderTest.java index dd45573a59..9269390640 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/parentchild/ParentChildBuilderTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/parentchild/ParentChildBuilderTest.java @@ -26,7 +26,7 @@ public class ParentChildBuilderTest { public void testParentChildBuilderMapper() { final MutableParent parent = new MutableParent(); parent.setCount( 4 ); - parent.setChildren( new ArrayList() ); + parent.setChildren( new ArrayList<>() ); parent.getChildren().add( new MutableChild( "Phineas" ) ); parent.getChildren().add( new MutableChild( "Ferb" ) ); @@ -42,7 +42,7 @@ public void testParentChildBuilderMapper() { } private Condition hasMatchingName(final String name) { - return new Condition( "Matching name" ) { + return new Condition<>( "Matching name" ) { @Override public boolean matches(ImmutableChild value) { return name.equals( value.getName() ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/ErroneousSimpleBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/ErroneousSimpleBuilderMapper.java similarity index 72% rename from processor/src/test/java/org/mapstruct/ap/test/builder/simple/ErroneousSimpleBuilderMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/ErroneousSimpleBuilderMapper.java index 305ae77e80..18e82ed19c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/ErroneousSimpleBuilderMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/ErroneousSimpleBuilderMapper.java @@ -3,12 +3,13 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.builder.simple; +package org.mapstruct.ap.test.builder.simple.innerclass; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson; @Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) public interface ErroneousSimpleBuilderMapper { @@ -18,5 +19,5 @@ public interface ErroneousSimpleBuilderMapper { @Mapping(target = "job", ignore = true ), @Mapping(target = "city", ignore = true ) }) - SimpleImmutablePerson toImmutable(SimpleMutablePerson source); + SimpleImmutablePersonWithInnerClassBuilder toImmutable(SimpleMutablePerson source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleBuilderMapper.java similarity index 74% rename from processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleBuilderMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleBuilderMapper.java index 0b0e961b30..e2c673401a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleBuilderMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleBuilderMapper.java @@ -3,12 +3,13 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.builder.simple; +package org.mapstruct.ap.test.builder.simple.innerclass; import org.mapstruct.CollectionMappingStrategy; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; +import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson; @Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface SimpleBuilderMapper { @@ -18,5 +19,5 @@ public interface SimpleBuilderMapper { @Mapping(target = "job", constant = "programmer"), @Mapping(target = "city", expression = "java(\"Bengalore\")") }) - SimpleImmutablePerson toImmutable(SimpleMutablePerson source); + SimpleImmutablePersonWithInnerClassBuilder toImmutable(SimpleMutablePerson source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleImmutableBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleImmutableBuilderThroughInnerClassConstructorTest.java similarity index 78% rename from processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleImmutableBuilderTest.java rename to processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleImmutableBuilderThroughInnerClassConstructorTest.java index 7f77621644..0a2c5287bc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleImmutableBuilderTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleImmutableBuilderThroughInnerClassConstructorTest.java @@ -3,11 +3,12 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.builder.simple; +package org.mapstruct.ap.test.builder.simple.innerclass; import java.util.Arrays; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; @@ -20,16 +21,16 @@ @WithClasses({ SimpleMutablePerson.class, - SimpleImmutablePerson.class + SimpleImmutablePersonWithInnerClassBuilder.class }) -public class SimpleImmutableBuilderTest { +public class SimpleImmutableBuilderThroughInnerClassConstructorTest { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); @ProcessorTest @WithClasses({ SimpleBuilderMapper.class }) - public void testSimpleImmutableBuilderHappyPath() { + public void testSimpleImmutableBuilderThroughInnerClassConstructorHappyPath() { SimpleBuilderMapper mapper = Mappers.getMapper( SimpleBuilderMapper.class ); SimpleMutablePerson source = new SimpleMutablePerson(); source.setAge( 3 ); @@ -37,7 +38,7 @@ public void testSimpleImmutableBuilderHappyPath() { source.setChildren( Arrays.asList( "Alice", "Tom" ) ); source.setAddress( "Plaza 1" ); - SimpleImmutablePerson targetObject = mapper.toImmutable( source ); + SimpleImmutablePersonWithInnerClassBuilder targetObject = mapper.toImmutable( source ); assertThat( targetObject.getAge() ).isEqualTo( 3 ); assertThat( targetObject.getName() ).isEqualTo( "Bob" ); @@ -53,8 +54,8 @@ public void testSimpleImmutableBuilderHappyPath() { diagnostics = @Diagnostic( kind = javax.tools.Diagnostic.Kind.ERROR, type = ErroneousSimpleBuilderMapper.class, - line = 21, + line = 22, message = "Unmapped target property: \"name\".")) - public void testSimpleImmutableBuilderMissingPropertyFailsToCompile() { + public void testSimpleImmutableBuilderThroughInnerClassConstructorMissingPropertyFailsToCompile() { } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleImmutablePerson.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleImmutablePersonWithInnerClassBuilder.java similarity index 81% rename from processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleImmutablePerson.java rename to processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleImmutablePersonWithInnerClassBuilder.java index daa3dfad0b..a9bda42d33 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/SimpleImmutablePerson.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/innerclass/SimpleImmutablePersonWithInnerClassBuilder.java @@ -3,12 +3,12 @@ * * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package org.mapstruct.ap.test.builder.simple; +package org.mapstruct.ap.test.builder.simple.innerclass; import java.util.ArrayList; import java.util.List; -public class SimpleImmutablePerson { +public class SimpleImmutablePersonWithInnerClassBuilder { private final String name; private final int age; private final String job; @@ -16,17 +16,13 @@ public class SimpleImmutablePerson { private final String address; private final List children; - SimpleImmutablePerson(Builder builder) { + SimpleImmutablePersonWithInnerClassBuilder(Builder builder) { this.name = builder.name; this.age = builder.age; this.job = builder.job; this.city = builder.city; this.address = builder.address; - this.children = new ArrayList( builder.children ); - } - - public static Builder builder() { - return new Builder(); + this.children = new ArrayList<>(builder.children); } public int getAge() { @@ -59,15 +55,15 @@ public static class Builder { private String job; private String city; private String address; - private List children = new ArrayList(); + private List children = new ArrayList<>(); public Builder age(int age) { this.age = age; return this; } - public SimpleImmutablePerson build() { - return new SimpleImmutablePerson( this ); + public SimpleImmutablePersonWithInnerClassBuilder build() { + return new SimpleImmutablePersonWithInnerClassBuilder( this ); } public Builder name(String name) { diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/ErroneousSimpleBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/ErroneousSimpleBuilderMapper.java new file mode 100644 index 0000000000..6fc51ace3d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/ErroneousSimpleBuilderMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builder.simple.staticfactorymethod; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface ErroneousSimpleBuilderMapper { + + @Mappings({ + @Mapping(target = "address", ignore = true ), + @Mapping(target = "job", ignore = true ), + @Mapping(target = "city", ignore = true ) + }) + SimpleImmutablePersonWithStaticFactoryMethodBuilder toImmutable(SimpleMutablePerson source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleBuilderMapper.java new file mode 100644 index 0000000000..4bd200bc7c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleBuilderMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builder.simple.staticfactorymethod; + +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson; + +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) +public interface SimpleBuilderMapper { + + @Mappings({ + @Mapping(target = "name", source = "fullName"), + @Mapping(target = "job", constant = "programmer"), + @Mapping(target = "city", expression = "java(\"Bengalore\")") + }) + SimpleImmutablePersonWithStaticFactoryMethodBuilder toImmutable(SimpleMutablePerson source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleImmutableBuilderThroughStaticFactoryMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleImmutableBuilderThroughStaticFactoryMethodTest.java new file mode 100644 index 0000000000..90fac6061e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleImmutableBuilderThroughStaticFactoryMethodTest.java @@ -0,0 +1,61 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builder.simple.staticfactorymethod; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.mapstruct.factory.Mappers; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + SimpleMutablePerson.class, + SimpleImmutablePersonWithStaticFactoryMethodBuilder.class +}) +public class SimpleImmutableBuilderThroughStaticFactoryMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ SimpleBuilderMapper.class }) + public void testSimpleImmutableBuilderThroughStaticFactoryMethodHappyPath() { + SimpleBuilderMapper mapper = Mappers.getMapper( SimpleBuilderMapper.class ); + SimpleMutablePerson source = new SimpleMutablePerson(); + source.setAge( 3 ); + source.setFullName( "Bob" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + source.setAddress( "Plaza 1" ); + + SimpleImmutablePersonWithStaticFactoryMethodBuilder targetObject = mapper.toImmutable( source ); + + assertThat( targetObject.getAge() ).isEqualTo( 3 ); + assertThat( targetObject.getName() ).isEqualTo( "Bob" ); + assertThat( targetObject.getJob() ).isEqualTo( "programmer" ); + assertThat( targetObject.getCity() ).isEqualTo( "Bengalore" ); + assertThat( targetObject.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( targetObject.getChildren() ).contains( "Alice", "Tom" ); + } + + @ProcessorTest + @WithClasses({ ErroneousSimpleBuilderMapper.class }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousSimpleBuilderMapper.class, + line = 22, + message = "Unmapped target property: \"name\".")) + public void testSimpleImmutableBuilderThroughStaticFactoryMethodMissingPropertyFailsToCompile() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleImmutablePersonWithStaticFactoryMethodBuilder.java b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleImmutablePersonWithStaticFactoryMethodBuilder.java new file mode 100644 index 0000000000..c3dc590210 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/simple/staticfactorymethod/SimpleImmutablePersonWithStaticFactoryMethodBuilder.java @@ -0,0 +1,105 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builder.simple.staticfactorymethod; + +import java.util.ArrayList; +import java.util.List; + +public class SimpleImmutablePersonWithStaticFactoryMethodBuilder { + private final String name; + private final int age; + private final String job; + private final String city; + private final String address; + private final List children; + + SimpleImmutablePersonWithStaticFactoryMethodBuilder(Builder builder) { + this.name = builder.name; + this.age = builder.age; + this.job = builder.job; + this.city = builder.city; + this.address = builder.address; + this.children = new ArrayList<>( builder.children ); + } + + public static Builder builder() { + return new Builder(); + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + public String getJob() { + return job; + } + + public String getCity() { + return city; + } + + public String getAddress() { + return address; + } + + public List getChildren() { + return children; + } + + public static class Builder { + private String name; + private int age; + private String job; + private String city; + private String address; + private List children = new ArrayList<>(); + + private Builder() { + } + + public Builder age(int age) { + this.age = age; + return this; + } + + public SimpleImmutablePersonWithStaticFactoryMethodBuilder build() { + return new SimpleImmutablePersonWithStaticFactoryMethodBuilder( this ); + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder job(String job) { + this.job = job; + return this; + } + + public Builder city(String city) { + this.city = city; + return this; + } + + public Builder address(String address) { + this.address = address; + return this; + } + + public List getChildren() { + throw new UnsupportedOperationException( "This is just a marker method" ); + } + + public Builder addChild(String child) { + this.children.add( child ); + return this; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/BuiltInTest.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/BuiltInTest.java index 0f1983a851..c8ee922af9 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/BuiltInTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/BuiltInTest.java @@ -29,6 +29,8 @@ import org.mapstruct.ap.test.builtin.bean.BigDecimalProperty; import org.mapstruct.ap.test.builtin.bean.CalendarProperty; import org.mapstruct.ap.test.builtin.bean.DateProperty; +import org.mapstruct.ap.test.builtin.bean.JakartaJaxbElementListProperty; +import org.mapstruct.ap.test.builtin.bean.JakartaJaxbElementProperty; import org.mapstruct.ap.test.builtin.bean.JaxbElementListProperty; import org.mapstruct.ap.test.builtin.bean.JaxbElementProperty; import org.mapstruct.ap.test.builtin.bean.SomeType; @@ -45,6 +47,8 @@ import org.mapstruct.ap.test.builtin.mapper.DateToCalendarMapper; import org.mapstruct.ap.test.builtin.mapper.DateToXmlGregCalMapper; import org.mapstruct.ap.test.builtin.mapper.IterableSourceTargetMapper; +import org.mapstruct.ap.test.builtin.mapper.JakartaJaxbListMapper; +import org.mapstruct.ap.test.builtin.mapper.JakartaJaxbMapper; import org.mapstruct.ap.test.builtin.mapper.JaxbListMapper; import org.mapstruct.ap.test.builtin.mapper.JaxbMapper; import org.mapstruct.ap.test.builtin.mapper.MapSourceTargetMapper; @@ -58,6 +62,8 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaJaxb; +import org.mapstruct.ap.testutil.WithJavaxJaxb; import static org.assertj.core.api.Assertions.assertThat; @@ -71,8 +77,6 @@ MapTarget.class, CalendarProperty.class, DateProperty.class, - JaxbElementListProperty.class, - JaxbElementProperty.class, StringListProperty.class, StringProperty.class, BigDecimalProperty.class, @@ -81,13 +85,16 @@ XmlGregorianCalendarProperty.class, ZonedDateTimeProperty.class, IterableSource.class, - MapSource.class }) @DefaultTimeZone("Europe/Berlin") public class BuiltInTest { @ProcessorTest - @WithClasses( JaxbMapper.class ) + @WithClasses( { + JaxbMapper.class, + JaxbElementProperty.class, + } ) + @WithJavaxJaxb public void shouldApplyBuiltInOnJAXBElement() { JaxbElementProperty source = new JaxbElementProperty(); source.setProp( createJaxb( "TEST" ) ); @@ -100,7 +107,28 @@ public void shouldApplyBuiltInOnJAXBElement() { } @ProcessorTest - @WithClasses( JaxbMapper.class ) + @WithClasses( { + JakartaJaxbMapper.class, + JakartaJaxbElementProperty.class, + } ) + @WithJakartaJaxb + public void shouldApplyBuiltInOnJakartaJaxbElement() { + JakartaJaxbElementProperty source = new JakartaJaxbElementProperty(); + source.setProp( createJakartaJaxb( "TEST" ) ); + source.publicProp = createJakartaJaxb( "PUBLIC TEST" ); + + StringProperty target = JakartaJaxbMapper.INSTANCE.map( source ); + assertThat( target ).isNotNull(); + assertThat( target.getProp() ).isEqualTo( "TEST" ); + assertThat( target.publicProp ).isEqualTo( "PUBLIC TEST" ); + } + + @ProcessorTest + @WithClasses( { + JaxbMapper.class, + JaxbElementProperty.class, + } ) + @WithJavaxJaxb @IssueKey( "1698" ) public void shouldApplyBuiltInOnJAXBElementExtra() { JaxbElementProperty source = new JaxbElementProperty(); @@ -123,7 +151,38 @@ public void shouldApplyBuiltInOnJAXBElementExtra() { } @ProcessorTest - @WithClasses( JaxbListMapper.class ) + @WithClasses( { + JakartaJaxbMapper.class, + JakartaJaxbElementProperty.class, + } ) + @WithJakartaJaxb + @IssueKey( "1698" ) + public void shouldApplyBuiltInOnJakartaJAXBElementExtra() { + JakartaJaxbElementProperty source = new JakartaJaxbElementProperty(); + source.setProp( createJakartaJaxb( "5" ) ); + source.publicProp = createJakartaJaxb( "5" ); + + BigDecimalProperty target = JakartaJaxbMapper.INSTANCE.mapBD( source ); + assertThat( target ).isNotNull(); + assertThat( target.getProp() ).isEqualTo( new BigDecimal( "5" ) ); + assertThat( target.publicProp ).isEqualTo( new BigDecimal( "5" ) ); + + JakartaJaxbElementProperty source2 = new JakartaJaxbElementProperty(); + source2.setProp( createJakartaJaxb( "5" ) ); + source2.publicProp = createJakartaJaxb( "5" ); + + SomeTypeProperty target2 = JakartaJaxbMapper.INSTANCE.mapSomeType( source2 ); + assertThat( target2 ).isNotNull(); + assertThat( target2.publicProp ).isNotNull(); + assertThat( target2.getProp() ).isNotNull(); + } + + @ProcessorTest + @WithClasses( { + JaxbListMapper.class, + JaxbElementListProperty.class, + } ) + @WithJavaxJaxb @IssueKey( "141" ) public void shouldApplyBuiltInOnJAXBElementList() { @@ -137,6 +196,24 @@ public void shouldApplyBuiltInOnJAXBElementList() { assertThat( target.publicProp.get( 0 ) ).isEqualTo( "PUBLIC TEST2" ); } + @ProcessorTest + @WithClasses( { + JakartaJaxbListMapper.class, + JakartaJaxbElementListProperty.class, + } ) + @WithJakartaJaxb + @IssueKey( "141" ) + public void shouldApplyBuiltInOnJakartaJAXBElementList() { + JakartaJaxbElementListProperty source = new JakartaJaxbElementListProperty(); + source.setProp( createJakartaJaxbList( "TEST2" ) ); + source.publicProp = createJakartaJaxbList( "PUBLIC TEST2" ); + + StringListProperty target = JakartaJaxbListMapper.INSTANCE.map( source ); + assertThat( target ).isNotNull(); + assertThat( target.getProp().get( 0 ) ).isEqualTo( "TEST2" ); + assertThat( target.publicProp.get( 0 ) ).isEqualTo( "PUBLIC TEST2" ); + } + @ProcessorTest @WithClasses( DateToXmlGregCalMapper.class ) public void shouldApplyBuiltInOnDateToXmlGregCal() throws ParseException { @@ -347,7 +424,11 @@ public void shouldApplyBuiltInOnIterable() throws DatatypeConfigurationException } @ProcessorTest - @WithClasses( MapSourceTargetMapper.class ) + @WithClasses( { + MapSourceTargetMapper.class, + MapSource.class, + } ) + @WithJavaxJaxb public void shouldApplyBuiltInOnMap() throws DatatypeConfigurationException { MapSource source = new MapSource(); @@ -400,12 +481,22 @@ private JAXBElement createJaxb(String test) { return new JAXBElement<>( new QName( "www.mapstruct.org", "test" ), String.class, test ); } + private jakarta.xml.bind.JAXBElement createJakartaJaxb(String test) { + return new jakarta.xml.bind.JAXBElement<>( new QName( "www.mapstruct.org", "test" ), String.class, test ); + } + private List> createJaxbList(String test) { List> result = new ArrayList<>(); result.add( createJaxb( test ) ); return result; } + private List> createJakartaJaxbList(String test) { + List> result = new ArrayList<>(); + result.add( createJakartaJaxb( test ) ); + return result; + } + private Date createDate(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat( "dd-M-yyyy hh:mm:ss" ); return sdf.parse( date ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JakartaJaxbElementListProperty.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JakartaJaxbElementListProperty.java new file mode 100644 index 0000000000..f6ab537253 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JakartaJaxbElementListProperty.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builtin.bean; + +import java.util.List; + +import jakarta.xml.bind.JAXBElement; + +public class JakartaJaxbElementListProperty { + + // CHECKSTYLE:OFF + public List> publicProp; + // CHECKSTYLE:ON + + private List> prop; + + public List> getProp() { + return prop; + } + + public void setProp( List> prop ) { + this.prop = prop; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JakartaJaxbElementProperty.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JakartaJaxbElementProperty.java new file mode 100644 index 0000000000..0336afe814 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JakartaJaxbElementProperty.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builtin.bean; + +import jakarta.xml.bind.JAXBElement; + +public class JakartaJaxbElementProperty { + + // CHECKSTYLE:OFF + public JAXBElement publicProp; + // CHECKSTYLE:ON + + private JAXBElement prop; + + public JAXBElement getProp() { + return prop; + } + + public void setProp( JAXBElement prop ) { + this.prop = prop; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JaxbElementListProperty.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JaxbElementListProperty.java index dd2d2ec41f..f69655b618 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JaxbElementListProperty.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/bean/JaxbElementListProperty.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.builtin.bean; import java.util.List; - import javax.xml.bind.JAXBElement; public class JaxbElementListProperty { diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/jodatime/JodaTimeTest.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/jodatime/JodaTimeTest.java index 77c62c9cac..cb0c3d2378 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/jodatime/JodaTimeTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/jodatime/JodaTimeTest.java @@ -30,6 +30,8 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxJaxb; +import org.mapstruct.ap.testutil.WithJoda; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +47,8 @@ XmlGregorianCalendarBean.class }) @IssueKey( "689" ) +@WithJoda +@WithJavaxJaxb public class JodaTimeTest { @ProcessorTest diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/IterableSourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/IterableSourceTargetMapper.java index 5e5e669a38..619a72514c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/IterableSourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/IterableSourceTargetMapper.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.builtin.mapper; import java.util.List; - import javax.xml.datatype.XMLGregorianCalendar; import org.mapstruct.IterableMapping; diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JakartaJaxbListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JakartaJaxbListMapper.java new file mode 100644 index 0000000000..602e2180f1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JakartaJaxbListMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builtin.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builtin.bean.JakartaJaxbElementListProperty; +import org.mapstruct.ap.test.builtin.bean.StringListProperty; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface JakartaJaxbListMapper { + + JakartaJaxbListMapper INSTANCE = Mappers.getMapper( JakartaJaxbListMapper.class ); + + StringListProperty map(JakartaJaxbElementListProperty source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JakartaJaxbMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JakartaJaxbMapper.java new file mode 100644 index 0000000000..9334792572 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JakartaJaxbMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.builtin.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builtin.bean.BigDecimalProperty; +import org.mapstruct.ap.test.builtin.bean.JakartaJaxbElementProperty; +import org.mapstruct.ap.test.builtin.bean.SomeType; +import org.mapstruct.ap.test.builtin.bean.SomeTypeProperty; +import org.mapstruct.ap.test.builtin.bean.StringProperty; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface JakartaJaxbMapper { + + JakartaJaxbMapper INSTANCE = Mappers.getMapper( JakartaJaxbMapper.class ); + + StringProperty map(JakartaJaxbElementProperty source); + + BigDecimalProperty mapBD(JakartaJaxbElementProperty source); + + SomeTypeProperty mapSomeType(JakartaJaxbElementProperty source); + + @SuppressWarnings( "unused" ) + default SomeType map( String in ) { + return new SomeType(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JaxbMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JaxbMapper.java index 5b407e19d6..ec2ca65b29 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JaxbMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/JaxbMapper.java @@ -7,9 +7,9 @@ import org.mapstruct.Mapper; import org.mapstruct.ap.test.builtin.bean.BigDecimalProperty; +import org.mapstruct.ap.test.builtin.bean.JaxbElementProperty; import org.mapstruct.ap.test.builtin.bean.SomeType; import org.mapstruct.ap.test.builtin.bean.SomeTypeProperty; -import org.mapstruct.ap.test.builtin.bean.JaxbElementProperty; import org.mapstruct.ap.test.builtin.bean.StringProperty; import org.mapstruct.factory.Mappers; diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/MapSourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/MapSourceTargetMapper.java index 8d0524332f..04a8f6bf0d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/MapSourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/mapper/MapSourceTargetMapper.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.builtin.mapper; import java.util.Map; - import javax.xml.bind.JAXBElement; import javax.xml.datatype.XMLGregorianCalendar; diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/source/IterableSource.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/source/IterableSource.java index da21ed6cd5..69a3bab2bc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/source/IterableSource.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/source/IterableSource.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.builtin.source; import java.util.List; - import javax.xml.datatype.XMLGregorianCalendar; /** diff --git a/processor/src/test/java/org/mapstruct/ap/test/builtin/source/MapSource.java b/processor/src/test/java/org/mapstruct/ap/test/builtin/source/MapSource.java index c05ed16625..6b8e8f3b94 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/builtin/source/MapSource.java +++ b/processor/src/test/java/org/mapstruct/ap/test/builtin/source/MapSource.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.builtin.source; import java.util.Map; - import javax.xml.bind.JAXBElement; import javax.xml.datatype.XMLGregorianCalendar; diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/BaseMapper.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/BaseMapper.java index bee462accc..4e02045ed5 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/callbacks/BaseMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/BaseMapper.java @@ -24,7 +24,7 @@ public abstract class BaseMapper { @Qualified public abstract Target sourceToTargetQualified(Source source); - private static final List INVOCATIONS = new ArrayList(); + private static final List INVOCATIONS = new ArrayList<>(); @BeforeMapping public void noArgsBeforeMapping() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/CallbackMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/CallbackMethodTest.java index 7026a335d8..825b359152 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/callbacks/CallbackMethodTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/CallbackMethodTest.java @@ -111,7 +111,7 @@ public void callbackMethodsForEnumMappingCalled() { SourceEnum source = SourceEnum.B; TargetEnum target = SourceTargetMapper.INSTANCE.toTargetEnum( source ); - List invocations = new ArrayList(); + List invocations = new ArrayList<>(); invocations.addAll( allBeforeMappingMethods( source, target, TargetEnum.class ) ); invocations.addAll( allAfterMappingMethods( source, target, TargetEnum.class ) ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/ClassContainingCallbacks.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/ClassContainingCallbacks.java index 762247045d..2889a4ad30 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/callbacks/ClassContainingCallbacks.java +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/ClassContainingCallbacks.java @@ -18,7 +18,7 @@ * @author Andreas Gudian */ public class ClassContainingCallbacks { - private static final List INVOCATIONS = new ArrayList(); + private static final List INVOCATIONS = new ArrayList<>(); @BeforeMapping public void noArgsBeforeMapping() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java index 8c391bbfeb..10d2a29cd7 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/CallbacksWithReturnValuesTest.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; import org.mapstruct.ap.test.callbacks.returning.NodeMapperContext.ContextListener; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; @@ -25,30 +26,31 @@ @WithClasses( { Attribute.class, AttributeDto.class, Node.class, NodeDto.class, NodeMapperDefault.class, NodeMapperWithContext.class, NodeMapperContext.class, Number.class, NumberMapperDefault.class, NumberMapperContext.class, NumberMapperWithContext.class } ) -public class CallbacksWithReturnValuesTest { +class CallbacksWithReturnValuesTest { + @AfterEach + void cleanup() { + NumberMapperContext.clearCache(); + NumberMapperContext.clearVisited(); + } + @ProcessorTest - public void mappingWithDefaultHandlingRaisesStackOverflowError() { + void mappingWithDefaultHandlingRaisesStackOverflowError() { Node root = buildNodes(); assertThatThrownBy( () -> NodeMapperDefault.INSTANCE.nodeToNodeDto( root ) ) .isInstanceOf( StackOverflowError.class ); } @ProcessorTest - public void updatingWithDefaultHandlingRaisesStackOverflowError() { + void updatingWithDefaultHandlingRaisesStackOverflowError() { Node root = buildNodes(); assertThatThrownBy( () -> NodeMapperDefault.INSTANCE.nodeToNodeDto( root, new NodeDto() ) ) .isInstanceOf( StackOverflowError.class ); } @ProcessorTest - public void mappingWithContextCorrectlyResolvesCycles() { + void mappingWithContextCorrectlyResolvesCycles() { final AtomicReference contextLevel = new AtomicReference<>( null ); - ContextListener contextListener = new ContextListener() { - @Override - public void methodCalled(Integer level, String method, Object source, Object target) { - contextLevel.set( level ); - } - }; + ContextListener contextListener = (level, method, source, target) -> contextLevel.set( level ); NodeMapperContext.addContextListener( contextListener ); try { @@ -75,28 +77,49 @@ private static Node buildNodes() { } @ProcessorTest - public void numberMappingWithoutContextDoesNotUseCache() { + void numberMappingWithoutContextDoesNotUseCache() { Number n1 = NumberMapperDefault.INSTANCE.integerToNumber( 2342 ); Number n2 = NumberMapperDefault.INSTANCE.integerToNumber( 2342 ); + assertThat( n1 ).isEqualTo( n2 ); assertThat( n1 ).isNotSameAs( n2 ); } @ProcessorTest - public void numberMappingWithContextUsesCache() { + void numberMappingWithContextUsesCache() { NumberMapperContext.putCache( new Number( 2342 ) ); Number n1 = NumberMapperWithContext.INSTANCE.integerToNumber( 2342 ); Number n2 = NumberMapperWithContext.INSTANCE.integerToNumber( 2342 ); + assertThat( n1 ).isEqualTo( n2 ); assertThat( n1 ).isSameAs( n2 ); - NumberMapperContext.clearCache(); } @ProcessorTest - public void numberMappingWithContextCallsVisitNumber() { + void numberMappingWithContextCallsVisitNumber() { Number n1 = NumberMapperWithContext.INSTANCE.integerToNumber( 1234 ); Number n2 = NumberMapperWithContext.INSTANCE.integerToNumber( 5678 ); + assertThat( NumberMapperContext.getVisited() ).isEqualTo( Arrays.asList( n1, n2 ) ); - NumberMapperContext.clearVisited(); + } + + @ProcessorTest + @IssueKey( "2955" ) + void numberUpdateMappingWithContextUsesCacheAndThereforeDoesNotVisitNumber() { + Number target = new Number(); + Number expectedReturn = new Number( 2342 ); + NumberMapperContext.putCache( expectedReturn ); + NumberMapperWithContext.INSTANCE.integerToNumber( 2342, target ); + + assertThat( NumberMapperContext.getVisited() ).isEmpty(); + } + + @ProcessorTest + @IssueKey( "2955" ) + void numberUpdateMappingWithContextCallsVisitNumber() { + Number target = new Number(); + NumberMapperWithContext.INSTANCE.integerToNumber( 2342, target ); + + assertThat( NumberMapperContext.getVisited() ).contains( target ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java index 40a1a63937..325778b85e 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java +++ b/processor/src/test/java/org/mapstruct/ap/test/callbacks/returning/NodeMapperContext.java @@ -19,11 +19,11 @@ * @author Pascal Grün */ public class NodeMapperContext { - private static final ThreadLocal LEVEL = new ThreadLocal(); - private static final ThreadLocal> MAPPING = new ThreadLocal>(); + private static final ThreadLocal LEVEL = new ThreadLocal<>(); + private static final ThreadLocal> MAPPING = new ThreadLocal<>(); /** Only for test-inspection */ - private static final List LISTENERS = new CopyOnWriteArrayList(); + private static final List LISTENERS = new CopyOnWriteArrayList<>(); private NodeMapperContext() { // Only allow static access diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java new file mode 100644 index 0000000000..8bac5841f1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java @@ -0,0 +1,49 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static java.lang.reflect.Modifier.isPrivate; +import static java.lang.reflect.Modifier.isProtected; +import static java.lang.reflect.Modifier.isPublic; +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + Source.class, + Target.class, + PublicAbstractionMapper.class, + PackageAbstractionMapper.class, + ForcedPublicMapper.class, + ForcedPackagePrivateMapper.class +}) +public class ClassAccessibilityTest { + + @ProcessorTest + public void shouldCreateModifierAccordingToAnnotation() throws Exception { + Class publicLike = loadForMapper( PublicAbstractionMapper.class ); + assertThat( isPublic( publicLike.getModifiers() ) ).isTrue(); + + Class packageLike = loadForMapper( PackageAbstractionMapper.class ); + assertThat( isDefault( packageLike.getModifiers() ) ).isTrue(); + + Class forcedPublic = loadForMapper( ForcedPublicMapper.class ); + assertThat( isPublic( forcedPublic.getModifiers() ) ).isTrue(); + + Class forcedDefault = loadForMapper( ForcedPackagePrivateMapper.class ); + assertThat( isDefault( forcedDefault.getModifiers() ) ).isTrue(); + } + + private static Class loadForMapper(Class mapper) throws ClassNotFoundException { + return Thread.currentThread().getContextClassLoader().loadClass( mapper.getName() + "Impl" ); + } + + private static boolean isDefault(int modifiers) { + return !isPublic( modifiers ) && !isProtected( modifiers ) && !isPrivate( modifiers ); + } +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java new file mode 100644 index 0000000000..191aadb2a0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +import org.mapstruct.ClassAccessibility; +import org.mapstruct.Mapper; + +@Mapper(accessibility = ClassAccessibility.PACKAGE_PRIVATE) +public interface ForcedPackagePrivateMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java new file mode 100644 index 0000000000..fd6ced910a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +import org.mapstruct.ClassAccessibility; +import org.mapstruct.Mapper; + +@Mapper(accessibility = ClassAccessibility.PUBLIC) +interface ForcedPublicMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java new file mode 100644 index 0000000000..d9f426b382 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +import org.mapstruct.Mapper; + +@Mapper +interface PackageAbstractionMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java new file mode 100644 index 0000000000..7c1b5de10f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +import org.mapstruct.Mapper; + +@Mapper +public interface PublicAbstractionMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java new file mode 100644 index 0000000000..feff965719 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +/** + * @author Raimund Klein + **/ +public class Source { + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java new file mode 100644 index 0000000000..e8c50a2531 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.classaccessibility; + +/** + * @author Raimund Klein + **/ +public class Target { + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/adder/PetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/adder/PetMapper.java index ef80c4b6c0..601b62c6b6 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/adder/PetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/adder/PetMapper.java @@ -9,13 +9,12 @@ import java.util.List; import java.util.Map; +import com.google.common.collect.ImmutableMap; import org.mapstruct.TargetType; import org.mapstruct.ap.test.collection.adder._target.IndoorPet; import org.mapstruct.ap.test.collection.adder._target.OutdoorPet; import org.mapstruct.ap.test.collection.adder._target.Pet; -import com.google.common.collect.ImmutableMap; - /** * @author Sjaak Derksen */ diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/adder/_target/Target2.java b/processor/src/test/java/org/mapstruct/ap/test/collection/adder/_target/Target2.java index 850c6ee94d..bd6e8f9979 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/adder/_target/Target2.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/adder/_target/Target2.java @@ -16,7 +16,7 @@ */ public class Target2 { - private List attributes = new ArrayList(); + private List attributes = new ArrayList<>(); public Foo addAttribute( Foo foo ) { attributes.add( foo ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/DefaultCollectionImplementationTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/DefaultCollectionImplementationTest.java index 9d360656a0..dccfcf01ca 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/DefaultCollectionImplementationTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/DefaultCollectionImplementationTest.java @@ -159,7 +159,21 @@ public void shouldUseAndReturnTargetParameterForMapping() { SourceTargetMapper.INSTANCE .sourceFoosToTargetFoosUsingTargetParameterAndReturn( createSourceFooList(), target ); - assertThat( target == result ).isTrue(); + assertThat( result ).isSameAs( target ); + assertResultList( target ); + } + + @ProcessorTest + @IssueKey("1752") + public void shouldUseAndReturnTargetParameterForNullMapping() { + List target = new ArrayList<>(); + target.add( new TargetFoo( "Bob" ) ); + target.add( new TargetFoo( "Alice" ) ); + Iterable result = + SourceTargetMapper.INSTANCE + .sourceFoosToTargetFoosUsingTargetParameterAndReturn( null, target ); + + assertThat( result ).isSameAs( target ); assertResultList( target ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/NoSetterTarget.java b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/NoSetterTarget.java index 562966dfd1..817879fa01 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/NoSetterTarget.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/NoSetterTarget.java @@ -15,8 +15,8 @@ * */ public class NoSetterTarget { - private List listValues = new ArrayList(); - private Map mapValues = new HashMap(); + private List listValues = new ArrayList<>(); + private Map mapValues = new HashMap<>(); public List getListValues() { return listValues; diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/jdk21/SequencedCollectionsDefaultImplementationTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/jdk21/SequencedCollectionsDefaultImplementationTest.java new file mode 100644 index 0000000000..4551d5b0b3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/jdk21/SequencedCollectionsDefaultImplementationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.defaultimplementation.jdk21; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SequencedMap; +import java.util.SequencedSet; + +import org.mapstruct.ap.test.collection.defaultimplementation.Source; +import org.mapstruct.ap.test.collection.defaultimplementation.SourceFoo; +import org.mapstruct.ap.test.collection.defaultimplementation.Target; +import org.mapstruct.ap.test.collection.defaultimplementation.TargetFoo; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +@WithClasses({ + Source.class, + Target.class, + SourceFoo.class, + TargetFoo.class, + SequencedCollectionsMapper.class +}) +@IssueKey("3420") +class SequencedCollectionsDefaultImplementationTest { + + @ProcessorTest + public void shouldUseDefaultImplementationForSequencedMap() { + SequencedMap target = + SequencedCollectionsMapper.INSTANCE.sourceFooMapToTargetFooSequencedMap( createSourceFooMap() ); + + assertResultMap( target ); + } + + @ProcessorTest + public void shouldUseDefaultImplementationForSequencedSet() { + SequencedSet target = + SequencedCollectionsMapper.INSTANCE.sourceFoosToTargetFooSequencedSet( createSourceFooList() ); + + assertResultList( target ); + } + + private void assertResultList(Iterable fooIterable) { + assertThat( fooIterable ).isNotNull(); + assertThat( fooIterable ).containsOnly( new TargetFoo( "Bob" ), new TargetFoo( "Alice" ) ); + } + + private void assertResultMap(Map result) { + assertThat( result ).isNotNull(); + assertThat( result ).hasSize( 2 ); + assertThat( result ).contains( entry( "1", new TargetFoo( "Bob" ) ), entry( "2", new TargetFoo( "Alice" ) ) ); + } + + private Map createSourceFooMap() { + Map map = new HashMap<>(); + map.put( 1L, new SourceFoo( "Bob" ) ); + map.put( 2L, new SourceFoo( "Alice" ) ); + + return map; + } + + private List createSourceFooList() { + return Arrays.asList( new SourceFoo( "Bob" ), new SourceFoo( "Alice" ) ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/jdk21/SequencedCollectionsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/jdk21/SequencedCollectionsMapper.java new file mode 100644 index 0000000000..bbffb56b0c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/defaultimplementation/jdk21/SequencedCollectionsMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.defaultimplementation.jdk21; + +import java.util.Collection; +import java.util.Map; +import java.util.SequencedMap; +import java.util.SequencedSet; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.collection.defaultimplementation.SourceFoo; +import org.mapstruct.ap.test.collection.defaultimplementation.TargetFoo; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SequencedCollectionsMapper { + + SequencedCollectionsMapper INSTANCE = Mappers.getMapper( SequencedCollectionsMapper.class ); + + SequencedSet sourceFoosToTargetFooSequencedSet(Collection foos); + + SequencedMap sourceFooMapToTargetFooSequencedMap(Map foos); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardEntityOnlyGetter.java b/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardEntityOnlyGetter.java index bbf3a6d24e..1f2f2f00b7 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardEntityOnlyGetter.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardEntityOnlyGetter.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.test.collection.immutabletarget; +import java.util.ArrayList; import java.util.List; /** @@ -13,7 +14,7 @@ */ public class CupboardEntityOnlyGetter { - private List content; + private List content = new ArrayList<>(); public List getContent() { return content; diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ErroneousCupboardMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardNoSetterMapper.java similarity index 80% rename from processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ErroneousCupboardMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardNoSetterMapper.java index 028ac4d765..a6674e629f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ErroneousCupboardMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/CupboardNoSetterMapper.java @@ -15,9 +15,9 @@ * @author Sjaak Derksen */ @Mapper( collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE ) -public interface ErroneousCupboardMapper { +public interface CupboardNoSetterMapper { - ErroneousCupboardMapper INSTANCE = Mappers.getMapper( ErroneousCupboardMapper.class ); + CupboardNoSetterMapper INSTANCE = Mappers.getMapper( CupboardNoSetterMapper.class ); void map( CupboardDto in, @MappingTarget CupboardEntityOnlyGetter out ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ImmutableProductTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ImmutableProductTest.java index b3d2096bc3..8097dfdfe9 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ImmutableProductTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/immutabletarget/ImmutableProductTest.java @@ -41,19 +41,24 @@ public void shouldHandleImmutableTarget() { @ProcessorTest @WithClasses({ - ErroneousCupboardMapper.class, + CupboardNoSetterMapper.class, CupboardEntityOnlyGetter.class }) - @ExpectedCompilationOutcome( - value = CompilationResult.FAILED, + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, diagnostics = { - @Diagnostic(type = ErroneousCupboardMapper.class, - kind = javax.tools.Diagnostic.Kind.ERROR, + @Diagnostic(type = CupboardNoSetterMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, line = 22, - message = "No write accessor found for property \"content\" in target type.") - } - ) - public void testShouldFailOnPropertyMappingNoPropertySetterOnlyGetter() { + message = "No target property found for target \"CupboardEntityOnlyGetter\"."), + }) + public void shouldIgnoreImmutableTarget() { + CupboardDto in = new CupboardDto(); + in.setContent( Arrays.asList( "flour", "peas" ) ); + CupboardEntityOnlyGetter out = new CupboardEntityOnlyGetter(); + out.getContent().add( "bread" ); + CupboardNoSetterMapper.INSTANCE.map( in, out ); + + assertThat( out.getContent() ).containsExactly( "bread" ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/Fruit.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/Fruit.java new file mode 100644 index 0000000000..c989aa4e19 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/Fruit.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletolist; + +/** + * + * @author Xiu-Hong Kooi + */ +public class Fruit { + + private String type; + + public Fruit(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitSalad.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitSalad.java new file mode 100644 index 0000000000..6a04121044 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitSalad.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletolist; + +import java.util.List; + +/** + * + * @author Xiu-Hong Kooi + */ +public class FruitSalad { + + private Iterable fruits; + + public FruitSalad(List fruits) { + this.fruits = fruits; + } + + public Iterable getFruits() { + return fruits; + } + + public void setFruits(List fruits) { + this.fruits = fruits; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitsMapper.java new file mode 100644 index 0000000000..bb4fdc66af --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitsMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletolist; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * + * @author Xiu-Hong Kooi + */ +@Mapper +public interface FruitsMapper { + + FruitsMapper INSTANCE = Mappers.getMapper( + FruitsMapper.class ); + + FruitsMenu fruitSaladToMenu(FruitSalad salad); + + FruitSalad fruitsMenuToSalad(FruitsMenu menu); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitsMenu.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitsMenu.java new file mode 100644 index 0000000000..6f272dc454 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/FruitsMenu.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletolist; + +import java.util.Iterator; +import java.util.List; + +/** + * + * @author Xiu-Hong Kooi + */ +public class FruitsMenu implements Iterable { + + private List fruits; + + public FruitsMenu(List fruits) { + this.fruits = fruits; + } + + public List getFruits() { + return fruits; + } + + public void setFruits(List fruits) { + this.fruits = fruits; + } + + @Override + public Iterator iterator() { + return this.fruits.iterator(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/IterableToListMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/IterableToListMappingTest.java new file mode 100644 index 0000000000..81a640499b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletolist/IterableToListMappingTest.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletolist; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Xiu-Hong Kooi + */ +@WithClasses({ FruitsMenu.class, FruitSalad.class, Fruit.class, FruitsMapper.class }) +public class IterableToListMappingTest { + + @ProcessorTest + @IssueKey("3376") + public void shouldMapIterableToList() { + List fruits = Arrays.asList( new Fruit( "mango" ), new Fruit( "apple" ), + new Fruit( "banana" ) ); + FruitsMenu menu = new FruitsMenu(fruits); + FruitSalad salad = FruitsMapper.INSTANCE.fruitsMenuToSalad( menu ); + Iterator itr = salad.getFruits().iterator(); + assertThat( itr.next().getType() ).isEqualTo( "mango" ); + assertThat( itr.next().getType() ).isEqualTo( "apple" ); + assertThat( itr.next().getType() ).isEqualTo( "banana" ); + } + + @ProcessorTest + @IssueKey("3376") + public void shouldMapListToIterable() { + List fruits = Arrays.asList( new Fruit( "mango" ), new Fruit( "apple" ), + new Fruit( "banana" ) ); + FruitSalad salad = new FruitSalad(fruits); + FruitsMenu menu = FruitsMapper.INSTANCE.fruitSaladToMenu( salad ); + assertThat( menu.getFruits() ).extracting( Fruit::getType ).containsExactly( "mango", "apple", "banana" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletononiterable/IterableToNonIterableMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletononiterable/IterableToNonIterableMappingTest.java index 892e24df4c..7467387d8b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletononiterable/IterableToNonIterableMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletononiterable/IterableToNonIterableMappingTest.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.test.collection.iterabletononiterable; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.List; @@ -14,6 +12,8 @@ import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import static org.assertj.core.api.Assertions.assertThat; + @WithClasses({ Source.class, Target.class, SourceTargetMapper.class, StringListMapper.class, FruitsMenu.class, FruitSalad.class, Fruit.class, FruitsMapper.class }) public class IterableToNonIterableMappingTest { diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/Fruit.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/Fruit.java new file mode 100644 index 0000000000..a15b344908 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/Fruit.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletoset; + +/** + * + * @author Xiu-Hong Kooi + */ +public class Fruit { + + private String type; + + public Fruit(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitSalad.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitSalad.java new file mode 100644 index 0000000000..b92ac3160c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitSalad.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletoset; + +import java.util.List; + +/** + * + * @author Xiu-Hong Kooi + */ +public class FruitSalad { + + private Iterable fruits; + + public FruitSalad(Iterable fruits) { + this.fruits = fruits; + } + + public Iterable getFruits() { + return fruits; + } + + public void setFruits(List fruits) { + this.fruits = fruits; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitsMapper.java new file mode 100644 index 0000000000..413dd73617 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitsMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletoset; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * + * @author Xiu-Hong Kooi + */ +@Mapper +public interface FruitsMapper { + + FruitsMapper INSTANCE = Mappers.getMapper( + FruitsMapper.class ); + + FruitsMenu fruitSaladToMenu(FruitSalad salad); + + FruitSalad fruitsMenuToSalad(FruitsMenu menu); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitsMenu.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitsMenu.java new file mode 100644 index 0000000000..8ae4b8b632 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/FruitsMenu.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletoset; + +import java.util.Iterator; +import java.util.Set; + +/** + * + * @author Xiu-Hong Kooi + */ +public class FruitsMenu implements Iterable { + + private Set fruits; + + public FruitsMenu(Set fruits) { + this.fruits = fruits; + } + + public Set getFruits() { + return fruits; + } + + public void setFruits(Set fruits) { + this.fruits = fruits; + } + + @Override + public Iterator iterator() { + return this.fruits.iterator(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/IterableToSetMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/IterableToSetMappingTest.java new file mode 100644 index 0000000000..0b967503bb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/iterabletoset/IterableToSetMappingTest.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.collection.iterabletoset; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Xiu-Hong Kooi + */ +@WithClasses({ FruitsMenu.class, FruitSalad.class, Fruit.class, FruitsMapper.class }) +public class IterableToSetMappingTest { + + @ProcessorTest + @IssueKey("3376") + public void shouldMapIterableToSet() { + Set fruits = new HashSet<>( Arrays.asList( new Fruit( "mango" ), new Fruit( "apple" ), + new Fruit( "banana" ) ) ); + FruitsMenu menu = new FruitsMenu(fruits); + FruitSalad salad = FruitsMapper.INSTANCE.fruitsMenuToSalad( menu ); + Iterator itr = salad.getFruits().iterator(); + Set fruitTypes = fruits.stream().map( Fruit::getType ).collect( Collectors.toSet() ); + assertThat( fruitTypes.contains( itr.next().getType() ) ); + assertThat( fruitTypes.contains( itr.next().getType() ) ); + assertThat( fruitTypes.contains( itr.next().getType() ) ); + } + + @ProcessorTest + @IssueKey("3376") + public void shouldMapSetToIterable() { + Set fruits = new HashSet<>( Arrays.asList( new Fruit( "mango" ), new Fruit( "apple" ), + new Fruit( "banana" ) ) ); + FruitSalad salad = new FruitSalad(fruits); + FruitsMenu menu = FruitsMapper.INSTANCE.fruitSaladToMenu( salad ); + assertThat( menu.getFruits() ).extracting( Fruit::getType ).contains( "mango", "apple", "banana" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/map/MapMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/map/MapMappingTest.java index ddba51f826..1261525d4f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/map/MapMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/map/MapMappingTest.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.Map; +import org.junitpioneer.jupiter.DefaultTimeZone; import org.mapstruct.ap.test.collection.map.other.ImportedType; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; @@ -26,6 +27,7 @@ */ @WithClasses({ SourceTargetMapper.class, CustomNumberMapper.class, Source.class, Target.class, ImportedType.class }) @IssueKey("44") +@DefaultTimeZone("UTC") public class MapMappingTest { @ProcessorTest @@ -82,6 +84,20 @@ public void shouldCreateMapMethodImplementationWithReturnedTargetParameter() { assertResult( target ); } + @ProcessorTest + @IssueKey("1752") + public void shouldCreateMapMethodImplementationWithReturnedTargetParameterAndNullSource() { + Map target = new HashMap<>(); + target.put( 42L, new GregorianCalendar( 1980, Calendar.JANUARY, 1 ).getTime() ); + target.put( 121L, new GregorianCalendar( 2013, Calendar.JULY, 20 ).getTime() ); + + Map returnedTarget = SourceTargetMapper.INSTANCE + .stringStringMapToLongDateMapUsingTargetParameterAndReturn( null, target ); + + assertThat( target ).isSameAs( returnedTarget ); + assertResult( target ); + } + private void assertResult(Map target) { assertThat( target ).isNotNull(); assertThat( target ).hasSize( 2 ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/BeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/BeanMapper.java index 0fd26e11fc..42a52eb233 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/BeanMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/BeanMapper.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.collection.wildcard; import java.math.BigDecimal; - import javax.xml.bind.JAXBElement; import javax.xml.namespace.QName; diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/CunningPlan.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/CunningPlan.java index 154112f642..b1a901e0f3 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/CunningPlan.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/CunningPlan.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.collection.wildcard; import java.math.BigDecimal; - import javax.xml.bind.JAXBElement; /** diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/ExtendsBoundSourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/ExtendsBoundSourceTargetMapper.java index 059a474490..937cc1f49f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/ExtendsBoundSourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/ExtendsBoundSourceTargetMapper.java @@ -8,7 +8,6 @@ import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; - /** * * @author Sjaak Derksen diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/GoodIdea.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/GoodIdea.java index d919f99e08..9b509e2d10 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/GoodIdea.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/GoodIdea.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.collection.wildcard; import java.math.BigDecimal; - import javax.xml.bind.JAXBElement; /** diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Idea.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Idea.java index fc9b89b571..4e21b223db 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Idea.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Idea.java @@ -11,4 +11,13 @@ */ public class Idea { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Plan.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Plan.java index 29fcfc2f3a..00db02170c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Plan.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/Plan.java @@ -11,4 +11,13 @@ */ public class Plan { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/SourceSuperBoundTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/SourceSuperBoundTargetMapper.java index 181543ba28..b5bce93345 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/SourceSuperBoundTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/SourceSuperBoundTargetMapper.java @@ -8,7 +8,6 @@ import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; - /** * * @author Sjaak Derksen diff --git a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/WildCardTest.java b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/WildCardTest.java index 37ed4606f6..4da6f8648a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/WildCardTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/collection/wildcard/WildCardTest.java @@ -13,6 +13,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxJaxb; import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; @@ -133,6 +134,7 @@ public void shouldFailOnTypeVarTarget() { @ProcessorTest @WithClasses( { BeanMapper.class, GoodIdea.class, CunningPlan.class } ) + @WithJavaxJaxb public void shouldMapBean() { GoodIdea aGoodIdea = new GoodIdea(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/complex/CarMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/complex/CarMapperTest.java index 5eaf44765d..a1881a7613 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/complex/CarMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/complex/CarMapperTest.java @@ -11,6 +11,7 @@ import java.util.GregorianCalendar; import java.util.List; +import org.junitpioneer.jupiter.DefaultTimeZone; import org.mapstruct.ap.test.complex._target.CarDto; import org.mapstruct.ap.test.complex._target.PersonDto; import org.mapstruct.ap.test.complex.other.DateMapper; @@ -31,6 +32,7 @@ Category.class, DateMapper.class }) +@DefaultTimeZone("UTC") public class CarMapperTest { @ProcessorTest diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java index 0fc791fb13..cb2907fd26 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMappingTest.java @@ -50,6 +50,12 @@ public void conditionalMethodInMapper() { assertThat( employee.getName() ).isNull(); } + @IssueKey( "2882" ) + @ProcessorTest + @WithClasses( { ConditionalMethodWithTargetType.class } ) + public void conditionalMethodWithTargetTypeShouldCompile() { + } + @ProcessorTest @WithClasses({ ConditionalMethodAndBeanPresenceCheckMapper.class @@ -202,6 +208,84 @@ public void conditionalMethodForCollection() { .containsExactly( "Test", "Test Vol. 2" ); } + @ProcessorTest + @WithClasses({ + ConditionalMethodForSourceBeanMapper.class + }) + public void conditionalMethodForSourceBean() { + ConditionalMethodForSourceBeanMapper mapper = ConditionalMethodForSourceBeanMapper.INSTANCE; + + ConditionalMethodForSourceBeanMapper.Employee employee = mapper.map( + new ConditionalMethodForSourceBeanMapper.EmployeeDto( + "1", + "Tester" + ) ); + + assertThat( employee ).isNotNull(); + assertThat( employee.getId() ).isEqualTo( "1" ); + assertThat( employee.getName() ).isEqualTo( "Tester" ); + + employee = mapper.map( null ); + + assertThat( employee ).isNull(); + + employee = mapper.map( new ConditionalMethodForSourceBeanMapper.EmployeeDto( null, "Tester" ) ); + + assertThat( employee ).isNull(); + + employee = mapper.map( new ConditionalMethodForSourceBeanMapper.EmployeeDto( "test-123", "Tester" ) ); + + assertThat( employee ).isNotNull(); + assertThat( employee.getId() ).isEqualTo( "test-123" ); + assertThat( employee.getName() ).isEqualTo( "Tester" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodForSourceParameterAndPropertyMapper.class + }) + public void conditionalMethodForSourceParameterAndProperty() { + ConditionalMethodForSourceParameterAndPropertyMapper mapper = + ConditionalMethodForSourceParameterAndPropertyMapper.INSTANCE; + + ConditionalMethodForSourceParameterAndPropertyMapper.Employee employee = mapper.map( + new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( + "1", + "Tester" + ) ); + + assertThat( employee ).isNotNull(); + assertThat( employee.getId() ).isEqualTo( "1" ); + assertThat( employee.getName() ).isEqualTo( "Tester" ); + assertThat( employee.getManager() ).isNull(); + + employee = mapper.map( null ); + + assertThat( employee ).isNull(); + + employee = mapper.map( new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( + "1", + "Tester", + new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( null, "Manager" ) + ) ); + + assertThat( employee ).isNotNull(); + assertThat( employee.getManager() ).isNull(); + + employee = mapper.map( new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( + "1", + "Tester", + new ConditionalMethodForSourceParameterAndPropertyMapper.EmployeeDto( "2", "Manager" ) + ) ); + + assertThat( employee ).isNotNull(); + assertThat( employee.getId() ).isEqualTo( "1" ); + assertThat( employee.getName() ).isEqualTo( "Tester" ); + assertThat( employee.getManager() ).isNotNull(); + assertThat( employee.getManager().getId() ).isEqualTo( "2" ); + assertThat( employee.getManager().getName() ).isEqualTo( "Manager" ); + } + @ProcessorTest @WithClasses({ OptionalLikeConditionalMapper.class @@ -223,4 +307,139 @@ public void optionalLikeConditional() { } + @ProcessorTest + @WithClasses( { + ConditionalMethodWithMappingTargetInUpdateMapper.class + } ) + @IssueKey( "2758" ) + public void conditionalMethodWithMappingTarget() { + ConditionalMethodWithMappingTargetInUpdateMapper mapper = + ConditionalMethodWithMappingTargetInUpdateMapper.INSTANCE; + + BasicEmployee targetEmployee = new BasicEmployee(); + targetEmployee.setName( "CurrentName" ); + mapper.map( new BasicEmployeeDto( "ReplacementName" ), targetEmployee ); + + assertThat( targetEmployee.getName() ).isEqualTo( "CurrentName" ); + } + + @ProcessorTest + @WithClasses({ + ErroneousConditionalWithoutAppliesToMethodMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousConditionalWithoutAppliesToMethodMapper.class, + line = 19, + message = "'appliesTo' has to have at least one value in @Condition" + ) + } + ) + public void emptyConditional() { + } + + @ProcessorTest + @WithClasses({ + ErroneousSourceParameterConditionalWithMappingTargetMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousSourceParameterConditionalWithMappingTargetMapper.class, + line = 21, + message = "Parameter \"@MappingTarget BasicEmployee employee\"" + + " cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." + + " Only source and @Context parameters are allowed for conditions applicable to source parameters." + ) + } + ) + public void sourceParameterConditionalWithMappingTarget() { + } + + @ProcessorTest + @WithClasses({ + ErroneousSourceParameterConditionalWithTargetTypeMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousSourceParameterConditionalWithTargetTypeMapper.class, + line = 21, + message = "Parameter \"@TargetType Class targetClass\"" + + " cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." + + " Only source and @Context parameters are allowed for conditions applicable to source parameters." + ) + } + ) + public void sourceParameterConditionalWithTargetType() { + } + + @ProcessorTest + @WithClasses({ + ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.class, + line = 21, + message = "Parameter \"@TargetPropertyName String targetProperty\"" + + " cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." + + " Only source and @Context parameters are allowed for conditions applicable to source parameters." + ) + } + ) + public void sourceParameterConditionalWithTargetPropertyName() { + } + + @ProcessorTest + @WithClasses({ + ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.class, + line = 21, + message = "Parameter \"@SourcePropertyName String sourceProperty\"" + + " cannot be used with the ConditionStrategy#SOURCE_PARAMETERS." + + " Only source and @Context parameters are allowed for conditions applicable to source parameters." + ) + } + ) + public void sourceParametersConditionalWithSourcePropertyName() { + } + + @ProcessorTest + @WithClasses({ + ErroneousAmbiguousSourceParameterConditionalMethodMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousAmbiguousSourceParameterConditionalMethodMapper.class, + line = 17, + message = "Ambiguous source parameter check methods found for checking BasicEmployeeDto: " + + "boolean hasName(BasicEmployeeDto value), " + + "boolean hasStrategy(BasicEmployeeDto value). " + + "See https://mapstruct.org/faq/#ambiguous for more info." + ) + } + ) + public void ambiguousSourceParameterConditionalMethod() { + + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java new file mode 100644 index 0000000000..dd2fe4ac90 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceBeanMapper.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Mapper; +import org.mapstruct.SourceParameterCondition; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ConditionalMethodForSourceBeanMapper { + + ConditionalMethodForSourceBeanMapper INSTANCE = Mappers.getMapper( ConditionalMethodForSourceBeanMapper.class ); + + Employee map(EmployeeDto employee); + + @SourceParameterCondition + default boolean canMapEmployeeDto(EmployeeDto employee) { + return employee != null && employee.getId() != null; + } + + class EmployeeDto { + + private final String id; + private final String name; + + public EmployeeDto(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + + class Employee { + + private final String id; + private final String name; + + public Employee(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java new file mode 100644 index 0000000000..11a97b7239 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodForSourceParameterAndPropertyMapper.java @@ -0,0 +1,85 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.ConditionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ConditionalMethodForSourceParameterAndPropertyMapper { + + ConditionalMethodForSourceParameterAndPropertyMapper INSTANCE = Mappers.getMapper( + ConditionalMethodForSourceParameterAndPropertyMapper.class ); + + Employee map(EmployeeDto employee); + + @Condition(appliesTo = { + ConditionStrategy.SOURCE_PARAMETERS, + ConditionStrategy.PROPERTIES + }) + default boolean canMapEmployeeDto(EmployeeDto employee) { + return employee != null && employee.getId() != null; + } + + class EmployeeDto { + + private final String id; + private final String name; + private final EmployeeDto manager; + + public EmployeeDto(String id, String name) { + this( id, name, null ); + } + + public EmployeeDto(String id, String name, EmployeeDto manager) { + this.id = id; + this.name = name; + this.manager = manager; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public EmployeeDto getManager() { + return manager; + } + } + + class Employee { + + private final String id; + private final String name; + private final Employee manager; + + public Employee(String id, String name, Employee manager) { + this.id = id; + this.name = name; + this.manager = manager; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public Employee getManager() { + return manager; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithMappingTargetInUpdateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithMappingTargetInUpdateMapper.java new file mode 100644 index 0000000000..12f4a00212 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithMappingTargetInUpdateMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper( nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE ) +public interface ConditionalMethodWithMappingTargetInUpdateMapper { + + ConditionalMethodWithMappingTargetInUpdateMapper INSTANCE = + Mappers.getMapper( ConditionalMethodWithMappingTargetInUpdateMapper.class ); + + void map(BasicEmployeeDto employee, @MappingTarget BasicEmployee targetEmployee); + + @Condition + default boolean isNotBlankAndNotPresent(String value, @MappingTarget BasicEmployee targetEmployee) { + return value != null && !value.trim().isEmpty() && targetEmployee.getName() == null; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithTargetType.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithTargetType.java new file mode 100644 index 0000000000..5ec242d8ff --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ConditionalMethodWithTargetType.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.TargetType; +import org.mapstruct.factory.Mappers; + +/** + * @author Ben Zegveld + */ +@Mapper +public interface ConditionalMethodWithTargetType { + + ConditionalMethodWithTargetType INSTANCE = Mappers.getMapper( ConditionalMethodWithTargetType.class ); + + BasicEmployee map(BasicEmployeeDto employee); + + @Condition + default boolean isNotBlank(String value, @TargetType Class targetType) { + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java new file mode 100644 index 0000000000..39433e2641 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousAmbiguousSourceParameterConditionalMethodMapper.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Mapper; +import org.mapstruct.SourceParameterCondition; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousAmbiguousSourceParameterConditionalMethodMapper { + + BasicEmployee map(BasicEmployeeDto employee); + + @SourceParameterCondition + default boolean hasName(BasicEmployeeDto value) { + return value != null && value.getName() != null; + } + + @SourceParameterCondition + default boolean hasStrategy(BasicEmployeeDto value) { + return value != null && value.getStrategy() != null; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java new file mode 100644 index 0000000000..76f62845d5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousConditionalWithoutAppliesToMethodMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousConditionalWithoutAppliesToMethodMapper { + + BasicEmployee map(BasicEmployeeDto employee); + + @Condition(appliesTo = {}) + default boolean isNotBlank(String value) { + return value != null && !value.trim().isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java new file mode 100644 index 0000000000..b1206798d6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithMappingTargetMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.ConditionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousSourceParameterConditionalWithMappingTargetMapper { + + BasicEmployee map(BasicEmployeeDto employee); + + @Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS) + default boolean isNotBlank(String value, @MappingTarget BasicEmployee employee) { + return value != null && !value.trim().isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java new file mode 100644 index 0000000000..1bfd150afc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithSourcePropertyNameMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.ConditionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.SourcePropertyName; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousSourceParameterConditionalWithSourcePropertyNameMapper { + + BasicEmployee map(BasicEmployeeDto employee); + + @Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS) + default boolean isNotBlank(String value, @SourcePropertyName String sourceProperty) { + return value != null && !value.trim().isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java new file mode 100644 index 0000000000..e9dac1ece5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetPropertyNameMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.ConditionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.TargetPropertyName; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousSourceParameterConditionalWithTargetPropertyNameMapper { + + BasicEmployee map(BasicEmployeeDto employee); + + @Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS) + default boolean isNotBlank(String value, @TargetPropertyName String targetProperty) { + return value != null && !value.trim().isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java new file mode 100644 index 0000000000..9ff55597ae --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/basic/ErroneousSourceParameterConditionalWithTargetTypeMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.basic; + +import org.mapstruct.Condition; +import org.mapstruct.ConditionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.TargetType; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousSourceParameterConditionalWithTargetTypeMapper { + + BasicEmployee map(BasicEmployeeDto employee); + + @Condition(appliesTo = ConditionStrategy.SOURCE_PARAMETERS) + default boolean isNotBlank(String value, @TargetType Class targetClass) { + return value != null && !value.trim().isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java index 60179cdeac..1ad36f7de1 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalExpressionTest.java @@ -74,6 +74,44 @@ public void conditionalSimpleExpression() { assertThat( target.getValue() ).isEqualTo( 0 ); } + @ProcessorTest + @WithClasses({ + ConditionalWithSourceToTargetExpressionMapper.class + }) + @IssueKey("2666") + public void conditionalExpressionForSourceToTarget() { + ConditionalWithSourceToTargetExpressionMapper mapper = ConditionalWithSourceToTargetExpressionMapper.INSTANCE; + + ConditionalWithSourceToTargetExpressionMapper.OrderDTO orderDto = + new ConditionalWithSourceToTargetExpressionMapper.OrderDTO(); + + ConditionalWithSourceToTargetExpressionMapper.Order order = mapper.convertToOrder( orderDto ); + assertThat( order ).isNotNull(); + assertThat( order.getCustomer() ).isNull(); + + orderDto = new ConditionalWithSourceToTargetExpressionMapper.OrderDTO(); + orderDto.setCustomerName( "Tester" ); + order = mapper.convertToOrder( orderDto ); + + assertThat( order ).isNotNull(); + assertThat( order.getCustomer() ).isNotNull(); + assertThat( order.getCustomer().getName() ).isEqualTo( "Tester" ); + assertThat( order.getCustomer().getAddress() ).isNull(); + + orderDto = new ConditionalWithSourceToTargetExpressionMapper.OrderDTO(); + orderDto.setLine1( "Line 1" ); + order = mapper.convertToOrder( orderDto ); + + assertThat( order ).isNotNull(); + assertThat( order.getCustomer() ).isNotNull(); + assertThat( order.getCustomer().getName() ).isNull(); + assertThat( order.getCustomer().getAddress() ).isNotNull(); + assertThat( order.getCustomer().getAddress().getLine1() ).isEqualTo( "Line 1" ); + assertThat( order.getCustomer().getAddress().getLine2() ).isNull(); + + } + + @IssueKey("2794") @ProcessorTest @ExpectedCompilationOutcome( value = CompilationResult.FAILED, @@ -82,6 +120,18 @@ public void conditionalSimpleExpression() { kind = javax.tools.Diagnostic.Kind.ERROR, line = 19, message = "Value for condition expression must be given in the form \"java()\"." + ), + @Diagnostic(type = ErroneousConditionExpressionMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 22, + message = "Constant and condition expression are both defined in @Mapping," + + " either define a constant or a condition expression." + ), + @Diagnostic(type = ErroneousConditionExpressionMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 25, + message = "Expression and condition expression are both defined in @Mapping," + + " either define an expression or a condition expression." ) } ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalWithSourceToTargetExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalWithSourceToTargetExpressionMapper.java new file mode 100644 index 0000000000..c32fe99e03 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ConditionalWithSourceToTargetExpressionMapper.java @@ -0,0 +1,132 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.expression; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(imports = ConditionalWithSourceToTargetExpressionMapper.Util.class) +public interface ConditionalWithSourceToTargetExpressionMapper { + + ConditionalWithSourceToTargetExpressionMapper INSTANCE = + Mappers.getMapper( ConditionalWithSourceToTargetExpressionMapper.class ); + + @Mapping(source = "orderDTO", target = "customer", + conditionExpression = "java(Util.mapCustomerFromOrder( orderDTO ))") + Order convertToOrder(OrderDTO orderDTO); + + @Mapping(source = "customerName", target = "name") + @Mapping(source = "orderDTO", target = "address", + conditionExpression = "java(Util.mapAddressFromOrder( orderDTO ))") + Customer convertToCustomer(OrderDTO orderDTO); + + Address convertToAddress(OrderDTO orderDTO); + + class OrderDTO { + + private String customerName; + private String line1; + private String line2; + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public String getLine1() { + return line1; + } + + public void setLine1(String line1) { + this.line1 = line1; + } + + public String getLine2() { + return line2; + } + + public void setLine2(String line2) { + this.line2 = line2; + } + + } + + class Order { + + private Customer customer; + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + } + + class Customer { + private String name; + private Address address; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + } + + class Address { + + private String line1; + private String line2; + + public String getLine1() { + return line1; + } + + public void setLine1(String line1) { + this.line1 = line1; + } + + public String getLine2() { + return line2; + } + + public void setLine2(String line2) { + this.line2 = line2; + } + } + + interface Util { + + static boolean mapCustomerFromOrder(OrderDTO orderDTO) { + return orderDTO != null && ( orderDTO.getCustomerName() != null || mapAddressFromOrder( orderDTO ) ); + } + + static boolean mapAddressFromOrder(OrderDTO orderDTO) { + return orderDTO != null && ( orderDTO.getLine1() != null || orderDTO.getLine2() != null ); + } + + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java index c74c552531..c8e5db243e 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/expression/ErroneousConditionExpressionMapper.java @@ -18,4 +18,10 @@ public interface ErroneousConditionExpressionMapper { @Mapping(target = "name", conditionExpression = "!employee.getName().isEmpty()") BasicEmployee map(EmployeeDto employee); + + @Mapping(target = "name", conditionExpression = "java(true)", constant = "test") + BasicEmployee mapConstant(EmployeeDto employee); + + @Mapping(target = "name", conditionExpression = "java(true)", expression = "java(\"test\")") + BasicEmployee mapExpression(EmployeeDto employee); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/Address.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/Address.java new file mode 100644 index 0000000000..339af75944 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/Address.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname; + +/** + * @author Nikola Ivačič + */ +public class Address implements DomainModel { + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/AddressDto.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/AddressDto.java new file mode 100644 index 0000000000..c6a63065d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/AddressDto.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname; + +/** + * @author Nikola Ivačič + */ +public class AddressDto implements DomainModel { + private final String street; + + public AddressDto(String street) { + this.street = street; + } + + public String getStreet() { + return street; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/DomainModel.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/DomainModel.java new file mode 100644 index 0000000000..8e5fa8695d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/DomainModel.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname; + +/** + * Target Property Name test entities + * + * @author Nikola Ivačič + */ +public interface DomainModel { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/Employee.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/Employee.java new file mode 100644 index 0000000000..497717a592 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/Employee.java @@ -0,0 +1,99 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname; + +import java.util.List; + +/** + * @author Nikola Ivačič + */ +public class Employee implements DomainModel { + + private String firstName; + private String lastName; + private String title; + private String country; + private boolean active; + private int age; + + private Employee boss; + + private Address primaryAddress; + + private List

      addresses; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Employee getBoss() { + return boss; + } + + public void setBoss(Employee boss) { + this.boss = boss; + } + + public Address getPrimaryAddress() { + return primaryAddress; + } + + public void setPrimaryAddress(Address primaryAddress) { + this.primaryAddress = primaryAddress; + } + + public List
      getAddresses() { + return addresses; + } + + public void setAddresses(List
      addresses) { + this.addresses = addresses; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/EmployeeDto.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/EmployeeDto.java new file mode 100644 index 0000000000..f49f25e4a3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/EmployeeDto.java @@ -0,0 +1,98 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname; + +import java.util.List; + +/** + * @author Nikola Ivačič + */ +public class EmployeeDto implements DomainModel { + + private String firstName; + private String lastName; + private String title; + private String originCountry; + private boolean active; + private int age; + + private EmployeeDto boss; + + private AddressDto primaryAddress; + private List originAddresses; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOriginCountry() { + return originCountry; + } + + public void setOriginCountry(String originCountry) { + this.originCountry = originCountry; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public EmployeeDto getBoss() { + return boss; + } + + public void setBoss(EmployeeDto boss) { + this.boss = boss; + } + + public AddressDto getPrimaryAddress() { + return primaryAddress; + } + + public void setPrimaryAddress(AddressDto primaryAddress) { + this.primaryAddress = primaryAddress; + } + + public List getOriginAddresses() { + return originAddresses; + } + + public void setOriginAddresses(List originAddresses) { + this.originAddresses = originAddresses; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodForCollectionMapperWithSourcePropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodForCollectionMapperWithSourcePropertyName.java new file mode 100644 index 0000000000..944fe5d146 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodForCollectionMapperWithSourcePropertyName.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import java.util.Collection; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@Mapper +public interface ConditionalMethodForCollectionMapperWithSourcePropertyName { + + ConditionalMethodForCollectionMapperWithSourcePropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodForCollectionMapperWithSourcePropertyName.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotEmpty(Collection collection, @SourcePropertyName String propName) { + if ( "originAddresses".equalsIgnoreCase( propName ) ) { + return false; + } + return collection != null && !collection.isEmpty(); + } + + @Condition + default boolean isNotBlank(String value, @SourcePropertyName String propName) { + if ( propName.equalsIgnoreCase( "originCountry" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithAllExceptTarget.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithAllExceptTarget.java new file mode 100644 index 0000000000..ea6a77d94e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithAllExceptTarget.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@Mapper +public interface ConditionalMethodInMapperWithAllExceptTarget { + + ConditionalMethodInMapperWithAllExceptTarget INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithAllExceptTarget.class ); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + Set visitedSources = new LinkedHashSet<>(); + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtils utils); + + @Condition + default boolean isNotBlank(String value, + DomainModel source, + @SourcePropertyName String propName, + @Context PresenceUtils utils) { + utils.visited.add( propName ); + utils.visitedSources.add( source.getClass().getSimpleName() ); + if ( propName.equalsIgnoreCase( "firstName" ) ) { + return true; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithAllOptions.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithAllOptions.java new file mode 100644 index 0000000000..ee3be57648 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithAllOptions.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@Mapper +public interface ConditionalMethodInMapperWithAllOptions { + + ConditionalMethodInMapperWithAllOptions INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithAllOptions.class ); + + class PresenceUtils { + Set visitedSourceNames = new LinkedHashSet<>(); + Set visitedTargetNames = new LinkedHashSet<>(); + Set visitedSources = new LinkedHashSet<>(); + Set visitedTargets = new LinkedHashSet<>(); + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + void map(EmployeeDto employeeDto, + @MappingTarget Employee employee, + @Context PresenceUtils utils); + + @Condition + default boolean isNotBlank(String value, + DomainModel source, + @MappingTarget DomainModel target, + @SourcePropertyName String sourcePropName, + @TargetPropertyName String targetPropName, + @Context PresenceUtils utils) { + utils.visitedSourceNames.add( sourcePropName ); + utils.visitedTargetNames.add( targetPropName ); + utils.visitedSources.add( source.getClass().getSimpleName() ); + utils.visitedTargets.add( target.getClass().getSimpleName() ); + if ( sourcePropName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithSourcePropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithSourcePropertyName.java new file mode 100644 index 0000000000..41ff6c5264 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInMapperWithSourcePropertyName.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@Mapper +public interface ConditionalMethodInMapperWithSourcePropertyName { + + ConditionalMethodInMapperWithSourcePropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithSourcePropertyName.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotBlank(String value, @SourcePropertyName String propName) { + if ( propName.equalsIgnoreCase( "originCountry" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInUsesMapperWithSourcePropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInUsesMapperWithSourcePropertyName.java new file mode 100644 index 0000000000..8ba222512f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodInUsesMapperWithSourcePropertyName.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@Mapper(uses = ConditionalMethodInUsesMapperWithSourcePropertyName.PresenceUtils.class) +public interface ConditionalMethodInUsesMapperWithSourcePropertyName { + + ConditionalMethodInUsesMapperWithSourcePropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodInUsesMapperWithSourcePropertyName.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + class PresenceUtils { + + @Condition + public boolean isNotBlank(String value, @SourcePropertyName String propName) { + if ( propName.equalsIgnoreCase( "originCountry" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodWithSourcePropertyNameInContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodWithSourcePropertyNameInContextMapper.java new file mode 100644 index 0000000000..f69695baa0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ConditionalMethodWithSourcePropertyNameInContextMapper.java @@ -0,0 +1,108 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.TargetType; +import org.mapstruct.ap.test.conditional.propertyname.Address; +import org.mapstruct.ap.test.conditional.propertyname.AddressDto; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@Mapper +public interface ConditionalMethodWithSourcePropertyNameInContextMapper { + + ConditionalMethodWithSourcePropertyNameInContextMapper INSTANCE + = Mappers.getMapper( ConditionalMethodWithSourcePropertyNameInContextMapper.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtils utils); + + Address map(AddressDto addressDto, @Context PresenceUtils utils); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + + @Condition + public boolean isNotBlank(String value, @SourcePropertyName String propName) { + visited.add( propName ); + return value != null && !value.trim().isEmpty(); + } + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtilsAllProps utils); + + Address map(AddressDto addressDto, @Context PresenceUtilsAllProps utils); + + class PresenceUtilsAllProps { + Set visited = new LinkedHashSet<>(); + + @Condition + public boolean collect(@SourcePropertyName String propName) { + visited.add( propName ); + return true; + } + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtilsAllPropsWithSource utils); + + Address map(AddressDto addressDto, @Context PresenceUtilsAllPropsWithSource utils); + + @BeforeMapping + default void before(DomainModel source, @Context PresenceUtilsAllPropsWithSource utils) { + String lastProp = utils.visitedSegments.peekLast(); + if ( lastProp != null && source != null ) { + utils.path.offerLast( lastProp ); + } + } + + @AfterMapping + default void after(@TargetType Class targetClass, @Context PresenceUtilsAllPropsWithSource utils) { + // intermediate method for collection mapping must not change the path + if ( targetClass != List.class ) { + utils.path.pollLast(); + } + } + + class PresenceUtilsAllPropsWithSource { + Deque visitedSegments = new LinkedList<>(); + Deque visited = new LinkedList<>(); + Deque path = new LinkedList<>(); + + @Condition + public boolean collect(@SourcePropertyName String propName) { + visitedSegments.offerLast( propName ); + path.offerLast( propName ); + visited.offerLast( String.join( ".", path ) ); + path.pollLast(); + return true; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ErroneousNonStringSourcePropertyNameParameter.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ErroneousNonStringSourcePropertyNameParameter.java new file mode 100644 index 0000000000..5ed118676d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/ErroneousNonStringSourcePropertyNameParameter.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; + +@Mapper +public interface ErroneousNonStringSourcePropertyNameParameter { + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotBlank(String value, @SourcePropertyName int propName) { + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/SourcePropertyNameTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/SourcePropertyNameTest.java new file mode 100644 index 0000000000..2471ed647d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/sourcepropertyname/SourcePropertyNameTest.java @@ -0,0 +1,312 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.sourcepropertyname; + +import java.util.Collections; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.conditional.propertyname.Address; +import org.mapstruct.ap.test.conditional.propertyname.AddressDto; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + * @author Mohammad Al Zouabi + * @author Oliver Erhart + */ +@IssueKey("3323") +@WithClasses({ + Address.class, + AddressDto.class, + Employee.class, + EmployeeDto.class, + DomainModel.class +}) +public class SourcePropertyNameTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithSourcePropertyName.class + }) + public void conditionalMethodInMapperWithSourcePropertyName() { + ConditionalMethodInMapperWithSourcePropertyName mapper + = ConditionalMethodInMapperWithSourcePropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isEqualTo( "Testirovich" ); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isNull(); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodForCollectionMapperWithSourcePropertyName.class + }) + public void conditionalMethodForCollectionMapperWithSourcePropertyName() { + ConditionalMethodForCollectionMapperWithSourcePropertyName mapper + = ConditionalMethodForCollectionMapperWithSourcePropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isEqualTo( "Testirovich" ); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isNull(); + assertThat( employee.getAddresses() ).isNull(); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInUsesMapperWithSourcePropertyName.class + }) + public void conditionalMethodInUsesMapperWithSourcePropertyName() { + ConditionalMethodInUsesMapperWithSourcePropertyName mapper + = ConditionalMethodInUsesMapperWithSourcePropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isEqualTo( "Testirovich" ); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isNull(); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithAllOptions.class + }) + public void conditionalMethodInMapperWithAllOptions() { + ConditionalMethodInMapperWithAllOptions mapper + = ConditionalMethodInMapperWithAllOptions.INSTANCE; + + ConditionalMethodInMapperWithAllOptions.PresenceUtils utils = + new ConditionalMethodInMapperWithAllOptions.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = new Employee(); + mapper.map( employeeDto, employee, utils ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visitedSourceNames ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "originCountry" ); + assertThat( utils.visitedTargetNames ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country" ); + assertThat( utils.visitedSources ).containsExactly( "EmployeeDto" ); + assertThat( utils.visitedTargets ).containsExactly( "Employee" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithAllExceptTarget.class + }) + public void conditionalMethodInMapperWithAllExceptTarget() { + ConditionalMethodInMapperWithAllExceptTarget mapper + = ConditionalMethodInMapperWithAllExceptTarget.INSTANCE; + + ConditionalMethodInMapperWithAllExceptTarget.PresenceUtils utils = + new ConditionalMethodInMapperWithAllExceptTarget.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto, utils ); + assertThat( employee.getLastName() ).isEqualTo( "Testirovich" ); + assertThat( employee.getFirstName() ).isEqualTo( " " ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "originCountry", "street" ); + assertThat( utils.visitedSources ).containsExactlyInAnyOrder( "EmployeeDto", "AddressDto" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodWithSourcePropertyNameInContextMapper.class + }) + public void conditionalMethodWithSourcePropertyNameInUsesContextMapper() { + ConditionalMethodWithSourcePropertyNameInContextMapper mapper + = ConditionalMethodWithSourcePropertyNameInContextMapper.INSTANCE; + + ConditionalMethodWithSourcePropertyNameInContextMapper.PresenceUtils utils = + new ConditionalMethodWithSourcePropertyNameInContextMapper.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setLastName( " " ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto, utils ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "originCountry", "street" ); + + ConditionalMethodWithSourcePropertyNameInContextMapper.PresenceUtilsAllProps allPropsUtils = + new ConditionalMethodWithSourcePropertyNameInContextMapper.PresenceUtilsAllProps(); + + employeeDto = new EmployeeDto(); + employeeDto.setLastName( "Tester" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + employee = mapper.map( employeeDto, allPropsUtils ); + assertThat( employee.getLastName() ).isEqualTo( "Tester" ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( allPropsUtils.visited ) + .containsExactlyInAnyOrder( + "firstName", + "lastName", + "title", + "originCountry", + "active", + "age", + "boss", + "primaryAddress", + "originAddresses", + "street" + ); + + ConditionalMethodWithSourcePropertyNameInContextMapper.PresenceUtilsAllPropsWithSource allPropsUtilsWithSource = + new ConditionalMethodWithSourcePropertyNameInContextMapper.PresenceUtilsAllPropsWithSource(); + + EmployeeDto bossEmployeeDto = new EmployeeDto(); + bossEmployeeDto.setLastName( "Boss Tester" ); + bossEmployeeDto.setOriginCountry( "US" ); + bossEmployeeDto.setOriginAddresses( Collections.singletonList( new AddressDto( + "Testing St. 10" ) ) ); + + employeeDto = new EmployeeDto(); + employeeDto.setLastName( "Tester" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setBoss( bossEmployeeDto ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + employee = mapper.map( employeeDto, allPropsUtilsWithSource ); + assertThat( employee.getLastName() ).isEqualTo( "Tester" ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ).isNotEmpty(); + assertThat( employee.getAddresses().get( 0 ).getStreet() ).isEqualTo( "Testing St. 6" ); + assertThat( employee.getBoss() ).isNotNull(); + assertThat( employee.getBoss().getCountry() ).isEqualTo( "US" ); + assertThat( employee.getBoss().getLastName() ).isEqualTo( "Boss Tester" ); + assertThat( employee.getBoss().getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 10" ); + assertThat( allPropsUtilsWithSource.visited ) + .containsExactly( + "originCountry", + "originAddresses", + "originAddresses.street", + "firstName", + "lastName", + "title", + "active", + "age", + "boss", + "boss.originCountry", + "boss.originAddresses", + "boss.originAddresses.street", + "boss.firstName", + "boss.lastName", + "boss.title", + "boss.active", + "boss.age", + "boss.boss", + "boss.primaryAddress", + "primaryAddress" + ); + } + + @ProcessorTest + @WithClasses({ + ErroneousNonStringSourcePropertyNameParameter.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousNonStringSourcePropertyNameParameter.class, + line = 23, + message = "@SourcePropertyName can only by applied to a String parameter." + ) + } + ) + public void nonStringSourcePropertyNameParameter() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java new file mode 100644 index 0000000000..a51a318eb6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodForCollectionMapperWithTargetPropertyName.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import java.util.Collection; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodForCollectionMapperWithTargetPropertyName { + + ConditionalMethodForCollectionMapperWithTargetPropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodForCollectionMapperWithTargetPropertyName.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotEmpty(Collection collection, @TargetPropertyName String propName) { + if ( "addresses".equalsIgnoreCase( propName ) ) { + return false; + } + return collection != null && !collection.isEmpty(); + } + + @Condition + default boolean isNotBlank(String value, @TargetPropertyName String propName) { + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java new file mode 100644 index 0000000000..1d0dd3fa51 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithAllExceptTarget.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodInMapperWithAllExceptTarget { + + ConditionalMethodInMapperWithAllExceptTarget INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithAllExceptTarget.class ); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + Set visitedSources = new LinkedHashSet<>(); + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtils utils); + + @Condition + default boolean isNotBlank(String value, + DomainModel source, + @TargetPropertyName String propName, + @Context PresenceUtils utils) { + utils.visited.add( propName ); + utils.visitedSources.add( source.getClass().getSimpleName() ); + if ( propName.equalsIgnoreCase( "firstName" ) ) { + return true; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java new file mode 100644 index 0000000000..d3ccb9ba2e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithAllOptions.java @@ -0,0 +1,62 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.SourcePropertyName; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodInMapperWithAllOptions { + + ConditionalMethodInMapperWithAllOptions INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithAllOptions.class ); + + class PresenceUtils { + Set visitedSourceNames = new LinkedHashSet<>(); + Set visitedTargetNames = new LinkedHashSet<>(); + Set visitedSources = new LinkedHashSet<>(); + Set visitedTargets = new LinkedHashSet<>(); + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + void map(EmployeeDto employeeDto, + @MappingTarget Employee employee, + @Context PresenceUtils utils); + + @Condition + default boolean isNotBlank(String value, + DomainModel source, + @MappingTarget DomainModel target, + @SourcePropertyName String sourcePropName, + @TargetPropertyName String targetPropName, + @Context PresenceUtils utils) { + utils.visitedSourceNames.add( sourcePropName ); + utils.visitedTargetNames.add( targetPropName ); + utils.visitedSources.add( source.getClass().getSimpleName() ); + utils.visitedTargets.add( target.getClass().getSimpleName() ); + if ( targetPropName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java new file mode 100644 index 0000000000..d5dc378f32 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInMapperWithTargetPropertyName.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodInMapperWithTargetPropertyName { + + ConditionalMethodInMapperWithTargetPropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodInMapperWithTargetPropertyName.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotBlank(String value, @TargetPropertyName String propName) { + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java new file mode 100644 index 0000000000..381b99d4f4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodInUsesMapperWithTargetPropertyName.java @@ -0,0 +1,41 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@Mapper(uses = ConditionalMethodInUsesMapperWithTargetPropertyName.PresenceUtils.class) +public interface ConditionalMethodInUsesMapperWithTargetPropertyName { + + ConditionalMethodInUsesMapperWithTargetPropertyName INSTANCE + = Mappers.getMapper( ConditionalMethodInUsesMapperWithTargetPropertyName.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + class PresenceUtils { + + @Condition + public boolean isNotBlank(String value, @TargetPropertyName String propName) { + if ( propName.equalsIgnoreCase( "lastName" ) ) { + return false; + } + return value != null && !value.trim().isEmpty(); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java new file mode 100644 index 0000000000..85704b0601 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ConditionalMethodWithTargetPropertyNameInContextMapper.java @@ -0,0 +1,106 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Condition; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.TargetType; +import org.mapstruct.ap.test.conditional.propertyname.Address; +import org.mapstruct.ap.test.conditional.propertyname.AddressDto; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Nikola Ivačič + */ +@Mapper +public interface ConditionalMethodWithTargetPropertyNameInContextMapper { + + ConditionalMethodWithTargetPropertyNameInContextMapper INSTANCE + = Mappers.getMapper( ConditionalMethodWithTargetPropertyNameInContextMapper.class ); + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtils utils); + + Address map(AddressDto addressDto, @Context PresenceUtils utils); + + class PresenceUtils { + Set visited = new LinkedHashSet<>(); + + @Condition + public boolean isNotBlank(String value, @TargetPropertyName String propName) { + visited.add( propName ); + return value != null && !value.trim().isEmpty(); + } + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtilsAllProps utils); + + Address map(AddressDto addressDto, @Context PresenceUtilsAllProps utils); + + class PresenceUtilsAllProps { + Set visited = new LinkedHashSet<>(); + + @Condition + public boolean collect(@TargetPropertyName String propName) { + visited.add( propName ); + return true; + } + } + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee, @Context PresenceUtilsAllPropsWithSource utils); + + Address map(AddressDto addressDto, @Context PresenceUtilsAllPropsWithSource utils); + + @BeforeMapping + default void before(DomainModel source, @Context PresenceUtilsAllPropsWithSource utils) { + String lastProp = utils.visitedSegments.peekLast(); + if ( lastProp != null && source != null ) { + utils.path.offerLast( lastProp ); + } + } + + @AfterMapping + default void after(@TargetType Class targetClass, @Context PresenceUtilsAllPropsWithSource utils) { + // intermediate method for collection mapping must not change the path + if ( targetClass != List.class ) { + utils.path.pollLast(); + } + } + + class PresenceUtilsAllPropsWithSource { + Deque visitedSegments = new LinkedList<>(); + Deque visited = new LinkedList<>(); + Deque path = new LinkedList<>(); + + @Condition + public boolean collect(@TargetPropertyName String propName) { + visitedSegments.offerLast( propName ); + path.offerLast( propName ); + visited.offerLast( String.join( ".", path ) ); + path.pollLast(); + return true; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ErroneousNonStringTargetPropertyNameParameter.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ErroneousNonStringTargetPropertyNameParameter.java new file mode 100644 index 0000000000..d56277abf4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/ErroneousNonStringTargetPropertyNameParameter.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.TargetPropertyName; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; + +@Mapper +public interface ErroneousNonStringTargetPropertyNameParameter { + + @Mapping(target = "country", source = "originCountry") + @Mapping(target = "addresses", source = "originAddresses") + Employee map(EmployeeDto employee); + + @Condition + default boolean isNotBlank(String value, @TargetPropertyName int propName) { + return value != null && !value.trim().isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/TargetPropertyNameTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/TargetPropertyNameTest.java new file mode 100644 index 0000000000..bb90c0b069 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/propertyname/targetpropertyname/TargetPropertyNameTest.java @@ -0,0 +1,311 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.propertyname.targetpropertyname; + +import java.util.Collections; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.conditional.propertyname.Address; +import org.mapstruct.ap.test.conditional.propertyname.AddressDto; +import org.mapstruct.ap.test.conditional.propertyname.DomainModel; +import org.mapstruct.ap.test.conditional.propertyname.Employee; +import org.mapstruct.ap.test.conditional.propertyname.EmployeeDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + * @author Nikola Ivačič + */ +@IssueKey("2051") +@WithClasses({ + Address.class, + AddressDto.class, + Employee.class, + EmployeeDto.class, + DomainModel.class +}) +public class TargetPropertyNameTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithTargetPropertyName.class + }) + public void conditionalMethodInMapperWithTargetPropertyName() { + ConditionalMethodInMapperWithTargetPropertyName mapper + = ConditionalMethodInMapperWithTargetPropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodForCollectionMapperWithTargetPropertyName.class + }) + public void conditionalMethodForCollectionMapperWithTargetPropertyName() { + ConditionalMethodForCollectionMapperWithTargetPropertyName mapper + = ConditionalMethodForCollectionMapperWithTargetPropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ).isNull(); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInUsesMapperWithTargetPropertyName.class + }) + public void conditionalMethodInUsesMapperWithTargetPropertyName() { + ConditionalMethodInUsesMapperWithTargetPropertyName mapper + = ConditionalMethodInUsesMapperWithTargetPropertyName.INSTANCE; + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithAllOptions.class + }) + public void conditionalMethodInMapperWithAllOptions() { + ConditionalMethodInMapperWithAllOptions mapper + = ConditionalMethodInMapperWithAllOptions.INSTANCE; + + ConditionalMethodInMapperWithAllOptions.PresenceUtils utils = + new ConditionalMethodInMapperWithAllOptions.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = new Employee(); + mapper.map( employeeDto, employee, utils ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getFirstName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visitedSourceNames ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "originCountry" ); + assertThat( utils.visitedTargetNames ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country" ); + assertThat( utils.visitedSources ).containsExactly( "EmployeeDto" ); + assertThat( utils.visitedTargets ).containsExactly( "Employee" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodInMapperWithAllExceptTarget.class + }) + public void conditionalMethodInMapperWithAllExceptTarget() { + ConditionalMethodInMapperWithAllExceptTarget mapper + = ConditionalMethodInMapperWithAllExceptTarget.INSTANCE; + + ConditionalMethodInMapperWithAllExceptTarget.PresenceUtils utils = + new ConditionalMethodInMapperWithAllExceptTarget.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setFirstName( " " ); + employeeDto.setLastName( "Testirovich" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto, utils ); + assertThat( employee.getLastName() ).isEqualTo( "Testirovich" ); + assertThat( employee.getFirstName() ).isEqualTo( " " ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country", "street" ); + assertThat( utils.visitedSources ).containsExactlyInAnyOrder( "EmployeeDto", "AddressDto" ); + } + + @ProcessorTest + @WithClasses({ + ConditionalMethodWithTargetPropertyNameInContextMapper.class + }) + public void conditionalMethodWithTargetPropertyNameInUsesContextMapper() { + ConditionalMethodWithTargetPropertyNameInContextMapper mapper + = ConditionalMethodWithTargetPropertyNameInContextMapper.INSTANCE; + + ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtils utils = + new ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtils(); + + EmployeeDto employeeDto = new EmployeeDto(); + employeeDto.setLastName( " " ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + Employee employee = mapper.map( employeeDto, utils ); + assertThat( employee.getLastName() ).isNull(); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( utils.visited ) + .containsExactlyInAnyOrder( "firstName", "lastName", "title", "country", "street" ); + + ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllProps allPropsUtils = + new ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllProps(); + + employeeDto = new EmployeeDto(); + employeeDto.setLastName( "Tester" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + employee = mapper.map( employeeDto, allPropsUtils ); + assertThat( employee.getLastName() ).isEqualTo( "Tester" ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 6" ); + assertThat( allPropsUtils.visited ) + .containsExactlyInAnyOrder( + "firstName", + "lastName", + "title", + "country", + "active", + "age", + "boss", + "primaryAddress", + "addresses", + "street" + ); + + ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllPropsWithSource allPropsUtilsWithSource = + new ConditionalMethodWithTargetPropertyNameInContextMapper.PresenceUtilsAllPropsWithSource(); + + EmployeeDto bossEmployeeDto = new EmployeeDto(); + bossEmployeeDto.setLastName( "Boss Tester" ); + bossEmployeeDto.setOriginCountry( "US" ); + bossEmployeeDto.setOriginAddresses( Collections.singletonList( new AddressDto( + "Testing St. 10" ) ) ); + + employeeDto = new EmployeeDto(); + employeeDto.setLastName( "Tester" ); + employeeDto.setOriginCountry( "US" ); + employeeDto.setBoss( bossEmployeeDto ); + employeeDto.setOriginAddresses( + Collections.singletonList( new AddressDto( "Testing St. 6" ) ) + ); + + employee = mapper.map( employeeDto, allPropsUtilsWithSource ); + assertThat( employee.getLastName() ).isEqualTo( "Tester" ); + assertThat( employee.getCountry() ).isEqualTo( "US" ); + assertThat( employee.getAddresses() ).isNotEmpty(); + assertThat( employee.getAddresses().get( 0 ).getStreet() ).isEqualTo( "Testing St. 6" ); + assertThat( employee.getBoss() ).isNotNull(); + assertThat( employee.getBoss().getCountry() ).isEqualTo( "US" ); + assertThat( employee.getBoss().getLastName() ).isEqualTo( "Boss Tester" ); + assertThat( employee.getBoss().getAddresses() ) + .extracting( Address::getStreet ) + .containsExactly( "Testing St. 10" ); + assertThat( allPropsUtilsWithSource.visited ) + .containsExactly( + "country", + "addresses", + "addresses.street", + "firstName", + "lastName", + "title", + "active", + "age", + "boss", + "boss.country", + "boss.addresses", + "boss.addresses.street", + "boss.firstName", + "boss.lastName", + "boss.title", + "boss.active", + "boss.age", + "boss.boss", + "boss.primaryAddress", + "primaryAddress" + ); + } + + @IssueKey("2863") + @ProcessorTest + @WithClasses({ + ErroneousNonStringTargetPropertyNameParameter.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousNonStringTargetPropertyNameParameter.class, + line = 23, + message = "@TargetPropertyName can only by applied to a String parameter." + ) + } + ) + public void nonStringTargetPropertyNameParameter() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithSourceToTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithSourceToTargetMapper.java new file mode 100644 index 0000000000..6577a6fd95 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalMethodWithSourceToTargetMapper.java @@ -0,0 +1,132 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conditional.qualifier; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.SourceParameterCondition; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ConditionalMethodWithSourceToTargetMapper { + + ConditionalMethodWithSourceToTargetMapper INSTANCE = + Mappers.getMapper( ConditionalMethodWithSourceToTargetMapper.class ); + + @Mapping(source = "orderDTO", target = "customer", conditionQualifiedByName = "mapCustomerFromOrder") + Order convertToOrder(OrderDTO orderDTO); + + @Mapping(source = "customerName", target = "name") + @Mapping(source = "orderDTO", target = "address", conditionQualifiedByName = "mapAddressFromOrder") + Customer convertToCustomer(OrderDTO orderDTO); + + Address convertToAddress(OrderDTO orderDTO); + + @SourceParameterCondition + @Named("mapCustomerFromOrder") + default boolean mapCustomerFromOrder(OrderDTO orderDTO) { + return orderDTO != null && ( orderDTO.getCustomerName() != null || mapAddressFromOrder( orderDTO ) ); + } + + @SourceParameterCondition + @Named("mapAddressFromOrder") + default boolean mapAddressFromOrder(OrderDTO orderDTO) { + return orderDTO != null && ( orderDTO.getLine1() != null || orderDTO.getLine2() != null ); + } + + class OrderDTO { + + private String customerName; + private String line1; + private String line2; + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public String getLine1() { + return line1; + } + + public void setLine1(String line1) { + this.line1 = line1; + } + + public String getLine2() { + return line2; + } + + public void setLine2(String line2) { + this.line2 = line2; + } + + } + + class Order { + + private Customer customer; + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + } + + class Customer { + private String name; + private Address address; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + } + + class Address { + + private String line1; + private String line2; + + public String getLine1() { + return line1; + } + + public void setLine1(String line1) { + this.line1 = line1; + } + + public String getLine2() { + return line2; + } + + public void setLine2(String line2) { + this.line2 = line2; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java index 5d13787e65..69c9a048cd 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/qualifier/ConditionalQualifierTest.java @@ -86,4 +86,41 @@ public void conditionalClassQualifiers() { assertThat( employee.getNin() ).isNull(); assertThat( employee.getSsid() ).isNull(); } + + @ProcessorTest + @WithClasses({ + ConditionalMethodWithSourceToTargetMapper.class + }) + @IssueKey("2666") + public void conditionalQualifiersForSourceToTarget() { + ConditionalMethodWithSourceToTargetMapper mapper = ConditionalMethodWithSourceToTargetMapper.INSTANCE; + + ConditionalMethodWithSourceToTargetMapper.OrderDTO orderDto = + new ConditionalMethodWithSourceToTargetMapper.OrderDTO(); + + ConditionalMethodWithSourceToTargetMapper.Order order = mapper.convertToOrder( orderDto ); + assertThat( order ).isNotNull(); + assertThat( order.getCustomer() ).isNull(); + + orderDto = new ConditionalMethodWithSourceToTargetMapper.OrderDTO(); + orderDto.setCustomerName( "Tester" ); + order = mapper.convertToOrder( orderDto ); + + assertThat( order ).isNotNull(); + assertThat( order.getCustomer() ).isNotNull(); + assertThat( order.getCustomer().getName() ).isEqualTo( "Tester" ); + assertThat( order.getCustomer().getAddress() ).isNull(); + + orderDto = new ConditionalMethodWithSourceToTargetMapper.OrderDTO(); + orderDto.setLine1( "Line 1" ); + order = mapper.convertToOrder( orderDto ); + + assertThat( order ).isNotNull(); + assertThat( order.getCustomer() ).isNotNull(); + assertThat( order.getCustomer().getName() ).isNull(); + assertThat( order.getCustomer().getAddress() ).isNotNull(); + assertThat( order.getCustomer().getAddress().getLine1() ).isEqualTo( "Line 1" ); + assertThat( order.getCustomer().getAddress().getLine2() ).isNull(); + + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java index 705421724e..930db7f8a4 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java @@ -6,8 +6,8 @@ package org.mapstruct.ap.test.constructor.erroneous; import org.mapstruct.Mapper; -import org.mapstruct.ap.test.constructor.PersonDto; import org.mapstruct.ap.test.constructor.ConstructorProperties; +import org.mapstruct.ap.test.constructor.PersonDto; /** * @author Filip Hrisafov diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java index 2799e10847..5a8483f23b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java @@ -29,8 +29,10 @@ public class ErroneousConstructorTest { kind = javax.tools.Diagnostic.Kind.ERROR, line = 17, message = "Ambiguous constructors found for creating org.mapstruct.ap.test.constructor.erroneous" + - ".ErroneousAmbiguousConstructorsMapper.PersonWithMultipleConstructors. Either declare parameterless " + - "constructor or annotate the default constructor with an annotation named @Default." + ".ErroneousAmbiguousConstructorsMapper.PersonWithMultipleConstructors: " + + "PersonWithMultipleConstructors(java.lang.String), " + + "PersonWithMultipleConstructors(java.lang.String, int). Either declare parameterless constructor " + + "or annotate the default constructor with an annotation named @Default." ) }) public void shouldUseMultipleConstructors() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/visibility/ConstructorVisibilityTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/visibility/ConstructorVisibilityTest.java index b7a254296d..5eebaf5d05 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/constructor/visibility/ConstructorVisibilityTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/visibility/ConstructorVisibilityTest.java @@ -10,6 +10,9 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +47,15 @@ public void shouldUseSinglePublicConstructorAlways() { @WithClasses({ SimpleWithPublicParameterlessConstructorMapper.class }) + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = SimpleWithPublicParameterlessConstructorMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 21, + message = "No target property found for target " + + "\"SimpleWithPublicParameterlessConstructorMapper.Person\"."), + }) + public void shouldUsePublicParameterConstructorIfPresent() { PersonDto source = new PersonDto(); source.setName( "Bob" ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/context/ContextParameterErroneousTest.java b/processor/src/test/java/org/mapstruct/ap/test/context/ContextParameterErroneousTest.java index 77fefe756e..46bb08cb70 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/context/ContextParameterErroneousTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/context/ContextParameterErroneousTest.java @@ -19,7 +19,7 @@ /** * Tests the erroneous usage of the {@link Context} annotation in the following situations: *
        - *
      • using the the same context parameter type twice in the same method + *
      • using the same context parameter type twice in the same method *
      * * @author Andreas Gudian diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerConversionTest.java new file mode 100644 index 0000000000..67d99dfbc6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerConversionTest.java @@ -0,0 +1,77 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests conversions between {@link Enum} and Integer. + * + * @author Jose Carlos Campanero Ortiz + */ +@IssueKey("2963") +@WithClasses({ + EnumToIntegerSource.class, + EnumToIntegerTarget.class, + EnumToIntegerMapper.class, + EnumToIntegerEnum.class +}) +public class EnumToIntegerConversionTest { + + @ProcessorTest + public void shouldApplyEnumToIntegerConversion() { + EnumToIntegerSource source = new EnumToIntegerSource(); + + for ( EnumToIntegerEnum value : EnumToIntegerEnum.values() ) { + source.setEnumValue( value ); + + EnumToIntegerTarget target = EnumToIntegerMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getEnumValue() ).isEqualTo( source.getEnumValue().ordinal() ); + } + } + + @ProcessorTest + public void shouldApplyReverseEnumToIntegerConversion() { + EnumToIntegerTarget target = new EnumToIntegerTarget(); + + int numberOfValues = EnumToIntegerEnum.values().length; + for ( int value = 0; value < numberOfValues; value++ ) { + target.setEnumValue( value ); + + EnumToIntegerSource source = EnumToIntegerMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getEnumValue() ).isEqualTo( EnumToIntegerEnum.values()[ target.getEnumValue() ] ); + } + } + + @ProcessorTest + public void shouldHandleOutOfBoundsEnumOrdinal() { + EnumToIntegerTarget target = new EnumToIntegerTarget(); + target.setInvalidEnumValue( EnumToIntegerEnum.values().length + 1 ); + + assertThatThrownBy( () -> EnumToIntegerMapper.INSTANCE.targetToSource( target ) ) + .isInstanceOf( ArrayIndexOutOfBoundsException.class ); + } + + @ProcessorTest + public void shouldHandleNullIntegerValue() { + EnumToIntegerTarget target = new EnumToIntegerTarget(); + + EnumToIntegerSource source = EnumToIntegerMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getEnumValue() ).isNull(); + assertThat( source.getInvalidEnumValue() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerEnum.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerEnum.java new file mode 100644 index 0000000000..4aad7bef57 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerEnum.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +public enum EnumToIntegerEnum { + ARBITRARY_VALUE_ZERO, + ARBITRARY_VALUE_ONE, + ARBITRARY_VALUE_TWO, + ARBITRARY_VALUE_THREE +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerMapper.java new file mode 100644 index 0000000000..946ee7c463 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface EnumToIntegerMapper { + EnumToIntegerMapper INSTANCE = Mappers.getMapper( EnumToIntegerMapper.class ); + + EnumToIntegerTarget sourceToTarget(EnumToIntegerSource source); + + EnumToIntegerSource targetToSource(EnumToIntegerTarget target); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerSource.java new file mode 100644 index 0000000000..4e60311d9b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerSource.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +public class EnumToIntegerSource { + private EnumToIntegerEnum enumValue; + + private EnumToIntegerEnum invalidEnumValue; + + public EnumToIntegerEnum getEnumValue() { + return enumValue; + } + + public void setEnumValue(final EnumToIntegerEnum enumValue) { + this.enumValue = enumValue; + } + + public EnumToIntegerEnum getInvalidEnumValue() { + return invalidEnumValue; + } + + public void setInvalidEnumValue(EnumToIntegerEnum invalidEnumValue) { + this.invalidEnumValue = invalidEnumValue; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerTarget.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerTarget.java new file mode 100644 index 0000000000..a8819c27ed --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/EnumToIntegerTarget.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +public class EnumToIntegerTarget { + private Integer enumValue; + + private Integer invalidEnumValue; + + public Integer getEnumValue() { + return enumValue; + } + + public void setEnumValue(final Integer enumValue) { + this.enumValue = enumValue; + } + + public Integer getInvalidEnumValue() { + return invalidEnumValue; + } + + public void setInvalidEnumValue(Integer invalidEnumValue) { + this.invalidEnumValue = invalidEnumValue; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerConversionTest.java new file mode 100644 index 0000000000..e1e557a897 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerConversionTest.java @@ -0,0 +1,73 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +import java.util.Optional; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@WithClasses({ + OptionalEnumToIntegerSource.class, + EnumToIntegerTarget.class, + OptionalEnumToIntegerMapper.class, + EnumToIntegerEnum.class +}) +public class OptionalEnumToIntegerConversionTest { + + @ProcessorTest + public void shouldApplyEnumToIntegerConversion() { + OptionalEnumToIntegerSource source = new OptionalEnumToIntegerSource(); + + for ( EnumToIntegerEnum value : EnumToIntegerEnum.values() ) { + source.setEnumValue( Optional.of( value ) ); + + EnumToIntegerTarget target = OptionalEnumToIntegerMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getEnumValue() ).isEqualTo( value.ordinal() ); + } + } + + @ProcessorTest + public void shouldApplyReverseEnumToIntegerConversion() { + EnumToIntegerTarget target = new EnumToIntegerTarget(); + + EnumToIntegerEnum[] enumValues = EnumToIntegerEnum.values(); + int numberOfValues = enumValues.length; + for ( int value = 0; value < numberOfValues; value++ ) { + target.setEnumValue( value ); + + OptionalEnumToIntegerSource source = OptionalEnumToIntegerMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getEnumValue() ).contains( enumValues[target.getEnumValue()] ); + } + } + + @ProcessorTest + public void shouldHandleOutOfBoundsEnumOrdinal() { + EnumToIntegerTarget target = new EnumToIntegerTarget(); + target.setInvalidEnumValue( EnumToIntegerEnum.values().length + 1 ); + + assertThatThrownBy( () -> OptionalEnumToIntegerMapper.INSTANCE.targetToSource( target ) ) + .isInstanceOf( ArrayIndexOutOfBoundsException.class ); + } + + @ProcessorTest + public void shouldHandleNullIntegerValue() { + EnumToIntegerTarget target = new EnumToIntegerTarget(); + + OptionalEnumToIntegerSource source = OptionalEnumToIntegerMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getEnumValue() ).isEmpty(); + assertThat( source.getInvalidEnumValue() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerMapper.java new file mode 100644 index 0000000000..d07d9b879c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OptionalEnumToIntegerMapper { + OptionalEnumToIntegerMapper INSTANCE = Mappers.getMapper( OptionalEnumToIntegerMapper.class ); + + EnumToIntegerTarget sourceToTarget(OptionalEnumToIntegerSource source); + + OptionalEnumToIntegerSource targetToSource(EnumToIntegerTarget target); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerSource.java new file mode 100644 index 0000000000..700c3198bc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/_enum/OptionalEnumToIntegerSource.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion._enum; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class OptionalEnumToIntegerSource { + private Optional enumValue = Optional.empty(); + + private Optional invalidEnumValue = Optional.empty(); + + public Optional getEnumValue() { + return enumValue; + } + + public void setEnumValue(Optional enumValue) { + this.enumValue = enumValue; + } + + public Optional getInvalidEnumValue() { + return invalidEnumValue; + } + + public void setInvalidEnumValue(Optional invalidEnumValue) { + this.invalidEnumValue = invalidEnumValue; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigDecimalOptionalMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigDecimalOptionalMapper.java new file mode 100644 index 0000000000..986de6a0ed --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigDecimalOptionalMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.bignumbers; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface BigDecimalOptionalMapper { + + BigDecimalOptionalMapper INSTANCE = Mappers.getMapper( BigDecimalOptionalMapper.class ); + + BigDecimalTarget sourceToTarget(BigDecimalOptionalSource source); + + BigDecimalOptionalSource targetToSource(BigDecimalTarget target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigDecimalOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigDecimalOptionalSource.java new file mode 100644 index 0000000000..bf4e53e5d9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigDecimalOptionalSource.java @@ -0,0 +1,139 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.bignumbers; + +import java.math.BigDecimal; +import java.util.Optional; + +public class BigDecimalOptionalSource { + + private Optional b; + private Optional bb; + private Optional s; + private Optional ss; + private Optional i; + private Optional ii; + private Optional l; + private Optional ll; + private Optional f; + private Optional ff; + private Optional d; + private Optional dd; + private Optional string; + private Optional bigInteger; + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } + + public Optional getString() { + return string; + } + + public void setString(Optional string) { + this.string = string; + } + + public Optional getBigInteger() { + return bigInteger; + } + + public void setBigInteger(Optional bigInteger) { + this.bigInteger = bigInteger; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigIntegerOptionalMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigIntegerOptionalMapper.java new file mode 100644 index 0000000000..d39b48c7e5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigIntegerOptionalMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.bignumbers; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface BigIntegerOptionalMapper { + + BigIntegerOptionalMapper INSTANCE = Mappers.getMapper( BigIntegerOptionalMapper.class ); + + BigIntegerTarget sourceToTarget(BigIntegerOptionalSource source); + + BigIntegerOptionalSource targetToSource(BigIntegerTarget target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigIntegerOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigIntegerOptionalSource.java new file mode 100644 index 0000000000..f4bb0f03c8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigIntegerOptionalSource.java @@ -0,0 +1,131 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.bignumbers; + +import java.math.BigInteger; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class BigIntegerOptionalSource { + + private Optional b; + private Optional bb; + private Optional s; + private Optional ss; + private Optional i; + private Optional ii; + private Optional l; + private Optional ll; + private Optional f; + private Optional ff; + private Optional d; + private Optional dd; + private Optional string; + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } + + public Optional getString() { + return string; + } + + public void setString(Optional string) { + this.string = string; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigNumbersConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigNumbersConversionTest.java index 69f87f3980..2ddddec02e 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigNumbersConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/bignumbers/BigNumbersConversionTest.java @@ -7,6 +7,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Optional; import org.junit.jupiter.api.extension.RegisterExtension; import org.mapstruct.ap.testutil.IssueKey; @@ -100,6 +101,79 @@ public void shouldApplyReverseBigIntegerConversions() { assertThat( source.getString() ).isEqualTo( new BigInteger( "13" ) ); } + @ProcessorTest + @WithClasses({ BigIntegerOptionalSource.class, BigIntegerTarget.class, BigIntegerOptionalMapper.class }) + public void shouldApplyOptionalBigIntegerConversions() { + BigIntegerOptionalSource source = new BigIntegerOptionalSource(); + source.setB( Optional.of( new BigInteger( "1" ) ) ); + source.setBb( Optional.of( new BigInteger( "2" ) ) ); + source.setS( Optional.of( new BigInteger( "3" ) ) ); + source.setSs( Optional.of( new BigInteger( "4" ) ) ); + source.setI( Optional.of( new BigInteger( "5" ) ) ); + source.setIi( Optional.of( new BigInteger( "6" ) ) ); + source.setL( Optional.of( new BigInteger( "7" ) ) ); + source.setLl( Optional.of( new BigInteger( "8" ) ) ); + source.setF( Optional.of( new BigInteger( "9" ) ) ); + source.setFf( Optional.of( new BigInteger( "10" ) ) ); + source.setD( Optional.of( new BigInteger( "11" ) ) ); + source.setDd( Optional.of( new BigInteger( "12" ) ) ); + source.setString( Optional.of( new BigInteger( "13" ) ) ); + + BigIntegerTarget target = BigIntegerOptionalMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( (byte) 2 ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( (short) 4 ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( 6 ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( 8 ); + assertThat( target.getF() ).isEqualTo( 9.0f ); + assertThat( target.getFf() ).isEqualTo( 10.0f ); + assertThat( target.getD() ).isEqualTo( 11.0d ); + assertThat( target.getDd() ).isEqualTo( 12.0d ); + assertThat( target.getString() ).isEqualTo( "13" ); + } + + @ProcessorTest + @IssueKey("21") + @WithClasses({ BigIntegerOptionalSource.class, BigIntegerTarget.class, BigIntegerOptionalMapper.class }) + public void shouldApplyReverseOptionalBigIntegerConversions() { + BigIntegerTarget target = new BigIntegerTarget(); + target.setB( (byte) 1 ); + target.setBb( (byte) 2 ); + target.setS( (short) 3 ); + target.setSs( (short) 4 ); + target.setI( 5 ); + target.setIi( 6 ); + target.setL( 7 ); + target.setLl( 8L ); + target.setF( 9.0f ); + target.setFf( 10.0f ); + target.setD( 11.0d ); + target.setDd( 12.0d ); + target.setString( "13" ); + + BigIntegerOptionalSource source = BigIntegerOptionalMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getB() ).contains( new BigInteger( "1" ) ); + assertThat( source.getBb() ).contains( new BigInteger( "2" ) ); + assertThat( source.getS() ).contains( new BigInteger( "3" ) ); + assertThat( source.getSs() ).contains( new BigInteger( "4" ) ); + assertThat( source.getI() ).contains( new BigInteger( "5" ) ); + assertThat( source.getIi() ).contains( new BigInteger( "6" ) ); + assertThat( source.getL() ).contains( new BigInteger( "7" ) ); + assertThat( source.getLl() ).contains( new BigInteger( "8" ) ); + assertThat( source.getF() ).contains( new BigInteger( "9" ) ); + assertThat( source.getFf() ).contains( new BigInteger( "10" ) ); + assertThat( source.getD() ).contains( new BigInteger( "11" ) ); + assertThat( source.getDd() ).contains( new BigInteger( "12" ) ); + assertThat( source.getString() ).contains( new BigInteger( "13" ) ); + } + @ProcessorTest @IssueKey("21") @WithClasses({ BigDecimalSource.class, BigDecimalTarget.class, BigDecimalMapper.class }) @@ -178,6 +252,82 @@ public void shouldApplyReverseBigDecimalConversions() { assertThat( source.getBigInteger() ).isEqualTo( new BigDecimal( "14" ) ); } + @ProcessorTest + @WithClasses({ BigDecimalOptionalSource.class, BigDecimalTarget.class, BigDecimalOptionalMapper.class }) + public void shouldApplyOptionalBigDecimalConversions() { + BigDecimalOptionalSource source = new BigDecimalOptionalSource(); + source.setB( Optional.of( new BigDecimal( "1.45" ) ) ); + source.setBb( Optional.of( new BigDecimal( "2.45" ) ) ); + source.setS( Optional.of( new BigDecimal( "3.45" ) ) ); + source.setSs( Optional.of( new BigDecimal( "4.45" ) ) ); + source.setI( Optional.of( new BigDecimal( "5.45" ) ) ); + source.setIi( Optional.of( new BigDecimal( "6.45" ) ) ); + source.setL( Optional.of( new BigDecimal( "7.45" ) ) ); + source.setLl( Optional.of( new BigDecimal( "8.45" ) ) ); + source.setF( Optional.of( new BigDecimal( "9.45" ) ) ); + source.setFf( Optional.of( new BigDecimal( "10.45" ) ) ); + source.setD( Optional.of( new BigDecimal( "11.45" ) ) ); + source.setDd( Optional.of( new BigDecimal( "12.45" ) ) ); + source.setString( Optional.of( new BigDecimal( "13.45" ) ) ); + source.setBigInteger( Optional.of( new BigDecimal( "14.45" ) ) ); + + BigDecimalTarget target = BigDecimalOptionalMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( (byte) 2 ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( (short) 4 ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( 6 ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( 8 ); + assertThat( target.getF() ).isEqualTo( 9.45f ); + assertThat( target.getFf() ).isEqualTo( 10.45f ); + assertThat( target.getD() ).isEqualTo( 11.45d ); + assertThat( target.getDd() ).isEqualTo( 12.45d ); + assertThat( target.getString() ).isEqualTo( "13.45" ); + assertThat( target.getBigInteger() ).isEqualTo( new BigInteger( "14" ) ); + } + + @ProcessorTest + @WithClasses({ BigDecimalOptionalSource.class, BigDecimalTarget.class, BigDecimalOptionalMapper.class }) + public void shouldApplyReverseOptionalBigDecimalConversions() { + BigDecimalTarget target = new BigDecimalTarget(); + target.setB( (byte) 1 ); + target.setBb( (byte) 2 ); + target.setS( (short) 3 ); + target.setSs( (short) 4 ); + target.setI( 5 ); + target.setIi( 6 ); + target.setL( 7 ); + target.setLl( 8L ); + target.setF( 9.0f ); + target.setFf( 10.0f ); + target.setD( 11.0d ); + target.setDd( 12.0d ); + target.setString( "13.45" ); + target.setBigInteger( new BigInteger( "14" ) ); + + BigDecimalOptionalSource source = BigDecimalOptionalMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getB() ).contains( new BigDecimal( "1" ) ); + assertThat( source.getBb() ).contains( new BigDecimal( "2" ) ); + assertThat( source.getS() ).contains( new BigDecimal( "3" ) ); + assertThat( source.getSs() ).contains( new BigDecimal( "4" ) ); + assertThat( source.getI() ).contains( new BigDecimal( "5" ) ); + assertThat( source.getIi() ).contains( new BigDecimal( "6" ) ); + assertThat( source.getL() ).contains( new BigDecimal( "7" ) ); + assertThat( source.getLl() ).contains( new BigDecimal( "8" ) ); + assertThat( source.getF() ).contains( new BigDecimal( "9.0" ) ); + assertThat( source.getFf() ).contains( new BigDecimal( "10.0" ) ); + assertThat( source.getD() ).contains( new BigDecimal( "11.0" ) ); + assertThat( source.getDd() ).contains( new BigDecimal( "12.0" ) ); + assertThat( source.getString() ).contains( new BigDecimal( "13.45" ) ); + assertThat( source.getBigInteger() ).contains( new BigDecimal( "14" ) ); + } + @ProcessorTest @IssueKey("1009") @WithClasses({ BigIntegerSource.class, BigIntegerTarget.class, BigIntegerMapper.class }) diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/date/DateConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/date/DateConversionTest.java index 83464f81a0..346f34a054 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/date/DateConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/date/DateConversionTest.java @@ -13,10 +13,8 @@ import java.util.GregorianCalendar; import java.util.List; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.EnabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junitpioneer.jupiter.DefaultLocale; +import org.junitpioneer.jupiter.ReadsDefaultTimeZone; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; @@ -35,12 +33,11 @@ }) @IssueKey("43") @DefaultLocale("de") +@ReadsDefaultTimeZone public class DateConversionTest { @ProcessorTest - @EnabledOnJre( JRE.JAVA_8 ) - // See https://bugs.openjdk.java.net/browse/JDK-8211262, there is a difference in the default formats on Java 9+ - public void shouldApplyDateFormatForConversions() { + public void shouldApplyDateFormatForConversionsJdk11() { Source source = new Source(); source.setDate( new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime() ); source.setAnotherDate( new GregorianCalendar( 2013, Calendar.FEBRUARY, 14 ).getTime() ); @@ -49,31 +46,27 @@ public void shouldApplyDateFormatForConversions() { assertThat( target ).isNotNull(); assertThat( target.getDate() ).isEqualTo( "06.07.2013" ); - assertThat( target.getAnotherDate() ).isEqualTo( "14.02.13 00:00" ); + assertThat( target.getAnotherDate() ).isEqualTo( "14.02.13, 00:00" ); } @ProcessorTest - @EnabledForJreRange(min = JRE.JAVA_11) - // See https://bugs.openjdk.java.net/browse/JDK-8211262, there is a difference in the default formats on Java 9+ - public void shouldApplyDateFormatForConversionsJdk11() { + public void shouldApplyDateFormatForConversionsJdk11WithCustomLocale() { Source source = new Source(); source.setDate( new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime() ); source.setAnotherDate( new GregorianCalendar( 2013, Calendar.FEBRUARY, 14 ).getTime() ); - Target target = SourceTargetMapper.INSTANCE.sourceToTarget( source ); + Target target = SourceTargetMapper.INSTANCE.sourceToTargetWithCustomLocale( source ); assertThat( target ).isNotNull(); - assertThat( target.getDate() ).isEqualTo( "06.07.2013" ); + assertThat( target.getDate() ).isEqualTo( "juillet 06, 2013" ); assertThat( target.getAnotherDate() ).isEqualTo( "14.02.13, 00:00" ); } @ProcessorTest - @EnabledOnJre(JRE.JAVA_8) - // See https://bugs.openjdk.java.net/browse/JDK-8211262, there is a difference in the default formats on Java 9+ - public void shouldApplyDateFormatForConversionInReverseMapping() { + public void shouldApplyDateFormatForConversionInReverseMappingJdk11() { Target target = new Target(); target.setDate( "06.07.2013" ); - target.setAnotherDate( "14.02.13 8:30" ); + target.setAnotherDate( "14.02.13, 8:30" ); Source source = SourceTargetMapper.INSTANCE.targetToSource( target ); @@ -85,14 +78,12 @@ public void shouldApplyDateFormatForConversionInReverseMapping() { } @ProcessorTest - @EnabledForJreRange(min = JRE.JAVA_11) - // See https://bugs.openjdk.java.net/browse/JDK-8211262, there is a difference in the default formats on Java 9+ - public void shouldApplyDateFormatForConversionInReverseMappingJdk11() { + public void shouldApplyDateFormatForConversionInReverseMappingJdk11WithCustomLocale() { Target target = new Target(); - target.setDate( "06.07.2013" ); + target.setDate( "juillet 06, 2013" ); target.setAnotherDate( "14.02.13, 8:30" ); - Source source = SourceTargetMapper.INSTANCE.targetToSource( target ); + Source source = SourceTargetMapper.INSTANCE.targetToSourceWithCustomLocale( target ); assertThat( source ).isNotNull(); assertThat( source.getDate() ).isEqualTo( new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime() ); @@ -115,6 +106,20 @@ public void shouldApplyStringConversionForIterableMethod() { assertThat( stringDates ).containsExactly( "06.07.2013", "14.02.2013", "11.04.2013" ); } + @ProcessorTest + public void shouldApplyStringConversionForIterableMethodWithCustomLocale() { + List dates = Arrays.asList( + new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime(), + new GregorianCalendar( 2013, Calendar.FEBRUARY, 14 ).getTime(), + new GregorianCalendar( 2013, Calendar.APRIL, 11 ).getTime() + ); + + List stringDates = SourceTargetMapper.INSTANCE.stringListToDateListWithCustomLocale( dates ); + + assertThat( stringDates ).isNotNull(); + assertThat( stringDates ).containsExactly( "juillet 06, 2013", "février 14, 2013", "avril 11, 2013" ); + } + @ProcessorTest public void shouldApplyStringConversionForArrayMethod() { List dates = Arrays.asList( @@ -129,6 +134,20 @@ public void shouldApplyStringConversionForArrayMethod() { assertThat( stringDates ).isEqualTo( new String[]{ "06.07.2013", "14.02.2013", "11.04.2013" } ); } + @ProcessorTest + public void shouldApplyStringConversionForArrayMethodWithCustomLocale() { + List dates = Arrays.asList( + new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime(), + new GregorianCalendar( 2013, Calendar.FEBRUARY, 14 ).getTime(), + new GregorianCalendar( 2013, Calendar.APRIL, 11 ).getTime() + ); + + String[] stringDates = SourceTargetMapper.INSTANCE.stringListToDateArrayWithCustomLocale( dates ); + + assertThat( stringDates ).isNotNull(); + assertThat( stringDates ).isEqualTo( new String[]{ "juillet 06, 2013", "février 14, 2013", "avril 11, 2013" } ); + } + @ProcessorTest public void shouldApplyStringConversionForReverseIterableMethod() { List stringDates = Arrays.asList( "06.07.2013", "14.02.2013", "11.04.2013" ); @@ -143,6 +162,20 @@ public void shouldApplyStringConversionForReverseIterableMethod() { ); } + @ProcessorTest + public void shouldApplyStringConversionForReverseIterableMethodWithCustomLocale() { + List stringDates = Arrays.asList( "juillet 06, 2013", "février 14, 2013", "avril 11, 2013" ); + + List dates = SourceTargetMapper.INSTANCE.dateListToStringListWithCustomLocale( stringDates ); + + assertThat( dates ).isNotNull(); + assertThat( dates ).containsExactly( + new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime(), + new GregorianCalendar( 2013, Calendar.FEBRUARY, 14 ).getTime(), + new GregorianCalendar( 2013, Calendar.APRIL, 11 ).getTime() + ); + } + @ProcessorTest public void shouldApplyStringConversionForReverseArrayMethod() { String[] stringDates = new String[]{ "06.07.2013", "14.02.2013", "11.04.2013" }; @@ -157,6 +190,20 @@ public void shouldApplyStringConversionForReverseArrayMethod() { ); } + @ProcessorTest + public void shouldApplyStringConversionForReverseArrayMethodWithCustomLocale() { + String[] stringDates = new String[]{ "juillet 06, 2013", "février 14, 2013", "avril 11, 2013" }; + + List dates = SourceTargetMapper.INSTANCE.stringArrayToDateListWithCustomLocale( stringDates ); + + assertThat( dates ).isNotNull(); + assertThat( dates ).containsExactly( + new GregorianCalendar( 2013, Calendar.JULY, 6 ).getTime(), + new GregorianCalendar( 2013, Calendar.FEBRUARY, 14 ).getTime(), + new GregorianCalendar( 2013, Calendar.APRIL, 11 ).getTime() + ); + } + @ProcessorTest public void shouldApplyStringConversionForReverseArrayArrayMethod() { Date[] dates = new Date[]{ diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/date/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/date/SourceTargetMapper.java index 2ef6a7bd96..1971a8c066 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/date/SourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/date/SourceTargetMapper.java @@ -22,21 +22,39 @@ public interface SourceTargetMapper { @Mapping(target = "date", dateFormat = "dd.MM.yyyy") Target sourceToTarget(Source source); - @InheritInverseConfiguration + @Mapping(target = "date", dateFormat = "MMMM dd, yyyy", locale = "fr") + Target sourceToTargetWithCustomLocale(Source source); + + @InheritInverseConfiguration(name = "sourceToTarget") Source targetToSource(Target target); + @InheritInverseConfiguration(name = "sourceToTargetWithCustomLocale") + Source targetToSourceWithCustomLocale(Target target); + @IterableMapping(dateFormat = "dd.MM.yyyy") List stringListToDateList(List dates); + @IterableMapping(dateFormat = "MMMM dd, yyyy", locale = "fr") + List stringListToDateListWithCustomLocale(List dates); + @IterableMapping(dateFormat = "dd.MM.yyyy") String[] stringListToDateArray(List dates); - @InheritInverseConfiguration + @IterableMapping(dateFormat = "MMMM dd, yyyy", locale = "fr") + String[] stringListToDateArrayWithCustomLocale(List dates); + + @InheritInverseConfiguration(name = "stringListToDateList") List dateListToStringList(List strings); - @InheritInverseConfiguration + @InheritInverseConfiguration(name = "stringListToDateListWithCustomLocale") + List dateListToStringListWithCustomLocale(List strings); + + @InheritInverseConfiguration(name = "stringListToDateArray") List stringArrayToDateList(String[] dates); + @InheritInverseConfiguration(name = "stringListToDateArrayWithCustomLocale") + List stringArrayToDateListWithCustomLocale(String[] dates); + @IterableMapping(dateFormat = "dd.MM.yyyy") String[] dateArrayToStringArray(Date[] dates); diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/erroneous/InvalidDateFormatTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/erroneous/InvalidDateFormatTest.java index 5eb67bee05..81bf3eb568 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/erroneous/InvalidDateFormatTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/erroneous/InvalidDateFormatTest.java @@ -8,6 +8,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJoda; import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; @@ -19,6 +20,7 @@ Source.class, Target.class }) +@WithJoda @IssueKey("725") public class InvalidDateFormatTest { diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8OptionalTimeConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8OptionalTimeConversionTest.java new file mode 100644 index 0000000000..1a9bb50b67 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8OptionalTimeConversionTest.java @@ -0,0 +1,466 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; +import java.util.Optional; +import java.util.TimeZone; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junitpioneer.jupiter.DefaultTimeZone; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for conversions to/from Java 8 date and time types wrapped in {@link Optional}. + */ +@WithClasses({ OptionalSource.class, Target.class, OptionalSourceTargetMapper.class }) +public class Java8OptionalTimeConversionTest { + + @RegisterExtension + GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + void generatedCode() { + generatedSource.addComparisonToFixtureFor( OptionalSourceTargetMapper.class ); + } + + @ProcessorTest + public void testDateTimeToString() { + OptionalSource src = new OptionalSource(); + src.setZonedDateTime( Optional.of( ZonedDateTime.of( + LocalDateTime.of( 2014, 1, 1, 0, 0 ), + ZoneId.of( "UTC" ) + ) ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDateTimeMapped( src ); + assertThat( target ).isNotNull(); + assertThat( target.getZonedDateTime() ).isEqualTo( "01.01.2014 00:00 UTC" ); + } + + @ProcessorTest + public void testLocalDateTimeToString() { + OptionalSource src = new OptionalSource(); + src.setLocalDateTime( Optional.of( LocalDateTime.of( 2014, 1, 1, 0, 0 ) ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetLocalDateTimeMapped( src ); + assertThat( target ).isNotNull(); + assertThat( target.getLocalDateTime() ).isEqualTo( "01.01.2014 00:00" ); + } + + @ProcessorTest + public void testLocalDateToString() { + OptionalSource src = new OptionalSource(); + src.setLocalDate( Optional.of( LocalDate.of( 2014, 1, 1 ) ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetLocalDateMapped( src ); + assertThat( target ).isNotNull(); + assertThat( target.getLocalDate() ).isEqualTo( "01.01.2014" ); + } + + @ProcessorTest + public void testLocalTimeToString() { + OptionalSource src = new OptionalSource(); + src.setLocalTime( Optional.ofNullable( LocalTime.of( 0, 0 ) ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetLocalTimeMapped( src ); + assertThat( target ).isNotNull(); + assertThat( target.getLocalTime() ).isEqualTo( "00:00" ); + } + + @ProcessorTest + public void testSourceToTargetMappingForStrings() { + OptionalSource src = new OptionalSource(); + src.setLocalTime( Optional.ofNullable( LocalTime.of( 0, 0 ) ) ); + src.setLocalDate( Optional.of( LocalDate.of( 2014, 1, 1 ) ) ); + src.setLocalDateTime( Optional.of( LocalDateTime.of( 2014, 1, 1, 0, 0 ) ) ); + src.setZonedDateTime( Optional.of( ZonedDateTime.of( + LocalDateTime.of( 2014, 1, 1, 0, 0 ), + ZoneId.of( "UTC" ) + ) ) ); + + // with given format + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( src ); + + assertThat( target ).isNotNull(); + assertThat( target.getZonedDateTime() ).isEqualTo( "01.01.2014 00:00 UTC" ); + assertThat( target.getLocalDateTime() ).isEqualTo( "01.01.2014 00:00" ); + assertThat( target.getLocalDate() ).isEqualTo( "01.01.2014" ); + assertThat( target.getLocalTime() ).isEqualTo( "00:00" ); + + // and now with default mappings + target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( src ); + assertThat( target ).isNotNull(); + assertThat( target.getZonedDateTime() ).isEqualTo( "01.01.2014 00:00 UTC" ); + assertThat( target.getLocalDateTime() ).isEqualTo( "01.01.2014 00:00" ); + assertThat( target.getLocalDate() ).isEqualTo( "01.01.2014" ); + assertThat( target.getLocalTime() ).isEqualTo( "00:00" ); + } + + @ProcessorTest + public void testStringToDateTime() { + String dateTimeAsString = "01.01.2014 00:00 UTC"; + Target target = new Target(); + target.setZonedDateTime( dateTimeAsString ); + ZonedDateTime sourceDateTime = + ZonedDateTime.of( LocalDateTime.of( 2014, 1, 1, 0, 0 ), ZoneId.of( "UTC" ) ); + + OptionalSource src = OptionalSourceTargetMapper.INSTANCE.targetToSourceDateTimeMapped( target ); + assertThat( src ).isNotNull(); + assertThat( src.getZonedDateTime() ).contains( sourceDateTime ); + } + + @ProcessorTest + public void testStringToLocalDateTime() { + String dateTimeAsString = "01.01.2014 00:00"; + Target target = new Target(); + target.setLocalDateTime( dateTimeAsString ); + LocalDateTime sourceDateTime = + LocalDateTime.of( 2014, 1, 1, 0, 0, 0 ); + + OptionalSource src = OptionalSourceTargetMapper.INSTANCE.targetToSourceLocalDateTimeMapped( target ); + assertThat( src ).isNotNull(); + assertThat( src.getLocalDateTime() ).contains( sourceDateTime ); + } + + @ProcessorTest + public void testStringToLocalDate() { + String dateTimeAsString = "01.01.2014"; + Target target = new Target(); + target.setLocalDate( dateTimeAsString ); + LocalDate sourceDate = + LocalDate.of( 2014, 1, 1 ); + + OptionalSource src = OptionalSourceTargetMapper.INSTANCE.targetToSourceLocalDateMapped( target ); + assertThat( src ).isNotNull(); + assertThat( src.getLocalDate() ).contains( sourceDate ); + } + + @ProcessorTest + public void testStringToLocalTime() { + String dateTimeAsString = "00:00"; + Target target = new Target(); + target.setLocalTime( dateTimeAsString ); + LocalTime sourceTime = + LocalTime.of( 0, 0 ); + + OptionalSource src = OptionalSourceTargetMapper.INSTANCE.targetToSourceLocalTimeMapped( target ); + assertThat( src ).isNotNull(); + assertThat( src.getLocalTime() ).contains( sourceTime ); + } + + @ProcessorTest + public void testTargetToSourceNullMapping() { + Target target = new Target(); + OptionalSource src = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( src ).isNotNull(); + assertThat( src.getZonedDateTime() ).isEmpty(); + assertThat( src.getLocalDate() ).isEmpty(); + assertThat( src.getLocalDateTime() ).isEmpty(); + assertThat( src.getLocalTime() ).isEmpty(); + } + + @ProcessorTest + public void testTargetToSourceMappingForStrings() { + Target target = new Target(); + + target.setZonedDateTime( "01.01.2014 00:00 UTC" ); + target.setLocalDateTime( "01.01.2014 00:00" ); + target.setLocalDate( "01.01.2014" ); + target.setLocalTime( "00:00" ); + + OptionalSource src = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( src.getZonedDateTime() ).contains( + ZonedDateTime.of( + LocalDateTime.of( + 2014, + 1, + 1, + 0, + 0 + ), ZoneId.of( "UTC" ) + ) ); + assertThat( src.getLocalDateTime() ).contains( LocalDateTime.of( 2014, 1, 1, 0, 0 ) ); + assertThat( src.getLocalDate() ).contains( LocalDate.of( 2014, 1, 1 ) ); + assertThat( src.getLocalTime() ).contains( LocalTime.of( 0, 0 ) ); + } + + @ProcessorTest + public void testCalendarMapping() { + OptionalSource source = new OptionalSource(); + ZonedDateTime dateTime = ZonedDateTime.of( LocalDateTime.of( 2014, 1, 1, 0, 0 ), ZoneId.of( "UTC" ) ); + source.setForCalendarConversion( Optional.of( dateTime ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target.getForCalendarConversion() ).isNotNull(); + assertThat( target.getForCalendarConversion().getTimeZone() ).isEqualTo( + TimeZone.getTimeZone( + "UTC" ) ); + assertThat( target.getForCalendarConversion().get( Calendar.YEAR ) ).isEqualTo( dateTime.getYear() ); + assertThat( target.getForCalendarConversion().get( Calendar.MONTH ) ).isEqualTo( + dateTime.getMonthValue() - 1 ); + assertThat( target.getForCalendarConversion().get( Calendar.DATE ) ).isEqualTo( dateTime.getDayOfMonth() ); + assertThat( target.getForCalendarConversion().get( Calendar.MINUTE ) ).isEqualTo( dateTime.getMinute() ); + assertThat( target.getForCalendarConversion().get( Calendar.HOUR ) ).isEqualTo( dateTime.getHour() ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source.getForCalendarConversion() ).contains( dateTime ); + } + + @ProcessorTest + @DefaultTimeZone("UTC") + public void testZonedDateTimeToDateMapping() { + OptionalSource source = new OptionalSource(); + ZonedDateTime dateTime = ZonedDateTime.of( LocalDateTime.of( 2014, 1, 1, 0, 0 ), ZoneId.of( "UTC" ) ); + source.setForDateConversionWithZonedDateTime( Optional.of( dateTime ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + + assertThat( target.getForDateConversionWithZonedDateTime() ).isNotNull(); + + Calendar instance = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + instance.setTimeInMillis( target.getForDateConversionWithZonedDateTime().getTime() ); + + assertThat( instance.get( Calendar.YEAR ) ).isEqualTo( dateTime.getYear() ); + assertThat( instance.get( Calendar.MONTH ) ).isEqualTo( dateTime.getMonthValue() - 1 ); + assertThat( instance.get( Calendar.DATE ) ).isEqualTo( dateTime.getDayOfMonth() ); + assertThat( instance.get( Calendar.MINUTE ) ).isEqualTo( dateTime.getMinute() ); + assertThat( instance.get( Calendar.HOUR ) ).isEqualTo( dateTime.getHour() ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source.getForDateConversionWithZonedDateTime() ).contains( dateTime ); + } + + @ProcessorTest + public void testInstantToDateMapping() { + Instant instant = Instant.ofEpochMilli( 1539366615000L ); + + OptionalSource source = new OptionalSource(); + source.setForDateConversionWithInstant( Optional.ofNullable( instant ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + Date date = target.getForDateConversionWithInstant(); + assertThat( date ).isNotNull(); + assertThat( date.getTime() ).isEqualTo( 1539366615000L ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForDateConversionWithInstant() ).contains( instant ); + } + + @ProcessorTest + public void testLocalDateTimeToLocalDateMapping() { + LocalDate localDate = LocalDate.of( 2014, 1, 1 ); + + OptionalSource source = new OptionalSource(); + source.setForLocalDateTimeConversionWithLocalDate( Optional.of( localDate ) ); + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + LocalDateTime localDateTime = target.getForLocalDateTimeConversionWithLocalDate(); + assertThat( localDateTime ).isNotNull(); + assertThat( localDateTime ).isEqualTo( LocalDateTime.of( 2014, 1, 1, 0, 0 ) ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForLocalDateTimeConversionWithLocalDate() ).contains( localDate ); + } + + @ProcessorTest + @DefaultTimeZone("Australia/Melbourne") + public void testLocalDateTimeToDateMapping() { + + OptionalSource source = new OptionalSource(); + LocalDateTime dateTime = LocalDateTime.of( 2014, 1, 1, 0, 0 ); + source.setForDateConversionWithLocalDateTime( Optional.of( dateTime ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + + assertThat( target.getForDateConversionWithLocalDateTime() ).isNotNull(); + + Calendar instance = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + instance.setTimeInMillis( target.getForDateConversionWithLocalDateTime().getTime() ); + + assertThat( instance.get( Calendar.YEAR ) ).isEqualTo( dateTime.getYear() ); + assertThat( instance.get( Calendar.MONTH ) ).isEqualTo( dateTime.getMonthValue() - 1 ); + assertThat( instance.get( Calendar.DATE ) ).isEqualTo( dateTime.getDayOfMonth() ); + assertThat( instance.get( Calendar.MINUTE ) ).isEqualTo( dateTime.getMinute() ); + assertThat( instance.get( Calendar.HOUR ) ).isEqualTo( dateTime.getHour() ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source.getForDateConversionWithLocalDateTime() ).contains( dateTime ); + } + + @ProcessorTest + @DefaultTimeZone("Australia/Melbourne") + public void testLocalDateToDateMapping() { + + OptionalSource source = new OptionalSource(); + LocalDate localDate = LocalDate.of( 2016, 3, 1 ); + source.setForDateConversionWithLocalDate( Optional.of( localDate ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + + assertThat( target.getForDateConversionWithLocalDate() ).isNotNull(); + + Calendar instance = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + instance.setTimeInMillis( target.getForDateConversionWithLocalDate().getTime() ); + + assertThat( instance.get( Calendar.YEAR ) ).isEqualTo( localDate.getYear() ); + assertThat( instance.get( Calendar.MONTH ) ).isEqualTo( localDate.getMonthValue() - 1 ); + assertThat( instance.get( Calendar.DATE ) ).isEqualTo( localDate.getDayOfMonth() ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source.getForDateConversionWithLocalDate() ).contains( localDate ); + } + + @ProcessorTest + @DefaultTimeZone("Australia/Melbourne") + public void testLocalDateToSqlDateMapping() { + + OptionalSource source = new OptionalSource(); + LocalDate localDate = LocalDate.of( 2016, 3, 1 ); + source.setForSqlDateConversionWithLocalDate( Optional.of( localDate ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + + assertThat( target.getForSqlDateConversionWithLocalDate() ).isNotNull(); + + Calendar instance = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + instance.setTimeInMillis( target.getForSqlDateConversionWithLocalDate().getTime() ); + + assertThat( instance.get( Calendar.YEAR ) ).isEqualTo( localDate.getYear() ); + assertThat( instance.get( Calendar.MONTH ) ).isEqualTo( localDate.getMonthValue() - 1 ); + assertThat( instance.get( Calendar.DATE ) ).isEqualTo( localDate.getDayOfMonth() ); + + source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source.getForSqlDateConversionWithLocalDate() ).contains( localDate ); + } + + @ProcessorTest + public void testInstantToStringMapping() { + OptionalSource source = new OptionalSource(); + source.setForInstantConversionWithString( Optional.ofNullable( Instant.ofEpochSecond( 42L ) ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + String periodString = target.getForInstantConversionWithString(); + assertThat( periodString ).isEqualTo( "1970-01-01T00:00:42Z" ); + } + + @ProcessorTest + public void testInstantToStringNullMapping() { + OptionalSource source = new OptionalSource(); + source.setForInstantConversionWithString( Optional.empty() ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + String periodString = target.getForInstantConversionWithString(); + assertThat( periodString ).isNull(); + } + + @ProcessorTest + public void testStringToInstantMapping() { + Target target = new Target(); + target.setForInstantConversionWithString( "1970-01-01T00:00:00.000Z" ); + + OptionalSource source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForInstantConversionWithString() ).contains( Instant.EPOCH ); + } + + @ProcessorTest + public void testStringToInstantNullMapping() { + Target target = new Target(); + target.setForInstantConversionWithString( null ); + + OptionalSource source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForInstantConversionWithString() ).isEmpty(); + } + + @ProcessorTest + public void testPeriodToStringMapping() { + OptionalSource source = new OptionalSource(); + source.setForPeriodConversionWithString( Optional.ofNullable( Period.ofDays( 42 ) ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + String periodString = target.getForPeriodConversionWithString(); + assertThat( periodString ).isEqualTo( "P42D" ); + } + + @ProcessorTest + public void testPeriodToStringNullMapping() { + OptionalSource source = new OptionalSource(); + source.setForPeriodConversionWithString( Optional.empty() ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + String periodString = target.getForPeriodConversionWithString(); + assertThat( periodString ).isNull(); + } + + @ProcessorTest + public void testStringToPeriodMapping() { + Target target = new Target(); + target.setForPeriodConversionWithString( "P1Y2M3D" ); + + OptionalSource source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForPeriodConversionWithString() ).contains( Period.of( 1, 2, 3 ) ); + } + + @ProcessorTest + public void testStringToPeriodNullMapping() { + Target target = new Target(); + target.setForPeriodConversionWithString( null ); + + OptionalSource source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForPeriodConversionWithString() ).isEmpty(); + } + + @ProcessorTest + public void testDurationToStringMapping() { + OptionalSource source = new OptionalSource(); + source.setForDurationConversionWithString( Optional.ofNullable( Duration.ofMinutes( 42L ) ) ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + String durationString = target.getForDurationConversionWithString(); + assertThat( durationString ).isEqualTo( "PT42M" ); + } + + @ProcessorTest + public void testDurationToStringNullMapping() { + OptionalSource source = new OptionalSource(); + source.setForDurationConversionWithString( Optional.empty() ); + + Target target = OptionalSourceTargetMapper.INSTANCE.sourceToTarget( source ); + String durationString = target.getForDurationConversionWithString(); + assertThat( durationString ).isNull(); + } + + @ProcessorTest + public void testStringToDurationMapping() { + Target target = new Target(); + target.setForDurationConversionWithString( "PT20.345S" ); + + OptionalSource source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForDurationConversionWithString() ).contains( Duration.ofSeconds( 20L, 345000000L ) ); + } + + @ProcessorTest + public void testStringToDurationNullMapping() { + Target target = new Target(); + target.setForDurationConversionWithString( null ); + + OptionalSource source = OptionalSourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForDurationConversionWithString() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8TimeConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8TimeConversionTest.java index aa30865425..4eab59fbe3 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8TimeConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Java8TimeConversionTest.java @@ -17,8 +17,8 @@ import java.util.Date; import java.util.TimeZone; -import org.junitpioneer.jupiter.DefaultTimeZone; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junitpioneer.jupiter.DefaultTimeZone; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; @@ -252,6 +252,21 @@ public void testInstantToDateMapping() { assertThat( source.getForDateConversionWithInstant() ).isEqualTo( instant ); } + @ProcessorTest + public void testLocalDateTimeToLocalDateMapping() { + LocalDate localDate = LocalDate.of( 2014, 1, 1 ); + + Source source = new Source(); + source.setForLocalDateTimeConversionWithLocalDate( localDate ); + Target target = SourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( source ); + LocalDateTime localDateTime = target.getForLocalDateTimeConversionWithLocalDate(); + assertThat( localDateTime ).isNotNull(); + assertThat( localDateTime ).isEqualTo( LocalDateTime.of( 2014, 1, 1, 0, 0 ) ); + + source = SourceTargetMapper.INSTANCE.targetToSource( target ); + assertThat( source.getForLocalDateTimeConversionWithLocalDate() ).isEqualTo( localDate ); + } + @ProcessorTest @DefaultTimeZone("Australia/Melbourne") public void testLocalDateTimeToDateMapping() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/OptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/OptionalSource.java new file mode 100644 index 0000000000..cf94f31869 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/OptionalSource.java @@ -0,0 +1,160 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class OptionalSource { + + private Optional zonedDateTime = Optional.empty(); + + private Optional localDateTime = Optional.empty(); + + private Optional localDate = Optional.empty(); + + private Optional localTime = Optional.empty(); + + private Optional forCalendarConversion = Optional.empty(); + + private Optional forDateConversionWithZonedDateTime = Optional.empty(); + + private Optional forDateConversionWithLocalDateTime = Optional.empty(); + + private Optional forDateConversionWithLocalDate = Optional.empty(); + + private Optional forSqlDateConversionWithLocalDate = Optional.empty(); + + private Optional forDateConversionWithInstant = Optional.empty(); + + private Optional forLocalDateTimeConversionWithLocalDate = Optional.empty(); + + private Optional forInstantConversionWithString = Optional.empty(); + + private Optional forPeriodConversionWithString = Optional.empty(); + + private Optional forDurationConversionWithString = Optional.empty(); + + public Optional getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(Optional dateTime) { + this.zonedDateTime = dateTime; + } + + public Optional getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(Optional localDateTime) { + this.localDateTime = localDateTime; + } + + public Optional getLocalDate() { + return localDate; + } + + public void setLocalDate(Optional localDate) { + this.localDate = localDate; + } + + public Optional getLocalTime() { + return localTime; + } + + public void setLocalTime(Optional localTime) { + this.localTime = localTime; + } + + public Optional getForCalendarConversion() { + return forCalendarConversion; + } + + public void setForCalendarConversion(Optional forCalendarConversion) { + this.forCalendarConversion = forCalendarConversion; + } + + public Optional getForDateConversionWithZonedDateTime() { + return forDateConversionWithZonedDateTime; + } + + public void setForDateConversionWithZonedDateTime(Optional forDateConversionWithZonedDateTime) { + this.forDateConversionWithZonedDateTime = forDateConversionWithZonedDateTime; + } + + public Optional getForDateConversionWithLocalDateTime() { + return forDateConversionWithLocalDateTime; + } + + public void setForDateConversionWithLocalDateTime(Optional forDateConversionWithLocalDateTime) { + this.forDateConversionWithLocalDateTime = forDateConversionWithLocalDateTime; + } + + public Optional getForDateConversionWithLocalDate() { + return forDateConversionWithLocalDate; + } + + public void setForDateConversionWithLocalDate(Optional forDateConversionWithLocalDate) { + this.forDateConversionWithLocalDate = forDateConversionWithLocalDate; + } + + public Optional getForSqlDateConversionWithLocalDate() { + return forSqlDateConversionWithLocalDate; + } + + public void setForSqlDateConversionWithLocalDate(Optional forSqlDateConversionWithLocalDate) { + this.forSqlDateConversionWithLocalDate = forSqlDateConversionWithLocalDate; + } + + public Optional getForDateConversionWithInstant() { + return forDateConversionWithInstant; + } + + public void setForDateConversionWithInstant(Optional forDateConversionWithInstant) { + this.forDateConversionWithInstant = forDateConversionWithInstant; + } + + public Optional getForLocalDateTimeConversionWithLocalDate() { + return forLocalDateTimeConversionWithLocalDate; + } + + public void setForLocalDateTimeConversionWithLocalDate( + Optional forLocalDateTimeConversionWithLocalDate) { + this.forLocalDateTimeConversionWithLocalDate = forLocalDateTimeConversionWithLocalDate; + } + + public Optional getForInstantConversionWithString() { + return forInstantConversionWithString; + } + + public void setForInstantConversionWithString(Optional forInstantConversionWithString) { + this.forInstantConversionWithString = forInstantConversionWithString; + } + + public Optional getForPeriodConversionWithString() { + return forPeriodConversionWithString; + } + + public void setForPeriodConversionWithString(Optional forPeriodConversionWithString) { + this.forPeriodConversionWithString = forPeriodConversionWithString; + } + + public Optional getForDurationConversionWithString() { + return forDurationConversionWithString; + } + + public void setForDurationConversionWithString(Optional forDurationConversionWithString) { + this.forDurationConversionWithString = forDurationConversionWithString; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/OptionalSourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/OptionalSourceTargetMapper.java new file mode 100644 index 0000000000..a266309b61 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/OptionalSourceTargetMapper.java @@ -0,0 +1,70 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OptionalSourceTargetMapper { + + String DATE_TIME_FORMAT = "dd.MM.yyyy HH:mm z"; + + String LOCAL_DATE_TIME_FORMAT = "dd.MM.yyyy HH:mm"; + + String LOCAL_DATE_FORMAT = "dd.MM.yyyy"; + + String LOCAL_TIME_FORMAT = "HH:mm"; + + OptionalSourceTargetMapper INSTANCE = Mappers.getMapper( OptionalSourceTargetMapper.class ); + + @Mapping(target = "zonedDateTime", dateFormat = DATE_TIME_FORMAT) + @Mapping(target = "localDateTime", dateFormat = LOCAL_DATE_TIME_FORMAT) + @Mapping(target = "localDate", dateFormat = LOCAL_DATE_FORMAT) + @Mapping(target = "localTime", dateFormat = LOCAL_TIME_FORMAT) + Target sourceToTarget(OptionalSource source); + + @Mapping(target = "zonedDateTime", dateFormat = DATE_TIME_FORMAT) + @Mapping(target = "localDateTime", dateFormat = LOCAL_DATE_TIME_FORMAT) + @Mapping(target = "localDate", dateFormat = LOCAL_DATE_FORMAT) + @Mapping(target = "localTime", dateFormat = LOCAL_TIME_FORMAT) + Target sourceToTargetDefaultMapping(OptionalSource source); + + @Mapping(target = "zonedDateTime", dateFormat = DATE_TIME_FORMAT) + Target sourceToTargetDateTimeMapped(OptionalSource source); + + @Mapping(target = "localDateTime", dateFormat = LOCAL_DATE_TIME_FORMAT) + Target sourceToTargetLocalDateTimeMapped(OptionalSource source); + + @Mapping(target = "localDate", dateFormat = LOCAL_DATE_FORMAT) + Target sourceToTargetLocalDateMapped(OptionalSource source); + + @Mapping(target = "localTime", dateFormat = LOCAL_TIME_FORMAT) + Target sourceToTargetLocalTimeMapped(OptionalSource source); + + @Mapping(target = "zonedDateTime", dateFormat = DATE_TIME_FORMAT) + @Mapping(target = "localDateTime", dateFormat = LOCAL_DATE_TIME_FORMAT) + @Mapping(target = "localDate", dateFormat = LOCAL_DATE_FORMAT) + @Mapping(target = "localTime", dateFormat = LOCAL_TIME_FORMAT) + OptionalSource targetToSource(Target target); + + @Mapping(target = "zonedDateTime", dateFormat = DATE_TIME_FORMAT) + OptionalSource targetToSourceDateTimeMapped(Target target); + + @Mapping(target = "localDateTime", dateFormat = LOCAL_DATE_TIME_FORMAT) + OptionalSource targetToSourceLocalDateTimeMapped(Target target); + + @Mapping(target = "localDate", dateFormat = LOCAL_DATE_FORMAT) + OptionalSource targetToSourceLocalDateMapped(Target target); + + @Mapping(target = "localTime", dateFormat = LOCAL_TIME_FORMAT) + OptionalSource targetToSourceLocalTimeMapped(Target target); + + @InheritInverseConfiguration(name = "sourceToTarget") + OptionalSource targetToSourceDefaultMapping(Target target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Source.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Source.java index 91298617cd..93479f8a86 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Source.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Source.java @@ -38,6 +38,8 @@ public class Source { private Instant forDateConversionWithInstant; + private LocalDate forLocalDateTimeConversionWithLocalDate; + private Instant forInstantConversionWithString; private Period forPeriodConversionWithString; @@ -124,6 +126,14 @@ public void setForDateConversionWithInstant(Instant forDateConversionWithInstant this.forDateConversionWithInstant = forDateConversionWithInstant; } + public LocalDate getForLocalDateTimeConversionWithLocalDate() { + return forLocalDateTimeConversionWithLocalDate; + } + + public void setForLocalDateTimeConversionWithLocalDate(LocalDate forLocalDateTimeConversionWithLocalDate) { + this.forLocalDateTimeConversionWithLocalDate = forLocalDateTimeConversionWithLocalDate; + } + public Instant getForInstantConversionWithString() { return forInstantConversionWithString; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/SourceTargetMapper.java index 37fc7d6cbe..f2351f4175 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/SourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/SourceTargetMapper.java @@ -26,6 +26,7 @@ public interface SourceTargetMapper { String LOCAL_TIME_FORMAT = "HH:mm"; SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); + @Mappings( { @Mapping( target = "zonedDateTime", dateFormat = DATE_TIME_FORMAT ), @Mapping( target = "localDateTime", dateFormat = LOCAL_DATE_TIME_FORMAT ), @Mapping( target = "localDate", dateFormat = LOCAL_DATE_FORMAT ), diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Target.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Target.java index 188a6d0e20..d2a4878731 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Target.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/Target.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.test.conversion.java8time; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.Date; @@ -33,6 +34,8 @@ public class Target { private Date forDateConversionWithInstant; + private LocalDateTime forLocalDateTimeConversionWithLocalDate; + private String forInstantConversionWithString; private String forPeriodConversionWithString; @@ -119,6 +122,14 @@ public void setForDateConversionWithInstant(Date forDateConversionWithInstant) { this.forDateConversionWithInstant = forDateConversionWithInstant; } + public LocalDateTime getForLocalDateTimeConversionWithLocalDate() { + return forLocalDateTimeConversionWithLocalDate; + } + + public void setForLocalDateTimeConversionWithLocalDate(LocalDateTime forLocalDateTimeConversionWithLocalDate) { + this.forLocalDateTimeConversionWithLocalDate = forLocalDateTimeConversionWithLocalDate; + } + public String getForInstantConversionWithString() { return forInstantConversionWithString; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java new file mode 100644 index 0000000000..ab17980e96 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java @@ -0,0 +1,113 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.Source; +import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.SourceTargetMapper; +import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.Target; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ Source.class, Target.class, SourceTargetMapper.class }) +@IssueKey("4027") +public class ZonedOffsetDateTimeToLocalDateTimeConversionTest { + + @RegisterExtension + GeneratedSource generatedSource = new GeneratedSource() + .addComparisonToFixtureFor( SourceTargetMapper.class ); + + @ProcessorTest + public void testZonedDateTimeToLocalDateTimeMapping() { + ZonedDateTime zonedDateTime = ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneId.of( "UTC" ) ); + Source source = new Source(); + source.setZonedDateTime( zonedDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getZonedDateTime() ) + .isEqualTo( LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ) ); + } + + @ProcessorTest + public void testOffsetDateTimeToLocalDateTimeMapping() { + OffsetDateTime offsetDateTime = OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ); + Source source = new Source(); + source.setOffsetDateTime( offsetDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getOffsetDateTime() ) + .isEqualTo( LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ) ); + } + + @ProcessorTest + public void testLocalDateTimeToZonedDateTimeMapping() { + LocalDateTime localDateTime = LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ); + Target target = new Target(); + target.setZonedDateTime( localDateTime ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getZonedDateTime() ) + .isEqualTo( ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } + + @ProcessorTest + public void testLocalDateTimeToOffsetDateTimeMapping() { + LocalDateTime localDateTime = LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ); + Target target = new Target(); + target.setOffsetDateTime( localDateTime ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getOffsetDateTime() ) + .isEqualTo( OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } + + @ProcessorTest + public void testZonedDateTimeToInstantMapping() { + ZonedDateTime zonedDateTime = ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ); + Source source = new Source(); + source.setZonedDateTimeAsInstant( zonedDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getZonedDateTimeAsInstant() ) + .isEqualTo( Instant.parse( "2024-01-01T12:30:00Z" ) ); + } + + @ProcessorTest + public void testOffsetDateTimeToInstantMapping() { + OffsetDateTime offsetDateTime = OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ); + Source source = new Source(); + source.setOffsetDateTimeAsInstant( offsetDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getOffsetDateTimeAsInstant() ) + .isEqualTo( Instant.parse( "2024-01-01T12:30:00Z" ) ); + } + + @ProcessorTest + public void testInstantToZonedDateTimeMapping() { + Instant instant = Instant.parse( "2024-01-01T12:30:00Z" ); + Target target = new Target(); + target.setZonedDateTimeAsInstant( instant ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getZonedDateTimeAsInstant() ) + .isEqualTo( ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } + + @ProcessorTest + public void testInstantToOffsetDateTimeMapping() { + Instant instant = Instant.parse( "2024-01-01T12:30:00Z" ); + Target target = new Target(); + target.setOffsetDateTimeAsInstant( instant ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getOffsetDateTimeAsInstant() ) + .isEqualTo( OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java new file mode 100644 index 0000000000..18bd99825c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; + +public class Source { + private ZonedDateTime zonedDateTime; + private OffsetDateTime offsetDateTime; + private ZonedDateTime zonedDateTimeAsInstant; + private OffsetDateTime offsetDateTimeAsInstant; + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public ZonedDateTime getZonedDateTimeAsInstant() { + return zonedDateTimeAsInstant; + } + + public void setZonedDateTimeAsInstant(ZonedDateTime zonedDateTimeAsInstant) { + this.zonedDateTimeAsInstant = zonedDateTimeAsInstant; + } + + public OffsetDateTime getOffsetDateTimeAsInstant() { + return offsetDateTimeAsInstant; + } + + public void setOffsetDateTimeAsInstant(OffsetDateTime offsetDateTimeAsInstant) { + this.offsetDateTimeAsInstant = offsetDateTimeAsInstant; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java new file mode 100644 index 0000000000..43bee5f77f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SourceTargetMapper { + SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); + + Target toTarget(Source source); + + Source toSource(Target target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java new file mode 100644 index 0000000000..b43b9f6843 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import java.time.Instant; +import java.time.LocalDateTime; + +public class Target { + private LocalDateTime zonedDateTime; + private LocalDateTime offsetDateTime; + private Instant zonedDateTimeAsInstant; + private Instant offsetDateTimeAsInstant; + + public LocalDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(LocalDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public LocalDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(LocalDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + + public Instant getZonedDateTimeAsInstant() { + return zonedDateTimeAsInstant; + } + + public void setZonedDateTimeAsInstant(Instant zonedDateTimeAsInstant) { + this.zonedDateTimeAsInstant = zonedDateTimeAsInstant; + } + + public Instant getOffsetDateTimeAsInstant() { + return offsetDateTimeAsInstant; + } + + public void setOffsetDateTimeAsInstant(Instant offsetDateTimeAsInstant) { + this.offsetDateTimeAsInstant = offsetDateTimeAsInstant; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/jodatime/JodaConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/jodatime/JodaConversionTest.java index 001b6390e2..d447c455ff 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/jodatime/JodaConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/jodatime/JodaConversionTest.java @@ -13,13 +13,11 @@ import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.LocalTime; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.EnabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junitpioneer.jupiter.DefaultLocale; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJoda; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +29,7 @@ @WithClasses({ Source.class, Target.class, SourceTargetMapper.class }) @IssueKey("75") @DefaultLocale("de") +@WithJoda public class JodaConversionTest { @ProcessorTest @@ -70,8 +69,6 @@ public void testLocalTimeToString() { } @ProcessorTest - @EnabledOnJre(JRE.JAVA_8) - // See https://bugs.openjdk.java.net/browse/JDK-8211262, there is a difference in the default formats on Java 9+ public void testSourceToTargetMappingForStrings() { Source src = new Source(); src.setLocalTime( new LocalTime( 0, 0 ) ); @@ -91,36 +88,8 @@ public void testSourceToTargetMappingForStrings() { // and now with default mappings target = SourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( src ); assertThat( target ).isNotNull(); - assertThat( target.getDateTime() ).isEqualTo( "1. Januar 2014 00:00:00 UTC" ); - assertThat( target.getLocalDateTime() ).isEqualTo( "1. Januar 2014 00:00:00" ); - assertThat( target.getLocalDate() ).isEqualTo( "1. Januar 2014" ); - assertThat( target.getLocalTime() ).isEqualTo( "00:00:00" ); - } - - @ProcessorTest - @EnabledForJreRange(min = JRE.JAVA_11) - // See https://bugs.openjdk.java.net/browse/JDK-8211262, there is a difference in the default formats on Java 9+ - public void testSourceToTargetMappingForStringsJdk11() { - Source src = new Source(); - src.setLocalTime( new LocalTime( 0, 0 ) ); - src.setLocalDate( new LocalDate( 2014, 1, 1 ) ); - src.setLocalDateTime( new LocalDateTime( 2014, 1, 1, 0, 0 ) ); - src.setDateTime( new DateTime( 2014, 1, 1, 0, 0, 0, DateTimeZone.UTC ) ); - - // with given format - Target target = SourceTargetMapper.INSTANCE.sourceToTarget( src ); - - assertThat( target ).isNotNull(); - assertThat( target.getDateTime() ).isEqualTo( "01.01.2014 00:00 UTC" ); - assertThat( target.getLocalDateTime() ).isEqualTo( "01.01.2014 00:00" ); - assertThat( target.getLocalDate() ).isEqualTo( "01.01.2014" ); - assertThat( target.getLocalTime() ).isEqualTo( "00:00" ); - - // and now with default mappings - target = SourceTargetMapper.INSTANCE.sourceToTargetDefaultMapping( src ); - assertThat( target ).isNotNull(); - assertThat( target.getDateTime() ).isEqualTo( "1. Januar 2014 um 00:00:00 UTC" ); - assertThat( target.getLocalDateTime() ).isEqualTo( "1. Januar 2014 um 00:00:00" ); + assertThat( target.getDateTime() ).isEqualTo( "1. Januar 2014, 00:00:00 UTC" ); + assertThat( target.getLocalDateTime() ).isEqualTo( "1. Januar 2014, 00:00:00" ); assertThat( target.getLocalDate() ).isEqualTo( "1. Januar 2014" ); assertThat( target.getLocalTime() ).isEqualTo( "00:00:00" ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleConversionTest.java new file mode 100644 index 0000000000..cba7785512 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleConversionTest.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.locale; + +import java.util.Locale; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests conversions between {@link Locale} and String. + * + * @author Jason Bodnar + */ +@IssueKey("3172") +@WithClasses({ LocaleSource.class, LocaleTarget.class, LocaleMapper.class }) +public class LocaleConversionTest { + + @ProcessorTest + public void shouldApplyLocaleConversion() { + LocaleSource source = new LocaleSource(); + source.setLocaleA( Locale.getDefault() ); + + LocaleTarget target = LocaleMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getLocaleA() ).isEqualTo( source.getLocaleA().toLanguageTag() ); + } + + @ProcessorTest + public void shouldApplyReverseLocaleConversion() { + LocaleTarget target = new LocaleTarget(); + target.setLocaleA( Locale.getDefault().toLanguageTag() ); + + LocaleSource source = LocaleMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getLocaleA() ).isEqualTo( Locale.forLanguageTag( target.getLocaleA() ) ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleMapper.java new file mode 100644 index 0000000000..3da3fc4ae4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.locale; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface LocaleMapper { + + LocaleMapper INSTANCE = Mappers.getMapper( LocaleMapper.class ); + + LocaleTarget sourceToTarget(LocaleSource source); + + LocaleSource targetToSource(LocaleTarget target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleSource.java new file mode 100644 index 0000000000..69ea5bda2d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleSource.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.locale; + +import java.util.Locale; + +/** + * @author Jason Bodnar + */ +public class LocaleSource { + private Locale localeA; + + public Locale getLocaleA() { + return localeA; + } + + public void setLocaleA(Locale localeA) { + this.localeA = localeA; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleTarget.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleTarget.java new file mode 100644 index 0000000000..000ff86b05 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/locale/LocaleTarget.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.locale; + +/** + * @author Jason Bodnar + */ +public class LocaleTarget { + private String localeA; + + public String getLocaleA() { + return localeA; + } + + public void setLocaleA(String localeA) { + this.localeA = localeA; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryDto.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryDto.java index 5a96d6deb2..3bc144511e 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryDto.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryDto.java @@ -10,6 +10,7 @@ public class CutleryInventoryDto { private short numberOfKnifes; private int numberOfForks; private byte numberOfSpoons; + private int drawerId; private float approximateKnifeLength; @@ -44,4 +45,12 @@ public float getApproximateKnifeLength() { public void setApproximateKnifeLength(float approximateKnifeLength) { this.approximateKnifeLength = approximateKnifeLength; } + + public int getDrawerId() { + return drawerId; + } + + public void setDrawerId(int drawerId) { + this.drawerId = drawerId; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryEntity.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryEntity.java index 9575ed99dc..755dbc3637 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryEntity.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/CutleryInventoryEntity.java @@ -10,6 +10,7 @@ public class CutleryInventoryEntity { private int numberOfKnifes; private Long numberOfForks; private short numberOfSpoons; + private String drawerId; private double approximateKnifeLength; @@ -44,4 +45,12 @@ public double getApproximateKnifeLength() { public void setApproximateKnifeLength(double approximateKnifeLength) { this.approximateKnifeLength = approximateKnifeLength; } + + public String getDrawerId() { + return drawerId; + } + + public void setDrawerId(String drawerId) { + this.drawerId = drawerId; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerMapper6.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerMapper6.java new file mode 100644 index 0000000000..c33f1bd662 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerMapper6.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +@Mapper( typeConversionPolicy = ReportingPolicy.ERROR ) +public interface ErroneousKitchenDrawerMapper6 { + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "drawerId", source = "drawerId" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper1.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper1.java new file mode 100644 index 0000000000..d83b368bce --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper1.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( typeConversionPolicy = ReportingPolicy.ERROR ) +public interface ErroneousKitchenDrawerOptionalMapper1 { + + ErroneousKitchenDrawerOptionalMapper1 INSTANCE = Mappers.getMapper( ErroneousKitchenDrawerOptionalMapper1.class ); + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "numberOfForks", source = "numberOfForks" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper3.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper3.java new file mode 100644 index 0000000000..7d1824a5b1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper3.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( typeConversionPolicy = ReportingPolicy.ERROR, uses = VerySpecialNumberMapper.class ) +public interface ErroneousKitchenDrawerOptionalMapper3 { + + ErroneousKitchenDrawerOptionalMapper3 INSTANCE = Mappers.getMapper( ErroneousKitchenDrawerOptionalMapper3.class ); + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "numberOfSpoons", source = "numberOfSpoons" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper4.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper4.java new file mode 100644 index 0000000000..edd33c71d9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper4.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( typeConversionPolicy = ReportingPolicy.ERROR ) +public interface ErroneousKitchenDrawerOptionalMapper4 { + + ErroneousKitchenDrawerOptionalMapper4 INSTANCE = Mappers.getMapper( ErroneousKitchenDrawerOptionalMapper4.class ); + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "depth", source = "depth" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper5.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper5.java new file mode 100644 index 0000000000..bad921a7c2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper5.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( typeConversionPolicy = ReportingPolicy.ERROR ) +public interface ErroneousKitchenDrawerOptionalMapper5 { + + ErroneousKitchenDrawerOptionalMapper5 INSTANCE = Mappers.getMapper( ErroneousKitchenDrawerOptionalMapper5.class ); + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "length", source = "length" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper6.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper6.java new file mode 100644 index 0000000000..acd2e0876f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ErroneousKitchenDrawerOptionalMapper6.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +@Mapper( typeConversionPolicy = ReportingPolicy.ERROR ) +public interface ErroneousKitchenDrawerOptionalMapper6 { + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "drawerId", source = "drawerId" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/KitchenDrawerOptionalMapper2.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/KitchenDrawerOptionalMapper2.java new file mode 100644 index 0000000000..92862d3320 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/KitchenDrawerOptionalMapper2.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( typeConversionPolicy = ReportingPolicy.WARN ) +public interface KitchenDrawerOptionalMapper2 { + + KitchenDrawerOptionalMapper2 INSTANCE = Mappers.getMapper( KitchenDrawerOptionalMapper2.class ); + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "numberOfKnifes", source = "numberOfKnifes" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/KitchenDrawerOptionalMapper6.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/KitchenDrawerOptionalMapper6.java new file mode 100644 index 0000000000..2b3e982584 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/KitchenDrawerOptionalMapper6.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( typeConversionPolicy = ReportingPolicy.WARN, uses = VerySpecialNumberMapper.class ) +public interface KitchenDrawerOptionalMapper6 { + + KitchenDrawerOptionalMapper6 INSTANCE = Mappers.getMapper( KitchenDrawerOptionalMapper6.class ); + + @BeanMapping( ignoreByDefault = true ) + @Mapping( target = "height", source = "height" ) + RegularKitchenDrawerEntity map( OversizedKitchenDrawerOptionalDto dto ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ListMapper.java index 3d7f36c026..b253a26f1d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ListMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/ListMapper.java @@ -8,6 +8,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; + import org.mapstruct.Mapper; import org.mapstruct.ReportingPolicy; diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/LossyConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/LossyConversionTest.java index d27f04374a..3468da4535 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/LossyConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/LossyConversionTest.java @@ -22,6 +22,7 @@ */ @WithClasses({ OversizedKitchenDrawerDto.class, + OversizedKitchenDrawerOptionalDto.class, RegularKitchenDrawerEntity.class, VerySpecialNumber.class, VerySpecialNumberMapper.class, @@ -40,12 +41,14 @@ public void testNoErrorCase() { dto.setNumberOfKnifes( (short) 7 ); dto.setNumberOfSpoons( (byte) 3 ); dto.setApproximateKnifeLength( 3.7f ); + dto.setDrawerId( 1 ); CutleryInventoryEntity entity = CutleryInventoryMapper.INSTANCE.map( dto ); assertThat( entity.getNumberOfForks() ).isEqualTo( 5L ); assertThat( entity.getNumberOfKnifes() ).isEqualTo( 7 ); assertThat( entity.getNumberOfSpoons() ).isEqualTo( (short) 3 ); assertThat( entity.getApproximateKnifeLength() ).isCloseTo( 3.7d, withinPercentage( 0.0001d ) ); + assertThat( entity.getDrawerId() ).isEqualTo( "1" ); } @ProcessorTest @@ -61,6 +64,19 @@ public void testNoErrorCase() { public void testConversionFromLongToInt() { } + @ProcessorTest + @WithClasses(ErroneousKitchenDrawerOptionalMapper1.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousKitchenDrawerOptionalMapper1.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "Can't map property \"OptionalLong numberOfForks\". It has a possibly lossy conversion from " + + "OptionalLong to int.") + }) + public void testConversionFromOptionalLongToInt() { + } + @ProcessorTest @WithClasses(KitchenDrawerMapper2.class) @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, @@ -74,6 +90,45 @@ public void testConversionFromLongToInt() { public void testConversionFromBigIntegerToInteger() { } + @ProcessorTest + @WithClasses(KitchenDrawerOptionalMapper2.class) + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = KitchenDrawerOptionalMapper2.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 20, + message = "property \"Optional numberOfKnifes\" has a possibly lossy conversion " + + "from Optional to Integer.") + }) + public void testConversionFromOptionalBigIntegerToInteger() { + } + + @ProcessorTest + @WithClasses(ErroneousKitchenDrawerMapper6.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousKitchenDrawerMapper6.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 17, + message = "Can't map property \"String drawerId\". It has a possibly lossy conversion from " + + "String to int.") + }) + public void testConversionFromStringToInt() { + } + + @ProcessorTest + @WithClasses(ErroneousKitchenDrawerOptionalMapper6.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousKitchenDrawerOptionalMapper6.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 17, + message = "Can't map property \"Optional drawerId\". It has a possibly lossy conversion from " + + "Optional to int.") + }) + public void testConversionFromOptionalStringToInt() { + } + @ProcessorTest @WithClasses(ErroneousKitchenDrawerMapper3.class) @ExpectedCompilationOutcome(value = CompilationResult.FAILED, @@ -87,6 +142,19 @@ public void testConversionFromBigIntegerToInteger() { public void test2StepConversionFromBigIntegerToLong() { } + @ProcessorTest + @WithClasses(ErroneousKitchenDrawerOptionalMapper3.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousKitchenDrawerOptionalMapper3.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "Can't map property \"Optional numberOfSpoons\". " + + "It has a possibly lossy conversion from BigInteger to Long.") + }) + public void test2StepConversionFromOptionalBigIntegerToLong() { + } + @ProcessorTest @WithClasses(ErroneousKitchenDrawerMapper4.class) @ExpectedCompilationOutcome(value = CompilationResult.FAILED, @@ -100,6 +168,20 @@ public void test2StepConversionFromBigIntegerToLong() { public void testConversionFromDoubleToFloat() { } + @ProcessorTest + @WithClasses(ErroneousKitchenDrawerOptionalMapper4.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousKitchenDrawerOptionalMapper4.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = + "Can't map property \"Optional depth\". " + + "It has a possibly lossy conversion from Optional to float.") + }) + public void testConversionFromOptionalDoubleToFloat() { + } + @ProcessorTest @WithClasses(ErroneousKitchenDrawerMapper5.class) @ExpectedCompilationOutcome(value = CompilationResult.FAILED, @@ -113,6 +195,20 @@ public void testConversionFromDoubleToFloat() { public void testConversionFromBigDecimalToFloat() { } + @ProcessorTest + @WithClasses(ErroneousKitchenDrawerOptionalMapper5.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousKitchenDrawerOptionalMapper5.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = + "Can't map property \"Optional length\". " + + "It has a possibly lossy conversion from Optional to Float.") + }) + public void testConversionFromOptionalBigDecimalToFloat() { + } + @ProcessorTest @WithClasses(KitchenDrawerMapper6.class) @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, @@ -125,13 +221,26 @@ public void testConversionFromBigDecimalToFloat() { public void test2StepConversionFromDoubleToFloat() { } + @ProcessorTest + @WithClasses(KitchenDrawerOptionalMapper6.class) + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = KitchenDrawerOptionalMapper6.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 20, + message = "property \"OptionalDouble height\" has a possibly lossy conversion from " + + "OptionalDouble to float.") + }) + public void test2StepConversionFromOptionalDoubleToFloat() { + } + @ProcessorTest @WithClasses(ListMapper.class) @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, diagnostics = { @Diagnostic(type = ListMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 21, + line = 22, message = "collection element has a possibly lossy conversion from BigDecimal to BigInteger.") }) public void testListElementConversion() { @@ -143,11 +252,11 @@ public void testListElementConversion() { diagnostics = { @Diagnostic(type = MapMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 19, + line = 20, message = "map key has a possibly lossy conversion from Long to Integer."), @Diagnostic(type = MapMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 19, + line = 20, message = "map value has a possibly lossy conversion from Double to Float.") }) public void testMapElementConversion() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/MapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/MapMapper.java index 22cec92e3e..547971de43 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/MapMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/MapMapper.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.test.conversion.lossy; import java.util.Map; + import org.mapstruct.Mapper; import org.mapstruct.ReportingPolicy; diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerDto.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerDto.java index 110bb2f07c..c42b24cc57 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerDto.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerDto.java @@ -21,6 +21,7 @@ public class OversizedKitchenDrawerDto { private Double depth; private BigDecimal length; private double height; + private String drawerId; public long getNumberOfForks() { return numberOfForks; @@ -70,4 +71,11 @@ public void setHeight(double height) { this.height = height; } + public String getDrawerId() { + return drawerId; + } + + public void setDrawerId(String drawerId) { + this.drawerId = drawerId; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerOptionalDto.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerOptionalDto.java new file mode 100644 index 0000000000..f214fecd1d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/OversizedKitchenDrawerOptionalDto.java @@ -0,0 +1,84 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.lossy; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalLong; + +/** + * @author Filip Hrisafov + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class OversizedKitchenDrawerOptionalDto { + + /* yes, its a big drawer */ + private OptionalLong numberOfForks; + private Optional numberOfKnifes; + private Optional numberOfSpoons; + private Optional depth; + private Optional length; + private OptionalDouble height; + private Optional drawerId; + + public OptionalLong getNumberOfForks() { + return numberOfForks; + } + + public void setNumberOfForks(OptionalLong numberOfForks) { + this.numberOfForks = numberOfForks; + } + + public Optional getNumberOfKnifes() { + return numberOfKnifes; + } + + public void setNumberOfKnifes(Optional numberOfKnifes) { + this.numberOfKnifes = numberOfKnifes; + } + + public Optional getNumberOfSpoons() { + return numberOfSpoons; + } + + public void setNumberOfSpoons(Optional numberOfSpoons) { + this.numberOfSpoons = numberOfSpoons; + } + + public Optional getDepth() { + return depth; + } + + public void setDepth(Optional depth) { + this.depth = depth; + } + + public Optional getLength() { + return length; + } + + public void setLength(Optional length) { + this.length = length; + } + + public OptionalDouble getHeight() { + return height; + } + + public void setHeight(OptionalDouble height) { + this.height = height; + } + + public Optional getDrawerId() { + return drawerId; + } + + public void setDrawerId(Optional drawerId) { + this.drawerId = drawerId; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/RegularKitchenDrawerEntity.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/RegularKitchenDrawerEntity.java index d49a89052d..e3ae10bc1f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/RegularKitchenDrawerEntity.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/RegularKitchenDrawerEntity.java @@ -17,6 +17,7 @@ public class RegularKitchenDrawerEntity { private float depth; private Float length; private VerySpecialNumber height; + private int drawerId; public int getNumberOfForks() { return numberOfForks; @@ -66,4 +67,11 @@ public void setHeight(VerySpecialNumber height) { this.height = height; } + public int getDrawerId() { + return drawerId; + } + + public void setDrawerId(int drawerId) { + this.drawerId = drawerId; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/VerySpecialNumberMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/VerySpecialNumberMapper.java index d49a263deb..00b543c728 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/VerySpecialNumberMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/lossy/VerySpecialNumberMapper.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.test.conversion.lossy; import java.math.BigInteger; +import java.util.Optional; /** * @author Sjaak Derksen @@ -19,4 +20,9 @@ VerySpecialNumber fromFloat(float f) { BigInteger toBigInteger(VerySpecialNumber v) { return new BigInteger( "10" ); } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + BigInteger toBigInteger(Optional v) { + return new BigInteger( "10" ); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/ByteWrapperOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/ByteWrapperOptionalSource.java new file mode 100644 index 0000000000..a92b029b14 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/ByteWrapperOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class ByteWrapperOptionalSource { + + private Optional b = Optional.empty(); + private Optional bb = Optional.empty(); + private Optional s = Optional.empty(); + private Optional ss = Optional.empty(); + private Optional i = Optional.empty(); + private Optional ii = Optional.empty(); + private Optional l = Optional.empty(); + private Optional ll = Optional.empty(); + private Optional f = Optional.empty(); + private Optional ff = Optional.empty(); + private Optional d = Optional.empty(); + private Optional dd = Optional.empty(); + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/DoubleOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/DoubleOptionalSource.java new file mode 100644 index 0000000000..e469c44390 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/DoubleOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.OptionalDouble; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class DoubleOptionalSource { + + private OptionalDouble b = OptionalDouble.empty(); + private OptionalDouble bb = OptionalDouble.empty(); + private OptionalDouble s = OptionalDouble.empty(); + private OptionalDouble ss = OptionalDouble.empty(); + private OptionalDouble i = OptionalDouble.empty(); + private OptionalDouble ii = OptionalDouble.empty(); + private OptionalDouble l = OptionalDouble.empty(); + private OptionalDouble ll = OptionalDouble.empty(); + private OptionalDouble f = OptionalDouble.empty(); + private OptionalDouble ff = OptionalDouble.empty(); + private OptionalDouble d = OptionalDouble.empty(); + private OptionalDouble dd = OptionalDouble.empty(); + + public OptionalDouble getB() { + return b; + } + + public void setB(OptionalDouble b) { + this.b = b; + } + + public OptionalDouble getBb() { + return bb; + } + + public void setBb(OptionalDouble bb) { + this.bb = bb; + } + + public OptionalDouble getS() { + return s; + } + + public void setS(OptionalDouble s) { + this.s = s; + } + + public OptionalDouble getSs() { + return ss; + } + + public void setSs(OptionalDouble ss) { + this.ss = ss; + } + + public OptionalDouble getI() { + return i; + } + + public void setI(OptionalDouble i) { + this.i = i; + } + + public OptionalDouble getIi() { + return ii; + } + + public void setIi(OptionalDouble ii) { + this.ii = ii; + } + + public OptionalDouble getL() { + return l; + } + + public void setL(OptionalDouble l) { + this.l = l; + } + + public OptionalDouble getLl() { + return ll; + } + + public void setLl(OptionalDouble ll) { + this.ll = ll; + } + + public OptionalDouble getF() { + return f; + } + + public void setF(OptionalDouble f) { + this.f = f; + } + + public OptionalDouble getFf() { + return ff; + } + + public void setFf(OptionalDouble ff) { + this.ff = ff; + } + + public OptionalDouble getD() { + return d; + } + + public void setD(OptionalDouble d) { + this.d = d; + } + + public OptionalDouble getDd() { + return dd; + } + + public void setDd(OptionalDouble dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/DoubleWrapperOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/DoubleWrapperOptionalSource.java new file mode 100644 index 0000000000..ecf0d9bc41 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/DoubleWrapperOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class DoubleWrapperOptionalSource { + + private Optional b = Optional.empty(); + private Optional bb = Optional.empty(); + private Optional s = Optional.empty(); + private Optional ss = Optional.empty(); + private Optional i = Optional.empty(); + private Optional ii = Optional.empty(); + private Optional l = Optional.empty(); + private Optional ll = Optional.empty(); + private Optional f = Optional.empty(); + private Optional ff = Optional.empty(); + private Optional d = Optional.empty(); + private Optional dd = Optional.empty(); + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/FloatWrapperOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/FloatWrapperOptionalSource.java new file mode 100644 index 0000000000..f20e6fa5d9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/FloatWrapperOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class FloatWrapperOptionalSource { + + private Optional b = Optional.empty(); + private Optional bb = Optional.empty(); + private Optional s = Optional.empty(); + private Optional ss = Optional.empty(); + private Optional i = Optional.empty(); + private Optional ii = Optional.empty(); + private Optional l = Optional.empty(); + private Optional ll = Optional.empty(); + private Optional f = Optional.empty(); + private Optional ff = Optional.empty(); + private Optional d = Optional.empty(); + private Optional dd = Optional.empty(); + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/IntOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/IntOptionalSource.java new file mode 100644 index 0000000000..6543b7bc0b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/IntOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.OptionalInt; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class IntOptionalSource { + + private OptionalInt b = OptionalInt.empty(); + private OptionalInt bb = OptionalInt.empty(); + private OptionalInt s = OptionalInt.empty(); + private OptionalInt ss = OptionalInt.empty(); + private OptionalInt i = OptionalInt.empty(); + private OptionalInt ii = OptionalInt.empty(); + private OptionalInt l = OptionalInt.empty(); + private OptionalInt ll = OptionalInt.empty(); + private OptionalInt f = OptionalInt.empty(); + private OptionalInt ff = OptionalInt.empty(); + private OptionalInt d = OptionalInt.empty(); + private OptionalInt dd = OptionalInt.empty(); + + public OptionalInt getB() { + return b; + } + + public void setB(OptionalInt b) { + this.b = b; + } + + public OptionalInt getBb() { + return bb; + } + + public void setBb(OptionalInt bb) { + this.bb = bb; + } + + public OptionalInt getS() { + return s; + } + + public void setS(OptionalInt s) { + this.s = s; + } + + public OptionalInt getSs() { + return ss; + } + + public void setSs(OptionalInt ss) { + this.ss = ss; + } + + public OptionalInt getI() { + return i; + } + + public void setI(OptionalInt i) { + this.i = i; + } + + public OptionalInt getIi() { + return ii; + } + + public void setIi(OptionalInt ii) { + this.ii = ii; + } + + public OptionalInt getL() { + return l; + } + + public void setL(OptionalInt l) { + this.l = l; + } + + public OptionalInt getLl() { + return ll; + } + + public void setLl(OptionalInt ll) { + this.ll = ll; + } + + public OptionalInt getF() { + return f; + } + + public void setF(OptionalInt f) { + this.f = f; + } + + public OptionalInt getFf() { + return ff; + } + + public void setFf(OptionalInt ff) { + this.ff = ff; + } + + public OptionalInt getD() { + return d; + } + + public void setD(OptionalInt d) { + this.d = d; + } + + public OptionalInt getDd() { + return dd; + } + + public void setDd(OptionalInt dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/IntWrapperOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/IntWrapperOptionalSource.java new file mode 100644 index 0000000000..b1243d9c1c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/IntWrapperOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class IntWrapperOptionalSource { + + private Optional b = Optional.empty(); + private Optional bb = Optional.empty(); + private Optional s = Optional.empty(); + private Optional ss = Optional.empty(); + private Optional i = Optional.empty(); + private Optional ii = Optional.empty(); + private Optional l = Optional.empty(); + private Optional ll = Optional.empty(); + private Optional f = Optional.empty(); + private Optional ff = Optional.empty(); + private Optional d = Optional.empty(); + private Optional dd = Optional.empty(); + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/LongOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/LongOptionalSource.java new file mode 100644 index 0000000000..1df7baaec9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/LongOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.OptionalLong; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class LongOptionalSource { + + private OptionalLong b = OptionalLong.empty(); + private OptionalLong bb = OptionalLong.empty(); + private OptionalLong s = OptionalLong.empty(); + private OptionalLong ss = OptionalLong.empty(); + private OptionalLong i = OptionalLong.empty(); + private OptionalLong ii = OptionalLong.empty(); + private OptionalLong l = OptionalLong.empty(); + private OptionalLong ll = OptionalLong.empty(); + private OptionalLong f = OptionalLong.empty(); + private OptionalLong ff = OptionalLong.empty(); + private OptionalLong d = OptionalLong.empty(); + private OptionalLong dd = OptionalLong.empty(); + + public OptionalLong getB() { + return b; + } + + public void setB(OptionalLong b) { + this.b = b; + } + + public OptionalLong getBb() { + return bb; + } + + public void setBb(OptionalLong bb) { + this.bb = bb; + } + + public OptionalLong getS() { + return s; + } + + public void setS(OptionalLong s) { + this.s = s; + } + + public OptionalLong getSs() { + return ss; + } + + public void setSs(OptionalLong ss) { + this.ss = ss; + } + + public OptionalLong getI() { + return i; + } + + public void setI(OptionalLong i) { + this.i = i; + } + + public OptionalLong getIi() { + return ii; + } + + public void setIi(OptionalLong ii) { + this.ii = ii; + } + + public OptionalLong getL() { + return l; + } + + public void setL(OptionalLong l) { + this.l = l; + } + + public OptionalLong getLl() { + return ll; + } + + public void setLl(OptionalLong ll) { + this.ll = ll; + } + + public OptionalLong getF() { + return f; + } + + public void setF(OptionalLong f) { + this.f = f; + } + + public OptionalLong getFf() { + return ff; + } + + public void setFf(OptionalLong ff) { + this.ff = ff; + } + + public OptionalLong getD() { + return d; + } + + public void setD(OptionalLong d) { + this.d = d; + } + + public OptionalLong getDd() { + return dd; + } + + public void setDd(OptionalLong dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/LongWrapperOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/LongWrapperOptionalSource.java new file mode 100644 index 0000000000..bed8ab6b59 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/LongWrapperOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class LongWrapperOptionalSource { + + private Optional b = Optional.empty(); + private Optional bb = Optional.empty(); + private Optional s = Optional.empty(); + private Optional ss = Optional.empty(); + private Optional i = Optional.empty(); + private Optional ii = Optional.empty(); + private Optional l = Optional.empty(); + private Optional ll = Optional.empty(); + private Optional f = Optional.empty(); + private Optional ff = Optional.empty(); + private Optional d = Optional.empty(); + private Optional dd = Optional.empty(); + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/NumberOptionalConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/NumberOptionalConversionTest.java new file mode 100644 index 0000000000..0857b20473 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/NumberOptionalConversionTest.java @@ -0,0 +1,357 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + ByteTarget.class, + ByteWrapperOptionalSource.class, + ByteWrapperTarget.class, + ShortTarget.class, + ShortWrapperOptionalSource.class, + ShortWrapperTarget.class, + IntOptionalSource.class, + IntTarget.class, + IntWrapperOptionalSource.class, + IntWrapperTarget.class, + LongOptionalSource.class, + LongTarget.class, + LongWrapperOptionalSource.class, + LongWrapperTarget.class, + FloatWrapperOptionalSource.class, + FloatWrapperTarget.class, + DoubleOptionalSource.class, + DoubleTarget.class, + DoubleWrapperOptionalSource.class, + DoubleWrapperTarget.class, + OptionalNumberConversionMapper.class +}) +public class NumberOptionalConversionTest { + + @ProcessorTest + public void shouldApplyByteWrapperConversions() { + ByteWrapperOptionalSource source = new ByteWrapperOptionalSource(); + source.setB( Optional.of( (byte) 1 ) ); + source.setBb( Optional.of( (byte) 2 ) ); + source.setS( Optional.of( (byte) 3 ) ); + source.setSs( Optional.of( (byte) 4 ) ); + source.setI( Optional.of( (byte) 5 ) ); + source.setIi( Optional.of( (byte) 6 ) ); + source.setL( Optional.of( (byte) 7 ) ); + source.setLl( Optional.of( (byte) 8 ) ); + source.setF( Optional.of( (byte) 9 ) ); + source.setFf( Optional.of( (byte) 10 ) ); + source.setD( Optional.of( (byte) 11 ) ); + source.setDd( Optional.of( (byte) 12 ) ); + + ByteWrapperTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyShortWrapperConversions() { + ShortWrapperOptionalSource source = new ShortWrapperOptionalSource(); + source.setB( Optional.of( (short) 1 ) ); + source.setBb( Optional.of( (short) 2 ) ); + source.setS( Optional.of( (short) 3 ) ); + source.setSs( Optional.of( (short) 4 ) ); + source.setI( Optional.of( (short) 5 ) ); + source.setIi( Optional.of( (short) 6 ) ); + source.setL( Optional.of( (short) 7 ) ); + source.setLl( Optional.of( (short) 8 ) ); + source.setF( Optional.of( (short) 9 ) ); + source.setFf( Optional.of( (short) 10 ) ); + source.setD( Optional.of( (short) 11 ) ); + source.setDd( Optional.of( (short) 12 ) ); + + ShortWrapperTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyIntConversions() { + IntOptionalSource source = new IntOptionalSource(); + source.setB( OptionalInt.of( 1 ) ); + source.setBb( OptionalInt.of( 2 ) ); + source.setS( OptionalInt.of( 3 ) ); + source.setSs( OptionalInt.of( 4 ) ); + source.setI( OptionalInt.of( 5 ) ); + source.setIi( OptionalInt.of( 6 ) ); + source.setL( OptionalInt.of( 7 ) ); + source.setLl( OptionalInt.of( 8 ) ); + source.setF( OptionalInt.of( 9 ) ); + source.setFf( OptionalInt.of( 10 ) ); + source.setD( OptionalInt.of( 11 ) ); + source.setDd( OptionalInt.of( 12 ) ); + + IntTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyIntWrapperConversions() { + IntWrapperOptionalSource source = new IntWrapperOptionalSource(); + source.setB( Optional.of( 1 ) ); + source.setBb( Optional.of( 2 ) ); + source.setS( Optional.of( 3 ) ); + source.setSs( Optional.of( 4 ) ); + source.setI( Optional.of( 5 ) ); + source.setIi( Optional.of( 6 ) ); + source.setL( Optional.of( 7 ) ); + source.setLl( Optional.of( 8 ) ); + source.setF( Optional.of( 9 ) ); + source.setFf( Optional.of( 10 ) ); + source.setD( Optional.of( 11 ) ); + source.setDd( Optional.of( 12 ) ); + + IntWrapperTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyLongConversions() { + LongOptionalSource source = new LongOptionalSource(); + source.setB( OptionalLong.of( 1 ) ); + source.setBb( OptionalLong.of( 2 ) ); + source.setS( OptionalLong.of( 3 ) ); + source.setSs( OptionalLong.of( 4 ) ); + source.setI( OptionalLong.of( 5 ) ); + source.setIi( OptionalLong.of( 6 ) ); + source.setL( OptionalLong.of( 7 ) ); + source.setLl( OptionalLong.of( 8 ) ); + source.setF( OptionalLong.of( 9 ) ); + source.setFf( OptionalLong.of( 10 ) ); + source.setD( OptionalLong.of( 11 ) ); + source.setDd( OptionalLong.of( 12 ) ); + + LongTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyLongWrapperConversions() { + LongWrapperOptionalSource source = new LongWrapperOptionalSource(); + source.setB( Optional.of( (long) 1 ) ); + source.setBb( Optional.of( (long) 2 ) ); + source.setS( Optional.of( (long) 3 ) ); + source.setSs( Optional.of( (long) 4 ) ); + source.setI( Optional.of( (long) 5 ) ); + source.setIi( Optional.of( (long) 6 ) ); + source.setL( Optional.of( (long) 7 ) ); + source.setLl( Optional.of( (long) 8 ) ); + source.setF( Optional.of( (long) 9 ) ); + source.setFf( Optional.of( (long) 10 ) ); + source.setD( Optional.of( (long) 11 ) ); + source.setDd( Optional.of( (long) 12 ) ); + + LongWrapperTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyFloatWrapperConversions() { + FloatWrapperOptionalSource source = new FloatWrapperOptionalSource(); + source.setB( Optional.of( 1f ) ); + source.setBb( Optional.of( 2f ) ); + source.setS( Optional.of( 3f ) ); + source.setSs( Optional.of( 4f ) ); + source.setI( Optional.of( 5f ) ); + source.setIi( Optional.of( 6f ) ); + source.setL( Optional.of( 7f ) ); + source.setLl( Optional.of( 8f ) ); + source.setF( Optional.of( 9f ) ); + source.setFf( Optional.of( 10f ) ); + source.setD( Optional.of( 11f ) ); + source.setDd( Optional.of( 12f ) ); + + FloatWrapperTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyDoubleConversions() { + DoubleOptionalSource source = new DoubleOptionalSource(); + source.setB( OptionalDouble.of( 1 ) ); + source.setBb( OptionalDouble.of( 2 ) ); + source.setS( OptionalDouble.of( 3 ) ); + source.setSs( OptionalDouble.of( 4 ) ); + source.setI( OptionalDouble.of( 5 ) ); + source.setIi( OptionalDouble.of( 6 ) ); + source.setL( OptionalDouble.of( 7 ) ); + source.setLl( OptionalDouble.of( 8 ) ); + source.setF( OptionalDouble.of( 9 ) ); + source.setFf( OptionalDouble.of( 10 ) ); + source.setD( OptionalDouble.of( 11 ) ); + source.setDd( OptionalDouble.of( 12 ) ); + + DoubleTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + public void shouldApplyDoubleWrapperConversions() { + DoubleWrapperOptionalSource source = new DoubleWrapperOptionalSource(); + source.setB( Optional.of( 1d ) ); + source.setBb( Optional.of( 2d ) ); + source.setS( Optional.of( 3d ) ); + source.setSs( Optional.of( 4d ) ); + source.setI( Optional.of( 5d ) ); + source.setIi( Optional.of( 6d ) ); + source.setL( Optional.of( 7d ) ); + source.setLl( Optional.of( 8d ) ); + source.setF( Optional.of( 9d ) ); + source.setFf( Optional.of( 10d ) ); + source.setD( Optional.of( 11d ) ); + source.setDd( Optional.of( 12d ) ); + + DoubleWrapperTarget target = OptionalNumberConversionMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( (byte) 1 ); + assertThat( target.getBb() ).isEqualTo( Byte.valueOf( (byte) 2 ) ); + assertThat( target.getS() ).isEqualTo( (short) 3 ); + assertThat( target.getSs() ).isEqualTo( Short.valueOf( (short) 4 ) ); + assertThat( target.getI() ).isEqualTo( 5 ); + assertThat( target.getIi() ).isEqualTo( Integer.valueOf( 6 ) ); + assertThat( target.getL() ).isEqualTo( 7 ); + assertThat( target.getLl() ).isEqualTo( Long.valueOf( 8 ) ); + assertThat( target.getF() ).isEqualTo( 9f ); + assertThat( target.getFf() ).isEqualTo( Float.valueOf( 10f ) ); + assertThat( target.getD() ).isEqualTo( 11d ); + assertThat( target.getDd() ).isEqualTo( Double.valueOf( 12d ) ); + } + + @ProcessorTest + @IssueKey("229") + public void wrapperToPrimitiveIsNullSafe() { + assertThat( OptionalNumberConversionMapper.INSTANCE.sourceToTarget( new ByteWrapperOptionalSource() ) ) + .isNotNull(); + assertThat( OptionalNumberConversionMapper.INSTANCE.sourceToTarget( new DoubleWrapperOptionalSource() ) ) + .isNotNull(); + assertThat( OptionalNumberConversionMapper.INSTANCE.sourceToTarget( new ShortWrapperOptionalSource() ) ) + .isNotNull(); + assertThat( OptionalNumberConversionMapper.INSTANCE.sourceToTarget( new IntWrapperOptionalSource() ) ) + .isNotNull(); + assertThat( OptionalNumberConversionMapper.INSTANCE.sourceToTarget( new FloatWrapperOptionalSource() ) ) + .isNotNull(); + assertThat( OptionalNumberConversionMapper.INSTANCE.sourceToTarget( new LongWrapperOptionalSource() ) ) + .isNotNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/OptionalNumberConversionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/OptionalNumberConversionMapper.java new file mode 100644 index 0000000000..94472586fa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/OptionalNumberConversionMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OptionalNumberConversionMapper { + + OptionalNumberConversionMapper INSTANCE = Mappers.getMapper( OptionalNumberConversionMapper.class ); + + ByteWrapperTarget sourceToTarget(ByteWrapperOptionalSource source); + + ByteWrapperOptionalSource targetToSource(ByteWrapperTarget target); + + ShortWrapperTarget sourceToTarget(ShortWrapperOptionalSource source); + + ShortWrapperOptionalSource targetToSource(ShortWrapperTarget target); + + IntTarget sourceToTarget(IntOptionalSource source); + + IntOptionalSource targetToSource(IntTarget target); + + IntWrapperTarget sourceToTarget(IntWrapperOptionalSource source); + + IntWrapperOptionalSource targetToSource(IntWrapperTarget target); + + LongTarget sourceToTarget(LongOptionalSource source); + + LongOptionalSource targetToSource(LongTarget target); + + LongWrapperTarget sourceToTarget(LongWrapperOptionalSource source); + + LongWrapperOptionalSource targetToSource(LongWrapperTarget target); + + FloatWrapperTarget sourceToTarget(FloatWrapperOptionalSource source); + + FloatWrapperOptionalSource targetToSource(FloatWrapperTarget target); + + DoubleTarget sourceToTarget(DoubleOptionalSource source); + + DoubleOptionalSource targetToSource(DoubleTarget target); + + DoubleWrapperTarget sourceToTarget(DoubleWrapperOptionalSource source); + + DoubleWrapperOptionalSource targetToSource(DoubleWrapperTarget target); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/ShortWrapperOptionalSource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/ShortWrapperOptionalSource.java new file mode 100644 index 0000000000..16acf6c69b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/nativetypes/ShortWrapperOptionalSource.java @@ -0,0 +1,121 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.nativetypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class ShortWrapperOptionalSource { + + private Optional b = Optional.empty(); + private Optional bb = Optional.empty(); + private Optional s = Optional.empty(); + private Optional ss = Optional.empty(); + private Optional i = Optional.empty(); + private Optional ii = Optional.empty(); + private Optional l = Optional.empty(); + private Optional ll = Optional.empty(); + private Optional f = Optional.empty(); + private Optional ff = Optional.empty(); + private Optional d = Optional.empty(); + private Optional dd = Optional.empty(); + + public Optional getB() { + return b; + } + + public void setB(Optional b) { + this.b = b; + } + + public Optional getBb() { + return bb; + } + + public void setBb(Optional bb) { + this.bb = bb; + } + + public Optional getS() { + return s; + } + + public void setS(Optional s) { + this.s = s; + } + + public Optional getSs() { + return ss; + } + + public void setSs(Optional ss) { + this.ss = ss; + } + + public Optional getI() { + return i; + } + + public void setI(Optional i) { + this.i = i; + } + + public Optional getIi() { + return ii; + } + + public void setIi(Optional ii) { + this.ii = ii; + } + + public Optional getL() { + return l; + } + + public void setL(Optional l) { + this.l = l; + } + + public Optional getLl() { + return ll; + } + + public void setLl(Optional ll) { + this.ll = ll; + } + + public Optional getF() { + return f; + } + + public void setF(Optional f) { + this.f = f; + } + + public Optional getFf() { + return ff; + } + + public void setFf(Optional ff) { + this.ff = ff; + } + + public Optional getD() { + return d; + } + + public void setD(Optional d) { + this.d = d; + } + + public Optional getDd() { + return dd; + } + + public void setDd(Optional dd) { + this.dd = dd; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/NumberFormatConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/NumberFormatConversionTest.java index 6dfc1b3ca1..39b4e98319 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/NumberFormatConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/NumberFormatConversionTest.java @@ -7,14 +7,15 @@ import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junitpioneer.jupiter.DefaultLocale; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -27,6 +28,10 @@ @DefaultLocale("en") public class NumberFormatConversionTest { + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource() + .addComparisonToFixtureFor( SourceTargetMapper.class ); + @ProcessorTest public void shouldApplyStringConversions() { Source source = new Source(); @@ -68,6 +73,47 @@ public void shouldApplyStringConversions() { assertThat( target.getBigInteger1() ).isEqualTo( "1.23456789E12" ); } + @ProcessorTest + public void shouldApplyStringConversionsWithCustomLocale() { + Source source = new Source(); + source.setI( 1 ); + source.setIi( 2 ); + source.setD( 3.0 ); + source.setDd( 4.0 ); + source.setF( 3.0f ); + source.setFf( 4.0f ); + source.setL( 5L ); + source.setLl( 6L ); + source.setB( (byte) 7 ); + source.setBb( (byte) 8 ); + + source.setComplex1( 345346.456756 ); + source.setComplex2( 5007034.3 ); + + source.setBigDecimal1( new BigDecimal( "987E-20" ) ); + source.setBigInteger1( new BigInteger( "1234567890000" ) ); + + Target target = SourceTargetMapper.INSTANCE.sourceToTargetWithCustomLocale( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getI() ).isEqualTo( "1.00" ); + assertThat( target.getIi() ).isEqualTo( "2.00" ); + assertThat( target.getD() ).isEqualTo( "3.00" ); + assertThat( target.getDd() ).isEqualTo( "4.00" ); + assertThat( target.getF() ).isEqualTo( "3.00" ); + assertThat( target.getFf() ).isEqualTo( "4.00" ); + assertThat( target.getL() ).isEqualTo( "5.00" ); + assertThat( target.getLl() ).isEqualTo( "6.00" ); + assertThat( target.getB() ).isEqualTo( "7.00" ); + assertThat( target.getBb() ).isEqualTo( "8.00" ); + + assertThat( target.getComplex1() ).isEqualTo( "345.35E3" ); + assertThat( target.getComplex2() ).isEqualTo( "$5007034.30" ); + + assertThat( target.getBigDecimal1() ).isEqualTo( "9,87E-18" ); + assertThat( target.getBigInteger1() ).isEqualTo( "1,23456789E12" ); + } + @ProcessorTest public void shouldApplyReverseStringConversions() { Target target = new Target(); @@ -109,17 +155,73 @@ public void shouldApplyReverseStringConversions() { assertThat( source.getBigInteger1() ).isEqualTo( new BigInteger( "1234567890000" ) ); } + @ProcessorTest + public void shouldApplyReverseStringConversionsWithCustomLocale() { + Target target = new Target(); + target.setI( "1.00" ); + target.setIi( "2.00" ); + target.setD( "3.00" ); + target.setDd( "4.00" ); + target.setF( "3.00" ); + target.setFf( "4.00" ); + target.setL( "5.00" ); + target.setLl( "6.00" ); + target.setB( "7.00" ); + target.setBb( "8.00" ); + + target.setComplex1( "345.35E3" ); + target.setComplex2( "$5007034.30" ); + + target.setBigDecimal1( "9,87E-18" ); + target.setBigInteger1( "1,23456789E12" ); + + Source source = SourceTargetMapper.INSTANCE.targetToSourceWithCustomLocale( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getI() ).isEqualTo( 1 ); + assertThat( source.getIi() ).isEqualTo( Integer.valueOf( 2 ) ); + assertThat( source.getD() ).isEqualTo( 3.0 ); + assertThat( source.getDd() ).isEqualTo( Double.valueOf( 4.0 ) ); + assertThat( source.getF() ).isEqualTo( 3.0f ); + assertThat( source.getFf() ).isEqualTo( Float.valueOf( 4.0f ) ); + assertThat( source.getL() ).isEqualTo( 5L ); + assertThat( source.getLl() ).isEqualTo( Long.valueOf( 6L ) ); + assertThat( source.getB() ).isEqualTo( (byte) 7 ); + assertThat( source.getBb() ).isEqualTo( (byte) 8 ); + + assertThat( source.getComplex1() ).isEqualTo( 345350.0 ); + assertThat( source.getComplex2() ).isEqualTo( 5007034.3 ); + + assertThat( source.getBigDecimal1() ).isEqualTo( new BigDecimal( "987E-20" ) ); + assertThat( source.getBigInteger1() ).isEqualTo( new BigInteger( "1234567890000" ) ); + } + @ProcessorTest public void shouldApplyStringConversionsToIterables() { - List target = SourceTargetMapper.INSTANCE.sourceToTarget( Arrays.asList( 2f ) ); + List target = SourceTargetMapper.INSTANCE.sourceToTarget( List.of( 2f ) ); assertThat( target ).hasSize( 1 ); - assertThat( target ).isEqualTo( Arrays.asList( "2.00" ) ); + assertThat( target ).isEqualTo( List.of( "2.00" ) ); List source = SourceTargetMapper.INSTANCE.targetToSource( target ); assertThat( source ).hasSize( 1 ); - assertThat( source ).isEqualTo( Arrays.asList( 2.00f ) ); + assertThat( source ).isEqualTo( List.of( 2.00f ) ); + } + + @ProcessorTest + public void shouldApplyStringConversionsToIterablesWithCustomLocale() { + + List target = SourceTargetMapper.INSTANCE.sourceToTargetWithCustomLocale( + List.of( new BigDecimal("987E-20") ) + ); + + assertThat( target ).hasSize( 1 ); + assertThat( target ).isEqualTo( List.of( "9,87E-18" ) ); + + List source = SourceTargetMapper.INSTANCE.targetToSourceWithCustomLocale( target ); + assertThat( source ).hasSize( 1 ); + assertThat( source ).isEqualTo( List.of( new BigDecimal("987E-20") ) ); } @ProcessorTest @@ -137,4 +239,20 @@ public void shouldApplyStringConversionsToMaps() { assertThat( source2 ).contains( entry( 1.00f, 2f ) ); } + + @ProcessorTest + public void shouldApplyStringConversionsToMapsWithCustomLocale() { + + Map source1 = new HashMap<>(); + source1.put( new BigDecimal( "987E-20" ), new BigDecimal( "97E-10" ) ); + + Map target = SourceTargetMapper.INSTANCE.sourceToTargetWithCustomLocale( source1 ); + assertThat( target ).hasSize( 1 ); + assertThat( target ).contains( entry( "9,87E-18", "9,7E-9" ) ); + + Map source2 = SourceTargetMapper.INSTANCE.targetToSourceWithCustomLocale( target ); + assertThat( source2 ).hasSize( 1 ); + assertThat( source2 ).contains( entry( new BigDecimal( "987E-20" ), new BigDecimal( "97E-10" ) ) ); + + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/SourceTargetMapper.java index 537452fca2..70b16970c6 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/SourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/numbers/SourceTargetMapper.java @@ -5,8 +5,10 @@ */ package org.mapstruct.ap.test.conversion.numbers; +import java.math.BigDecimal; import java.util.List; import java.util.Map; + import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.IterableMapping; import org.mapstruct.MapMapping; @@ -41,21 +43,55 @@ public interface SourceTargetMapper { } ) Target sourceToTarget(Source source); - @InheritInverseConfiguration + @Mappings( { + @Mapping( target = "i", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "ii", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "d", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "dd", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "f", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "ff", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "l", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "ll", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "b", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "bb", numberFormat = NUMBER_FORMAT, locale = "ru" ), + @Mapping( target = "complex1", numberFormat = "##0.##E0", locale = "ru" ), + @Mapping( target = "complex2", numberFormat = "$#.00", locale = "ru" ), + @Mapping( target = "bigDecimal1", numberFormat = "#0.#E0", locale = "ru" ), + @Mapping( target = "bigInteger1", numberFormat = "0.#############E0", locale = "ru" ) + + } ) + Target sourceToTargetWithCustomLocale(Source source); + + @InheritInverseConfiguration( name = "sourceToTarget" ) Source targetToSource(Target target); + @InheritInverseConfiguration( name = "sourceToTargetWithCustomLocale" ) + Source targetToSourceWithCustomLocale(Target target); + @IterableMapping( numberFormat = NUMBER_FORMAT ) List sourceToTarget(List source); - @InheritInverseConfiguration + @InheritInverseConfiguration( name = "sourceToTarget" ) List targetToSource(List source); + @IterableMapping( numberFormat = "#0.#E0", locale = "fr" ) + List sourceToTargetWithCustomLocale(List source); + + @InheritInverseConfiguration( name = "sourceToTargetWithCustomLocale" ) + List targetToSourceWithCustomLocale(List source); + @MapMapping( keyNumberFormat = NUMBER_FORMAT, valueNumberFormat = "##" ) Map sourceToTarget(Map source); - @InheritInverseConfiguration + @MapMapping( keyNumberFormat = "#0.#E0", valueNumberFormat = "0.#############E0", locale = "fr" ) + Map sourceToTargetWithCustomLocale(Map source); + + @InheritInverseConfiguration( name = "sourceToTarget" ) Map targetToSource(Map source); + @InheritInverseConfiguration( name = "sourceToTargetWithCustomLocale" ) + Map targetToSourceWithCustomLocale(Map source); + } diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/string/StringConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/string/StringConversionTest.java index 994b47957b..37d3c77b04 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/string/StringConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/string/StringConversionTest.java @@ -69,6 +69,33 @@ public void shouldApplyStringConversions() { assertThat( target.getSb() ).isEqualTo( "SB" ); } + @IssueKey("2846") + @ProcessorTest + public void shouldNotApplyStringConversionsWhenNull() { + Source source = new Source(); + + Target target = SourceTargetMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getB() ).isEqualTo( "0" ); + assertThat( target.getBb() ).isNull(); + assertThat( target.getS() ).isEqualTo( "0" ); + assertThat( target.getSs() ).isNull(); + assertThat( target.getI() ).isEqualTo( "0" ); + assertThat( target.getIi() ).isNull(); + assertThat( target.getL() ).isEqualTo( "0" ); + assertThat( target.getLl() ).isNull(); + assertThat( target.getF() ).isEqualTo( "0.0" ); + assertThat( target.getFf() ).isNull(); + assertThat( target.getD() ).isEqualTo( "0.0" ); + assertThat( target.getDd() ).isNull(); + assertThat( target.getBool() ).isEqualTo( "false" ); + assertThat( target.getBoolBool() ).isNull(); + assertThat( target.getC() ).isEqualTo( String.valueOf( '\u0000' ) ); + assertThat( target.getCc() ).isNull(); + assertThat( target.getSb() ).isNull(); + } + @ProcessorTest public void shouldApplyReverseStringConversions() { Target target = new Target(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URIConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URIConversionTest.java new file mode 100644 index 0000000000..d7d3ef4142 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URIConversionTest.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.uri; + +import java.net.URI; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests conversions between {@link java.net.URI} and String. + * + * @author Maciej Kucharczyk + */ +@IssueKey("4018") +@WithClasses({ URISource.class, URITarget.class, URIMapper.class }) +public class URIConversionTest { + + @ProcessorTest + public void shouldApplyURIConversion() { + URISource source = new URISource(); + source.setUriA( URI.create( "https://mapstruct.org/" ) ); + + URITarget target = URIMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getUriA() ).isEqualTo( source.getUriA().toString() ); + } + + @ProcessorTest + public void shouldApplyReverseURIConversion() { + URITarget target = new URITarget(); + target.setUriA( "https://mapstruct.org/" ); + + URISource source = URIMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getUriA() ).isEqualTo( URI.create( target.getUriA() ) ); + } + + @ProcessorTest + public void shouldHandleInvalidURIString() { + URITarget target = new URITarget(); + target.setInvalidURI( "ht!tp://example.com" ); + + assertThatThrownBy( () -> URIMapper.INSTANCE.targetToSource( target ) ) + .isInstanceOf( IllegalArgumentException.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URIMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URIMapper.java new file mode 100644 index 0000000000..cac94a4efa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URIMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.uri; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface URIMapper { + + URIMapper INSTANCE = Mappers.getMapper( URIMapper.class ); + + URITarget sourceToTarget(URISource source); + + URISource targetToSource(URITarget target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URISource.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URISource.java new file mode 100644 index 0000000000..502169fccb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URISource.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.uri; + +import java.net.URI; + +/** + * @author Maciej Kucharczyk + */ +public class URISource { + private URI uriA; + + private URI invalidURI; + + public URI getUriA() { + return uriA; + } + + public void setUriA(URI uriA) { + this.uriA = uriA; + } + + public URI getInvalidURI() { + return invalidURI; + } + + public void setInvalidURI(URI invalidURI) { + this.invalidURI = invalidURI; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URITarget.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URITarget.java new file mode 100644 index 0000000000..b221392127 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/uri/URITarget.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.uri; + +/** + * @author Maciej Kucharczyk + */ +public class URITarget { + private String uriA; + private String invalidURI; + + public String getUriA() { + return uriA; + } + + public void setUriA(String uriA) { + this.uriA = uriA; + } + + public String getInvalidURI() { + return invalidURI; + } + + public void setInvalidURI(String invalidURI) { + this.invalidURI = invalidURI; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/Source.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/Source.java new file mode 100644 index 0000000000..ece0501a08 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/Source.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.url; + +import java.net.URL; + +/** + * @author Adam Szatyin + */ +public class Source { + private URL url; + + private URL invalidURL; + + public URL getURL() { + return url; + } + + public void setURL(URL url) { + this.url = url; + } + + public URL getInvalidURL() { + return invalidURL; + } + + public void setInvalidURL(URL invalidURL) { + this.invalidURL = invalidURL; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/Target.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/Target.java new file mode 100644 index 0000000000..060f5582e1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/Target.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.url; + +/** + * @author Adam Szatyin + */ +public class Target { + private String url; + private String invalidURL; + + public String getURL() { + return this.url; + } + + public void setURL(final String url) { + this.url = url; + } + + public String getInvalidURL() { + return this.invalidURL; + } + + public void setInvalidURL(final String invalidURL) { + this.invalidURL = invalidURL; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java new file mode 100644 index 0000000000..f5a3b7f7e8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java @@ -0,0 +1,76 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.url; + +import java.net.MalformedURLException; +import java.net.URI; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests conversions between {@link java.net.URL} and String. + * + * @author Adam Szatyin + */ +@WithClasses({ Source.class, Target.class, URLMapper.class }) +public class URLConversionTest { + + @ProcessorTest + public void shouldApplyURLConversion() throws MalformedURLException { + Source source = new Source(); + source.setURL( URI.create( "https://mapstruct.org/" ).toURL() ); + + Target target = URLMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getURL() ).isEqualTo( source.getURL().toString() ); + } + + @ProcessorTest + public void shouldApplyReverseURLConversion() throws MalformedURLException { + Target target = new Target(); + target.setURL( "https://mapstruct.org/" ); + + Source source = URLMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getURL() ).isEqualTo( URI.create( target.getURL() ).toURL() ); + } + + @ProcessorTest + public void shouldHandleInvalidURLString() { + Target target = new Target(); + target.setInvalidURL( "xxxxxx://mapstruct.org/" ); + + assertThatThrownBy( () -> URLMapper.INSTANCE.targetToSource( target ) ) + .isInstanceOf( RuntimeException.class ) + .getRootCause().isInstanceOf( MalformedURLException.class ); + } + + @ProcessorTest + public void shouldHandleInvalidURLStringWithMalformedURLException() { + Target target = new Target(); + target.setInvalidURL( "xxxxxx://mapstruct.org/" ); + + assertThatThrownBy( () -> URLMapper.INSTANCE.targetToSourceWithMalformedURLException( target ) ) + .isInstanceOf( MalformedURLException.class ); + } + + @ProcessorTest + public void shouldHandleNullURLString() { + Source source = new Source(); + + Target target = URLMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getURL() ).isNull(); + assertThat( target.getInvalidURL() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLMapper.java new file mode 100644 index 0000000000..e93c47726c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.conversion.url; + +import java.net.MalformedURLException; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface URLMapper { + + URLMapper INSTANCE = Mappers.getMapper( URLMapper.class ); + + Target sourceToTarget(Source source); + + Source targetToSource(Target target); + + Source targetToSourceWithMalformedURLException(Target target) throws MalformedURLException; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/AnnotatedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/AnnotatedMapper.java new file mode 100644 index 0000000000..6da0d70c93 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/AnnotatedMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +@DecoratedWith(AnnotatedMapperDecorator.class) +public interface AnnotatedMapper { + + AnnotatedMapper INSTANCE = Mappers.getMapper( AnnotatedMapper.class ); + + Target toTarget(Source source); + + class Source { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class Target { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/AnnotatedMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/AnnotatedMapperDecorator.java new file mode 100644 index 0000000000..6bf7a1fd66 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/AnnotatedMapperDecorator.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator; + +import org.mapstruct.AnnotateWith; + +@AnnotateWith(value = TestAnnotation.class, elements = @AnnotateWith.Element(strings = "decoratorValue")) +public abstract class AnnotatedMapperDecorator implements AnnotatedMapper { + + private final AnnotatedMapper delegate; + + public AnnotatedMapperDecorator(AnnotatedMapper delegate) { + this.delegate = delegate; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/DecoratedWithAnnotatedWithTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/DecoratedWithAnnotatedWithTest.java new file mode 100644 index 0000000000..1e3d574a6d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/DecoratedWithAnnotatedWithTest.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the application of @AnnotatedWith on decorator classes. + */ +@IssueKey("3659") +@WithClasses({ + TestAnnotation.class, + AnnotatedMapper.class, + AnnotatedMapperDecorator.class +}) +public class DecoratedWithAnnotatedWithTest { + + @ProcessorTest + public void shouldApplyAnnotationFromDecorator() { + Class implementationClass = AnnotatedMapper.INSTANCE.getClass(); + + assertThat( implementationClass ).hasAnnotation( TestAnnotation.class ); + assertThat( implementationClass.getAnnotation( TestAnnotation.class ).value() ).isEqualTo( "decoratorValue" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/TestAnnotation.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/TestAnnotation.java new file mode 100644 index 0000000000..742184d261 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/TestAnnotation.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test annotation for testing @AnnotatedWith on decorators. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestAnnotation { + String value() default ""; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaAnnotateWithMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaAnnotateWithMapper.java new file mode 100644 index 0000000000..a1a754158b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaAnnotateWithMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jakarta.annotatewith; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +/** + * A mapper using Jakarta component model with a decorator. + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA) +@DecoratedWith(JakartaAnnotateWithWithMapperDecorator.class) +public interface JakartaAnnotateWithMapper { + + /** + * Maps a person to a person DTO. + * + * @param person the person to map + * @return the person DTO + */ + @Mapping(target = "name", ignore = true) + PersonDto personToPersonDto(Person person); + + /** + * Maps an address to an address DTO. + * + * @param address the address to map + * @return the address DTO + */ + AddressDto addressToAddressDto(Address address); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaAnnotateWithWithMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaAnnotateWithWithMapperDecorator.java new file mode 100644 index 0000000000..89a42aaf6e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaAnnotateWithWithMapperDecorator.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jakarta.annotatewith; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.mapstruct.AnnotateWith; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.test.decorator.TestAnnotation; + +/** + * A decorator for {@link JakartaAnnotateWithMapper}. + */ +@AnnotateWith(value = TestAnnotation.class) +public abstract class JakartaAnnotateWithWithMapperDecorator implements JakartaAnnotateWithMapper { + + @Inject + @Named("org.mapstruct.ap.test.decorator.jakarta.annotatewith.JakartaAnnotateWithMapperImpl_") + private JakartaAnnotateWithMapper delegate; + + @Override + public PersonDto personToPersonDto(Person person) { + PersonDto dto = delegate.personToPersonDto( person ); + dto.setName( person.getFirstName() + " " + person.getLastName() ); + return dto; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaDecoratorAnnotateWithTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaDecoratorAnnotateWithTest.java new file mode 100644 index 0000000000..ac213d96a6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/annotatewith/JakartaDecoratorAnnotateWithTest.java @@ -0,0 +1,105 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jakarta.annotatewith; + +import java.util.Calendar; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.test.decorator.TestAnnotation; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the application of @AnnotateWith on decorator classes with Jakarta component model. + */ +@IssueKey("3659") +@WithClasses({ + Person.class, + PersonDto.class, + Address.class, + AddressDto.class, + JakartaAnnotateWithMapper.class, + TestAnnotation.class, + JakartaAnnotateWithWithMapperDecorator.class +}) +@ComponentScan(basePackageClasses = JakartaDecoratorAnnotateWithTest.class) +@Configuration +@WithJakartaInject +public class JakartaDecoratorAnnotateWithTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Inject + @Named + private JakartaAnnotateWithMapper jakartaAnnotateWithMapper; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldInvokeDecoratorMethods() { + Calendar birthday = Calendar.getInstance(); + birthday.set( 1928, Calendar.MAY, 23 ); + Person person = new Person( "Gary", "Crant", birthday.getTime(), new Address( "42 Ocean View Drive" ) ); + + PersonDto personDto = jakartaAnnotateWithMapper.personToPersonDto( person ); + + assertThat( personDto ).isNotNull(); + assertThat( personDto.getName() ).isEqualTo( "Gary Crant" ); + assertThat( personDto.getAddress() ).isNotNull(); + assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } + + @ProcessorTest + public void hasCorrectImports() { + // check the decorator + generatedSource.forMapper( JakartaAnnotateWithMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@TestAnnotation" ) + .contains( "@Singleton" + lineSeparator() + "@Named" ) + .doesNotContain( "javax.inject" ); + // check the plain mapper + generatedSource.forDecoratedMapper( JakartaAnnotateWithMapper.class ).content() + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Singleton" + lineSeparator() + "@Named" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/JakartaJsr330DecoratorTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/JakartaJsr330DecoratorTest.java new file mode 100644 index 0000000000..1d285af365 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/JakartaJsr330DecoratorTest.java @@ -0,0 +1,108 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jakarta.jsr330; + +import java.util.Calendar; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + Person.class, + PersonDto.class, + Address.class, + AddressDto.class, + PersonMapper.class, + PersonMapperDecorator.class +}) +@IssueKey("2567") +@ComponentScan(basePackageClasses = JakartaJsr330DecoratorTest.class) +@Configuration +@WithJakartaInject +public class JakartaJsr330DecoratorTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Inject + @Named + private PersonMapper personMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldInvokeDecoratorMethods() { + Calendar birthday = Calendar.getInstance(); + birthday.set( 1928, Calendar.MAY, 23 ); + Person person = new Person( "Gary", "Crant", birthday.getTime(), new Address( "42 Ocean View Drive" ) ); + + PersonDto personDto = personMapper.personToPersonDto( person ); + + assertThat( personDto ).isNotNull(); + assertThat( personDto.getName() ).isEqualTo( "Gary Crant" ); + assertThat( personDto.getAddress() ).isNotNull(); + assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } + + @ProcessorTest + public void shouldDelegateNonDecoratedMethodsToDefaultImplementation() { + Address address = new Address( "42 Ocean View Drive" ); + + AddressDto addressDto = personMapper.addressToAddressDto( address ); + + assertThat( addressDto ).isNotNull(); + assertThat( addressDto.getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } + + @ProcessorTest + public void hasCorrectImports() { + // check the decorator + generatedSource.forMapper( PersonMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Singleton" + lineSeparator() + "@Named" ) + .doesNotContain( "javax.inject" ); + // check the plain mapper + generatedSource.forDecoratedMapper( PersonMapper.class ).content() + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Singleton" + lineSeparator() + "@Named" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/PersonMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/PersonMapper.java new file mode 100644 index 0000000000..74ef38439f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/PersonMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jakarta.jsr330; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +@Mapper(componentModel = MappingConstants.ComponentModel.JSR330) +@DecoratedWith(PersonMapperDecorator.class) +public interface PersonMapper { + + @Mapping( target = "name", ignore = true ) + PersonDto personToPersonDto(Person person); + + AddressDto addressToAddressDto(Address address); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/PersonMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/PersonMapperDecorator.java new file mode 100644 index 0000000000..7642dc5d52 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jakarta/jsr330/PersonMapperDecorator.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jakarta.jsr330; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +public abstract class PersonMapperDecorator implements PersonMapper { + + @Inject + @Named("org.mapstruct.ap.test.decorator.jakarta.jsr330.PersonMapperImpl_") + private PersonMapper delegate; + + @Override + public PersonDto personToPersonDto(Person person) { + PersonDto dto = delegate.personToPersonDto( person ); + dto.setName( person.getFirstName() + " " + person.getLastName() ); + + return dto; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/Jsr330DecoratorTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/Jsr330DecoratorTest.java index d00368dfd4..008f8cfe12 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/Jsr330DecoratorTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/Jsr330DecoratorTest.java @@ -5,12 +5,6 @@ */ package org.mapstruct.ap.test.decorator.jsr330; -import java.util.Calendar; -import javax.inject.Inject; -import javax.inject.Named; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.mapstruct.ap.test.decorator.Address; import org.mapstruct.ap.test.decorator.AddressDto; @@ -19,14 +13,10 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.ap.testutil.runner.GeneratedSource; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; import static java.lang.System.lineSeparator; -import static org.assertj.core.api.Assertions.assertThat; /** * Test for the application of decorators using component model jsr330. @@ -42,55 +32,12 @@ PersonMapperDecorator.class }) @IssueKey("592") -@ComponentScan(basePackageClasses = Jsr330DecoratorTest.class) -@Configuration +@WithJavaxInject public class Jsr330DecoratorTest { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); - @Inject - @Named - private PersonMapper personMapper; - private ConfigurableApplicationContext context; - - @BeforeEach - public void springUp() { - context = new AnnotationConfigApplicationContext( getClass() ); - context.getAutowireCapableBeanFactory().autowireBean( this ); - } - - @AfterEach - public void springDown() { - if ( context != null ) { - context.close(); - } - } - - @ProcessorTest - public void shouldInvokeDecoratorMethods() { - Calendar birthday = Calendar.getInstance(); - birthday.set( 1928, Calendar.MAY, 23 ); - Person person = new Person( "Gary", "Crant", birthday.getTime(), new Address( "42 Ocean View Drive" ) ); - - PersonDto personDto = personMapper.personToPersonDto( person ); - - assertThat( personDto ).isNotNull(); - assertThat( personDto.getName() ).isEqualTo( "Gary Crant" ); - assertThat( personDto.getAddress() ).isNotNull(); - assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); - } - - @ProcessorTest - public void shouldDelegateNonDecoratedMethodsToDefaultImplementation() { - Address address = new Address( "42 Ocean View Drive" ); - - AddressDto addressDto = personMapper.addressToAddressDto( address ); - - assertThat( addressDto ).isNotNull(); - assertThat( addressDto.getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); - } - @IssueKey("664") @ProcessorTest public void hasSingletonAnnotation() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330AnnotateWithMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330AnnotateWithMapper.java new file mode 100644 index 0000000000..7e6ab8b7bd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330AnnotateWithMapper.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jsr330.annotatewith; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +/** + * A mapper using JSR-330 component model with a decorator. + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JSR330) +@DecoratedWith(Jsr330AnnotateWithMapperDecorator.class) +public interface Jsr330AnnotateWithMapper { + + @Mapping(target = "name", ignore = true) + PersonDto personToPersonDto(Person person); + + AddressDto addressToAddressDto(Address address); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330AnnotateWithMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330AnnotateWithMapperDecorator.java new file mode 100644 index 0000000000..96cee9f824 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330AnnotateWithMapperDecorator.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jsr330.annotatewith; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.test.decorator.TestAnnotation; + +/** + * A decorator for {@link Jsr330AnnotateWithMapper}. + */ +@AnnotateWith(value = TestAnnotation.class) +public abstract class Jsr330AnnotateWithMapperDecorator implements Jsr330AnnotateWithMapper { + + @Inject + @Named("org.mapstruct.ap.test.decorator.jsr330.annotatewith.Jsr330AnnotateWithMapperImpl_") + private Jsr330AnnotateWithMapper delegate; + + @Override + public PersonDto personToPersonDto(Person person) { + PersonDto dto = delegate.personToPersonDto( person ); + dto.setName( person.getFirstName() + " " + person.getLastName() ); + return dto; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330DecoratorAnnotateWithTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330DecoratorAnnotateWithTest.java new file mode 100644 index 0000000000..6cb6384f85 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/jsr330/annotatewith/Jsr330DecoratorAnnotateWithTest.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.jsr330.annotatewith; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.test.decorator.TestAnnotation; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * Test for the application of @AnnotateWith on decorator classes with JSR-330 component model. + */ +@IssueKey("3659") +@WithClasses({ + Person.class, + PersonDto.class, + Address.class, + AddressDto.class, + Jsr330AnnotateWithMapper.class, + Jsr330AnnotateWithMapperDecorator.class, + TestAnnotation.class +}) +@WithJavaxInject +public class Jsr330DecoratorAnnotateWithTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldContainCustomAnnotation() { + generatedSource.forMapper( Jsr330AnnotateWithMapper.class ) + .content() + .contains( "@TestAnnotation" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/AnnotateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/AnnotateMapper.java new file mode 100644 index 0000000000..0ba8e88299 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/AnnotateMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +@DecoratedWith(AnnotateMapperDecorator.class) +public interface AnnotateMapper { + + @Mapping(target = "name", ignore = true) + PersonDto personToPersonDto(Person person); + + AddressDto addressToAddressDto(Address address); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/AnnotateMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/AnnotateMapperDecorator.java new file mode 100644 index 0000000000..cff0a4c148 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/AnnotateMapperDecorator.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@AnnotateWith(value = Component.class, elements = @AnnotateWith.Element(strings = "decoratorComponent")) +@AnnotateWith(value = Primary.class) +public abstract class AnnotateMapperDecorator implements AnnotateMapper { + + @Autowired + @Qualifier("delegate") + private AnnotateMapper delegate; + + @Override + public PersonDto personToPersonDto(Person person) { + PersonDto dto = delegate.personToPersonDto( person ); + dto.setName( person.getFirstName() + " " + person.getLastName() ); + return dto; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomAnnotateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomAnnotateMapper.java new file mode 100644 index 0000000000..9e2e35676c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomAnnotateMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +@DecoratedWith(CustomAnnotateMapperDecorator.class) +public interface CustomAnnotateMapper { + + @Mapping(target = "name", ignore = true) + PersonDto personToPersonDto(Person person); + + AddressDto addressToAddressDto(Address address); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomAnnotateMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomAnnotateMapperDecorator.java new file mode 100644 index 0000000000..3668d98b04 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomAnnotateMapperDecorator.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +@AnnotateWith(value = CustomComponent.class, elements = @AnnotateWith.Element(strings = "customComponentDecorator")) +@AnnotateWith(value = CustomPrimary.class) +public abstract class CustomAnnotateMapperDecorator implements CustomAnnotateMapper { + + @Autowired + @Qualifier("delegate") + private CustomAnnotateMapper delegate; + + @Override + public PersonDto personToPersonDto(Person person) { + PersonDto dto = delegate.personToPersonDto( person ); + dto.setName( person.getFirstName() + " " + person.getLastName() ); + return dto; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomComponent.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomComponent.java new file mode 100644 index 0000000000..be71249f2a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomComponent.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface CustomComponent { + String value() default ""; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomPrimary.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomPrimary.java new file mode 100644 index 0000000000..4e18bdbc7d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/CustomPrimary.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Primary; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Primary +public @interface CustomPrimary { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/SpringDecoratorAnnotateWithTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/SpringDecoratorAnnotateWithTest.java new file mode 100644 index 0000000000..7b3911c22b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/annotatewith/SpringDecoratorAnnotateWithTest.java @@ -0,0 +1,118 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.annotatewith; + +import java.util.Calendar; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the application of @AnnotateWith on decorator classes with Spring component model. + */ +@IssueKey("3659") +@WithClasses({ + Person.class, + PersonDto.class, + Address.class, + AddressDto.class, + AnnotateMapper.class, + AnnotateMapperDecorator.class, + CustomComponent.class, + CustomPrimary.class, + CustomAnnotateMapper.class, + CustomAnnotateMapperDecorator.class +}) +@WithSpring +@ComponentScan(basePackageClasses = SpringDecoratorAnnotateWithTest.class) +@Configuration +public class SpringDecoratorAnnotateWithTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Autowired + private AnnotateMapper annotateMapper; + + @Autowired + private CustomAnnotateMapper customAnnotateMapper; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldNotDuplicateComponentAnnotation() { + generatedSource.forMapper( AnnotateMapper.class ) + .content() + .contains( "@Component(value = \"decoratorComponent\")", "@Primary" ) + .doesNotContain( "@Component" + System.lineSeparator() ); + } + + @ProcessorTest + public void shouldNotDuplicateCustomComponentAnnotation() { + generatedSource.forMapper( CustomAnnotateMapper.class ) + .content() + .contains( "@CustomComponent(value = \"customComponentDecorator\")", "@CustomPrimary" ) + .doesNotContain( "@Component" ); + } + + @ProcessorTest + public void shouldInvokeAnnotateDecoratorMethods() { + Calendar birthday = Calendar.getInstance(); + birthday.set( 1928, Calendar.MAY, 23 ); + Person person = new Person( "Gary", "Crant", birthday.getTime(), new Address( "42 Ocean View Drive" ) ); + + PersonDto personDto = annotateMapper.personToPersonDto( person ); + + assertThat( personDto ).isNotNull(); + assertThat( personDto.getName() ).isEqualTo( "Gary Crant" ); + assertThat( personDto.getAddress() ).isNotNull(); + assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } + + @ProcessorTest + public void shouldInvokeCustomAnnotateDecoratorMethods() { + Calendar birthday = Calendar.getInstance(); + birthday.set( 1928, Calendar.MAY, 23 ); + Person person = new Person( "Gary", "Crant", birthday.getTime(), new Address( "42 Ocean View Drive" ) ); + + PersonDto personDto = customAnnotateMapper.personToPersonDto( person ); + + assertThat( personDto ).isNotNull(); + assertThat( personDto.getName() ).isEqualTo( "Gary Crant" ); + assertThat( personDto.getAddress() ).isNotNull(); + assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/constructor/SpringDecoratorTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/constructor/SpringDecoratorTest.java index dc834683e3..6d18555372 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/constructor/SpringDecoratorTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/constructor/SpringDecoratorTest.java @@ -16,6 +16,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -40,6 +41,7 @@ @IssueKey("592") @ComponentScan(basePackageClasses = SpringDecoratorTest.class) @Configuration +@WithSpring public class SpringDecoratorTest { @Autowired diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/field/SpringDecoratorTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/field/SpringDecoratorTest.java index 1b81ed8c48..ac0af86381 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/field/SpringDecoratorTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/field/SpringDecoratorTest.java @@ -16,6 +16,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -37,6 +38,7 @@ PersonMapper.class, PersonMapperDecorator.class }) +@WithSpring @IssueKey("592") @ComponentScan(basePackageClasses = SpringDecoratorTest.class) @Configuration diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/PersonMapper.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/PersonMapper.java new file mode 100644 index 0000000000..20ac9d874c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/PersonMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.setter; + +import org.mapstruct.DecoratedWith; +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, injectionStrategy = InjectionStrategy.SETTER) +@DecoratedWith(PersonMapperDecorator.class) +public interface PersonMapper { + + @Mapping( target = "name", ignore = true ) + PersonDto personToPersonDto(Person person); + + AddressDto addressToAddressDto(Address address); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/PersonMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/PersonMapperDecorator.java new file mode 100644 index 0000000000..8f306284a1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/PersonMapperDecorator.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.setter; + +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +public abstract class PersonMapperDecorator implements PersonMapper { + + private PersonMapper decoratorDelegate; + + @Override + public PersonDto personToPersonDto(Person person) { + PersonDto dto = decoratorDelegate.personToPersonDto( person ); + dto.setName( person.getFirstName() + " " + person.getLastName() ); + + return dto; + } + + @Autowired + public void setDecoratorDelegate(@Qualifier("delegate") PersonMapper decoratorDelegate) { + this.decoratorDelegate = decoratorDelegate; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/SpringDecoratorTest.java b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/SpringDecoratorTest.java new file mode 100644 index 0000000000..c03b05395a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/decorator/spring/setter/SpringDecoratorTest.java @@ -0,0 +1,88 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.decorator.spring.setter; + +import java.util.Calendar; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mapstruct.ap.test.decorator.Address; +import org.mapstruct.ap.test.decorator.AddressDto; +import org.mapstruct.ap.test.decorator.Person; +import org.mapstruct.ap.test.decorator.PersonDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + Person.class, + PersonDto.class, + Address.class, + AddressDto.class, + PersonMapper.class, + PersonMapperDecorator.class +}) +@WithSpring +@IssueKey("3229") +@ComponentScan(basePackageClasses = SpringDecoratorTest.class) +@Configuration +public class SpringDecoratorTest { + + @Autowired + private PersonMapper personMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldInvokeDecoratorMethods() { + //given + Calendar birthday = Calendar.getInstance(); + birthday.set( 1928, Calendar.MAY, 23 ); + Person person = new Person( "Gary", "Crant", birthday.getTime(), new Address( "42 Ocean View Drive" ) ); + + //when + PersonDto personDto = personMapper.personToPersonDto( person ); + + //then + assertThat( personDto ).isNotNull(); + assertThat( personDto.getName() ).isEqualTo( "Gary Crant" ); + assertThat( personDto.getAddress() ).isNotNull(); + assertThat( personDto.getAddress().getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } + + @ProcessorTest + public void shouldDelegateNonDecoratedMethodsToDefaultImplementation() { + //given + Address address = new Address( "42 Ocean View Drive" ); + + //when + AddressDto addressDto = personMapper.addressToAddressDto( address ); + + //then + assertThat( addressDto ).isNotNull(); + assertThat( addressDto.getAddressLine() ).isEqualTo( "42 Ocean View Drive" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationClassNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationClassNameMapper.java index 9498b832f7..8dd010cae3 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationClassNameMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationClassNameMapper.java @@ -16,5 +16,5 @@ public abstract class AbstractDestinationClassNameMapper { public static final AbstractDestinationClassNameMapper INSTANCE = Mappers.getMapper( AbstractDestinationClassNameMapper.class ); - public abstract String intToString(Integer source); + public abstract Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationPackageNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationPackageNameMapper.java index b59d5cd21b..e26a6bbe71 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationPackageNameMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/AbstractDestinationPackageNameMapper.java @@ -16,5 +16,5 @@ public abstract class AbstractDestinationPackageNameMapper { public static final AbstractDestinationPackageNameMapper INSTANCE = Mappers.getMapper( AbstractDestinationPackageNameMapper.class ); - public abstract String intToString(Integer source); + public abstract Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapper.java index a331096a63..99bd8915a5 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapper.java @@ -15,5 +15,5 @@ public interface DestinationClassNameMapper { DestinationClassNameMapper INSTANCE = Mappers.getMapper( DestinationClassNameMapper.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorated.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorated.java index ef44767379..454e5e15bc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorated.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorated.java @@ -17,5 +17,5 @@ public interface DestinationClassNameMapperDecorated { DestinationClassNameMapperDecorated INSTANCE = Mappers.getMapper( DestinationClassNameMapperDecorated.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorator.java index 216786da24..3ba1433e21 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorator.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperDecorator.java @@ -16,7 +16,7 @@ protected DestinationClassNameMapperDecorator(DestinationClassNameMapperDecorate } @Override - public String intToString(Integer source) { - return delegate.intToString( source ); + public Target map(Integer source) { + return delegate.map( source ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfig.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfig.java index d76392d11d..d13675f0ab 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfig.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfig.java @@ -15,5 +15,5 @@ public interface DestinationClassNameMapperWithConfig { DestinationClassNameMapperWithConfig INSTANCE = Mappers.getMapper( DestinationClassNameMapperWithConfig.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfigOverride.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfigOverride.java index b218fff158..a3f845462b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfigOverride.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameMapperWithConfigOverride.java @@ -16,5 +16,5 @@ public interface DestinationClassNameMapperWithConfigOverride { DestinationClassNameMapperWithConfigOverride INSTANCE = Mappers.getMapper( DestinationClassNameMapperWithConfigOverride.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameTest.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameTest.java index e61bd1cb2c..1f6e255b95 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameTest.java @@ -7,6 +7,7 @@ import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.factory.Mappers; import static org.assertj.core.api.Assertions.assertThat; @@ -15,6 +16,7 @@ /** * @author Christophe Labouisse on 27/05/2015. */ +@WithClasses( Target.class ) public class DestinationClassNameTest { @ProcessorTest @WithClasses({ DestinationClassNameMapper.class }) @@ -25,6 +27,7 @@ public void shouldGenerateRightName() { @ProcessorTest @WithClasses({ DestinationClassNameWithJsr330Mapper.class }) + @WithJavaxInject public void shouldNotGenerateSpi() throws Exception { Class clazz = DestinationClassNameWithJsr330Mapper.class; diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameWithJsr330Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameWithJsr330Mapper.java index 92c72a877d..3358f68fd4 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameWithJsr330Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationClassNameWithJsr330Mapper.java @@ -13,5 +13,5 @@ */ @Mapper(implementationName = "Jsr330Impl", componentModel = MappingConstants.ComponentModel.JSR330) public interface DestinationClassNameWithJsr330Mapper { - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapper.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapper.java index 8c4740e323..c049036a13 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapper.java @@ -15,5 +15,5 @@ public interface DestinationPackageNameMapper { DestinationPackageNameMapper INSTANCE = Mappers.getMapper( DestinationPackageNameMapper.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorated.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorated.java index 1dd371b48a..98f6236027 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorated.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorated.java @@ -17,5 +17,5 @@ public interface DestinationPackageNameMapperDecorated { DestinationPackageNameMapperDecorated INSTANCE = Mappers.getMapper( DestinationPackageNameMapperDecorated.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorator.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorator.java index 4f6dd3242b..667bd99349 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorator.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperDecorator.java @@ -16,7 +16,7 @@ protected DestinationPackageNameMapperDecorator(DestinationPackageNameMapperDeco } @Override - public String intToString(Integer source) { - return delegate.intToString( source ); + public Target map(Integer source) { + return delegate.map( source ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfig.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfig.java index fbe69ce43e..6a5a870520 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfig.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfig.java @@ -15,5 +15,5 @@ public interface DestinationPackageNameMapperWithConfig { DestinationPackageNameMapperWithConfig INSTANCE = Mappers.getMapper( DestinationPackageNameMapperWithConfig.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfigOverride.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfigOverride.java index b53dcaae42..79699006b1 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfigOverride.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithConfigOverride.java @@ -16,5 +16,5 @@ public interface DestinationPackageNameMapperWithConfigOverride { DestinationPackageNameMapperWithConfigOverride INSTANCE = Mappers.getMapper( DestinationPackageNameMapperWithConfigOverride.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithSuffix.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithSuffix.java index 2a221e5f5b..068f30331d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithSuffix.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameMapperWithSuffix.java @@ -15,5 +15,5 @@ public interface DestinationPackageNameMapperWithSuffix { DestinationPackageNameMapperWithSuffix INSTANCE = Mappers.getMapper( DestinationPackageNameMapperWithSuffix.class ); - String intToString(Integer source); + Target map(Integer source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameTest.java b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameTest.java index 63c7ece6d5..5e02c88d93 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/DestinationPackageNameTest.java @@ -15,6 +15,7 @@ * @author Christophe Labouisse on 27/05/2015. */ @IssueKey( "556" ) +@WithClasses( Target.class ) public class DestinationPackageNameTest { @ProcessorTest @WithClasses({ DestinationPackageNameMapper.class }) diff --git a/processor/src/test/java/org/mapstruct/ap/test/destination/Target.java b/processor/src/test/java/org/mapstruct/ap/test/destination/Target.java new file mode 100644 index 0000000000..5b3bb4ba34 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/destination/Target.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.destination; + +public class Target { + + private final Integer source; + + public Target(Integer source) { + this.source = source; + } + + public Integer getSource() { + return source; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTarget.java b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTarget.java new file mode 100644 index 0000000000..f1b3a0f073 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTarget.java @@ -0,0 +1,9 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.emptytarget; + +public class EmptyTarget { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTargetMapper.java new file mode 100644 index 0000000000..8c82f81b7b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTargetMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.emptytarget; + +import org.mapstruct.Mapper; + +@Mapper +public interface EmptyTargetMapper { + + TargetWithNoSetters mapToTargetWithSetters(Source source); + + EmptyTarget mapToEmptyTarget(Source source); + + Target mapToTarget(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTargetTest.java b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTargetTest.java new file mode 100644 index 0000000000..66fa65b796 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/EmptyTargetTest.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.emptytarget; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +@IssueKey("1140") +@WithClasses({ + EmptyTarget.class, + EmptyTargetMapper.class, + Source.class, + Target.class, + TargetWithNoSetters.class, +}) +class EmptyTargetTest { + + @ProcessorTest + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = EmptyTargetMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 13, + message = "No target property found for target \"TargetWithNoSetters\"."), + @Diagnostic(type = EmptyTargetMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 15, + message = "No target property found for target \"EmptyTarget\".") + }) + void shouldProvideWarnings() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/emptytarget/Source.java b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/Source.java new file mode 100644 index 0000000000..643cdccf3a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/Source.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.emptytarget; + +public class Source { + private String label; + private double weight; + private Object content; + + public Object getContent() { + return content; + } + + public void setContent(Object content) { + this.content = content; + } + + public double getWeight() { + return weight; + } + + public void setWeight(double weight) { + this.weight = weight; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/emptytarget/Target.java b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/Target.java new file mode 100644 index 0000000000..488a1fa8a6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/Target.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.emptytarget; + +/** + * @author Filip Hrisafov + */ +public class Target { + + private final String label; + private final double weight; + private final Object content; + + public Target(String label, double weight, Object content) { + this.label = label; + this.weight = weight; + this.content = content; + } + + public String getLabel() { + return label; + } + + public double getWeight() { + return weight; + } + + public Object getContent() { + return content; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/emptytarget/TargetWithNoSetters.java b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/TargetWithNoSetters.java new file mode 100644 index 0000000000..edeb80f0d5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/emptytarget/TargetWithNoSetters.java @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.emptytarget; + +public class TargetWithNoSetters { + private int flightNumber; + private String airplaneName; + + public String getAirplaneName() { + return airplaneName; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/AbstractMapper.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/AbstractMapper.java new file mode 100644 index 0000000000..5155ac386c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/AbstractMapper.java @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +public interface AbstractMapper { + + TARGET map(SOURCE source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousMapper1.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousMapper1.java new file mode 100644 index 0000000000..95a0784934 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousMapper1.java @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +import org.mapstruct.Mapper; + +@Mapper +public interface ErroneousMapper1 extends AbstractMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousMapper2.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousMapper2.java new file mode 100644 index 0000000000..b75f1a0e73 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousMapper2.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +import org.mapstruct.Mapper; + +@Mapper +public interface ErroneousMapper2 extends AbstractMapper { + @Override + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousPropertyMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousPropertyMappingTest.java new file mode 100644 index 0000000000..147f1d6929 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/ErroneousPropertyMappingTest.java @@ -0,0 +1,51 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +@IssueKey( "598" ) +@WithClasses( { Source.class, Target.class, UnmappableClass.class, AbstractMapper.class } ) +public class ErroneousPropertyMappingTest { + + @ProcessorTest + @WithClasses( ErroneousMapper1.class ) + @IssueKey( "598" ) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapper1.class, + line = 11, + messageRegExp = "Can't map property \"UnmappableClass property\" to \"String property\"\\. " + + "Consider to declare/implement a mapping method: \"String map\\(UnmappableClass value\\)\"\\. " + + "Occured at 'TARGET map\\(SOURCE source\\)' in 'AbstractMapper'\\.") + } + ) + public void testUnmappableSourcePropertyInSuperclass() { + } + + @ProcessorTest + @WithClasses( ErroneousMapper2.class ) + @IssueKey( "598" ) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = ErroneousMapper2.class, + line = 13, + message = "Can't map property \"UnmappableClass property\" to \"String property\". " + + "Consider to declare/implement a mapping method: \"String map(UnmappableClass value)\".") } ) + public void testMethodOverride() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/Source.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/Source.java new file mode 100644 index 0000000000..383eb1dbe6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/Source.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +public class Source { + + private UnmappableClass property; + + public UnmappableClass getProperty() { + return property; + } + + public void setProperty(UnmappableClass property) { + this.property = property; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/Target.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/Target.java new file mode 100644 index 0000000000..f647ad7afa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/Target.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +public class Target { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/UnmappableClass.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/UnmappableClass.java new file mode 100644 index 0000000000..af239fb046 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/supermappingwithsubclassmapper/UnmappableClass.java @@ -0,0 +1,9 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.erroneous.supermappingwithsubclassmapper; + +public class UnmappableClass { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java index 548aecc1ec..bcb78d8dcc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java @@ -261,6 +261,20 @@ void shouldMapFromNestedMap() { assertThat( target.getNestedTarget().getStringFromNestedMap() ).isEqualTo( "valueFromNestedMap" ); } + @IssueKey("2553") + @ProcessorTest + @WithClasses(MapToBeanFromMapAndNestedMapWithDefinedMapping.class) + void shouldMapFromNestedMapWithDefinedMapping() { + + MapToBeanFromMapAndNestedMapWithDefinedMapping.Source source = + new MapToBeanFromMapAndNestedMapWithDefinedMapping.Source(); + MapToBeanFromMapAndNestedMapWithDefinedMapping.Target target = + MapToBeanFromMapAndNestedMapWithDefinedMapping.INSTANCE.toTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getNested() ).isEqualTo( "valueFromNestedMap" ); + } + @ProcessorTest @WithClasses(ObjectMapToBeanWithQualifierMapper.class) void shouldUseObjectQualifiedMethod() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMapWithDefinedMapping.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMapWithDefinedMapping.java new file mode 100644 index 0000000000..2203836ea6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanFromMapAndNestedMapWithDefinedMapping.java @@ -0,0 +1,57 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.frommap; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface MapToBeanFromMapAndNestedMapWithDefinedMapping { + + MapToBeanFromMapAndNestedMapWithDefinedMapping INSTANCE = Mappers.getMapper( + MapToBeanFromMapAndNestedMapWithDefinedMapping.class ); + + @Mapping(target = "nested", source = "nestedTarget.nested") + Target toTarget(Source source); + + class Source { + + private Map nestedTarget = new HashMap<>(); + + public Map getNestedTarget() { + return nestedTarget; + } + + public void setNestedTarget(Map nestedTarget) { + this.nestedTarget = nestedTarget; + } + + public Source() { + nestedTarget.put( "nested", "valueFromNestedMap" ); + } + } + + class Target { + + private String nested; + + public String getNested() { + return nested; + } + + public void setNested(String nested) { + this.nested = nested; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java index 9a6344ff15..225ca00adc 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanUsingMappingMethodMapper.java @@ -19,7 +19,7 @@ public interface MapToBeanUsingMappingMethodMapper { MapToBeanUsingMappingMethodMapper INSTANCE = Mappers.getMapper( MapToBeanUsingMappingMethodMapper.class ); - @Mapping(target = "normalInt", source = "number") + @Mapping(target = "normalInt", source = "source.number") Target toTarget(Map source); default String mapIntegerToString( Integer input ) { diff --git a/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java b/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java index e3a5b7c264..25489a5b6b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/gem/ConstantTest.java @@ -34,11 +34,15 @@ public void constantsShouldBeEqual() { } @Test - public void componentModelContantsShouldBeEqual() { + public void componentModelConstantsShouldBeEqual() { assertThat( MappingConstants.ComponentModel.DEFAULT ) .isEqualTo( MappingConstantsGem.ComponentModelGem.DEFAULT ); assertThat( MappingConstants.ComponentModel.CDI ).isEqualTo( MappingConstantsGem.ComponentModelGem.CDI ); assertThat( MappingConstants.ComponentModel.SPRING ).isEqualTo( MappingConstantsGem.ComponentModelGem.SPRING ); assertThat( MappingConstants.ComponentModel.JSR330 ).isEqualTo( MappingConstantsGem.ComponentModelGem.JSR330 ); + assertThat( MappingConstants.ComponentModel.JAKARTA ) + .isEqualTo( MappingConstantsGem.ComponentModelGem.JAKARTA ); + assertThat( MappingConstants.ComponentModel.JAKARTA_CDI ) + .isEqualTo( MappingConstantsGem.ComponentModelGem.JAKARTA_CDI ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java index 0c92fc6bbe..57c2099b8b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java @@ -10,13 +10,17 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.mapstruct.ClassAccessibility; import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.ConditionStrategy; import org.mapstruct.InjectionStrategy; import org.mapstruct.MappingInheritanceStrategy; import org.mapstruct.NullValueCheckStrategy; import org.mapstruct.NullValueMappingStrategy; import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; +import org.mapstruct.ap.internal.gem.ConditionStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MappingInheritanceStrategyGem; import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem; @@ -67,6 +71,18 @@ public void injectionStrategyGemIsCorrect() { namesOf( InjectionStrategyGem.values() ) ); } + @Test + public void conditionStrategyGemIsCorrect() { + assertThat( namesOf( ConditionStrategy.values() ) ).isEqualTo( + namesOf( ConditionStrategyGem.values() ) ); + } + + @Test + public void classAccesibilityGemIsCorrect() { + assertThat( namesOf( ClassAccessibility.values() ) ).isEqualTo( + namesOf( ClassAccessibilityGem.values() ) ); + } + private static List namesOf(Enum[] values) { return Stream.of( values ) .map( Enum::name ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/BoundCopyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/BoundCopyMapper.java new file mode 100644 index 0000000000..e3324ac737 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/BoundCopyMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import org.mapstruct.Mapper; + +@Mapper +public interface BoundCopyMapper { + + CollectionSuperTypes copySuperCollection(CollectionSuperTypes collectionSuperTypes); + + CollectionExtendTypes copyExtendsCollection(CollectionExtendTypes collectionExtendTypes); + + MapSuperType copySuperMap(MapSuperType mapSuperType); + + MapExtendType copyExtendMap(MapExtendType mapExtendType); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/BoundDirectCopyTest.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/BoundDirectCopyTest.java new file mode 100644 index 0000000000..d43d319fc3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/BoundDirectCopyTest.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * @author hduelme + * + */ +public class BoundDirectCopyTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource().addComparisonToFixtureFor( + BoundCopyMapper.class + ); + + @ProcessorTest + @WithClasses({ + SimpleObject.class, + CollectionSuperTypes.class, + CollectionExtendTypes.class, + MapSuperType.class, + MapExtendType.class, + BoundCopyMapper.class + }) + public void shouldCopyBoundedDirectly() { + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendToSuperMapper.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendToSuperMapper.java new file mode 100644 index 0000000000..db511deee4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendToSuperMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CollectionExtendToSuperMapper { + + CollectionExtendToSuperMapper INSTANCE = Mappers.getMapper( CollectionExtendToSuperMapper.class ); + + CollectionSuperTypes toSuper(CollectionExtendTypes extendTypes); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendTypes.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendTypes.java new file mode 100644 index 0000000000..c515950eda --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendTypes.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import java.util.Collection; + +public class CollectionExtendTypes { + + private Collection simpleObjectsCollection; + + public Collection getSimpleObjectsCollection() { + return simpleObjectsCollection; + } + + public void setSimpleObjectsCollection(Collection simpleObjectsCollection) { + this.simpleObjectsCollection = simpleObjectsCollection; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionSuperTypes.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionSuperTypes.java new file mode 100644 index 0000000000..5bb2f1e0da --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionSuperTypes.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import java.util.Collection; + +public class CollectionSuperTypes { + + private Collection simpleObjectsCollection; + + public Collection getSimpleObjectsCollection() { + return simpleObjectsCollection; + } + + public void setSimpleObjectsCollection(Collection simpleObjectsCollection) { + this.simpleObjectsCollection = simpleObjectsCollection; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/DistinguishBetweenSuperAndExtendTest.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/DistinguishBetweenSuperAndExtendTest.java new file mode 100644 index 0000000000..998943ce1a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/DistinguishBetweenSuperAndExtendTest.java @@ -0,0 +1,81 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import java.util.ArrayList; +import java.util.List; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses(SimpleObject.class) +public class DistinguishBetweenSuperAndExtendTest { + + @ProcessorTest + @WithClasses({ + CollectionExtendTypes.class, + CollectionSuperTypes.class, + ErroneousCollectionSuperToExtendMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousCollectionSuperToExtendMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 13, + message = "Can't map property " + + "\"Collection " + + "simpleObjectsCollection\" to " + + "\"Collection " + + "simpleObjectsCollection\". Consider to declare/implement a mapping method: " + + "\"Collection " + + "map(Collection" + + " value)\".") + }) + public void shouldFailOnSuperToExtendMappingForCollection() { + + } + + @ProcessorTest + @WithClasses(ErroneousStreamSuperToExtendMapper.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousStreamSuperToExtendMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 15, + message = "Can't map property " + + "\"Stream " + + "simpleObjectsStream\" to " + + "\"Stream " + + "simpleObjectsStream\". Consider to declare/implement a mapping method: " + + "\"Stream " + + "map(Stream" + + " value)\".") + }) + public void shouldFailOnSuperToExtendMappingForStream() { + + } + + @ProcessorTest + @WithClasses({ + CollectionExtendTypes.class, + CollectionSuperTypes.class, + CollectionExtendToSuperMapper.class + }) + public void shouldMapExtendBoundToSuperBound() { + CollectionExtendTypes collectionExtendTypes = new CollectionExtendTypes(); + List simpleObjects = new ArrayList<>(); + simpleObjects.add( new SimpleObject() ); + collectionExtendTypes.setSimpleObjectsCollection( simpleObjects ); + CollectionSuperTypes result = CollectionExtendToSuperMapper.INSTANCE.toSuper( collectionExtendTypes ); + assertThat( result ).isNotNull(); + assertThat( result.getSimpleObjectsCollection() ).hasSize( 1 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousCollectionSuperToExtendMapper.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousCollectionSuperToExtendMapper.java new file mode 100644 index 0000000000..90005c9fd5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousCollectionSuperToExtendMapper.java @@ -0,0 +1,14 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import org.mapstruct.Mapper; + +@Mapper +public interface ErroneousCollectionSuperToExtendMapper { + + CollectionExtendTypes map(CollectionSuperTypes types); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousStreamSuperToExtendMapper.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousStreamSuperToExtendMapper.java new file mode 100644 index 0000000000..8029c029c3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousStreamSuperToExtendMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import java.util.stream.Stream; + +import org.mapstruct.Mapper; + +@Mapper +public interface ErroneousStreamSuperToExtendMapper { + + ExtendStreamTypes map(SuperStreamTypes types); + + class ExtendStreamTypes { + private Stream simpleObjectsStream; + + public Stream getSimpleObjectsStream() { + return simpleObjectsStream; + } + + public void setSimpleObjectsStream(Stream simpleObjectsStream) { + this.simpleObjectsStream = simpleObjectsStream; + } + } + + class SuperStreamTypes { + + private Stream simpleObjectsStream; + + public Stream getSimpleObjectsStream() { + return simpleObjectsStream; + } + + public void setSimpleObjectsStream(Stream simpleObjectsStream) { + this.simpleObjectsStream = simpleObjectsStream; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/MapExtendType.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/MapExtendType.java new file mode 100644 index 0000000000..9bb43b43ac --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/MapExtendType.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import java.util.Map; + +public class MapExtendType { + + private Map simpleMap; + + public Map getSimpleMap() { + return simpleMap; + } + + public void setSimpleMap( + Map simpleMap) { + this.simpleMap = simpleMap; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/MapSuperType.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/MapSuperType.java new file mode 100644 index 0000000000..4172efa3de --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/MapSuperType.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +import java.util.Map; + +public class MapSuperType { + + private Map simpleMap; + + public Map getSimpleMap() { + return simpleMap; + } + + public void setSimpleMap( + Map simpleMap) { + this.simpleMap = simpleMap; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/SimpleObject.java b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/SimpleObject.java new file mode 100644 index 0000000000..b4c7da3d80 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/SimpleObject.java @@ -0,0 +1,9 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.generics.wildcard; + +public class SimpleObject { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/ErroneousSourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/ErroneousSourceTargetMapper.java new file mode 100644 index 0000000000..4ec3c64500 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/ErroneousSourceTargetMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignorebydefaultsource; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( + unmappedTargetPolicy = ReportingPolicy.IGNORE, + unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface ErroneousSourceTargetMapper { + ErroneousSourceTargetMapper INSTANCE = Mappers.getMapper( ErroneousSourceTargetMapper.class ); + + @Mapping(source = "one", target = "one") + Target sourceToTarget(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/ErroneousSourceTargetMapperWithIgnoreByDefault.java b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/ErroneousSourceTargetMapperWithIgnoreByDefault.java new file mode 100644 index 0000000000..459e2f426a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/ErroneousSourceTargetMapperWithIgnoreByDefault.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignorebydefaultsource; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper( + unmappedTargetPolicy = ReportingPolicy.IGNORE, + unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface ErroneousSourceTargetMapperWithIgnoreByDefault { + ErroneousSourceTargetMapperWithIgnoreByDefault INSTANCE = Mappers.getMapper( + ErroneousSourceTargetMapperWithIgnoreByDefault.class ); + + @Mapping(source = "one", target = "one") + @BeanMapping(ignoreByDefault = true) + Target sourceToTarget(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/IgnoreByDefaultSourcesTest.java b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/IgnoreByDefaultSourcesTest.java new file mode 100644 index 0000000000..fce6d49648 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/IgnoreByDefaultSourcesTest.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignorebydefaultsource; + +import javax.tools.Diagnostic.Kind; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +@IssueKey("2560") +public class IgnoreByDefaultSourcesTest { + + @ProcessorTest + @WithClasses({ ErroneousSourceTargetMapperWithIgnoreByDefault.class, Source.class, Target.class }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousSourceTargetMapperWithIgnoreByDefault.class, + kind = Kind.ERROR, + line = 23, + message = "Unmapped source property: \"other\".") + } + ) + public void shouldRaiseErrorDueToNonIgnoredSourcePropertyWithBeanMappingIgnoreByDefault() { + } + + @ProcessorTest + @WithClasses({ ErroneousSourceTargetMapper.class, Source.class, Target.class }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousSourceTargetMapper.class, + kind = Kind.ERROR, + line = 20, + message = "Unmapped source property: \"other\".") + } + ) + public void shouldRaiseErrorDueToNonIgnoredSourceProperty() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/Source.java b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/Source.java new file mode 100644 index 0000000000..81d6441d57 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/Source.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignorebydefaultsource; + +class Source { + private int one; + private int other; + + public int getOne() { + return one; + } + + public void setOne(int one) { + this.one = one; + } + + public int getOther() { + return other; + } + + public void setOther(int other) { + this.other = other; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/Target.java b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/Target.java new file mode 100644 index 0000000000..68ec5c8933 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignorebydefaultsource/Target.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignorebydefaultsource; + +class Target { + private int one; + + public int getOne() { + return one; + } + + public void setOne(int one) { + this.one = one; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/Animal.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/Animal.java new file mode 100644 index 0000000000..223f99f705 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/Animal.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +public class Animal { + + //CHECKSTYLE:OFF + public Integer publicAge; + public String publicColour; + //CHECKSTYLE:OFN + private String colour; + private String name; + private int size; + private Integer age; + + // private String colour; + public Animal() { + } + + public Animal(String name, int size, Integer age, String colour) { + this.name = name; + this.size = size; + this.publicAge = age; + this.age = age; + this.publicColour = colour; + this.colour = colour; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getColour() { + return colour; + } + + public void setColour( String colour ) { + this.colour = colour; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/AnimalDto.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/AnimalDto.java new file mode 100644 index 0000000000..1651f9b56b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/AnimalDto.java @@ -0,0 +1,63 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +public class AnimalDto { + + //CHECKSTYLE:OFF + public Integer publicAge; + public String publicColor; + //CHECKSTYLE:ON + private String name; + private Integer size; + private Integer age; + private String color; + + public AnimalDto() { + + } + + public AnimalDto(String name, Integer size, Integer age, String color) { + this.name = name; + this.size = size; + this.publicAge = age; + this.age = age; + this.publicColor = color; + this.color = color; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/AnimalMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/AnimalMapper.java new file mode 100644 index 0000000000..2753b36070 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/AnimalMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +import org.mapstruct.Ignored; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface AnimalMapper { + + AnimalMapper INSTANCE = Mappers.getMapper( AnimalMapper.class ); + + @Ignored( targets = { "publicAge", "age", "publicColor", "color" } ) + AnimalDto animalToDto( Animal animal ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/ErroneousMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/ErroneousMapper.java new file mode 100644 index 0000000000..b113e715aa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/ErroneousMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +import org.mapstruct.Ignored; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ErroneousMapper { + + ErroneousMapper INSTANCE = Mappers.getMapper( ErroneousMapper.class ); + + @Mapping(target = "name", ignore = true) + @Ignored(targets = { "name", "color", "publicColor" }) + AnimalDto ignoredAndMappingAnimalToDto( Animal animal ); + + @Mapping(target = "publicColor", source = "publicColour") + @Ignored(targets = { "publicColor", "color" }) + AnimalDto ignoredAndMappingAnimalToDtoMap( Animal animal ); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/IgnoredPropertyTest.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/IgnoredPropertyTest.java new file mode 100644 index 0000000000..bc6fc2f6b7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/IgnoredPropertyTest.java @@ -0,0 +1,102 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for ignoring properties during the mapping. + * + * @author Ivashin Aleksey + */ +@WithClasses({ Animal.class, AnimalDto.class, Zoo.class, ZooDto.class, ZooMapper.class}) +public class IgnoredPropertyTest { + + @ProcessorTest + @IssueKey("1958") + @WithClasses( { AnimalMapper.class } ) + public void shouldNotPropagateIgnoredPropertyGivenViaTargetAttribute() { + Animal animal = new Animal( "Bruno", 100, 23, "black" ); + + AnimalDto animalDto = AnimalMapper.INSTANCE.animalToDto( animal ); + + assertThat( animalDto ).isNotNull(); + assertThat( animalDto.getName() ).isEqualTo( "Bruno" ); + assertThat( animalDto.getSize() ).isEqualTo( 100 ); + assertThat( animalDto.getAge() ).isNull(); + assertThat( animalDto.publicAge ).isNull(); + assertThat( animalDto.getColor() ).isNull(); + assertThat( animalDto.publicColor ).isNull(); + } + + @ProcessorTest + @IssueKey("1958") + @WithClasses( { ErroneousMapper.class } ) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "Target property \"name\" must not be mapped more than once." ), + @Diagnostic(type = ErroneousMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 24, + message = "Target property \"publicColor\" must not be mapped more than once." ) + } + ) + public void shouldFailToGenerateMappings() { + } + + @ProcessorTest + @IssueKey("1958") + @WithClasses( { AnimalMapper.class } ) + public void shouldNotPropagateIgnoredInnerPropertyGivenViaTargetAttribute() { + Animal animal = new Animal( "Bruno", 100, 23, "black" ); + Zoo zoo = new Zoo(animal, "Test name", "test address"); + + ZooDto zooDto = ZooMapper.INSTANCE.zooToDto( zoo ); + + assertThat( zooDto ).isNotNull(); + assertThat( zooDto.getName() ).isEqualTo( "Test name" ); + assertThat( zooDto.getAddress() ).isEqualTo( "test address" ); + assertThat( zooDto.getAnimal() ).isNotNull(); + assertThat( zooDto.getAnimal().getName() ).isEqualTo( "Bruno" ); + assertThat( zooDto.getAnimal().getAge() ).isNull(); + assertThat( zooDto.getAnimal().publicAge ).isNull(); + assertThat( zooDto.getAnimal().getColor() ).isNull(); + assertThat( zooDto.getAnimal().publicColor ).isNull(); + assertThat( zooDto.getAnimal().getSize() ).isNull(); + } + + @ProcessorTest + @IssueKey("1958") + @WithClasses( { AnimalMapper.class } ) + public void shouldNotPropagateIgnoredInnerPropertyGivenViaTargetAttribute2() { + Animal animal = new Animal( "Bruno", 100, 23, "black" ); + Zoo zoo = new Zoo(animal, "Test name", "test address"); + + ZooDto zooDto = ZooMapper.INSTANCE.zooToDto2( zoo ); + + assertThat( zooDto ).isNotNull(); + assertThat( zooDto.getName() ).isEqualTo( "Test name" ); + assertThat( zooDto.getAddress() ).isNull(); + assertThat( zooDto.getAnimal() ).isNotNull(); + assertThat( zooDto.getAnimal().getName() ).isEqualTo( "Bruno" ); + assertThat( zooDto.getAnimal().getAge() ).isNull(); + assertThat( zooDto.getAnimal().publicAge ).isNull(); + assertThat( zooDto.getAnimal().getColor() ).isNull(); + assertThat( zooDto.getAnimal().publicColor ).isNull(); + assertThat( zooDto.getAnimal().getSize() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/Zoo.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/Zoo.java new file mode 100644 index 0000000000..377e03b877 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/Zoo.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +public class Zoo { + + private Animal animal; + + private String name; + + private String address; + + public Zoo() { + } + + public Zoo(Animal animal, String name, String address ) { + this.animal = animal; + this.name = name; + this.address = address; + } + + public Animal getAnimal() { + return animal; + } + + public void setAnimal(Animal animal) { + this.animal = animal; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooDto.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooDto.java new file mode 100644 index 0000000000..7c062cab4c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooDto.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +public class ZooDto { + + private AnimalDto animal; + + private String name; + + private String address; + + public ZooDto() { + } + + public ZooDto(AnimalDto animal, String name, String address) { + this.animal = animal; + this.name = name; + this.address = address; + } + + public AnimalDto getAnimal() { + return animal; + } + + public void setAnimal(AnimalDto animal) { + this.animal = animal; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooMapper.java new file mode 100644 index 0000000000..f05e045b5f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignored; + +import org.mapstruct.Ignored; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ZooMapper { + + ZooMapper INSTANCE = Mappers.getMapper( ZooMapper.class ); + + @Ignored( prefix = "animal", targets = { "publicAge", "size", "publicColor", "age", "color" } ) + ZooDto zooToDto( Zoo zoo ); + + @Ignored( targets = { "address" } ) + @Ignored( prefix = "animal", targets = { "publicAge", "size", "publicColor", "age", "color" } ) + ZooDto zooToDto2( Zoo zoo ); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/IgnoredMappedPropertyTest.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/IgnoredMappedPropertyTest.java new file mode 100644 index 0000000000..c50fddd2f7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/IgnoredMappedPropertyTest.java @@ -0,0 +1,136 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped; + +import javax.tools.Diagnostic.Kind; + +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapper; +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapperWithIgnorePolicyInMapperConfig; +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapperWithIgnoreSourcePolicy; +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapperWithWarnPolicyInMapperConfig; +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapperWithWarnSourcePolicy; +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapperWithWarnSourcePolicyInMapper; +import org.mapstruct.ap.test.ignoreunmapped.mapper.UserMapperWithoutBeanMapping; +import org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous.UserMapperWithErrorPolicyInMapperConfig; +import org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous.UserMapperWithErrorSourcePolicy; +import org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous.UserMapperWithErrorSourcePolicyInMapper; +import org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous.UserMapperWithMultiMapping; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +/** + * Verifies that mapped properties listed in ignoreUnmappedSourceProperties trigger a warning. + * + * @author Ritesh Chopade(codeswithritesh) + */ +@IssueKey("3837") +@WithClasses({UserEntity.class, UserDto.class}) +public class IgnoredMappedPropertyTest { + + @ProcessorTest + @WithClasses({ + UserMapper.class, + UserMapperWithIgnoreSourcePolicy.class, + UserMapperWithoutBeanMapping.class, + UserMapperWithIgnorePolicyInMapperConfig.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED + ) + public void shouldNotWarnAboutRedundantIgnore() { + } + + @ProcessorTest + @WithClasses({ + UserMapperWithErrorSourcePolicy.class, + UserMapperWithErrorSourcePolicyInMapper.class, + UserMapperWithErrorPolicyInMapperConfig.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = UserMapperWithErrorSourcePolicy.class, + kind = Kind.ERROR, + line = 20, + message = "Source property \"email\" is mapped despite being " + + "listed in ignoreUnmappedSourceProperties." + ), + @Diagnostic( + type = UserMapperWithErrorSourcePolicyInMapper.class, + kind = Kind.ERROR, + line = 19, + message = "Source property \"email\" is mapped despite being " + + "listed in ignoreUnmappedSourceProperties." + ), + @Diagnostic( + type = UserMapperWithErrorPolicyInMapperConfig.class, + kind = Kind.ERROR, + line = 23, + message = "Source property \"email\" is mapped despite being " + + "listed in ignoreUnmappedSourceProperties." + ) + } + ) + public void shouldWarnAboutRedundantIgnoreWithErrorPolicy() { + } + + @ProcessorTest + @WithClasses({ + UserMapperWithWarnSourcePolicy.class, + UserMapperWithWarnSourcePolicyInMapper.class, + UserMapperWithWarnPolicyInMapperConfig.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + type = UserMapperWithWarnSourcePolicy.class, + kind = Kind.WARNING, + line = 20, + message = "Source property \"email\" is mapped despite being " + + "listed in ignoreUnmappedSourceProperties." + ), + @Diagnostic( + type = UserMapperWithWarnSourcePolicyInMapper.class, + kind = Kind.WARNING, + line = 19, + message = "Source property \"email\" is mapped despite being " + + "listed in ignoreUnmappedSourceProperties." + ), + @Diagnostic( + type = UserMapperWithWarnPolicyInMapperConfig.class, + kind = Kind.WARNING, + line = 23, + message = "Source property \"email\" is mapped despite being " + + "listed in ignoreUnmappedSourceProperties." + ) + } + ) + public void shouldWarnAboutRedundantIgnoreWithWarnPolicy() { + } + + @ProcessorTest + @WithClasses({UserMapperWithMultiMapping.class}) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = UserMapperWithMultiMapping.class, + kind = Kind.ERROR, + line = 21, + message = "Source properties \"email, username\" are mapped despite " + + "being listed in ignoreUnmappedSourceProperties." + ) + } + ) + public void shouldWarnAboutRedundantIgnoreWithMultiMapping() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/UserDto.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/UserDto.java new file mode 100644 index 0000000000..f153bbe8a7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/UserDto.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped; + +/** + * @author Ritesh Chopade(codeswithritesh) + */ +public class UserDto { + + private String username; + private String email; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/UserEntity.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/UserEntity.java new file mode 100644 index 0000000000..692cc4aa89 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/UserEntity.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped; + +/** + * @author Ritesh Chopade(codeswithritesh) + */ +public class UserEntity { + + private String username; + private String email; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapper.java new file mode 100644 index 0000000000..bcb23677e4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper +public interface UserMapper { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithIgnorePolicyInMapperConfig.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithIgnorePolicyInMapperConfig.java new file mode 100644 index 0000000000..f73e0bc28c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithIgnorePolicyInMapperConfig.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MapperConfig; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@MapperConfig(unmappedSourcePolicy = ReportingPolicy.IGNORE) +interface IgnorePolicyConfig { } + +@Mapper(config = IgnorePolicyConfig.class) +public interface UserMapperWithIgnorePolicyInMapperConfig { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithIgnoreSourcePolicy.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithIgnoreSourcePolicy.java new file mode 100644 index 0000000000..d844f1229a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithIgnoreSourcePolicy.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper +public interface UserMapperWithIgnoreSourcePolicy { + + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}, unmappedSourcePolicy = ReportingPolicy.IGNORE) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnPolicyInMapperConfig.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnPolicyInMapperConfig.java new file mode 100644 index 0000000000..37184ea0d5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnPolicyInMapperConfig.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MapperConfig; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@MapperConfig(unmappedSourcePolicy = ReportingPolicy.WARN) +interface WarnPolicyConfig { } + +@Mapper(config = WarnPolicyConfig.class) +public interface UserMapperWithWarnPolicyInMapperConfig { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnSourcePolicy.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnSourcePolicy.java new file mode 100644 index 0000000000..5a67934114 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnSourcePolicy.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper +public interface UserMapperWithWarnSourcePolicy { + + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}, unmappedSourcePolicy = ReportingPolicy.WARN) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnSourcePolicyInMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnSourcePolicyInMapper.java new file mode 100644 index 0000000000..2f34fc336f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithWarnSourcePolicyInMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper(unmappedSourcePolicy = ReportingPolicy.WARN) +public interface UserMapperWithWarnSourcePolicyInMapper { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithoutBeanMapping.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithoutBeanMapping.java new file mode 100644 index 0000000000..10c9065bdf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/UserMapperWithoutBeanMapping.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper +public interface UserMapperWithoutBeanMapping { + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorPolicyInMapperConfig.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorPolicyInMapperConfig.java new file mode 100644 index 0000000000..0d25facd69 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorPolicyInMapperConfig.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MapperConfig; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@MapperConfig(unmappedSourcePolicy = ReportingPolicy.ERROR) +interface ErrorPolicyConfig { } + +@Mapper(config = ErrorPolicyConfig.class) +public interface UserMapperWithErrorPolicyInMapperConfig { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorSourcePolicy.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorSourcePolicy.java new file mode 100644 index 0000000000..db658b3636 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorSourcePolicy.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper +public interface UserMapperWithErrorSourcePolicy { + + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}, unmappedSourcePolicy = ReportingPolicy.ERROR) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorSourcePolicyInMapper.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorSourcePolicyInMapper.java new file mode 100644 index 0000000000..27a867ca61 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithErrorSourcePolicyInMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR) +public interface UserMapperWithErrorSourcePolicyInMapper { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email"}) + @Mapping(source = "email", target = "email") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithMultiMapping.java b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithMultiMapping.java new file mode 100644 index 0000000000..388c00de2c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/ignoreunmapped/mapper/erroneous/UserMapperWithMultiMapping.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.ignoreunmapped.mapper.erroneous; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.ignoreunmapped.UserDto; +import org.mapstruct.ap.test.ignoreunmapped.UserEntity; + +@Mapper +public interface UserMapperWithMultiMapping { + @BeanMapping(ignoreUnmappedSourceProperties = {"password", "email", "username"}, + unmappedSourcePolicy = ReportingPolicy.ERROR) + @Mapping(source = "email", target = "email") + @Mapping(source = "username", target = "username") + UserDto map(UserEntity user); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/ConflictingTypesNamesTest.java b/processor/src/test/java/org/mapstruct/ap/test/imports/ConflictingTypesNamesTest.java index d09ddf66a2..8d61eb8f8a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/imports/ConflictingTypesNamesTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/ConflictingTypesNamesTest.java @@ -15,6 +15,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.ap.testutil.runner.GeneratedSource; import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +42,7 @@ org.mapstruct.ap.test.imports.to.FooWrapper.class, SecondSourceTargetMapper.class }) +@WithJavaxInject public class ConflictingTypesNamesTest { @RegisterExtension diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/InnerClassesImportsTest.java b/processor/src/test/java/org/mapstruct/ap/test/imports/InnerClassesImportsTest.java index 91e5df6a27..abad5e4177 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/imports/InnerClassesImportsTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/InnerClassesImportsTest.java @@ -14,7 +14,6 @@ import org.mapstruct.ap.test.imports.innerclasses.SourceWithInnerClass; import org.mapstruct.ap.test.imports.innerclasses.SourceWithInnerClass.SourceInnerClass; import org.mapstruct.ap.test.imports.innerclasses.TargetWithInnerClass; -import org.mapstruct.ap.test.imports.innerclasses.TargetWithInnerClass.TargetInnerClass; import org.mapstruct.ap.test.imports.innerclasses.TargetWithInnerClass.TargetInnerClass.TargetInnerInnerClass; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; @@ -47,8 +46,7 @@ public void mapperRequiresInnerClassImports() { assertThat( target ).isNotNull(); assertThat( target.getInnerClassMember().getValue() ).isEqualTo( 412 ); - generatedSource.forMapper( InnerClassMapper.class ).containsImportFor( SourceInnerClass.class ); - generatedSource.forMapper( InnerClassMapper.class ).containsImportFor( TargetInnerClass.class ); + generatedSource.addComparisonToFixtureFor( InnerClassMapper.class ); } @ProcessorTest @@ -61,8 +59,7 @@ public void mapperRequiresInnerInnerClassImports() { assertThat( target ).isNotNull(); assertThat( target.getValue() ).isEqualTo( 412 ); - generatedSource.forMapper( InnerClassMapper.class ).containsImportFor( SourceInnerClass.class ); - generatedSource.forMapper( InnerClassMapper.class ).containsImportFor( TargetInnerInnerClass.class ); + generatedSource.addComparisonToFixtureFor( InnerClassMapper.class ); } @ProcessorTest @@ -82,6 +79,6 @@ public void mapperRequiresInnerEnumImports() { assertThat( sourceAgain ).isNotNull(); assertThat( sourceAgain.getInnerEnum() ).isEqualTo( InnerEnum.A ); - generatedSource.forMapper( BeanWithInnerEnumMapper.class ).containsImportFor( InnerEnum.class ); + generatedSource.addComparisonToFixtureFor( BeanWithInnerEnumMapper.class ); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/imports/SourceTargetMapper.java index 1380ca400a..845544ba9f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/imports/SourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/SourceTargetMapper.java @@ -25,7 +25,7 @@ public interface SourceTargetMapper { ParseException sourceToTarget(Named source); //custom types - Map listToMap(List list); + Value listToMap(List list); java.util.List namedsToExceptions(java.util.List source); @@ -36,4 +36,12 @@ public interface SourceTargetMapper { java.util.Map stringsToDates(java.util.Map stringDates); Target sourceToTarget(Source target); + + class Value { + private T value; + + public Value(List list) { + + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/NestedImportsTest.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/NestedImportsTest.java new file mode 100644 index 0000000000..37c522e4cd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/NestedImportsTest.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.imports.nested.other.SourceInOtherPackage; +import org.mapstruct.ap.test.imports.nested.other.TargetInOtherPackage; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + SourceInOtherPackage.class, + TargetInOtherPackage.class, + Source.class, + Target.class +}) +@IssueKey("1386,148") +class NestedImportsTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses( { + SourceInOtherPackageMapper.class + } ) + void shouldGenerateNestedInnerClassesForSourceInOtherPackage() { + generatedSource.addComparisonToFixtureFor( SourceInOtherPackageMapper.class ); + } + + @ProcessorTest + @WithClasses( { + NestedSourceInOtherPackageMapper.class + } ) + void shouldGenerateCorrectImportsForTopLevelClassesFromOnlyNestedInnerClasses() { + generatedSource.addComparisonToFixtureFor( NestedSourceInOtherPackageMapper.class ); + } + + @ProcessorTest + @WithClasses( { + TargetInOtherPackageMapper.class + } ) + void shouldGenerateNestedInnerClassesForTargetInOtherPackage() { + generatedSource.addComparisonToFixtureFor( TargetInOtherPackageMapper.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/NestedSourceInOtherPackageMapper.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/NestedSourceInOtherPackageMapper.java new file mode 100644 index 0000000000..7f98737d1d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/NestedSourceInOtherPackageMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.imports.nested.other.SourceInOtherPackage; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface NestedSourceInOtherPackageMapper { + + Target.Nested map(SourceInOtherPackage.Nested source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/Source.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/Source.java new file mode 100644 index 0000000000..2d2e9aa215 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/Source.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested; + +/** + * @author Filip Hrisafov + */ +public class Source { + + private Nested value; + + public Nested getValue() { + return value; + } + + public void setValue(Nested value) { + this.value = value; + } + + public static class Nested { + + private Inner inner; + + public static class Inner { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public Inner getInner() { + return inner; + } + + public void setInner(Inner inner) { + this.inner = inner; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/SourceInOtherPackageMapper.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/SourceInOtherPackageMapper.java new file mode 100644 index 0000000000..7150823c1a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/SourceInOtherPackageMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.imports.nested.other.SourceInOtherPackage; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface SourceInOtherPackageMapper { + + Target map(SourceInOtherPackage source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/Target.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/Target.java new file mode 100644 index 0000000000..78a50fcb15 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/Target.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested; + +/** + * @author Filip Hrisafov + */ +public class Target { + + private Nested value; + + public Nested getValue() { + return value; + } + + public void setValue(Nested value) { + this.value = value; + } + + public static class Nested { + + private Inner inner; + + public static class Inner { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public Inner getInner() { + return inner; + } + + public void setInner(Inner inner) { + this.inner = inner; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/TargetInOtherPackageMapper.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/TargetInOtherPackageMapper.java new file mode 100644 index 0000000000..a40d52eb1c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/TargetInOtherPackageMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.imports.nested.other.TargetInOtherPackage; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface TargetInOtherPackageMapper { + + TargetInOtherPackage map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/other/SourceInOtherPackage.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/other/SourceInOtherPackage.java new file mode 100644 index 0000000000..f6646c7160 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/other/SourceInOtherPackage.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested.other; + +/** + * @author Filip Hrisafov + */ +public class SourceInOtherPackage { + + private Nested value; + + public Nested getValue() { + return value; + } + + public void setValue(Nested value) { + this.value = value; + } + + public static class Nested { + + private Inner inner; + + public static class Inner { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public Inner getInner() { + return inner; + } + + public void setInner(Inner inner) { + this.inner = inner; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/imports/nested/other/TargetInOtherPackage.java b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/other/TargetInOtherPackage.java new file mode 100644 index 0000000000..da6b596c06 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/imports/nested/other/TargetInOtherPackage.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.imports.nested.other; + +/** + * @author Filip Hrisafov + */ +public class TargetInOtherPackage { + + private Nested value; + + public Nested getValue() { + return value; + } + + public void setValue(Nested value) { + this.value = value; + } + + public static class Nested { + + private Inner inner; + + public static class Inner { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public Inner getInner() { + return inner; + } + + public void setInner(Inner inner) { + this.inner = inner; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/inheritance/InheritanceTest.java b/processor/src/test/java/org/mapstruct/ap/test/inheritance/InheritanceTest.java index d127a9ac11..aaf299635e 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/inheritance/InheritanceTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/inheritance/InheritanceTest.java @@ -53,6 +53,20 @@ public void shouldMapAttributeFromSuperTypeUsingReturnedTargetParameter() { assertResult( target ); } + @ProcessorTest + @IssueKey("1752") + public void shouldMapAttributeFromSuperTypeUsingReturnedTargetParameterAndNullSource() { + + TargetExt target = new TargetExt(); + target.setFoo( 42L ); + target.publicFoo = 52L; + target.setBar( 23 ); + TargetBase result = SourceTargetMapper.INSTANCE.sourceToTargetWithTargetParameterAndReturn( null, target ); + + assertThat( target ).isSameAs( result ); + assertResult( target ); + } + private void assertResult(TargetExt target) { assertThat( target ).isNotNull(); assertThat( target.getFoo() ).isEqualTo( Long.valueOf( 42 ) ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/inheritfromconfig/multiple/CarMapper.java b/processor/src/test/java/org/mapstruct/ap/test/inheritfromconfig/multiple/CarMapper.java index 63333f2824..feee6e27d2 100755 --- a/processor/src/test/java/org/mapstruct/ap/test/inheritfromconfig/multiple/CarMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/inheritfromconfig/multiple/CarMapper.java @@ -5,11 +5,10 @@ */ package org.mapstruct.ap.test.inheritfromconfig.multiple; +import org.mapstruct.InheritConfiguration; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; import org.mapstruct.Mapping; - -import org.mapstruct.InheritConfiguration; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/CdiDefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/CdiDefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..bf8b292ec7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/CdiDefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi._default; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithCdi; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jakarta-cdi. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2950") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerCdiDefaultCompileOptionFieldMapper.class, + GenderCdiDefaultCompileOptionFieldMapper.class +}) +@WithCdi +public class CdiDefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveFieldInjection() { + generatedSource.forMapper( CustomerCdiDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import javax.enterprise.context.ApplicationScoped;" ) + .contains( "import javax.inject.Inject;" ) + .contains( "@Inject" + lineSeparator() + " private GenderCdiDefaultCompileOptionFieldMapper" ) + .contains( "@ApplicationScoped" + lineSeparator() + "public class" ) + .doesNotContain( "public CustomerCdiDefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "jakarta.inject" ) + .doesNotContain( "jakarta.enterprise" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/CustomerCdiDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/CustomerCdiDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..ed0ed0a76e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/CustomerCdiDefaultCompileOptionFieldMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi._default; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.CDI, + uses = GenderCdiDefaultCompileOptionFieldMapper.class) +public interface CustomerCdiDefaultCompileOptionFieldMapper { + + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/GenderCdiDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/GenderCdiDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..14d6a2e0d7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/_default/GenderCdiDefaultCompileOptionFieldMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi._default; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.CDI) +public interface GenderCdiDefaultCompileOptionFieldMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/CustomerCdiDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/CustomerCdiDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..4c893b7486 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/CustomerCdiDefaultCompileOptionFieldMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi.jakarta; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.CDI, + uses = GenderCdiDefaultCompileOptionFieldMapper.class) +public interface CustomerCdiDefaultCompileOptionFieldMapper { + + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/GenderCdiDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/GenderCdiDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..8664b0b433 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/GenderCdiDefaultCompileOptionFieldMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi.jakarta; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.CDI) +public interface GenderCdiDefaultCompileOptionFieldMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..55480399a3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi.jakarta; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithCdi; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaCdi; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jsr330. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2950") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerCdiDefaultCompileOptionFieldMapper.class, + GenderCdiDefaultCompileOptionFieldMapper.class +}) +@WithJakartaCdi +@WithCdi +public class JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveCdiInjection() { + generatedSource.forMapper( CustomerCdiDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import javax.enterprise.context.ApplicationScoped;" ) + .contains( "import javax.inject.Inject;" ) + .contains( "@Inject" + lineSeparator() + " private GenderCdiDefaultCompileOptionFieldMapper" ) + .contains( "@ApplicationScoped" + lineSeparator() + "public class" ) + .doesNotContain( "public CustomerCdiDefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "jakarta.inject" ) + .doesNotContain( "jakarta.enterprise" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/JakartaCdiCdiDefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/JakartaCdiCdiDefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..953747e404 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/cdi/jakarta/JakartaCdiCdiDefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.cdi.jakarta; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaCdi; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jsr330. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2950") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerCdiDefaultCompileOptionFieldMapper.class, + GenderCdiDefaultCompileOptionFieldMapper.class +}) +@WithJakartaCdi +public class JakartaCdiCdiDefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaCdiInjection() { + generatedSource.forMapper( CustomerCdiDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import jakarta.enterprise.context.ApplicationScoped;" ) + .contains( "import jakarta.inject.Inject;" ) + .contains( "@Inject" + lineSeparator() + " private GenderCdiDefaultCompileOptionFieldMapper" ) + .contains( "@ApplicationScoped" + lineSeparator() + "public class" ) + .doesNotContain( "public CustomerCdiDefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ) + .doesNotContain( "javax.enterprise" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/CustomerJakartaDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/CustomerJakartaDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..2fb6ae0d07 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/CustomerJakartaDefaultCompileOptionFieldMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta._default; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA, + uses = GenderJakartaDefaultCompileOptionFieldMapper.class) +public interface CustomerJakartaDefaultCompileOptionFieldMapper { + + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/GenderJakartaDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/GenderJakartaDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..da3409ef41 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/GenderJakartaDefaultCompileOptionFieldMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta._default; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA) +public interface GenderJakartaDefaultCompileOptionFieldMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/JakartaAndJsr330DefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/JakartaAndJsr330DefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..c647605e66 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/JakartaAndJsr330DefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta._default; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jakarta. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaDefaultCompileOptionFieldMapper.class, + GenderJakartaDefaultCompileOptionFieldMapper.class +}) +@WithJakartaInject +@WithJavaxInject +public class JakartaAndJsr330DefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJakartaDefaultCompileOptionFieldMapper" ) + .doesNotContain( "public CustomerJakartaDefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/JakartaDefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/JakartaDefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..8d33ae923f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/_default/JakartaDefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,98 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta._default; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test field injection for component model jakarta. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaDefaultCompileOptionFieldMapper.class, + GenderJakartaDefaultCompileOptionFieldMapper.class +}) +@ComponentScan(basePackageClasses = CustomerJakartaDefaultCompileOptionFieldMapper.class) +@WithJakartaInject +@Configuration +public class JakartaDefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Inject + @Named + private CustomerJakartaDefaultCompileOptionFieldMapper customerMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldConvertToTarget() { + // given + CustomerEntity customerEntity = new CustomerEntity(); + customerEntity.setName( "Samuel" ); + customerEntity.setGender( Gender.MALE ); + + // when + CustomerDto customerDto = customerMapper.asTarget( customerEntity ); + + // then + assertThat( customerDto ).isNotNull(); + assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); + assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); + } + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJakartaDefaultCompileOptionFieldMapper" ) + .doesNotContain( "public CustomerJakartaDefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/CustomerJakartaCompileOptionConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/CustomerJakartaCompileOptionConstructorMapper.java new file mode 100644 index 0000000000..9775d05c87 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/CustomerJakartaCompileOptionConstructorMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.compileoptionconstructor; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper( componentModel = MappingConstants.ComponentModel.JAKARTA, + uses = GenderJakartaCompileOptionConstructorMapper.class ) +public interface CustomerJakartaCompileOptionConstructorMapper { + + @Mapping(target = "gender", source = "gender") + CustomerDto asTarget(CustomerEntity customerEntity); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/GenderJakartaCompileOptionConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/GenderJakartaCompileOptionConstructorMapper.java new file mode 100644 index 0000000000..4539bb1c69 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/GenderJakartaCompileOptionConstructorMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.compileoptionconstructor; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA) +public interface GenderJakartaCompileOptionConstructorMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/JakartaAndJsr330CompileOptionConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/JakartaAndJsr330CompileOptionConstructorMapperTest.java new file mode 100644 index 0000000000..c897891b14 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/JakartaAndJsr330CompileOptionConstructorMapperTest.java @@ -0,0 +1,59 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.compileoptionconstructor; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test constructor injection for component model jakarta with compile option + * mapstruct.defaultInjectionStrategy=constructor + * + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaCompileOptionConstructorMapper.class, + GenderJakartaCompileOptionConstructorMapper.class +}) +@ProcessorOption(name = "mapstruct.defaultInjectionStrategy", value = "constructor") +@WithJakartaInject +@WithJavaxInject +public class JakartaAndJsr330CompileOptionConstructorMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaCompileOptionConstructorMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "private final GenderJakartaCompileOptionConstructorMapper" ) + .contains( "@Inject" + lineSeparator() + + " public CustomerJakartaCompileOptionConstructorMapperImpl" + + "(GenderJakartaCompileOptionConstructorMapper" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/JakartaCompileOptionConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/JakartaCompileOptionConstructorMapperTest.java new file mode 100644 index 0000000000..691d04ea89 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/compileoptionconstructor/JakartaCompileOptionConstructorMapperTest.java @@ -0,0 +1,100 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.compileoptionconstructor; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test constructor injection for component model jakarta with compile option + * mapstruct.defaultInjectionStrategy=constructor + * + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaCompileOptionConstructorMapper.class, + GenderJakartaCompileOptionConstructorMapper.class +}) +@ProcessorOption(name = "mapstruct.defaultInjectionStrategy", value = "constructor") +@ComponentScan(basePackageClasses = CustomerJakartaCompileOptionConstructorMapper.class) +@Configuration +@WithJakartaInject +public class JakartaCompileOptionConstructorMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Autowired + private CustomerJakartaCompileOptionConstructorMapper customerMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldConvertToTarget() { + // given + CustomerEntity customerEntity = new CustomerEntity(); + customerEntity.setName( "Samuel" ); + customerEntity.setGender( Gender.MALE ); + + // when + CustomerDto customerDto = customerMapper.asTarget( customerEntity ); + + // then + assertThat( customerDto ).isNotNull(); + assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); + assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); + } + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaCompileOptionConstructorMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "private final GenderJakartaCompileOptionConstructorMapper" ) + .contains( "@Inject" + lineSeparator() + + " public CustomerJakartaCompileOptionConstructorMapperImpl" + + "(GenderJakartaCompileOptionConstructorMapper" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/ConstructorJakartaConfig.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/ConstructorJakartaConfig.java new file mode 100644 index 0000000000..0bc9bb16e6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/ConstructorJakartaConfig.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.constructor; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingConstants; + +/** + * @author Filip Hrisafov + */ +@MapperConfig(componentModel = MappingConstants.ComponentModel.JAKARTA, + injectionStrategy = InjectionStrategy.CONSTRUCTOR) +public interface ConstructorJakartaConfig { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/CustomerJakartaConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/CustomerJakartaConstructorMapper.java new file mode 100644 index 0000000000..73d201cbfb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/CustomerJakartaConstructorMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.constructor; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper( componentModel = MappingConstants.ComponentModel.JAKARTA, + uses = GenderJakartaConstructorMapper.class, + injectionStrategy = InjectionStrategy.CONSTRUCTOR ) +public interface CustomerJakartaConstructorMapper { + + @Mapping(target = "gender", source = "gender") + CustomerDto asTarget(CustomerEntity customerEntity); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/GenderJakartaConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/GenderJakartaConstructorMapper.java new file mode 100644 index 0000000000..7cbd0bb3df --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/GenderJakartaConstructorMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.constructor; + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(config = ConstructorJakartaConfig.class) +public interface GenderJakartaConstructorMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/JakartaAndJsr330ConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/JakartaAndJsr330ConstructorMapperTest.java new file mode 100644 index 0000000000..03b5dcf2bb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/JakartaAndJsr330ConstructorMapperTest.java @@ -0,0 +1,58 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.constructor; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaConstructorMapper.class, + GenderJakartaConstructorMapper.class, + ConstructorJakartaConfig.class +}) +@ComponentScan(basePackageClasses = CustomerJakartaConstructorMapper.class) +@Configuration +@WithJakartaInject +@WithJavaxInject +public class JakartaAndJsr330ConstructorMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaConstructorMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "private final GenderJakartaConstructorMapper" ) + .contains( "@Inject" + lineSeparator() + + " public CustomerJakartaConstructorMapperImpl(GenderJakartaConstructorMapper" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/JakartaConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/JakartaConstructorMapperTest.java new file mode 100644 index 0000000000..79d33f10d2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/constructor/JakartaConstructorMapperTest.java @@ -0,0 +1,95 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.constructor; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaConstructorMapper.class, + GenderJakartaConstructorMapper.class, + ConstructorJakartaConfig.class +}) +@ComponentScan(basePackageClasses = CustomerJakartaConstructorMapper.class) +@Configuration +@WithJakartaInject +public class JakartaConstructorMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Autowired + private CustomerJakartaConstructorMapper customerMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldConvertToTarget() { + // given + CustomerEntity customerEntity = new CustomerEntity(); + customerEntity.setName( "Samuel" ); + customerEntity.setGender( Gender.MALE ); + + // when + CustomerDto customerDto = customerMapper.asTarget( customerEntity ); + + // then + assertThat( customerDto ).isNotNull(); + assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); + assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); + } + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaConstructorMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "private final GenderJakartaConstructorMapper" ) + .contains( "@Inject" + lineSeparator() + + " public CustomerJakartaConstructorMapperImpl(GenderJakartaConstructorMapper" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/CustomerFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/CustomerFieldMapper.java new file mode 100644 index 0000000000..550d4ee945 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/CustomerFieldMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.field; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA, uses = GenderJakartaFieldMapper.class, + injectionStrategy = InjectionStrategy.FIELD) +public interface CustomerFieldMapper { + + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/FieldJakartaConfig.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/FieldJakartaConfig.java new file mode 100644 index 0000000000..b3ad9474cf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/FieldJakartaConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.field; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingConstants; + +/** + * @author Filip Hrisafov + */ +@MapperConfig(componentModel = MappingConstants.ComponentModel.JAKARTA, injectionStrategy = InjectionStrategy.FIELD) +public interface FieldJakartaConfig { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/GenderJakartaFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/GenderJakartaFieldMapper.java new file mode 100644 index 0000000000..ee5b959307 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/GenderJakartaFieldMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.field; + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(config = FieldJakartaConfig.class) +public interface GenderJakartaFieldMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/JakartaAndJsr330FieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/JakartaAndJsr330FieldMapperTest.java new file mode 100644 index 0000000000..56ea535571 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/JakartaAndJsr330FieldMapperTest.java @@ -0,0 +1,53 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.field; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerFieldMapper.class, + GenderJakartaFieldMapper.class, + FieldJakartaConfig.class +}) +@IssueKey("2567") +@WithJakartaInject +@WithJavaxInject +public class JakartaAndJsr330FieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerFieldMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJakartaFieldMapper" ) + .doesNotContain( "public CustomerJakartaFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/JakartaFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/JakartaFieldMapperTest.java new file mode 100644 index 0000000000..c588075ca3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/field/JakartaFieldMapperTest.java @@ -0,0 +1,96 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.field; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerFieldMapper.class, + GenderJakartaFieldMapper.class, + FieldJakartaConfig.class +}) +@IssueKey("2567") +@ComponentScan(basePackageClasses = CustomerFieldMapper.class) +@Configuration +@WithJakartaInject +public class JakartaFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Inject + @Named + private CustomerFieldMapper customerMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldConvertToTarget() { + // given + CustomerEntity customerEntity = new CustomerEntity(); + customerEntity.setName( "Samuel" ); + customerEntity.setGender( Gender.MALE ); + + // when + CustomerDto customerDto = customerMapper.asTarget( customerEntity ); + + // then + assertThat( customerDto ).isNotNull(); + assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); + assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); + } + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerFieldMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJakartaFieldMapper" ) + .doesNotContain( "public CustomerJakartaFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/CustomerJakartaSetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/CustomerJakartaSetterMapper.java new file mode 100644 index 0000000000..d422442791 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/CustomerJakartaSetterMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper( componentModel = MappingConstants.ComponentModel.JAKARTA, + uses = GenderJakartaSetterMapper.class, + injectionStrategy = InjectionStrategy.SETTER ) +public interface CustomerJakartaSetterMapper { + + @Mapping(target = "gender", source = "gender") + CustomerDto asTarget(CustomerEntity customerEntity); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/GenderJakartaSetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/GenderJakartaSetterMapper.java new file mode 100644 index 0000000000..f0d9b851e9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/GenderJakartaSetterMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.setter; + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(config = SetterJakartaConfig.class) +public interface GenderJakartaSetterMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/JakartaSetterMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/JakartaSetterMapperTest.java new file mode 100644 index 0000000000..0e051fa759 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/JakartaSetterMapperTest.java @@ -0,0 +1,98 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.setter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaSetterMapper.class, + GenderJakartaSetterMapper.class, + SetterJakartaConfig.class +}) +@IssueKey("3229") +@ComponentScan(basePackageClasses = CustomerJakartaSetterMapper.class) +@Configuration +@WithJakartaInject +public class JakartaSetterMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Autowired + private CustomerJakartaSetterMapper customerMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldConvertToTarget() { + // given + CustomerEntity customerEntity = new CustomerEntity(); + customerEntity.setName( "Samuel" ); + customerEntity.setGender( Gender.MALE ); + + // when + CustomerDto customerDto = customerMapper.asTarget( customerEntity ); + + // then + assertThat( customerDto ).isNotNull(); + assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); + assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); + } + + @ProcessorTest + public void shouldHaveSetterInjection() { + String method = "@Inject" + lineSeparator() + + " public void setGenderJakartaSetterMapper(GenderJakartaSetterMapper genderJakartaSetterMapper) {" + + lineSeparator() + " this.genderJakartaSetterMapper = genderJakartaSetterMapper;" + + lineSeparator() + " }"; + generatedSource.forMapper( CustomerJakartaSetterMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "private GenderJakartaSetterMapper genderJakartaSetterMapper;" ) + .doesNotContain( "@Inject" + lineSeparator() + " private GenderJakartaSetterMapper" ) + .contains( method ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/SetterJakartaConfig.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/SetterJakartaConfig.java new file mode 100644 index 0000000000..a189244936 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta/setter/SetterJakartaConfig.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingConstants; + +/** + * @author Filip Hrisafov + */ +@MapperConfig(componentModel = MappingConstants.ComponentModel.JAKARTA, + injectionStrategy = InjectionStrategy.SETTER) +public interface SetterJakartaConfig { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/CustomerJakartaCdiDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/CustomerJakartaCdiDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..9ec0709f52 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/CustomerJakartaCdiDefaultCompileOptionFieldMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta_cdi._default; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, + uses = GenderJakartaCdiDefaultCompileOptionFieldMapper.class) +public interface CustomerJakartaCdiDefaultCompileOptionFieldMapper { + + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/GenderJakartaCdiDefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/GenderJakartaCdiDefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..0c1aef6ec6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/GenderJakartaCdiDefaultCompileOptionFieldMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta_cdi._default; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JSR330) +public interface GenderJakartaCdiDefaultCompileOptionFieldMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..b542c99c2a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta_cdi._default; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithCdi; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaCdi; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jakarta. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2950") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaCdiDefaultCompileOptionFieldMapper.class, + GenderJakartaCdiDefaultCompileOptionFieldMapper.class +}) +@WithJakartaCdi +@WithCdi +public class JakartaCdiAndCdiDefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJakartaCdiDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import jakarta.enterprise.context.ApplicationScoped;" ) + .contains( "import jakarta.inject.Inject;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJakartaCdiDefaultCompileOptionFieldMapper" ) + .contains( "@ApplicationScoped" + lineSeparator() + "public class" ) + .doesNotContain( "javax.inject" ) + .doesNotContain( "javax.enterprise" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/JakartaCdiDefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/JakartaCdiDefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..57714cdb01 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jakarta_cdi/_default/JakartaCdiDefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jakarta_cdi._default; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaCdi; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jakarta-cdi. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2950") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJakartaCdiDefaultCompileOptionFieldMapper.class, + GenderJakartaCdiDefaultCompileOptionFieldMapper.class +}) +@WithJakartaCdi +public class JakartaCdiDefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveFieldInjection() { + generatedSource.forMapper( CustomerJakartaCdiDefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import jakarta.enterprise.context.ApplicationScoped;" ) + .contains( "import jakarta.inject.Inject;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJakartaCdiDefaultCompileOptionFieldMapper" ) + .contains( "@ApplicationScoped" + lineSeparator() + "public class" ) + .doesNotContain( "public CustomerJakartaCdiDefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ) + .doesNotContain( "javax.enterprise" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/_default/Jsr330DefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/_default/Jsr330DefaultCompileOptionFieldMapperTest.java index 65d8acfb2c..3e562ff39a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/_default/Jsr330DefaultCompileOptionFieldMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/_default/Jsr330DefaultCompileOptionFieldMapperTest.java @@ -5,11 +5,6 @@ */ package org.mapstruct.ap.test.injectionstrategy.jsr330._default; -import javax.inject.Inject; -import javax.inject.Named; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; @@ -17,14 +12,10 @@ import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.ap.testutil.runner.GeneratedSource; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; import static java.lang.System.lineSeparator; -import static org.assertj.core.api.Assertions.assertThat; /** * Test field injection for component model jsr330. @@ -40,51 +31,19 @@ CustomerJsr330DefaultCompileOptionFieldMapper.class, GenderJsr330DefaultCompileOptionFieldMapper.class }) -@ComponentScan(basePackageClasses = CustomerJsr330DefaultCompileOptionFieldMapper.class) -@Configuration +@WithJavaxInject public class Jsr330DefaultCompileOptionFieldMapperTest { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); - @Inject - @Named - private CustomerJsr330DefaultCompileOptionFieldMapper customerMapper; - private ConfigurableApplicationContext context; - - @BeforeEach - public void springUp() { - context = new AnnotationConfigApplicationContext( getClass() ); - context.getAutowireCapableBeanFactory().autowireBean( this ); - } - - @AfterEach - public void springDown() { - if ( context != null ) { - context.close(); - } - } - - @ProcessorTest - public void shouldConvertToTarget() { - // given - CustomerEntity customerEntity = new CustomerEntity(); - customerEntity.setName( "Samuel" ); - customerEntity.setGender( Gender.MALE ); - - // when - CustomerDto customerDto = customerMapper.asTarget( customerEntity ); - - // then - assertThat( customerDto ).isNotNull(); - assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); - assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); - } - @ProcessorTest public void shouldHaveFieldInjection() { generatedSource.forMapper( CustomerJsr330DefaultCompileOptionFieldMapper.class ) .content() + .contains( "import javax.inject.Inject;" ) + .contains( "import javax.inject.Named;" ) + .contains( "import javax.inject.Singleton;" ) .contains( "@Inject" + lineSeparator() + " private GenderJsr330DefaultCompileOptionFieldMapper" ) .doesNotContain( "public CustomerJsr330DefaultCompileOptionFieldMapperImpl(" ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/compileoptionconstructor/Jsr330CompileOptionConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/compileoptionconstructor/Jsr330CompileOptionConstructorMapperTest.java index c564648ecf..2e4a44861f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/compileoptionconstructor/Jsr330CompileOptionConstructorMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/compileoptionconstructor/Jsr330CompileOptionConstructorMapperTest.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.test.injectionstrategy.jsr330.compileoptionconstructor; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; @@ -14,16 +12,11 @@ import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; import org.mapstruct.ap.testutil.runner.GeneratedSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; import static java.lang.System.lineSeparator; -import static org.assertj.core.api.Assertions.assertThat; /** * Test constructor injection for component model jsr330 with compile option @@ -40,50 +33,19 @@ GenderJsr330CompileOptionConstructorMapper.class }) @ProcessorOption( name = "mapstruct.defaultInjectionStrategy", value = "constructor") -@ComponentScan(basePackageClasses = CustomerJsr330CompileOptionConstructorMapper.class) -@Configuration +@WithJavaxInject public class Jsr330CompileOptionConstructorMapperTest { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); - @Autowired - private CustomerJsr330CompileOptionConstructorMapper customerMapper; - private ConfigurableApplicationContext context; - - @BeforeEach - public void springUp() { - context = new AnnotationConfigApplicationContext( getClass() ); - context.getAutowireCapableBeanFactory().autowireBean( this ); - } - - @AfterEach - public void springDown() { - if ( context != null ) { - context.close(); - } - } - - @ProcessorTest - public void shouldConvertToTarget() { - // given - CustomerEntity customerEntity = new CustomerEntity(); - customerEntity.setName( "Samuel" ); - customerEntity.setGender( Gender.MALE ); - - // when - CustomerDto customerDto = customerMapper.asTarget( customerEntity ); - - // then - assertThat( customerDto ).isNotNull(); - assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); - assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); - } - @ProcessorTest public void shouldHaveConstructorInjectionFromCompileOption() { generatedSource.forMapper( CustomerJsr330CompileOptionConstructorMapper.class ) .content() + .contains( "import javax.inject.Inject;" ) + .contains( "import javax.inject.Named;" ) + .contains( "import javax.inject.Singleton;" ) .contains( "private final GenderJsr330CompileOptionConstructorMapper" ) .contains( "@Inject" + lineSeparator() + " public CustomerJsr330CompileOptionConstructorMapperImpl" + diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/constructor/Jsr330ConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/constructor/Jsr330ConstructorMapperTest.java index 06f4180289..b872fc728b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/constructor/Jsr330ConstructorMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/constructor/Jsr330ConstructorMapperTest.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.test.injectionstrategy.jsr330.constructor; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; @@ -15,15 +13,10 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.ap.testutil.runner.GeneratedSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; import static java.lang.System.lineSeparator; -import static org.assertj.core.api.Assertions.assertThat; /** * Test constructor injection for component model spring. @@ -40,50 +33,19 @@ ConstructorJsr330Config.class }) @IssueKey("571") -@ComponentScan(basePackageClasses = CustomerJsr330ConstructorMapper.class) -@Configuration +@WithJavaxInject public class Jsr330ConstructorMapperTest { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); - @Autowired - private CustomerJsr330ConstructorMapper customerMapper; - private ConfigurableApplicationContext context; - - @BeforeEach - public void springUp() { - context = new AnnotationConfigApplicationContext( getClass() ); - context.getAutowireCapableBeanFactory().autowireBean( this ); - } - - @AfterEach - public void springDown() { - if ( context != null ) { - context.close(); - } - } - - @ProcessorTest - public void shouldConvertToTarget() { - // given - CustomerEntity customerEntity = new CustomerEntity(); - customerEntity.setName( "Samuel" ); - customerEntity.setGender( Gender.MALE ); - - // when - CustomerDto customerDto = customerMapper.asTarget( customerEntity ); - - // then - assertThat( customerDto ).isNotNull(); - assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); - assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); - } - @ProcessorTest public void shouldHaveConstructorInjection() { generatedSource.forMapper( CustomerJsr330ConstructorMapper.class ) .content() + .contains( "import javax.inject.Inject;" ) + .contains( "import javax.inject.Named;" ) + .contains( "import javax.inject.Singleton;" ) .contains( "private final GenderJsr330ConstructorMapper" ) .contains( "@Inject" + lineSeparator() + " public CustomerJsr330ConstructorMapperImpl(GenderJsr330ConstructorMapper" ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/field/Jsr330FieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/field/Jsr330FieldMapperTest.java index fc3bd09561..9f5b31d136 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/field/Jsr330FieldMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/field/Jsr330FieldMapperTest.java @@ -5,11 +5,6 @@ */ package org.mapstruct.ap.test.injectionstrategy.jsr330.field; -import javax.inject.Inject; -import javax.inject.Named; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; @@ -18,14 +13,10 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; import org.mapstruct.ap.testutil.runner.GeneratedSource; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; import static java.lang.System.lineSeparator; -import static org.assertj.core.api.Assertions.assertThat; /** * Test field injection for component model spring. @@ -42,51 +33,19 @@ FieldJsr330Config.class }) @IssueKey("571") -@ComponentScan(basePackageClasses = CustomerJsr330FieldMapper.class) -@Configuration +@WithJavaxInject public class Jsr330FieldMapperTest { @RegisterExtension final GeneratedSource generatedSource = new GeneratedSource(); - @Inject - @Named - private CustomerJsr330FieldMapper customerMapper; - private ConfigurableApplicationContext context; - - @BeforeEach - public void springUp() { - context = new AnnotationConfigApplicationContext( getClass() ); - context.getAutowireCapableBeanFactory().autowireBean( this ); - } - - @AfterEach - public void springDown() { - if ( context != null ) { - context.close(); - } - } - - @ProcessorTest - public void shouldConvertToTarget() { - // given - CustomerEntity customerEntity = new CustomerEntity(); - customerEntity.setName( "Samuel" ); - customerEntity.setGender( Gender.MALE ); - - // when - CustomerDto customerDto = customerMapper.asTarget( customerEntity ); - - // then - assertThat( customerDto ).isNotNull(); - assertThat( customerDto.getName() ).isEqualTo( "Samuel" ); - assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M ); - } - @ProcessorTest public void shouldHaveFieldInjection() { generatedSource.forMapper( CustomerJsr330FieldMapper.class ) .content() + .contains( "import javax.inject.Inject;" ) + .contains( "import javax.inject.Named;" ) + .contains( "import javax.inject.Singleton;" ) .contains( "@Inject" + lineSeparator() + " private GenderJsr330FieldMapper" ) .doesNotContain( "public CustomerJsr330FieldMapperImpl(" ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/CustomerJsr330DefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/CustomerJsr330DefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..80f5c60ef0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/CustomerJsr330DefaultCompileOptionFieldMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.jakarta; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Andrei Arlou + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JSR330, + uses = GenderJsr330DefaultCompileOptionFieldMapper.class) +public interface CustomerJsr330DefaultCompileOptionFieldMapper { + + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/GenderJsr330DefaultCompileOptionFieldMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/GenderJsr330DefaultCompileOptionFieldMapper.java new file mode 100644 index 0000000000..4a9b56c1b2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/GenderJsr330DefaultCompileOptionFieldMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.jakarta; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Andrei Arlou + */ +@Mapper(componentModel = MappingConstants.ComponentModel.JSR330) +public interface GenderJsr330DefaultCompileOptionFieldMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/JakartaAndJsr330DefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/JakartaAndJsr330DefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..da7176fc7a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/JakartaAndJsr330DefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.jakarta; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jsr330. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJsr330DefaultCompileOptionFieldMapper.class, + GenderJsr330DefaultCompileOptionFieldMapper.class +}) +@WithJakartaInject +@WithJavaxInject +public class JakartaAndJsr330DefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJavaxInjection() { + generatedSource.forMapper( CustomerJsr330DefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import javax.inject.Inject;" ) + .contains( "import javax.inject.Named;" ) + .contains( "import javax.inject.Singleton;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJsr330DefaultCompileOptionFieldMapper" ) + .doesNotContain( "public CustomerJsr330DefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "jakarta.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/JakartaJsr330DefaultCompileOptionFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/JakartaJsr330DefaultCompileOptionFieldMapperTest.java new file mode 100644 index 0000000000..f78721fb1c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/jakarta/JakartaJsr330DefaultCompileOptionFieldMapperTest.java @@ -0,0 +1,53 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.jakarta; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJakartaInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * Test field injection for component model jsr330. + * Default value option mapstruct.defaultInjectionStrategy is "field" + * + * @author Filip Hrisafov + */ +@IssueKey("2567") +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJsr330DefaultCompileOptionFieldMapper.class, + GenderJsr330DefaultCompileOptionFieldMapper.class +}) +@WithJakartaInject +public class JakartaJsr330DefaultCompileOptionFieldMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveJakartaInjection() { + generatedSource.forMapper( CustomerJsr330DefaultCompileOptionFieldMapper.class ) + .content() + .contains( "import jakarta.inject.Inject;" ) + .contains( "import jakarta.inject.Named;" ) + .contains( "import jakarta.inject.Singleton;" ) + .contains( "@Inject" + lineSeparator() + " private GenderJsr330DefaultCompileOptionFieldMapper" ) + .doesNotContain( "public CustomerJsr330DefaultCompileOptionFieldMapperImpl(" ) + .doesNotContain( "javax.inject" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/CustomerJsr330SetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/CustomerJsr330SetterMapper.java new file mode 100644 index 0000000000..ab269f83f8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/CustomerJsr330SetterMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Filip Hrisafov + */ +@Mapper( componentModel = MappingConstants.ComponentModel.JSR330, + uses = GenderJsr330SetterMapper.class, + injectionStrategy = InjectionStrategy.SETTER ) +public interface CustomerJsr330SetterMapper { + + @Mapping(target = "gender", source = "gender") + CustomerDto asTarget(CustomerEntity customerEntity); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/GenderJsr330SetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/GenderJsr330SetterMapper.java new file mode 100644 index 0000000000..d9b2f8ea9c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/GenderJsr330SetterMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.setter; + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Filip Hrisafov + */ +@Mapper(config = SetterJsr330Config.class) +public interface GenderJsr330SetterMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/Jsr330SetterMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/Jsr330SetterMapperTest.java new file mode 100644 index 0000000000..18ebdfc767 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/Jsr330SetterMapperTest.java @@ -0,0 +1,55 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.setter; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxInject; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static java.lang.System.lineSeparator; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerJsr330SetterMapper.class, + GenderJsr330SetterMapper.class, + SetterJsr330Config.class +}) +@IssueKey("3229") +@WithJavaxInject +public class Jsr330SetterMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveSetterInjection() { + String method = "@Inject" + lineSeparator() + + " public void setGenderJsr330SetterMapper(GenderJsr330SetterMapper genderJsr330SetterMapper) {" + + lineSeparator() + " this.genderJsr330SetterMapper = genderJsr330SetterMapper;" + + lineSeparator() + " }"; + generatedSource.forMapper( CustomerJsr330SetterMapper.class ) + .content() + .contains( "import javax.inject.Inject;" ) + .contains( "import javax.inject.Named;" ) + .contains( "import javax.inject.Singleton;" ) + .contains( "private GenderJsr330SetterMapper genderJsr330SetterMapper;" ) + .doesNotContain( "@Inject" + lineSeparator() + " private GenderJsr330SetterMapper" ) + .contains( method ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/SetterJsr330Config.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/SetterJsr330Config.java new file mode 100644 index 0000000000..2b13a56feb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/jsr330/setter/SetterJsr330Config.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.jsr330.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingConstants; + +/** + * @author Filip Hrisafov + */ +@MapperConfig(componentModel = MappingConstants.ComponentModel.JSR330, + injectionStrategy = InjectionStrategy.SETTER) +public interface SetterJsr330Config { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/_default/SpringDefaultMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/_default/SpringDefaultMapperTest.java index ef1b79b34b..2d9c374721 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/_default/SpringDefaultMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/_default/SpringDefaultMapperTest.java @@ -15,6 +15,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.mapstruct.ap.testutil.runner.GeneratedSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; @@ -41,6 +42,7 @@ @IssueKey("571") @ComponentScan(basePackageClasses = CustomerSpringDefaultMapper.class) @Configuration +@WithSpring public class SpringDefaultMapperTest { @RegisterExtension diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomStereotype.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomStereotype.java new file mode 100644 index 0000000000..ebbbfb935e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomStereotype.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface CustomStereotype { + String value() default ""; +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringComponentQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringComponentQualifiedMapper.java new file mode 100644 index 0000000000..984f9a2551 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringComponentQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Component; + +/** + * @author Ben Zegveld + */ +@AnnotateWith( value = Component.class, elements = @AnnotateWith.Element( strings = "AnnotateWithComponent" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringComponentQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringControllerQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringControllerQualifiedMapper.java new file mode 100644 index 0000000000..7203fad7a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringControllerQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Controller; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( value = Controller.class, elements = @AnnotateWith.Element( strings = "AnnotateWithController" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringControllerQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringCustomStereotypeQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringCustomStereotypeQualifiedMapper.java new file mode 100644 index 0000000000..18a062497d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringCustomStereotypeQualifiedMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( + value = CustomStereotype.class, + elements = @AnnotateWith.Element( strings = "AnnotateWithCustomStereotype" ) +) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringCustomStereotypeQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringRepositoryQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringRepositoryQualifiedMapper.java new file mode 100644 index 0000000000..7bbefbee2b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringRepositoryQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Repository; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( value = Repository.class, elements = @AnnotateWith.Element( strings = "AnnotateWithRepository" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringRepositoryQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringServiceQualifiedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringServiceQualifiedMapper.java new file mode 100644 index 0000000000..52dff8ef8f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/CustomerSpringServiceQualifiedMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.mapstruct.AnnotateWith; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Service; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@AnnotateWith( value = Service.class, elements = @AnnotateWith.Element( strings = "AnnotateWithService" ) ) +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING ) +public interface CustomerSpringServiceQualifiedMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/SpringAnnotateWithMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/SpringAnnotateWithMapperTest.java new file mode 100644 index 0000000000..cdc741814c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/annotateWith/SpringAnnotateWithMapperTest.java @@ -0,0 +1,91 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.annotateWith; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * Test field injection for component model spring. + * + * @author Filip Hrisafov + * @author Jose Carlos Campanero Ortiz + */ +@WithClasses({ + CustomerSpringComponentQualifiedMapper.class, + CustomerSpringControllerQualifiedMapper.class, + CustomerSpringServiceQualifiedMapper.class, + CustomerSpringRepositoryQualifiedMapper.class, + CustomStereotype.class, + CustomerSpringCustomStereotypeQualifiedMapper.class +}) +@IssueKey( "1427" ) +@WithSpring +public class SpringAnnotateWithMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void shouldHaveComponentAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringComponentQualifiedMapper.class ) + .content() + .contains( "@Component(value = \"AnnotateWithComponent\")" ) + .doesNotContain( "@Component" + System.lineSeparator() ); + + } + + @ProcessorTest + public void shouldHaveControllerAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringControllerQualifiedMapper.class ) + .content() + .contains( "@Controller(value = \"AnnotateWithController\")" ) + .doesNotContain( "@Component" ); + + } + + @ProcessorTest + public void shouldHaveServiceAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringServiceQualifiedMapper.class ) + .content() + .contains( "@Service(value = \"AnnotateWithService\")" ) + .doesNotContain( "@Component" ); + + } + + @ProcessorTest + public void shouldHaveRepositoryAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringRepositoryQualifiedMapper.class ) + .content() + .contains( "@Repository(value = \"AnnotateWithRepository\")" ) + .doesNotContain( "@Component" ); + + } + + @ProcessorTest + public void shouldHaveCustomStereotypeAnnotatedQualifiedMapper() { + + // then + generatedSource.forMapper( CustomerSpringCustomStereotypeQualifiedMapper.class ) + .content() + .contains( "@CustomStereotype(value = \"AnnotateWithCustomStereotype\")" ) + .doesNotContain( "@Component" ); + + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/compileoptionconstructor/SpringCompileOptionConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/compileoptionconstructor/SpringCompileOptionConstructorMapperTest.java index b106a5d788..c3291a5f3b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/compileoptionconstructor/SpringCompileOptionConstructorMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/compileoptionconstructor/SpringCompileOptionConstructorMapperTest.java @@ -21,6 +21,7 @@ import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; import org.mapstruct.ap.testutil.runner.GeneratedSource; import org.springframework.beans.factory.annotation.Autowired; @@ -52,6 +53,7 @@ @ProcessorOption( name = "mapstruct.defaultInjectionStrategy", value = "constructor") @ComponentScan(basePackageClasses = CustomerSpringCompileOptionConstructorMapper.class) @Configuration +@WithSpring @DefaultTimeZone("Europe/Berlin") public class SpringCompileOptionConstructorMapperTest { diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/constructor/SpringConstructorMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/constructor/SpringConstructorMapperTest.java index 1ff3fc8428..2594d5c23d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/constructor/SpringConstructorMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/constructor/SpringConstructorMapperTest.java @@ -23,6 +23,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.mapstruct.ap.testutil.runner.GeneratedSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; @@ -53,6 +54,7 @@ @IssueKey( "571" ) @ComponentScan(basePackageClasses = CustomerSpringConstructorMapper.class) @Configuration +@WithSpring @DefaultTimeZone("Europe/Berlin") public class SpringConstructorMapperTest { diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/field/SpringFieldMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/field/SpringFieldMapperTest.java index 8ee03f4bfb..d464dd5b16 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/field/SpringFieldMapperTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/field/SpringFieldMapperTest.java @@ -15,6 +15,7 @@ import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; import org.mapstruct.ap.testutil.runner.GeneratedSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; @@ -42,6 +43,7 @@ @IssueKey("571") @ComponentScan(basePackageClasses = CustomerSpringFieldMapper.class) @Configuration +@WithSpring public class SpringFieldMapperTest { @RegisterExtension diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/CustomerRecordSpringSetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/CustomerRecordSpringSetterMapper.java new file mode 100644 index 0000000000..1d09bdda03 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/CustomerRecordSpringSetterMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordEntity; + +/** + * @author Lucas Resch + */ +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, + uses = { CustomerSpringSetterMapper.class, GenderSpringSetterMapper.class }, + injectionStrategy = InjectionStrategy.SETTER) +public interface CustomerRecordSpringSetterMapper { + + CustomerRecordDto asTarget(CustomerRecordEntity customerRecordEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/CustomerSpringSetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/CustomerSpringSetterMapper.java new file mode 100644 index 0000000000..67dbab5e9c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/CustomerSpringSetterMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; + +/** + * @author Lucas Resch + */ +@Mapper( componentModel = MappingConstants.ComponentModel.SPRING, + uses = GenderSpringSetterMapper.class, + injectionStrategy = InjectionStrategy.SETTER ) +public interface CustomerSpringSetterMapper { + + @Mapping(target = "gender", source = "gender") + CustomerDto asTarget(CustomerEntity customerEntity); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/GenderSpringSetterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/GenderSpringSetterMapper.java new file mode 100644 index 0000000000..6243465ab5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/GenderSpringSetterMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.setter; + +import org.mapstruct.Mapper; +import org.mapstruct.ValueMapping; +import org.mapstruct.ValueMappings; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; + +/** + * @author Lucas Resch + */ +@Mapper(config = SetterSpringConfig.class) +public interface GenderSpringSetterMapper { + + @ValueMappings({ + @ValueMapping(source = "MALE", target = "M"), + @ValueMapping(source = "FEMALE", target = "F") + }) + GenderDto mapToDto(Gender gender); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/SetterSpringConfig.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/SetterSpringConfig.java new file mode 100644 index 0000000000..18227f315a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/SetterSpringConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.setter; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingConstants; + +/** + * @author Lucas Resch + */ +@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, injectionStrategy = InjectionStrategy.SETTER) +public interface SetterSpringConfig { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/SpringSetterMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/SpringSetterMapperTest.java new file mode 100644 index 0000000000..4dcf098a9e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/injectionstrategy/spring/setter/SpringSetterMapperTest.java @@ -0,0 +1,119 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.injectionstrategy.spring.setter; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junitpioneer.jupiter.DefaultTimeZone; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordDto; +import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordEntity; +import org.mapstruct.ap.test.injectionstrategy.shared.Gender; +import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithSpring; +import org.mapstruct.ap.testutil.runner.GeneratedSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import static java.lang.System.lineSeparator; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test setter injection for component model spring. + * + * @author Lucas Resch + */ +@WithClasses( { + CustomerRecordDto.class, + CustomerRecordEntity.class, + CustomerDto.class, + CustomerEntity.class, + Gender.class, + GenderDto.class, + CustomerRecordSpringSetterMapper.class, + CustomerSpringSetterMapper.class, + GenderSpringSetterMapper.class, + SetterSpringConfig.class +} ) +@IssueKey( "3229" ) +@ComponentScan(basePackageClasses = CustomerSpringSetterMapper.class) +@Configuration +@WithSpring +@DefaultTimeZone("Europe/Berlin") +public class SpringSetterMapperTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @Autowired + private CustomerRecordSpringSetterMapper customerRecordMapper; + private ConfigurableApplicationContext context; + + @BeforeEach + public void springUp() { + context = new AnnotationConfigApplicationContext( getClass() ); + context.getAutowireCapableBeanFactory().autowireBean( this ); + } + + @AfterEach + public void springDown() { + if ( context != null ) { + context.close(); + } + } + + @ProcessorTest + public void shouldConvertToTarget() throws Exception { + // given + CustomerEntity customerEntity = new CustomerEntity(); + customerEntity.setName( "Samuel" ); + customerEntity.setGender( Gender.MALE ); + CustomerRecordEntity customerRecordEntity = new CustomerRecordEntity(); + customerRecordEntity.setCustomer( customerEntity ); + customerRecordEntity.setRegistrationDate( createDate( "31-08-1982 10:20:56" ) ); + + // when + CustomerRecordDto customerRecordDto = customerRecordMapper.asTarget( customerRecordEntity ); + + // then + assertThat( customerRecordDto ).isNotNull(); + assertThat( customerRecordDto.getCustomer() ).isNotNull(); + assertThat( customerRecordDto.getCustomer().getName() ).isEqualTo( "Samuel" ); + assertThat( customerRecordDto.getCustomer().getGender() ).isEqualTo( GenderDto.M ); + assertThat( customerRecordDto.getRegistrationDate() ).isNotNull(); + assertThat( customerRecordDto.getRegistrationDate() ).hasToString( "1982-08-31T10:20:56.000+02:00" ); + } + + @ProcessorTest + public void shouldHaveSetterInjection() { + String method = "@Autowired" + lineSeparator() + + " public void setGenderSpringSetterMapper(GenderSpringSetterMapper genderSpringSetterMapper) {" + + lineSeparator() + " this.genderSpringSetterMapper = genderSpringSetterMapper;" + + lineSeparator() + " }"; + generatedSource.forMapper( CustomerSpringSetterMapper.class ) + .content() + .contains( "private GenderSpringSetterMapper genderSpringSetterMapper;" ) + .contains( method ); + } + + private Date createDate(String date) throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat( "dd-M-yyyy hh:mm:ss" ); + return sdf.parse( date ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/Source.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/Source.java index da1198d3ef..feea09626c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/Source.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/Source.java @@ -16,6 +16,10 @@ public class Source { private Stream integerStream; + private Stream integerStream2; + + private Stream integerArrayStream; + private Stream anotherIntegerStream; private Stream colours; @@ -24,6 +28,8 @@ public class Source { private Stream stringStream3; + private Stream> streamWithGenerics; + public Stream getStringStream() { return stringStream; } @@ -56,6 +62,22 @@ public void setIntegerStream(Stream integerStream) { this.integerStream = integerStream; } + public Stream getIntegerStream2() { + return integerStream2; + } + + public void setIntegerStream2(Stream integerStream2) { + this.integerStream2 = integerStream2; + } + + public Stream getIntegerArrayStream() { + return integerArrayStream; + } + + public void setIntegerArrayStream(Stream integerArrayStream) { + this.integerArrayStream = integerArrayStream; + } + public Stream getAnotherIntegerStream() { return anotherIntegerStream; } @@ -87,4 +109,12 @@ public Stream getStringStream3() { public void setStringStream3(Stream stringStream3) { this.stringStream3 = stringStream3; } + + public Stream> getStreamWithGenerics() { + return streamWithGenerics; + } + + public void setStreamWithGenerics(Stream> streamWithGenerics) { + this.streamWithGenerics = streamWithGenerics; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/SourceTargetMapper.java index 86d83844e8..089e3de156 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/SourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/SourceTargetMapper.java @@ -26,9 +26,12 @@ public abstract class SourceTargetMapper { @Mapping(target = "stringArrayList", source = "stringArrayStream"), @Mapping(target = "stringSet", source = "stringStreamToSet"), @Mapping(target = "integerCollection", source = "integerStream"), + @Mapping(target = "integerArray", source = "integerStream2"), + @Mapping(target = "integerMultiArray", source = "integerArrayStream"), @Mapping(target = "anotherStringSet", source = "anotherIntegerStream"), @Mapping(target = "stringListNoSetter", source = "stringStream2"), - @Mapping(target = "nonGenericStringList", source = "stringStream3") + @Mapping(target = "nonGenericStringList", source = "stringStream3"), + @Mapping(target = "arrayWithGenerics", source = "streamWithGenerics") }) public abstract Target sourceToTarget(Source source); diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/StreamMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/StreamMappingTest.java index a15468e5e1..d62b1fc384 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/StreamMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/StreamMappingTest.java @@ -225,4 +225,74 @@ public void shouldMapNonGenericList() { assertThat( mappedSource ).isNotNull(); assertThat( mappedSource.getStringStream3() ).containsExactly( "Bill", "Bob" ); } + + @ProcessorTest + public void shouldMapIntegerToIntegerArray() { + Source source = new Source(); + source.setIntegerStream2( Stream.of( 1, 2 ) ); + + Target target = SourceTargetMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getIntegerArray() ).containsExactly( 1, 2 ); + } + + @ProcessorTest + public void shouldReverseIntegerToIntegerArray() { + Target target = new Target(); + target.setIntegerArray( new Integer[]{ 1, 2 } ); + + Source source = SourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getIntegerStream2() ).containsExactly( 1, 2 ); + } + + @ProcessorTest + public void shouldMapIntegerArrayToIntegerMultiArray() { + Source source = new Source(); + source.setIntegerArrayStream( Stream.of( new Integer[]{ 1, 2 }, new Integer[]{ 2, 3 } ) ); + + Target target = SourceTargetMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getIntegerMultiArray() ) + .isDeepEqualTo( new Integer[][]{ new Integer[]{ 1, 2 }, new Integer[]{ 2, 3 } } ); + } + + @ProcessorTest + public void shouldReverseIntegerArrayToIntegerMultiArray() { + Target target = new Target(); + target.setIntegerMultiArray( new Integer[][]{ new Integer[]{ 1, 2 }, new Integer[]{ 2, 3 } } ); + + Source source = SourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getIntegerArrayStream() ).containsExactly( new Integer[]{ 1, 2 }, new Integer[]{ 2, 3 } ); + } + + @ProcessorTest + public void shouldMapTypeWithGenericsToArrayWithGenerics() { + Source source = new Source(); + Comparable comparable = o -> 0; + source.setStreamWithGenerics( Stream.of( comparable ) ); + + Target target = SourceTargetMapper.INSTANCE.sourceToTarget( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getArrayWithGenerics() ).containsExactly( comparable ); + } + + @ProcessorTest + @SuppressWarnings("unchecked") + public void shouldReverseMapTypeWithGenericsToArrayWithGenerics() { + Target target = new Target(); + Comparable comparable = o -> 0; + target.setArrayWithGenerics( new Comparable[]{ comparable } ); + + Source source = SourceTargetMapper.INSTANCE.targetToSource( target ); + + assertThat( source ).isNotNull(); + assertThat( source.getStreamWithGenerics() ).containsExactly( comparable ); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/Target.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/Target.java index 2d63c3f166..91d9e89908 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/Target.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/Target.java @@ -27,6 +27,12 @@ public class Target { private StringHolderArrayList nonGenericStringList; + private Integer[] integerArray; + + private Integer[][] integerMultiArray; + + private Comparable[] arrayWithGenerics; + public List getStringList() { return stringList; } @@ -77,7 +83,7 @@ public Set getColours() { public List getStringListNoSetter() { if ( stringListNoSetter == null ) { - stringListNoSetter = new ArrayList(); + stringListNoSetter = new ArrayList<>(); } return stringListNoSetter; } @@ -89,4 +95,28 @@ public StringHolderArrayList getNonGenericStringList() { public void setNonGenericStringList(StringHolderArrayList nonGenericStringList) { this.nonGenericStringList = nonGenericStringList; } + + public Integer[] getIntegerArray() { + return integerArray; + } + + public void setIntegerArray(Integer[] integerArray) { + this.integerArray = integerArray; + } + + public Integer[][] getIntegerMultiArray() { + return integerMultiArray; + } + + public void setIntegerMultiArray(Integer[][] integerMultiArray) { + this.integerMultiArray = integerMultiArray; + } + + public Comparable[] getArrayWithGenerics() { + return arrayWithGenerics; + } + + public void setArrayWithGenerics(Comparable[] arrayWithGenerics) { + this.arrayWithGenerics = arrayWithGenerics; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/defaultimplementation/DefaultStreamImplementationTest.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/defaultimplementation/DefaultStreamImplementationTest.java index 91be53fe64..8e69bafce2 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/defaultimplementation/DefaultStreamImplementationTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/defaultimplementation/DefaultStreamImplementationTest.java @@ -141,6 +141,19 @@ public void shouldUseAndReturnTargetParameterForArrayMappingAndSmallerArray() { assertThat( target ).containsOnly( new TargetFoo( "Bob" ) ); } + @ProcessorTest + @IssueKey("1752") + public void shouldUseAndReturnTargetParameterArrayForNullSource() { + TargetFoo[] target = new TargetFoo[1]; + target[0] = new TargetFoo( "Bob" ); + TargetFoo[] result = + SourceTargetMapper.INSTANCE.streamToArrayUsingTargetParameterAndReturn( null, target ); + + assertThat( result ).isSameAs( target ); + assertThat( target ).isNotNull(); + assertThat( target ).containsOnly( new TargetFoo( "Bob" ) ); + } + @ProcessorTest public void shouldUseAndReturnTargetParameterForMapping() { List target = new ArrayList<>(); @@ -152,6 +165,20 @@ public void shouldUseAndReturnTargetParameterForMapping() { assertResultList( target ); } + @ProcessorTest + @IssueKey("1752") + public void shouldUseAndReturnTargetParameterForNullMapping() { + List target = new ArrayList<>(); + target.add( new TargetFoo( "Bob" ) ); + target.add( new TargetFoo( "Alice" ) ); + Iterable result = + SourceTargetMapper.INSTANCE + .sourceFoosToTargetFoosUsingTargetParameterAndReturn( null, target ); + + assertThat( result ).isSameAs( target ); + assertResultList( target ); + } + @ProcessorTest public void shouldUseDefaultImplementationForListWithoutSetter() { Source source = new Source(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Idea.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Idea.java index bb8434fd63..063a5e5615 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Idea.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Idea.java @@ -11,4 +11,13 @@ */ public class Idea { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Plan.java b/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Plan.java index 8c55ebc273..7aa366ec95 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Plan.java +++ b/processor/src/test/java/org/mapstruct/ap/test/java8stream/wildcard/Plan.java @@ -11,4 +11,13 @@ */ public class Plan { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/javadoc/ErroneousJavadocMapper.java b/processor/src/test/java/org/mapstruct/ap/test/javadoc/ErroneousJavadocMapper.java new file mode 100644 index 0000000000..39f6da2890 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/javadoc/ErroneousJavadocMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.javadoc; + +import org.mapstruct.Javadoc; +import org.mapstruct.Mapper; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@Mapper +@Javadoc +public interface ErroneousJavadocMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocAnnotatedWithAttributesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocAnnotatedWithAttributesMapper.java new file mode 100644 index 0000000000..eb6285ff13 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocAnnotatedWithAttributesMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.javadoc; + +import org.mapstruct.Javadoc; +import org.mapstruct.Mapper; + +@Mapper +@Javadoc( + value = "This is the description", + authors = { "author1", "author2" }, + deprecated = "Use {@link OtherMapper} instead", + since = "0.1" +) +@Deprecated +public interface JavadocAnnotatedWithAttributesMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocAnnotatedWithValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocAnnotatedWithValueMapper.java new file mode 100644 index 0000000000..150d7e5f76 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocAnnotatedWithValueMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.javadoc; + +import org.mapstruct.Javadoc; +import org.mapstruct.Mapper; + +@Mapper +@Javadoc("This is the description\n" + + "\n" + + "@author author1\n" + + "@author author2\n" + + "\n" + + "@deprecated Use {@link OtherMapper} instead\n" + + "@since 0.1\n") +@Deprecated +public interface JavadocAnnotatedWithValueMapper { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocTest.java b/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocTest.java new file mode 100644 index 0000000000..316114389f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/javadoc/JavadocTest.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.javadoc; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +/** + * @author Jose Carlos Campanero Ortiz + */ +@IssueKey("2987") +class JavadocTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses( { JavadocAnnotatedWithValueMapper.class } ) + void javadocAnnotatedWithValueMapper() { + generatedSource.addComparisonToFixtureFor( JavadocAnnotatedWithValueMapper.class ); + } + + @ProcessorTest + @WithClasses( { JavadocAnnotatedWithAttributesMapper.class } ) + void javadocAnnotatedWithAttributesMapper() { + generatedSource.addComparisonToFixtureFor( JavadocAnnotatedWithAttributesMapper.class ); + } + + @ProcessorTest + @IssueKey("2987") + @WithClasses({ ErroneousJavadocMapper.class }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousJavadocMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 15, + message = "'value', 'authors', 'deprecated' and 'since' are undefined in @Javadoc, " + + "define at least one of them.") + } + ) + void shouldFailOnEmptyJavadocAnnotation() { + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/AllDefaultsProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/AllDefaultsProperty.kt new file mode 100644 index 0000000000..e452102f6e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/AllDefaultsProperty.kt @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class AllDefaultsProperty(val firstName: String? = null, val lastName: String? = null) diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/AllDefaultsPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/AllDefaultsPropertyMapper.java new file mode 100644 index 0000000000..1f8aab4624 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/AllDefaultsPropertyMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface AllDefaultsPropertyMapper { + + AllDefaultsPropertyMapper INSTANCE = Mappers.getMapper( AllDefaultsPropertyMapper.class ); + + AllDefaultsProperty map(Source source); + + class Source { + + private final String firstName; + private final String lastName; + + public Source(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerDto.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerDto.kt new file mode 100644 index 0000000000..3fbd48b0b1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerDto.kt @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class CustomerDto(var name: String?, var email: String?) diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerEntity.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerEntity.java new file mode 100644 index 0000000000..7ddd41bda0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerEntity.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +/** + * @author Filip Hrisafov + */ +public class CustomerEntity { + + private String name; + private String mail; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMail() { + return mail; + } + + public void setMail(String mail) { + this.mail = mail; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerMapper.java new file mode 100644 index 0000000000..1830a9c583 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/CustomerMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CustomerMapper { + + CustomerMapper INSTANCE = Mappers.getMapper( CustomerMapper.class ); + + @Mapping(target = "mail", source = "email") + CustomerEntity fromData(CustomerDto record); + + @InheritInverseConfiguration + CustomerDto toData(CustomerEntity entity); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/Default.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/Default.kt new file mode 100644 index 0000000000..58ebbd128d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/Default.kt @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +@Target(AnnotationTarget.CONSTRUCTOR) +@Retention(AnnotationRetention.BINARY) +annotation class Default diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/DefaultProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/DefaultProperty.kt new file mode 100644 index 0000000000..f5ff27d3d3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/DefaultProperty.kt @@ -0,0 +1,16 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class DefaultPropertySource(val default: Boolean, val identifier: String?) + +class DefaultPropertyTarget( + var default: Boolean, + var identifier: String? +) diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/DefaultPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/DefaultPropertyMapper.java new file mode 100644 index 0000000000..ce08e26e7e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/DefaultPropertyMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface DefaultPropertyMapper { + + DefaultPropertyMapper INSTANCE = Mappers.getMapper( DefaultPropertyMapper.class ); + + DefaultPropertyTarget map(DefaultPropertySource source); + + DefaultPropertySource map(DefaultPropertyTarget target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/KotlinDataTest.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/KotlinDataTest.java new file mode 100644 index 0000000000..94d0d90d75 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/KotlinDataTest.java @@ -0,0 +1,521 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.junit.jupiter.api.Nested; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithKotlin; +import org.mapstruct.ap.testutil.WithKotlinSources; +import org.mapstruct.ap.testutil.WithTestDependency; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +class KotlinDataTest { + + @Nested + @WithClasses({ + CustomerEntity.class, + CustomerMapper.class + }) + @WithKotlinSources("CustomerDto.kt") + class Standard { + + @ProcessorTest + void shouldMapData() { + CustomerEntity customer = CustomerMapper.INSTANCE.fromData( new CustomerDto( + "Kermit", + "kermit@test.com" + ) ); + + assertThat( customer ).isNotNull(); + assertThat( customer.getName() ).isEqualTo( "Kermit" ); + assertThat( customer.getMail() ).isEqualTo( "kermit@test.com" ); + } + + @ProcessorTest + void shouldMapIntoData() { + CustomerEntity entity = new CustomerEntity(); + entity.setName( "Kermit" ); + entity.setMail( "kermit@test.com" ); + + CustomerDto customer = CustomerMapper.INSTANCE.toData( entity ); + + assertThat( customer ).isNotNull(); + assertThat( customer.getName() ).isEqualTo( "Kermit" ); + assertThat( customer.getEmail() ).isEqualTo( "kermit@test.com" ); + } + + } + + @Nested + @WithClasses({ + SinglePropertyMapper.class, + }) + @WithKotlinSources("SingleProperty.kt") + class SingleData { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + + SingleProperty property = SinglePropertyMapper.INSTANCE.map( new SinglePropertyMapper.Source( "test" ) ); + assertThat( property ).isNotNull(); + assertThat( property.getValue() ).isEqualTo( "test" ); + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.WARNING, + type = SinglePropertyMapper.class, + line = 19, + message = "Unmapped target property: \"copy\"." + ) + } + ) + void shouldCompileWithWarnings() { + + SingleProperty property = SinglePropertyMapper.INSTANCE.map( new SinglePropertyMapper.Source( "test" ) ); + assertThat( property ).isNotNull(); + assertThat( property.getValue() ).isEqualTo( "test" ); + } + } + + @Nested + @WithClasses({ + AllDefaultsPropertyMapper.class, + }) + @WithKotlinSources("AllDefaultsProperty.kt") + class AllDefaults { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + + AllDefaultsProperty property = AllDefaultsPropertyMapper.INSTANCE.map( new AllDefaultsPropertyMapper.Source( + "Kermit", + "the Frog" + ) ); + assertThat( property ).isNotNull(); + assertThat( property.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( property.getLastName() ).isEqualTo( "the Frog" ); + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.WARNING, + type = AllDefaultsPropertyMapper.class, + line = 19, + message = "No target property found for target \"AllDefaultsProperty\"." + ) + } + ) + void shouldCompileWithWarnings() { + + AllDefaultsProperty property = AllDefaultsPropertyMapper.INSTANCE.map( new AllDefaultsPropertyMapper.Source( + "Kermit", + "the Frog" + ) ); + assertThat( property ).isNotNull(); + assertThat( property.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( property.getLastName() ).isEqualTo( "the Frog" ); + } + } + + @Nested + @WithClasses({ + MultiConstructorPropertyMapper.class, + }) + @WithKotlinSources("MultiConstructorProperty.kt") + class MultiConstructor { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + + MultiConstructorProperty property = MultiConstructorPropertyMapper.INSTANCE + .map( new MultiConstructorPropertyMapper.Source( + "Kermit", + "the Frog", + "Kermit the Frog" + ) ); + assertThat( property ).isNotNull(); + assertThat( property.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( property.getLastName() ).isEqualTo( "the Frog" ); + assertThat( property.getDisplayName() ).isEqualTo( "Kermit the Frog" ); + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiConstructorPropertyMapper.class, + line = 19, + messageRegExp = "Ambiguous constructors found for creating .*MultiConstructorProperty: " + + "MultiConstructorProperty\\(java.lang.String, java.lang.String.*\\), " + + "MultiConstructorProperty\\(java.lang.String, java.lang.String.*\\)\\. " + + "Either declare parameterless constructor or annotate the default constructor with an " + + "annotation named @Default\\." + ) + } + ) + void shouldFailToCompile() { + + } + } + + @Nested + @WithClasses({ + MultiSimilarConstructorPropertyMapper.class, + }) + @WithKotlinSources("MultiSimilarConstructorProperty.kt") + class MultiSimilarConstructor { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + PrimaryString primaryString = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + "Kermit the Frog" + ); + assertThat( primaryString ).isNotNull(); + assertThat( primaryString.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryString.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryString.getDisplayName() ).isEqualTo( "Kermit the Frog" ); + + PrimaryInt primaryInt = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + 42 + ); + assertThat( primaryInt ).isNotNull(); + assertThat( primaryInt.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryInt.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryInt.getAge() ).isEqualTo( 42 ); + + PrimaryLong primaryLong = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + 42L + ); + assertThat( primaryLong ).isNotNull(); + assertThat( primaryLong.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryLong.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryLong.getAge() ).isEqualTo( 42 ); + + PrimaryBoolean primaryBoolean = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + true + ); + assertThat( primaryBoolean ).isNotNull(); + assertThat( primaryBoolean.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryBoolean.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryBoolean.getActive() ).isTrue(); + + PrimaryByte primaryByte = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + (byte) 4 + ); + assertThat( primaryByte ).isNotNull(); + assertThat( primaryByte.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryByte.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryByte.getB() ).isEqualTo( (byte) 4 ); + + PrimaryShort primaryShort = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + (short) 4 + ); + assertThat( primaryShort ).isNotNull(); + assertThat( primaryShort.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryShort.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryShort.getAge() ).isEqualTo( (short) 4 ); + + PrimaryChar primaryChar = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + 't' + ); + assertThat( primaryChar ).isNotNull(); + assertThat( primaryChar.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryChar.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryChar.getC() ).isEqualTo( 't' ); + + PrimaryFloat primaryFloat = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + 42.2f + ); + assertThat( primaryFloat ).isNotNull(); + assertThat( primaryFloat.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryFloat.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryFloat.getPrice() ).isEqualTo( 42.2f ); + + PrimaryDouble primaryDouble = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + 42.2 + ); + assertThat( primaryDouble ).isNotNull(); + assertThat( primaryDouble.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryDouble.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryDouble.getPrice() ).isEqualTo( 42.2d ); + + PrimaryArray primaryArray = MultiSimilarConstructorPropertyMapper.INSTANCE.map( + new MultiSimilarConstructorPropertyMapper.Source( + "Kermit", + "the Frog" + ), + new String[] { "Kermit", "the Frog" } + ); + assertThat( primaryArray ).isNotNull(); + assertThat( primaryArray.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( primaryArray.getLastName() ).isEqualTo( "the Frog" ); + assertThat( primaryArray.getElements() ).containsExactly( "Kermit", "the Frog" ); + } + + @ProcessorTest + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 19, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryString" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 21, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryInt" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 23, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryLong" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 25, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryBoolean" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 27, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryByte" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 29, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryShort" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 31, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryChar" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 33, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryFloat" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 35, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryDouble" + ), + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.ERROR, + type = MultiSimilarConstructorPropertyMapper.class, + line = 37, + messageRegExp = "Ambiguous constructors found for creating .*PrimaryArray" + ) + + + } + ) + void shouldFailToCompile() { + + } + } + + @Nested + @WithClasses({ + MultiDefaultConstructorPropertyMapper.class, + }) + @WithKotlinSources({ + "MultiDefaultConstructorProperty.kt", + "Default.kt" + }) + class MultiDefaultConstructor { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + + MultiDefaultConstructorProperty property = MultiDefaultConstructorPropertyMapper.INSTANCE + .map( new MultiDefaultConstructorPropertyMapper.Source( + "Kermit", + "the Frog", + "Kermit the Frog" + ) ); + assertThat( property ).isNotNull(); + assertThat( property.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( property.getLastName() ).isEqualTo( "the Frog" ); + assertThat( property.getDisplayName() ).isNull(); + } + + @ProcessorTest + void shouldCompileWithoutKotlin() { + MultiDefaultConstructorProperty property = MultiDefaultConstructorPropertyMapper.INSTANCE + .map( new MultiDefaultConstructorPropertyMapper.Source( + "Kermit", + "the Frog", + "Kermit the Frog" + ) ); + assertThat( property ).isNotNull(); + assertThat( property.getFirstName() ).isEqualTo( "Kermit" ); + assertThat( property.getLastName() ).isEqualTo( "the Frog" ); + assertThat( property.getDisplayName() ).isNull(); + } + } + + @Nested + @WithClasses({ + UnsignedPropertyMapper.class, + }) + @WithKotlinSources("UnsignedProperty.kt") + class Unsigned { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + + UnsignedProperty property = UnsignedPropertyMapper.INSTANCE.map( new UnsignedPropertyMapper.Source( 10 ) ); + assertThat( property ).isNotNull(); + assertThat( property.getAge() ).isEqualTo( 10 ); + + UnsignedPropertyMapper.Source source = UnsignedPropertyMapper.INSTANCE.map( new UnsignedProperty( 20 ) ); + assertThat( source ).isNotNull(); + assertThat( source.getAge() ).isEqualTo( 20 ); + } + + @ProcessorTest + @WithTestDependency( "kotlin-stdlib" ) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic( + kind = javax.tools.Diagnostic.Kind.WARNING, + type = UnsignedPropertyMapper.class, + line = 19, + messageRegExp = "Unmapped target property: \"copy-.*\"\\." + ) + } + ) + void shouldCompileWithoutKotlinJvmMetadata() { + UnsignedProperty property = UnsignedPropertyMapper.INSTANCE.map( new UnsignedPropertyMapper.Source( 10 ) ); + assertThat( property ).isNotNull(); + assertThat( property.getAge() ).isEqualTo( 10 ); + + UnsignedPropertyMapper.Source source = UnsignedPropertyMapper.INSTANCE.map( new UnsignedProperty( 20 ) ); + assertThat( source ).isNotNull(); + assertThat( source.getAge() ).isEqualTo( 20 ); + } + } + + @Nested + @WithClasses({ + DefaultPropertyMapper.class, + }) + @WithKotlinSources("DefaultProperty.kt") + class Default { + + @ProcessorTest + @WithKotlin + void shouldCompileWithoutWarnings() { + + DefaultPropertyTarget target = DefaultPropertyMapper.INSTANCE.map( new DefaultPropertySource( + true, + "test" + ) ); + assertThat( target ).isNotNull(); + assertThat( target.getDefault() ).isTrue(); + assertThat( target.getIdentifier() ).isEqualTo( "test" ); + + DefaultPropertySource source = DefaultPropertyMapper.INSTANCE.map( new DefaultPropertyTarget( + false, + "private" + ) ); + assertThat( source ).isNotNull(); + assertThat( source.getDefault() ).isFalse(); + assertThat( source.getIdentifier() ).isEqualTo( "private" ); + + } + + @ProcessorTest + void shouldCompileWithoutKotlin() { + DefaultPropertyTarget target = DefaultPropertyMapper.INSTANCE.map( new DefaultPropertySource( + true, + "test" + ) ); + assertThat( target ).isNotNull(); + assertThat( target.getDefault() ).isTrue(); + assertThat( target.getIdentifier() ).isEqualTo( "test" ); + + DefaultPropertySource source = DefaultPropertyMapper.INSTANCE.map( new DefaultPropertyTarget( + false, + "private" + ) ); + assertThat( source ).isNotNull(); + assertThat( source.getDefault() ).isFalse(); + assertThat( source.getIdentifier() ).isEqualTo( "private" ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiConstructorProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiConstructorProperty.kt new file mode 100644 index 0000000000..46cb8034e9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiConstructorProperty.kt @@ -0,0 +1,13 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class MultiConstructorProperty(var firstName: String?, var lastName: String?, var displayName: String?) { + constructor(firstName: String?, lastName: String?) : this(firstName, lastName, null) +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiConstructorPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiConstructorPropertyMapper.java new file mode 100644 index 0000000000..3186226a57 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiConstructorPropertyMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface MultiConstructorPropertyMapper { + + MultiConstructorPropertyMapper INSTANCE = Mappers.getMapper( MultiConstructorPropertyMapper.class ); + + MultiConstructorProperty map(Source source); + + class Source { + + private final String firstName; + private final String lastName; + private final String displayName; + + public Source(String firstName, String lastName, String displayName) { + this.firstName = firstName; + this.lastName = lastName; + this.displayName = displayName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getDisplayName() { + return displayName; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiDefaultConstructorProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiDefaultConstructorProperty.kt new file mode 100644 index 0000000000..9f9af16053 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiDefaultConstructorProperty.kt @@ -0,0 +1,15 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class MultiDefaultConstructorProperty(val firstName: String?, val lastName: String?, val displayName: String?) { + @Default + constructor(firstName: String?, lastName: String?) : this(firstName, lastName, null) +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiDefaultConstructorPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiDefaultConstructorPropertyMapper.java new file mode 100644 index 0000000000..da04c01fb8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiDefaultConstructorPropertyMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface MultiDefaultConstructorPropertyMapper { + + MultiDefaultConstructorPropertyMapper INSTANCE = Mappers.getMapper( MultiDefaultConstructorPropertyMapper.class ); + + MultiDefaultConstructorProperty map(Source source); + + class Source { + + private final String firstName; + private final String lastName; + private final String displayName; + + public Source(String firstName, String lastName, String displayName) { + this.firstName = firstName; + this.lastName = lastName; + this.displayName = displayName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getDisplayName() { + return displayName; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiSimilarConstructorProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiSimilarConstructorProperty.kt new file mode 100644 index 0000000000..e122169075 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiSimilarConstructorProperty.kt @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class PrimaryString(var firstName: String?, var lastName: String?, var displayName: String?) { + constructor(firstName: String?, lastName: String?, age: Int) : this(firstName, lastName, null as String?) +} + +data class PrimaryInt(var firstName: String?, var lastName: String?, var age: Int) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, -1) +} + +data class PrimaryLong(var firstName: String?, var lastName: String?, var age: Long) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, -1) +} + +data class PrimaryBoolean(var firstName: String?, var lastName: String?, var active: Boolean) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, false) +} + +data class PrimaryByte(var firstName: String?, var lastName: String?, var b: Byte) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, 0) +} + +data class PrimaryShort(var firstName: String?, var lastName: String?, var age: Short) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, 0) +} + +data class PrimaryChar(var firstName: String?, var lastName: String?, var c: Char) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, 'a') +} + +data class PrimaryFloat(var firstName: String?, var lastName: String?, var price: Float) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, 0.0f) +} + +data class PrimaryDouble(var firstName: String?, var lastName: String?, var price: Double) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, 0.0) +} + +@Suppress("ArrayInDataClass") +data class PrimaryArray(var firstName: String?, var lastName: String?, var elements: Array) { + constructor(firstName: String?, lastName: String?, displayName: String?) : this(firstName, lastName, emptyArray()) +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiSimilarConstructorPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiSimilarConstructorPropertyMapper.java new file mode 100644 index 0000000000..86021cd6d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/MultiSimilarConstructorPropertyMapper.java @@ -0,0 +1,57 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface MultiSimilarConstructorPropertyMapper { + + MultiSimilarConstructorPropertyMapper INSTANCE = Mappers.getMapper( MultiSimilarConstructorPropertyMapper.class ); + + PrimaryString map(Source source, String displayName); + + PrimaryInt map(Source source, int age); + + PrimaryLong map(Source source, long age); + + PrimaryBoolean map(Source source, boolean active); + + PrimaryByte map(Source source, byte b); + + PrimaryShort map(Source source, short age); + + PrimaryChar map(Source source, char c); + + PrimaryFloat map(Source source, float price); + + PrimaryDouble map(Source source, double price); + + PrimaryArray map(Source source, String[] elements); + + class Source { + + private final String firstName; + private final String lastName; + + public Source(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/SingleProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/SingleProperty.kt new file mode 100644 index 0000000000..1bb63c410d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/SingleProperty.kt @@ -0,0 +1,11 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class SingleProperty(val value: String?) diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/SinglePropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/SinglePropertyMapper.java new file mode 100644 index 0000000000..d3d70f261c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/SinglePropertyMapper.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface SinglePropertyMapper { + + SinglePropertyMapper INSTANCE = Mappers.getMapper( SinglePropertyMapper.class ); + + SingleProperty map(Source source); + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/UnsignedProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/UnsignedProperty.kt new file mode 100644 index 0000000000..c52426b3af --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/UnsignedProperty.kt @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data + +/** + * @author Filip Hrisafov + */ +data class UnsignedProperty(val age: UInt?) { + // Java-friendly secondary constructor + constructor(age: Int?) : this(age?.toUInt()) + + @JvmName("getAge") + fun getAgeAsLong(): Long? = age?.toLong() +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/UnsignedPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/UnsignedPropertyMapper.java new file mode 100644 index 0000000000..4f055c7b41 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/UnsignedPropertyMapper.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface UnsignedPropertyMapper { + + UnsignedPropertyMapper INSTANCE = Mappers.getMapper( UnsignedPropertyMapper.class ); + + UnsignedProperty map(Source source); + + Source map(UnsignedProperty property); + + class Source { + + private final int age; + + public Source(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/KotlinDataJdk17Test.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/KotlinDataJdk17Test.java new file mode 100644 index 0000000000..bea5ca9bd4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/KotlinDataJdk17Test.java @@ -0,0 +1,63 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data.jdk17; + +import org.junit.jupiter.api.Nested; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithKotlin; +import org.mapstruct.ap.testutil.WithKotlinSources; +import org.mapstruct.ap.testutil.runner.Compiler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +class KotlinDataJdk17Test { + + @Nested + @WithKotlinSources(value = "RecordProperty.kt") + @WithClasses(RecordPropertyMapper.class) + class Record { + + @ProcessorTest(Compiler.JDK) + @WithKotlin + void withKotlin() { + RecordPropertyMapper.Source source = new RecordPropertyMapper.Source(); + source.setFirstName( "John" ); + source.setLastName( "Doe" ); + + RecordProperty property = RecordPropertyMapper.INSTANCE.map( source ); + assertThat( property ).isNotNull(); + assertThat( property.firstName() ).isEqualTo( "John" ); + assertThat( property.lastName() ).isEqualTo( "Doe" ); + + source = RecordPropertyMapper.INSTANCE.map( property ); + assertThat( source ).isNotNull(); + assertThat( source.getFirstName() ).isEqualTo( "John" ); + assertThat( source.getLastName() ).isEqualTo( "Doe" ); + } + + @ProcessorTest(Compiler.JDK) + void withoutKotlin() { + RecordPropertyMapper.Source source = new RecordPropertyMapper.Source(); + source.setFirstName( "John" ); + source.setLastName( "Doe" ); + + RecordProperty property = RecordPropertyMapper.INSTANCE.map( source ); + assertThat( property ).isNotNull(); + assertThat( property.firstName() ).isEqualTo( "John" ); + assertThat( property.lastName() ).isEqualTo( "Doe" ); + + source = RecordPropertyMapper.INSTANCE.map( property ); + assertThat( source ).isNotNull(); + assertThat( source.getFirstName() ).isEqualTo( "John" ); + assertThat( source.getLastName() ).isEqualTo( "Doe" ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/RecordProperty.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/RecordProperty.kt new file mode 100644 index 0000000000..28e8c9cfad --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/RecordProperty.kt @@ -0,0 +1,12 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data.jdk17 + +/** + * @author Filip Hrisafov + */ +@JvmRecord +data class RecordProperty(val firstName: String?, val lastName: String?) diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/RecordPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/RecordPropertyMapper.java new file mode 100644 index 0000000000..ecbc235aa5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/data/jdk17/RecordPropertyMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.data.jdk17; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface RecordPropertyMapper { + + RecordPropertyMapper INSTANCE = Mappers.getMapper( RecordPropertyMapper.class ); + + RecordProperty map(Source source); + + Source map(RecordProperty source); + + class Source { + + private String firstName; + private String lastName; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/Dtos.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/Dtos.kt new file mode 100644 index 0000000000..5a9084a685 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/Dtos.kt @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.sealed + +sealed class VehicleDto { + var name: String? = null + var maker: String? = null + + class BikeDto : VehicleDto() { + var numberOfGears: Int = 0 + } + + class CarDto : VehicleDto() { + var manual: Boolean = false + } + +} + +sealed class MotorDto : VehicleDto() { + var cc: Int = 0 + + class DavidsonDto : MotorDto() { + var numberOfExhausts: Int = 0 + } + + class HarleyDto : MotorDto() { + var engineDb: Int = 0 + } +} + +class VehicleCollectionDto { + var vehicles: MutableList = mutableListOf() +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/Entities.kt b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/Entities.kt new file mode 100644 index 0000000000..b380bac231 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/Entities.kt @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.sealed + +sealed class Vehicle { + var name: String? = null + var vehicleManufacturingCompany: String? = null + + class Bike : Vehicle() { + var numberOfGears: Int = 0 + } + + class Car : Vehicle() { + var manual: Boolean = false + } + +} + +sealed class Motor : Vehicle() { + var cc: Int = 0 + + class Davidson : Motor() { + var numberOfExhausts: Int = 0 + } + + class Harley : Motor() { + var engineDb: Int = 0 + } +} + +class VehicleCollection { + var vehicles: MutableList = mutableListOf() +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/SealedSubclassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/SealedSubclassMapper.java new file mode 100644 index 0000000000..bde7dc24cf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/SealedSubclassMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.sealed; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SealedSubclassMapper { + SealedSubclassMapper INSTANCE = Mappers.getMapper( SealedSubclassMapper.class ); + + VehicleCollectionDto map(VehicleCollection vehicles); + + @SubclassMapping( source = Vehicle.Car.class, target = VehicleDto.CarDto.class ) + @SubclassMapping( source = Vehicle.Bike.class, target = VehicleDto.BikeDto.class ) + @SubclassMapping( source = Motor.Harley.class, target = MotorDto.HarleyDto.class ) + @SubclassMapping( source = Motor.Davidson.class, target = MotorDto.DavidsonDto.class ) + @Mapping( source = "vehicleManufacturingCompany", target = "maker") + VehicleDto map(Vehicle vehicle); + + VehicleCollection mapInverse(VehicleCollectionDto vehicles); + + @InheritInverseConfiguration + Vehicle mapInverse(VehicleDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/SealedSubclassTest.java b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/SealedSubclassTest.java new file mode 100644 index 0000000000..f7162ec3a2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/kotlin/sealed/SealedSubclassTest.java @@ -0,0 +1,80 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.kotlin.sealed; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithKotlin; +import org.mapstruct.ap.testutil.WithKotlinSources; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + SealedSubclassMapper.class, +}) +@WithKotlinSources({ + "Dtos.kt", + "Entities.kt" +}) +@WithKotlin +public class SealedSubclassTest { + + @ProcessorTest + public void mappingIsDoneUsingSubclassMapping() { + VehicleCollection vehicles = new VehicleCollection(); + vehicles.getVehicles().add( new Vehicle.Car() ); + vehicles.getVehicles().add( new Vehicle.Bike() ); + vehicles.getVehicles().add( new Motor.Harley() ); + vehicles.getVehicles().add( new Motor.Davidson() ); + + VehicleCollectionDto result = SealedSubclassMapper.INSTANCE.map( vehicles ); + + assertThat( result.getVehicles() ).doesNotContainNull(); + assertThat( result.getVehicles() ) // remove generic so that test works. + .extracting( vehicle -> (Class) vehicle.getClass() ) + .containsExactly( + VehicleDto.CarDto.class, + VehicleDto.BikeDto.class, + MotorDto.HarleyDto.class, + MotorDto.DavidsonDto.class + ); + } + + @ProcessorTest + public void inverseMappingIsDoneUsingSubclassMapping() { + VehicleCollectionDto vehicles = new VehicleCollectionDto(); + vehicles.getVehicles().add( new VehicleDto.CarDto() ); + vehicles.getVehicles().add( new VehicleDto.BikeDto() ); + vehicles.getVehicles().add( new MotorDto.HarleyDto() ); + vehicles.getVehicles().add( new MotorDto.DavidsonDto() ); + + VehicleCollection result = SealedSubclassMapper.INSTANCE.mapInverse( vehicles ); + + assertThat( result.getVehicles() ).doesNotContainNull(); + assertThat( result.getVehicles() ) // remove generic so that test works. + .extracting( vehicle -> (Class) vehicle.getClass() ) + .containsExactly( + Vehicle.Car.class, + Vehicle.Bike.class, + Motor.Harley.class, + Motor.Davidson.class + ); + } + + @ProcessorTest + public void subclassMappingInheritsInverseMapping() { + VehicleCollectionDto vehiclesDto = new VehicleCollectionDto(); + VehicleDto.CarDto carDto = new VehicleDto.CarDto(); + carDto.setMaker( "BenZ" ); + vehiclesDto.getVehicles().add( carDto ); + + VehicleCollection result = SealedSubclassMapper.INSTANCE.mapInverse( vehiclesDto ); + + assertThat( result.getVehicles() ) + .extracting( Vehicle::getVehicleManufacturingCompany ) + .containsExactly( "BenZ" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java index 81dc9a452a..3caeb8cbba 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.test.mapperconfig; +import org.mapstruct.ClassAccessibility; import org.mapstruct.MapperConfig; import org.mapstruct.ReportingPolicy; @@ -12,7 +13,9 @@ * * @author Sjaak Derksen */ -@MapperConfig(uses = { CustomMapperViaMapperConfig.class }, unmappedTargetPolicy = ReportingPolicy.ERROR ) +@MapperConfig(uses = {CustomMapperViaMapperConfig.class}, + unmappedTargetPolicy = ReportingPolicy.ERROR, + accessibility = ClassAccessibility.PACKAGE_PRIVATE) public class CentralConfig { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java index 1f7fe423f5..a13db4ab0b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java @@ -12,6 +12,9 @@ import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import static java.lang.reflect.Modifier.isPrivate; +import static java.lang.reflect.Modifier.isProtected; +import static java.lang.reflect.Modifier.isPublic; import static org.assertj.core.api.Assertions.assertThat; /** @@ -49,6 +52,13 @@ public void shouldUseCustomMapperViaMapperConfigForFooToDto() { assertThat( source.getFoo().getCreatedBy() ).isEqualTo( CustomMapperViaMapperConfig.class.getSimpleName() ); } + @ProcessorTest + @WithClasses( { Target.class, SourceTargetMapper.class } ) + public void shouldDeclareMapperImplementationAsPackagePrivate() throws ClassNotFoundException { + Class implementation = loadForMapper( SourceTargetMapper.class ); + assertThat( isDefault( implementation.getModifiers() ) ).isTrue(); + } + @ProcessorTest @WithClasses( { TargetNoFoo.class, SourceTargetMapperWarn.class } ) @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, @@ -70,4 +80,12 @@ public void shouldUseWARNViaMapper() { }) public void shouldUseERRORViaMapperConfig() { } + + private static Class loadForMapper(Class mapper) throws ClassNotFoundException { + return Thread.currentThread().getContextClassLoader().loadClass( mapper.getName() + "Impl" ); + } + + private static boolean isDefault(int modifiers) { + return !isPublic( modifiers ) && !isProtected( modifiers ) && !isPrivate( modifiers ); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/CloningBeanMappingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/CloningBeanMappingMapper.java new file mode 100644 index 0000000000..a5c05d974d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/CloningBeanMappingMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.control.DeepClone; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CloningBeanMappingMapper { + + CloningBeanMappingMapper INSTANCE = Mappers.getMapper( CloningBeanMappingMapper.class ); + + @BeanMapping(mappingControl = DeepClone.class) + FridgeDTO clone(FridgeDTO in); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousBuiltInAndBuiltInMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousBuiltInAndBuiltInMapper.java new file mode 100644 index 0000000000..eb9f7ea9ce --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousBuiltInAndBuiltInMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import java.time.ZonedDateTime; +import java.util.Date; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper(mappingControl = NoConversion.class) +public interface ErroneousBuiltInAndBuiltInMapper { + + Target map(Source source); + + class Source { + private final ZonedDateTime time; + + public Source(ZonedDateTime time) { + this.time = time; + } + + public ZonedDateTime getTime() { + return time; + } + } + + class Target { + private final Date time; + + public Target(Date time) { + this.time = time; + } + + public Date getTime() { + return time; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousBuiltInAndMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousBuiltInAndMethodMapper.java new file mode 100644 index 0000000000..fdd7545da7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousBuiltInAndMethodMapper.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Calendar; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper(mappingControl = NoConversion.class) +public interface ErroneousBuiltInAndMethodMapper { + + Target map(Source source); + + default ZonedDateTime fromInt(int time) { + return ZonedDateTime.ofInstant( Instant.ofEpochMilli( time ), ZoneOffset.UTC ); + } + + class Source { + private final int time; + + public Source(int time) { + this.time = time; + } + + public int getTime() { + return time; + } + } + + class Target { + private Calendar time; + + public Target(Calendar time) { + this.time = time; + } + + public Calendar getTime() { + return time; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousConversionAndMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousConversionAndMethodMapper.java new file mode 100644 index 0000000000..91a94aeca2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousConversionAndMethodMapper.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import java.time.Instant; +import java.util.Date; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper(mappingControl = NoConversion.class) +public interface ErroneousConversionAndMethodMapper { + + Target map(Source source); + + default Instant fromDate(int time) { + return Instant.ofEpochMilli( time ); + } + + class Source { + private final int time; + + public Source(int time) { + this.time = time; + } + + public int getTime() { + return time; + } + } + + class Target { + private Date time; + + public Target(Date time) { + this.time = time; + } + + public Date getTime() { + return time; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousMethodAndBuiltInMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousMethodAndBuiltInMapper.java new file mode 100644 index 0000000000..d7798161c1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousMethodAndBuiltInMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper(mappingControl = NoConversion.class) +public interface ErroneousMethodAndBuiltInMapper { + + Target map(Source source); + + default Date fromCalendar(Calendar calendar) { + return calendar != null ? calendar.getTime() : null; + } + + class Source { + private final ZonedDateTime time; + + public Source(ZonedDateTime time) { + this.time = time; + } + + public ZonedDateTime getTime() { + return time; + } + } + + class Target { + private final Date time; + + public Target(Date time) { + this.time = time; + } + + public Date getTime() { + return time; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousMethodAndConversionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousMethodAndConversionMapper.java new file mode 100644 index 0000000000..5a372addd9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/ErroneousMethodAndConversionMapper.java @@ -0,0 +1,48 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import java.time.Instant; +import java.util.Date; + +import org.mapstruct.Mapper; + +/** + * @author Filip Hrisafov + */ +@Mapper(mappingControl = NoConversion.class) +public interface ErroneousMethodAndConversionMapper { + + Target map(Source source); + + default long fromInstant(Instant value) { + return value != null ? value.getEpochSecond() : null; + } + + class Source { + private final Date time; + + public Source(Date time) { + this.time = time; + } + + public Date getTime() { + return time; + } + } + + class Target { + private long time; + + public Target(long time) { + this.time = time; + } + + public long getTime() { + return time; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/MappingControlTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/MappingControlTest.java index d542801799..171fe4bc6a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/MappingControlTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/MappingControlTest.java @@ -69,6 +69,21 @@ public void testDeepCloning() { assertThat( out.getShelve().getCoolBeer().getBeerCount() ).isEqualTo( "5" ); } + @ProcessorTest + @IssueKey("3135") + @WithClasses(CloningBeanMappingMapper.class) + public void testDeepCloningViaBeanMapping() { + + FridgeDTO in = createFridgeDTO(); + FridgeDTO out = CloningBeanMappingMapper.INSTANCE.clone( in ); + + assertThat( out ).isNotNull(); + assertThat( out.getShelve() ).isNotNull(); + assertThat( out.getShelve() ).isNotSameAs( in.getShelve() ); + assertThat( out.getShelve().getCoolBeer() ).isNotSameAs( in.getShelve().getCoolBeer() ); + assertThat( out.getShelve().getCoolBeer().getBeerCount() ).isEqualTo( "5" ); + } + /** * Test the deep cloning annotation with lists */ @@ -225,6 +240,96 @@ public void complexSelectionNotAllowed() { public void complexSelectionNotAllowedWithConfig() { } + @ProcessorTest + @IssueKey("3186") + @WithClasses({ + ErroneousMethodAndBuiltInMapper.class, + NoConversion.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousMethodAndBuiltInMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "Can't map property \"ZonedDateTime time\" to \"Date time\". " + + "Consider to declare/implement a mapping method: \"Date map(ZonedDateTime value)\"." + ) + }) + public void conversionSelectionNotAllowedInTwoStepMethodBuiltIdConversion() { + } + + @ProcessorTest + @IssueKey("3186") + @WithClasses({ + ErroneousBuiltInAndBuiltInMapper.class, + NoConversion.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousBuiltInAndBuiltInMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 19, + message = "Can't map property \"ZonedDateTime time\" to \"Date time\". " + + "Consider to declare/implement a mapping method: \"Date map(ZonedDateTime value)\"." + ) + }) + public void conversionSelectionNotAllowedInTwoStepBuiltInBuiltInConversion() { + } + + @ProcessorTest + @IssueKey("3186") + @WithClasses({ + ErroneousBuiltInAndMethodMapper.class, + NoConversion.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousBuiltInAndMethodMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 21, + message = "Can't map property \"int time\" to \"Calendar time\". " + + "Consider to declare/implement a mapping method: \"Calendar map(int value)\"." + ) + }) + public void conversionSelectionNotAllowedInTwoStepBuiltInMethodConversion() { + } + + @ProcessorTest + @IssueKey("3186") + @WithClasses({ + ErroneousMethodAndConversionMapper.class, + NoConversion.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousMethodAndConversionMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 19, + message = "Can't map property \"Date time\" to \"long time\". " + + "Consider to declare/implement a mapping method: \"long map(Date value)\"." + ) + }) + public void conversionSelectionNotAllowedInTwoStepMethodConversionConversion() { + } + + @ProcessorTest + @IssueKey("3186") + @WithClasses({ + ErroneousConversionAndMethodMapper.class, + NoConversion.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousConversionAndMethodMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 19, + message = "Can't map property \"int time\" to \"Date time\". " + + "Consider to declare/implement a mapping method: \"Date map(int value)\"." + ) + }) + public void conversionSelectionNotAllowedInTwoStepConversionMethodConversion() { + } + private FridgeDTO createFridgeDTO() { FridgeDTO fridgeDTO = new FridgeDTO(); ShelveDTO shelveDTO = new ShelveDTO(); diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/NoConversion.java b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/NoConversion.java new file mode 100644 index 0000000000..6f05c16bb8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingcontrol/NoConversion.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.mappingcontrol; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.mapstruct.control.MappingControl; +import org.mapstruct.util.Experimental; + +@Retention(RetentionPolicy.CLASS) +@Experimental +@MappingControl( MappingControl.Use.DIRECT ) +@MappingControl( MappingControl.Use.MAPPING_METHOD ) +@MappingControl( MappingControl.Use.COMPLEX_MAPPING ) +public @interface NoConversion { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/ErroneousSourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/ErroneousSourceTargetMapper.java index 659867ca40..1196d773cb 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/ErroneousSourceTargetMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/ErroneousSourceTargetMapper.java @@ -8,14 +8,24 @@ import org.mapstruct.BeanMapping; import org.mapstruct.Mapper; import org.mapstruct.ReportingPolicy; -import org.mapstruct.factory.Mappers; @Mapper( unmappedTargetPolicy = ReportingPolicy.IGNORE, unmappedSourcePolicy = ReportingPolicy.IGNORE) public interface ErroneousSourceTargetMapper { - ErroneousSourceTargetMapper INSTANCE = Mappers.getMapper( ErroneousSourceTargetMapper.class ); @BeanMapping(ignoreUnmappedSourceProperties = "bar") - Object sourceToTarget(Object source); + Target map(Target source); + + class Target { + private final String value; + + public Target(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/MissingIgnoredSourceTest.java b/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/MissingIgnoredSourceTest.java index 77f80c5537..5794126604 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/MissingIgnoredSourceTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/missingignoredsource/MissingIgnoredSourceTest.java @@ -22,7 +22,7 @@ public class MissingIgnoredSourceTest { diagnostics = { @Diagnostic(type = ErroneousSourceTargetMapper.class, kind = Kind.ERROR, - line = 20, + line = 18, message = "Ignored unknown source property: \"bar\".") } ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/multisource/MultiSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/multisource/MultiSourceMapper.java new file mode 100644 index 0000000000..51ee997615 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/multisource/MultiSourceMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.multisource; + +import java.util.Collection; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MultiSourceMapper { + + MultiSourceMapper INSTANCE = Mappers.getMapper( MultiSourceMapper.class ); + + Target mapFromPrimitiveAndCollection(int value, Collection elements); + + Target mapFromCollectionAndPrimitive(Collection elements, int value); + + class Target { + private int value; + private Collection elements; + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public Collection getElements() { + return elements; + } + + public void setElements(Collection elements) { + this.elements = elements; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/multisource/MultiSourceMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/multisource/MultiSourceMapperTest.java new file mode 100644 index 0000000000..c4e1eaaee4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/multisource/MultiSourceMapperTest.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.multisource; + +import java.util.Collections; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +public class MultiSourceMapperTest { + + @IssueKey("2005") + @ProcessorTest + @WithClasses({ + MultiSourceMapper.class + }) + void shouldBeAbleToMapFromPrimitiveAndCollectionAsMultiSource() { + MultiSourceMapper.Target target = MultiSourceMapper.INSTANCE.mapFromPrimitiveAndCollection( + 10, + Collections.singleton( "test" ) + ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( 10 ); + assertThat( target.getElements() ).containsExactly( "test" ); + + target = MultiSourceMapper.INSTANCE.mapFromCollectionAndPrimitive( + Collections.singleton( "otherTest" ), + 20 + ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( 20 ); + assertThat( target.getElements() ).containsExactly( "otherTest" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/naming/VariableNamingTest.java b/processor/src/test/java/org/mapstruct/ap/test/naming/VariableNamingTest.java index 4b5c7982ea..a8e833ee06 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/naming/VariableNamingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/naming/VariableNamingTest.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.Map; +import org.junitpioneer.jupiter.ReadsDefaultTimeZone; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; @@ -26,6 +27,7 @@ */ @WithClasses({ SourceTargetMapper.class, While.class, Break.class, Source.class }) @IssueKey("53") +@ReadsDefaultTimeZone public class VariableNamingTest { @ProcessorTest diff --git a/processor/src/test/java/org/mapstruct/ap/test/naming/spi/CustomAccessorNamingStrategy.java b/processor/src/test/java/org/mapstruct/ap/test/naming/spi/CustomAccessorNamingStrategy.java index d8c4ddd8c0..21352396d0 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/naming/spi/CustomAccessorNamingStrategy.java +++ b/processor/src/test/java/org/mapstruct/ap/test/naming/spi/CustomAccessorNamingStrategy.java @@ -8,9 +8,9 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; -import org.mapstruct.ap.spi.util.IntrospectorUtils; import org.mapstruct.ap.spi.AccessorNamingStrategy; import org.mapstruct.ap.spi.DefaultAccessorNamingStrategy; +import org.mapstruct.ap.spi.util.IntrospectorUtils; /** * A custom {@link AccessorNamingStrategy} recognizing getters in the form of {@code property()} and setters in the diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/DottedErrorMessageTest.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/DottedErrorMessageTest.java index fb10616577..571f3feade 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/DottedErrorMessageTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/DottedErrorMessageTest.java @@ -38,25 +38,38 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.WheelDto; import org.mapstruct.ap.test.nestedbeans.unmappable.Word; import org.mapstruct.ap.test.nestedbeans.unmappable.WordDto; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableCollectionElementPropertyMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableDeepListMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableDeepMapKeyMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableDeepMapValueMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableDeepNestingMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableEnumMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableValuePropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceCollectionElementPropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceDeepListMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceDeepMapKeyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceDeepMapValueMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceDeepNestingMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceEnumMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableSourceValuePropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableTargetCollectionElementPropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableTargetDeepListMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableTargetDeepMapKeyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableTargetDeepMapValueMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableTargetDeepNestingMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.erroneous.UnmappableTargetValuePropertyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.ignore.UnmappableIgnoreCollectionElementPropertyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.ignore.UnmappableIgnoreDeepListMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.ignore.UnmappableIgnoreDeepMapKeyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.ignore.UnmappableIgnoreDeepMapValueMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.ignore.UnmappableIgnoreDeepNestingMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.ignore.UnmappableIgnoreValuePropertyMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableWarnCollectionElementPropertyMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableWarnDeepListMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableWarnDeepMapKeyMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableWarnDeepMapValueMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableWarnDeepNestingMapper; -import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableWarnValuePropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableSourceWarnCollectionElementPropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableSourceWarnDeepListMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableSourceWarnDeepMapKeyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableSourceWarnDeepMapValueMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableSourceWarnDeepNestingMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableSourceWarnValuePropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableTargetWarnCollectionElementPropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableTargetWarnDeepListMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableTargetWarnDeepMapKeyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableTargetWarnDeepMapValueMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableTargetWarnDeepNestingMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.warn.UnmappableTargetWarnValuePropertyMapper; +import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; @@ -88,114 +101,234 @@ public class DottedErrorMessageTest { @ProcessorTest @WithClasses({ - UnmappableDeepNestingMapper.class + UnmappableTargetDeepNestingMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = BaseDeepNestingMapper.class, + @Diagnostic(type = UnmappableTargetDeepNestingMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 10, + line = 14, message = "Unmapped target property: \"rgb\". Mapping from " + PROPERTY + - " \"Color house.roof.color\" to \"ColorDto house.roof.color\".") + " \"Color house.roof.color\" to \"ColorDto house.roof.color\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepNestingMapper'.") } ) - public void testDeepNestedBeans() { + public void testTargetDeepNestedBeans() { } @ProcessorTest @WithClasses({ - UnmappableDeepListMapper.class + UnmappableTargetDeepListMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = BaseDeepListMapper.class, + @Diagnostic(type = UnmappableTargetDeepListMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 10, + line = 14, message = "Unmapped target property: \"left\". Mapping from " + COLLECTION_ELEMENT + - " \"Wheel car.wheels\" to \"WheelDto car.wheels\".") + " \"Wheel car.wheels\" to \"WheelDto car.wheels\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepListMapper'.") } ) - public void testIterables() { + public void testTargetIterables() { } @ProcessorTest @WithClasses({ - UnmappableDeepMapKeyMapper.class + UnmappableTargetDeepMapKeyMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = BaseDeepMapKeyMapper.class, + @Diagnostic(type = UnmappableTargetDeepMapKeyMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 10, + line = 14, message = "Unmapped target property: \"pronunciation\". Mapping from " + MAP_KEY + - " \"Word dictionary.wordMap{:key}\" to \"WordDto dictionary.wordMap{:key}\".") + " \"Word dictionary.wordMap{:key}\" to \"WordDto dictionary.wordMap{:key}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapKeyMapper'.") } ) - public void testMapKeys() { + public void testTargetMapKeys() { } @ProcessorTest @WithClasses({ - UnmappableDeepMapValueMapper.class + UnmappableTargetDeepMapValueMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = BaseDeepMapValueMapper.class, + @Diagnostic(type = UnmappableTargetDeepMapValueMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 10, + line = 14, message = "Unmapped target property: \"pronunciation\". Mapping from " + MAP_VALUE + - " \"ForeignWord dictionary.wordMap{:value}\" to \"ForeignWordDto dictionary.wordMap{:value}\".") + " \"ForeignWord dictionary.wordMap{:value}\" to \"ForeignWordDto dictionary.wordMap{:value}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapValueMapper'.") } ) - public void testMapValues() { + public void testTargetMapValues() { } @ProcessorTest @WithClasses({ - UnmappableCollectionElementPropertyMapper.class + UnmappableTargetCollectionElementPropertyMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = BaseCollectionElementPropertyMapper.class, + @Diagnostic(type = UnmappableTargetCollectionElementPropertyMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 10, + line = 14, message = "Unmapped target property: \"color\". Mapping from " + PROPERTY + - " \"Info computers[].info\" to \"InfoDto computers[].info\".") + " \"Info computers[].info\" to \"InfoDto computers[].info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseCollectionElementPropertyMapper'.") } ) - public void testCollectionElementProperty() { + public void testTargetCollectionElementProperty() { } @ProcessorTest @WithClasses({ - UnmappableValuePropertyMapper.class + UnmappableTargetValuePropertyMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = BaseValuePropertyMapper.class, + @Diagnostic(type = UnmappableTargetValuePropertyMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, - line = 10, + line = 14, message = "Unmapped target property: \"color\". Mapping from " + PROPERTY + - " \"Info catNameMap{:value}.info\" to \"InfoDto catNameMap{:value}.info\".") + " \"Info catNameMap{:value}.info\" to \"InfoDto catNameMap{:value}.info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseValuePropertyMapper'.") } ) - public void testMapValueProperty() { + public void testTargetMapValueProperty() { } + @IssueKey( "2788" ) @ProcessorTest @WithClasses({ - UnmappableEnumMapper.class + UnmappableSourceDeepNestingMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = UnmappableEnumMapper.class, + @Diagnostic(type = UnmappableSourceDeepNestingMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 16, + message = "Unmapped source property: \"cmyk\". Mapping from " + PROPERTY + + " \"Color house.roof.color\" to \"ColorDto house.roof.color\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepNestingMapper'.") + } + ) + public void testSourceDeepNestedBeans() { + } + + @IssueKey( "2788" ) + @ProcessorTest + @WithClasses({ + UnmappableSourceDeepListMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = UnmappableSourceDeepListMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 16, + message = "Unmapped source property: \"right\". Mapping from " + COLLECTION_ELEMENT + + " \"Wheel car.wheels\" to \"WheelDto car.wheels\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepListMapper'.") + } + ) + public void testSourceIterables() { + } + + @IssueKey( "2788" ) + @ProcessorTest + @WithClasses({ + UnmappableSourceDeepMapKeyMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = UnmappableSourceDeepMapKeyMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 16, + message = "Unmapped source property: \"meaning\". Mapping from " + MAP_KEY + + " \"Word dictionary.wordMap{:key}\" to \"WordDto dictionary.wordMap{:key}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapKeyMapper'.") + } + ) + public void testSourceMapKeys() { + } + + @IssueKey( "2788" ) + @ProcessorTest + @WithClasses({ + UnmappableSourceDeepMapValueMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = UnmappableSourceDeepMapValueMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 16, + message = "Unmapped source property: \"meaning\". Mapping from " + MAP_VALUE + + " \"ForeignWord dictionary.wordMap{:value}\" to \"ForeignWordDto dictionary.wordMap{:value}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapValueMapper'.") + } + ) + public void testSourceMapValues() { + } + + @IssueKey( "2788" ) + @ProcessorTest + @WithClasses({ + UnmappableSourceCollectionElementPropertyMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = UnmappableSourceCollectionElementPropertyMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 16, + message = "Unmapped source property: \"size\". Mapping from " + PROPERTY + + " \"Info computers[].info\" to \"InfoDto computers[].info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseCollectionElementPropertyMapper'.") + } + ) + public void testSourceCollectionElementProperty() { + } + + @IssueKey( "2788" ) + @ProcessorTest + @WithClasses({ + UnmappableSourceValuePropertyMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = UnmappableSourceValuePropertyMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 16, + message = "Unmapped source property: \"size\". Mapping from " + PROPERTY + + " \"Info catNameMap{:value}.info\" to \"InfoDto catNameMap{:value}.info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseValuePropertyMapper'.") + } + ) + public void testSourceMapValueProperty() { + } + + @ProcessorTest + @WithClasses({ + UnmappableSourceEnumMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = UnmappableSourceEnumMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, line = 25, message = @@ -205,56 +338,116 @@ public void testMapValueProperty() { ) } ) - public void testMapEnumProperty() { + public void testSourceMapEnumProperty() { } @ProcessorTest @WithClasses({ - UnmappableWarnDeepNestingMapper.class, - UnmappableWarnDeepListMapper.class, - UnmappableWarnDeepMapKeyMapper.class, - UnmappableWarnDeepMapValueMapper.class, - UnmappableWarnCollectionElementPropertyMapper.class, - UnmappableWarnValuePropertyMapper.class + UnmappableTargetWarnDeepNestingMapper.class, + UnmappableTargetWarnDeepListMapper.class, + UnmappableTargetWarnDeepMapKeyMapper.class, + UnmappableTargetWarnDeepMapValueMapper.class, + UnmappableTargetWarnCollectionElementPropertyMapper.class, + UnmappableTargetWarnValuePropertyMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.SUCCEEDED, diagnostics = { - @Diagnostic(type = BaseDeepNestingMapper.class, + @Diagnostic(type = UnmappableTargetWarnDeepNestingMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 10, + line = 16, message = "Unmapped target property: \"rgb\". Mapping from " + PROPERTY + - " \"Color house.roof.color\" to \"ColorDto house.roof.color\"."), - @Diagnostic(type = BaseDeepListMapper.class, + " \"Color house.roof.color\" to \"ColorDto house.roof.color\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepNestingMapper'."), + @Diagnostic(type = UnmappableTargetWarnDeepListMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 10, + line = 16, message = "Unmapped target property: \"left\". Mapping from " + COLLECTION_ELEMENT + - " \"Wheel car.wheels\" to \"WheelDto car.wheels\"."), - @Diagnostic(type = BaseDeepMapKeyMapper.class, + " \"Wheel car.wheels\" to \"WheelDto car.wheels\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepListMapper'."), + @Diagnostic(type = UnmappableTargetWarnDeepMapKeyMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 10, + line = 16, message = "Unmapped target property: \"pronunciation\". Mapping from " + MAP_KEY + - " \"Word dictionary.wordMap{:key}\" to \"WordDto dictionary.wordMap{:key}\"."), - @Diagnostic(type = BaseDeepMapValueMapper.class, + " \"Word dictionary.wordMap{:key}\" to \"WordDto dictionary.wordMap{:key}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapKeyMapper'."), + @Diagnostic(type = UnmappableTargetWarnDeepMapValueMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 10, + line = 16, message = "Unmapped target property: \"pronunciation\". Mapping from " + MAP_VALUE + - " \"ForeignWord dictionary.wordMap{:value}\" to \"ForeignWordDto dictionary.wordMap{:value}\"."), - @Diagnostic(type = BaseCollectionElementPropertyMapper.class, + " \"ForeignWord dictionary.wordMap{:value}\" to \"ForeignWordDto dictionary.wordMap{:value}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapValueMapper'."), + @Diagnostic(type = UnmappableTargetWarnCollectionElementPropertyMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 10, + line = 16, message = "Unmapped target property: \"color\". Mapping from " + PROPERTY + - " \"Info computers[].info\" to \"InfoDto computers[].info\"."), - @Diagnostic(type = BaseValuePropertyMapper.class, + " \"Info computers[].info\" to \"InfoDto computers[].info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseCollectionElementPropertyMapper'."), + @Diagnostic(type = UnmappableTargetWarnValuePropertyMapper.class, kind = javax.tools.Diagnostic.Kind.WARNING, - line = 10, + line = 16, message = "Unmapped target property: \"color\". Mapping from " + PROPERTY + - " \"Info catNameMap{:value}.info\" to \"InfoDto catNameMap{:value}.info\".") + " \"Info catNameMap{:value}.info\" to \"InfoDto catNameMap{:value}.info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseValuePropertyMapper'.") } ) public void testWarnUnmappedTargetProperties() { } + @IssueKey( "2788" ) + @ProcessorTest + @WithClasses({ + UnmappableSourceWarnDeepNestingMapper.class, + UnmappableSourceWarnDeepListMapper.class, + UnmappableSourceWarnDeepMapKeyMapper.class, + UnmappableSourceWarnDeepMapValueMapper.class, + UnmappableSourceWarnCollectionElementPropertyMapper.class, + UnmappableSourceWarnValuePropertyMapper.class + }) + @ExpectedCompilationOutcome( + value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = UnmappableSourceWarnDeepNestingMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 16, + message = "Unmapped source property: \"cmyk\". Mapping from " + PROPERTY + + " \"Color house.roof.color\" to \"ColorDto house.roof.color\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepNestingMapper'."), + @Diagnostic(type = UnmappableSourceWarnDeepListMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 16, + message = "Unmapped source property: \"right\". Mapping from " + COLLECTION_ELEMENT + + " \"Wheel car.wheels\" to \"WheelDto car.wheels\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepListMapper'."), + @Diagnostic(type = UnmappableSourceWarnDeepMapKeyMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 16, + message = "Unmapped source property: \"meaning\". Mapping from " + MAP_KEY + + " \"Word dictionary.wordMap{:key}\" to \"WordDto dictionary.wordMap{:key}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapKeyMapper'."), + @Diagnostic(type = UnmappableSourceWarnDeepMapValueMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 16, + message = "Unmapped source property: \"meaning\". Mapping from " + MAP_VALUE + + " \"ForeignWord dictionary.wordMap{:value}\" to \"ForeignWordDto dictionary.wordMap{:value}\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseDeepMapValueMapper'."), + @Diagnostic(type = UnmappableSourceWarnCollectionElementPropertyMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 16, + message = "Unmapped source property: \"size\". Mapping from " + PROPERTY + + " \"Info computers[].info\" to \"InfoDto computers[].info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseCollectionElementPropertyMapper'."), + @Diagnostic(type = UnmappableSourceWarnValuePropertyMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 16, + message = "Unmapped source property: \"size\". Mapping from " + PROPERTY + + " \"Info catNameMap{:value}.info\" to \"InfoDto catNameMap{:value}.info\"." + + " Occured at 'UserDto userToUserDto(User user)' in 'BaseValuePropertyMapper'.") + } + ) + public void testWarnUnmappedSourceProperties() { + } + @ProcessorTest @WithClasses({ UnmappableIgnoreDeepNestingMapper.class, diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/HouseDto.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/HouseDto.java index 1d92eb7e4f..71beedaa93 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/HouseDto.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/HouseDto.java @@ -5,6 +5,8 @@ */ package org.mapstruct.ap.test.nestedbeans; +import java.util.Objects; + public class HouseDto { private String name; @@ -58,10 +60,10 @@ public boolean equals(Object o) { if ( year != houseDto.year ) { return false; } - if ( name != null ? !name.equals( houseDto.name ) : houseDto.name != null ) { + if ( !Objects.equals( name, houseDto.name ) ) { return false; } - return roof != null ? roof.equals( houseDto.roof ) : houseDto.roof == null; + return Objects.equals( roof, houseDto.roof ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/RecursionTest.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/RecursionTest.java index e8caa784b1..861b1a01b9 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/RecursionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/RecursionTest.java @@ -37,7 +37,7 @@ public void testRecursiveAutoMap() { } private void assertRootEquals(RecursionMapper.RootDto rootDto, RecursionMapper.Root root) { - if (bothNull( root, rootDto )) { + if ( bothNull( root, rootDto ) ) { return; } assertNotNull( root ); @@ -78,7 +78,7 @@ public void testRecursiveTreeAutoMap() { } private void assertTreeRootEquals(TreeRecursionMapper.RootDto rootDto, TreeRecursionMapper.Root root) { - if (bothNull( root, rootDto )) { + if ( bothNull( root, rootDto ) ) { return; } assertNotNull( root ); @@ -88,7 +88,7 @@ private void assertTreeRootEquals(TreeRecursionMapper.RootDto rootDto, TreeRecur private void assertChildrenEqual(List childrenDto, List children) { - if (bothNull( children, childrenDto )) { + if ( bothNull( children, childrenDto ) ) { return; } assertNotNull( childrenDto ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassic.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassic.java index 584c83a527..8c1c6925b9 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassic.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassic.java @@ -5,11 +5,11 @@ */ package org.mapstruct.ap.test.nestedbeans; +import java.util.List; + import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; -import java.util.List; - @Mapper public interface UserDtoMapperClassic { diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/maps/AntonymsDictionaryDto.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/maps/AntonymsDictionaryDto.java index 33ccfbc1cc..9ac7913c9f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/maps/AntonymsDictionaryDto.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/maps/AntonymsDictionaryDto.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.test.nestedbeans.maps; import java.util.Map; +import java.util.Objects; public class AntonymsDictionaryDto { private Map antonyms; @@ -36,8 +37,7 @@ public boolean equals(Object o) { AntonymsDictionaryDto antonymsDictionaryDto = (AntonymsDictionaryDto) o; - return antonyms != null ? antonyms.equals( antonymsDictionaryDto.antonyms ) : - antonymsDictionaryDto.antonyms == null; + return Objects.equals( antonyms, antonymsDictionaryDto.antonyms ); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceCollectionElementPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceCollectionElementPropertyMapper.java new file mode 100644 index 0000000000..28265073ea --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceCollectionElementPropertyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseCollectionElementPropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.ERROR; +import static org.mapstruct.ReportingPolicy.IGNORE; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = ERROR ) +public abstract class UnmappableSourceCollectionElementPropertyMapper extends BaseCollectionElementPropertyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepListMapper.java new file mode 100644 index 0000000000..7d4b89f4aa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepListMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepListMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.ERROR; +import static org.mapstruct.ReportingPolicy.IGNORE; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = ERROR ) +public abstract class UnmappableSourceDeepListMapper extends BaseDeepListMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepMapKeyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepMapKeyMapper.java new file mode 100644 index 0000000000..01a3e48de6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepMapKeyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapKeyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.ERROR; +import static org.mapstruct.ReportingPolicy.IGNORE; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = ERROR ) +public abstract class UnmappableSourceDeepMapKeyMapper extends BaseDeepMapKeyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepMapValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepMapValueMapper.java new file mode 100644 index 0000000000..ba606a6222 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepMapValueMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapValueMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.ERROR; +import static org.mapstruct.ReportingPolicy.IGNORE; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = ERROR ) +public abstract class UnmappableSourceDeepMapValueMapper extends BaseDeepMapValueMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepNestingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepNestingMapper.java new file mode 100644 index 0000000000..1ec7dbd36c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceDeepNestingMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepNestingMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.ERROR; +import static org.mapstruct.ReportingPolicy.IGNORE; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = ERROR ) +public abstract class UnmappableSourceDeepNestingMapper extends BaseDeepNestingMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableEnumMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceEnumMapper.java similarity index 96% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableEnumMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceEnumMapper.java index 1733c9af0b..8fcb737ee4 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableEnumMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceEnumMapper.java @@ -20,7 +20,7 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.UserDto; @Mapper -public abstract class UnmappableEnumMapper { +public abstract class UnmappableSourceEnumMapper { abstract UserDto userToUserDto(User user); diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceValuePropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceValuePropertyMapper.java new file mode 100644 index 0000000000..4b686642c3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableSourceValuePropertyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseValuePropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.ERROR; +import static org.mapstruct.ReportingPolicy.IGNORE; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = ERROR ) +public abstract class UnmappableSourceValuePropertyMapper extends BaseValuePropertyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableCollectionElementPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetCollectionElementPropertyMapper.java similarity index 68% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableCollectionElementPropertyMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetCollectionElementPropertyMapper.java index 493442cae0..5198324cbe 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableCollectionElementPropertyMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetCollectionElementPropertyMapper.java @@ -10,6 +10,6 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseCollectionElementPropertyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR) -public abstract class UnmappableCollectionElementPropertyMapper extends BaseCollectionElementPropertyMapper { +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR ) +public abstract class UnmappableTargetCollectionElementPropertyMapper extends BaseCollectionElementPropertyMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepListMapper.java similarity index 71% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepListMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepListMapper.java index 275d043cfb..993e30ec60 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepListMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepListMapper.java @@ -10,6 +10,6 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepListMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR) -public abstract class UnmappableDeepListMapper extends BaseDeepListMapper { +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR ) +public abstract class UnmappableTargetDeepListMapper extends BaseDeepListMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepMapKeyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepMapKeyMapper.java similarity index 71% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepMapKeyMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepMapKeyMapper.java index c26fbb7df6..e30533d00d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepMapKeyMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepMapKeyMapper.java @@ -10,6 +10,6 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapKeyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR) -public abstract class UnmappableDeepMapKeyMapper extends BaseDeepMapKeyMapper { +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR ) +public abstract class UnmappableTargetDeepMapKeyMapper extends BaseDeepMapKeyMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepMapValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepMapValueMapper.java similarity index 70% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepMapValueMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepMapValueMapper.java index 4c9c8d9f93..f46651dbc1 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepMapValueMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepMapValueMapper.java @@ -10,6 +10,6 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapValueMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR) -public abstract class UnmappableDeepMapValueMapper extends BaseDeepMapValueMapper { +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR ) +public abstract class UnmappableTargetDeepMapValueMapper extends BaseDeepMapValueMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepNestingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepNestingMapper.java similarity index 71% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepNestingMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepNestingMapper.java index 00b0401ee5..736aef3d84 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableDeepNestingMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetDeepNestingMapper.java @@ -10,6 +10,6 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepNestingMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR) -public abstract class UnmappableDeepNestingMapper extends BaseDeepNestingMapper { +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR ) +public abstract class UnmappableTargetDeepNestingMapper extends BaseDeepNestingMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableValuePropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetValuePropertyMapper.java similarity index 70% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableValuePropertyMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetValuePropertyMapper.java index dd657c8569..e43f7d2e1a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableValuePropertyMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/erroneous/UnmappableTargetValuePropertyMapper.java @@ -10,6 +10,6 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseValuePropertyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR) -public abstract class UnmappableValuePropertyMapper extends BaseValuePropertyMapper { +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = ReportingPolicy.ERROR ) +public abstract class UnmappableTargetValuePropertyMapper extends BaseValuePropertyMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnCollectionElementPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnCollectionElementPropertyMapper.java similarity index 55% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnCollectionElementPropertyMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnCollectionElementPropertyMapper.java index 64610a61f8..500b1d7a37 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnCollectionElementPropertyMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnCollectionElementPropertyMapper.java @@ -9,6 +9,9 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseCollectionElementPropertyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class) -public abstract class UnmappableWarnCollectionElementPropertyMapper extends BaseCollectionElementPropertyMapper { +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = WARN ) +public abstract class UnmappableSourceWarnCollectionElementPropertyMapper extends BaseCollectionElementPropertyMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepListMapper.java similarity index 56% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepListMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepListMapper.java index 6ca1ce71da..dafe6836a6 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepListMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepListMapper.java @@ -9,6 +9,9 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepListMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class) -public abstract class UnmappableWarnDeepListMapper extends BaseDeepListMapper { +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = WARN ) +public abstract class UnmappableSourceWarnDeepListMapper extends BaseDeepListMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepMapKeyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepMapKeyMapper.java similarity index 56% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepMapKeyMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepMapKeyMapper.java index a010ac05ed..8191ced79c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepMapKeyMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepMapKeyMapper.java @@ -9,6 +9,9 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapKeyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class) -public abstract class UnmappableWarnDeepMapKeyMapper extends BaseDeepMapKeyMapper { +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = WARN ) +public abstract class UnmappableSourceWarnDeepMapKeyMapper extends BaseDeepMapKeyMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepMapValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepMapValueMapper.java similarity index 56% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepMapValueMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepMapValueMapper.java index 53e60c6687..4152c22c30 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepMapValueMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepMapValueMapper.java @@ -9,6 +9,9 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapValueMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class) -public abstract class UnmappableWarnDeepMapValueMapper extends BaseDeepMapValueMapper { +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = WARN ) +public abstract class UnmappableSourceWarnDeepMapValueMapper extends BaseDeepMapValueMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepNestingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepNestingMapper.java similarity index 56% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepNestingMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepNestingMapper.java index 50d0b366b0..86631e25bf 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnDeepNestingMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnDeepNestingMapper.java @@ -9,6 +9,9 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepNestingMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class) -public abstract class UnmappableWarnDeepNestingMapper extends BaseDeepNestingMapper { +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = WARN ) +public abstract class UnmappableSourceWarnDeepNestingMapper extends BaseDeepNestingMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnValuePropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnValuePropertyMapper.java similarity index 56% rename from processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnValuePropertyMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnValuePropertyMapper.java index f2877b215b..90473cac6d 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableWarnValuePropertyMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableSourceWarnValuePropertyMapper.java @@ -9,6 +9,9 @@ import org.mapstruct.ap.test.nestedbeans.unmappable.BaseValuePropertyMapper; import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; -@Mapper(uses = RoofTypeMapper.class) -public abstract class UnmappableWarnValuePropertyMapper extends BaseValuePropertyMapper { +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = IGNORE, unmappedSourcePolicy = WARN ) +public abstract class UnmappableSourceWarnValuePropertyMapper extends BaseValuePropertyMapper { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnCollectionElementPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnCollectionElementPropertyMapper.java new file mode 100644 index 0000000000..4992ffbd69 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnCollectionElementPropertyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.warn; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseCollectionElementPropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = WARN, unmappedSourcePolicy = IGNORE ) +public abstract class UnmappableTargetWarnCollectionElementPropertyMapper extends BaseCollectionElementPropertyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepListMapper.java new file mode 100644 index 0000000000..36bab5a93c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepListMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.warn; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepListMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = WARN, unmappedSourcePolicy = IGNORE ) +public abstract class UnmappableTargetWarnDeepListMapper extends BaseDeepListMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepMapKeyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepMapKeyMapper.java new file mode 100644 index 0000000000..452574c5e5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepMapKeyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.warn; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapKeyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = WARN, unmappedSourcePolicy = IGNORE ) +public abstract class UnmappableTargetWarnDeepMapKeyMapper extends BaseDeepMapKeyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepMapValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepMapValueMapper.java new file mode 100644 index 0000000000..ebcca95b6d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepMapValueMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.warn; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepMapValueMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = WARN, unmappedSourcePolicy = IGNORE ) +public abstract class UnmappableTargetWarnDeepMapValueMapper extends BaseDeepMapValueMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepNestingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepNestingMapper.java new file mode 100644 index 0000000000..466947b2a3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnDeepNestingMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.warn; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseDeepNestingMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = WARN, unmappedSourcePolicy = IGNORE ) +public abstract class UnmappableTargetWarnDeepNestingMapper extends BaseDeepNestingMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnValuePropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnValuePropertyMapper.java new file mode 100644 index 0000000000..1af841048f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedbeans/unmappable/warn/UnmappableTargetWarnValuePropertyMapper.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nestedbeans.unmappable.warn; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.BaseValuePropertyMapper; +import org.mapstruct.ap.test.nestedbeans.unmappable.RoofTypeMapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; +import static org.mapstruct.ReportingPolicy.WARN; + +@Mapper( uses = RoofTypeMapper.class, unmappedTargetPolicy = WARN, unmappedSourcePolicy = IGNORE ) +public abstract class UnmappableTargetWarnValuePropertyMapper extends BaseValuePropertyMapper { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/NestedMappingMethodInvocationTest.java b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/NestedMappingMethodInvocationTest.java index 1e9b52cf62..03c47f7a49 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/NestedMappingMethodInvocationTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/NestedMappingMethodInvocationTest.java @@ -17,9 +17,11 @@ import javax.xml.namespace.QName; import org.junitpioneer.jupiter.DefaultLocale; +import org.junitpioneer.jupiter.ReadsDefaultTimeZone; import org.mapstruct.ap.testutil.IssueKey; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJavaxJaxb; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +32,7 @@ */ @IssueKey("134") @DefaultLocale("de") +@ReadsDefaultTimeZone public class NestedMappingMethodInvocationTest { public static final QName QNAME = new QName( "dont-care" ); @@ -42,6 +45,7 @@ public class NestedMappingMethodInvocationTest { OrderDetailsType.class, OrderType.class } ) + @WithJavaxJaxb public void shouldMapViaMethodAndMethod() throws DatatypeConfigurationException { OrderTypeToOrderDtoMapper instance = OrderTypeToOrderDtoMapper.INSTANCE; OrderDto target = instance.sourceToTarget( createOrderType() ); @@ -62,6 +66,7 @@ public void shouldMapViaMethodAndMethod() throws DatatypeConfigurationException ObjectFactory.class, TargetDto.class } ) + @WithJavaxJaxb public void shouldMapViaMethodAndConversion() { SourceTypeTargetDtoMapper instance = SourceTypeTargetDtoMapper.INSTANCE; @@ -78,6 +83,7 @@ public void shouldMapViaMethodAndConversion() { ObjectFactory.class, TargetDto.class } ) + @WithJavaxJaxb public void shouldMapViaConversionAndMethod() { SourceTypeTargetDtoMapper instance = SourceTypeTargetDtoMapper.INSTANCE; diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderDetailsType.java b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderDetailsType.java index 534166656f..98547b38ff 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderDetailsType.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderDetailsType.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.nestedmethodcall; import java.util.List; - import javax.xml.bind.JAXBElement; /** diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderType.java b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderType.java index 5925685f65..6fdef2705e 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderType.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderType.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.nestedmethodcall; import java.util.List; - import javax.xml.bind.JAXBElement; import javax.xml.datatype.XMLGregorianCalendar; diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderTypeToOrderDtoMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderTypeToOrderDtoMapper.java index 9cd162e9bc..24ea469135 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderTypeToOrderDtoMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedmethodcall/OrderTypeToOrderDtoMapper.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.test.nestedmethodcall; import java.util.List; - import javax.xml.bind.JAXBElement; import javax.xml.datatype.XMLGregorianCalendar; diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/AddressBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/AddressBean.java new file mode 100644 index 0000000000..4908d84aa7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/AddressBean.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class AddressBean { + + private String street; + private String city; + + @NonNull + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + @Nullable + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ConstructorTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ConstructorTargetBean.java new file mode 100644 index 0000000000..4ae7de5745 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ConstructorTargetBean.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class ConstructorTargetBean { + + private final String nonNullParam; + private final String nullableParam; + private final String unannotatedParam; + + public ConstructorTargetBean(@NonNull String nonNullParam, + @Nullable String nullableParam, + String unannotatedParam) { + this.nonNullParam = nonNullParam; + this.nullableParam = nullableParam; + this.unannotatedParam = unannotatedParam; + } + + public String getNonNullParam() { + return nonNullParam; + } + + public String getNullableParam() { + return nullableParam; + } + + public String getUnannotatedParam() { + return unannotatedParam; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyConstructorMapper.java new file mode 100644 index 0000000000..b2e2cc0311 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyConstructorMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Erroneous mapper: maps @Nullable source to @NonNull constructor parameter without default value. + */ +@Mapper +public interface ErroneousJSpecifyConstructorMapper { + + ErroneousJSpecifyConstructorMapper INSTANCE = Mappers.getMapper( ErroneousJSpecifyConstructorMapper.class ); + + @Mapping(target = "nonNullParam", source = "nullableValue") + @Mapping(target = "nullableParam", source = "nullableValue") + @Mapping(target = "unannotatedParam", source = "nonNullValue") + ConstructorTargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyDirectParamConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyDirectParamConstructorMapper.java new file mode 100644 index 0000000000..58fc6948a3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyDirectParamConstructorMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Erroneous mapper: a {@code @Nullable} method parameter is mapped directly (no property entries) + * to a {@code @NonNull} constructor parameter without {@code defaultValue}. + */ +@Mapper +public interface ErroneousJSpecifyDirectParamConstructorMapper { + + ErroneousJSpecifyDirectParamConstructorMapper INSTANCE = + Mappers.getMapper( ErroneousJSpecifyDirectParamConstructorMapper.class ); + + @Mapping(target = "nonNullParam", source = "nullableValue") + @Mapping(target = "nullableParam", source = "nullableValue") + @Mapping(target = "unannotatedParam", source = "nullableValue") + ConstructorTargetBean map(@Nullable String nullableValue); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyUnforgeableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyUnforgeableMapper.java new file mode 100644 index 0000000000..7fb7da3d26 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/ErroneousJSpecifyUnforgeableMapper.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * Erroneous mapper: the nullable source property cannot be mapped to the target constructor + * parameter's type at all (String -> AddressBean, with no mapping method declared). The + * "cannot find mapping" error must be the only diagnostic; the JSpecify + * "@NonNull constructor parameter" error must not also fire when assignment resolution failed. + */ +@Mapper +public interface ErroneousJSpecifyUnforgeableMapper { + + @Mapping(target = "payload", source = "nullableValue") + UnmappableConstructorTargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/FlatTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/FlatTargetBean.java new file mode 100644 index 0000000000..76e3c02dbd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/FlatTargetBean.java @@ -0,0 +1,59 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; + +public class FlatTargetBean { + + private String street; + private boolean streetSet; + + private String city; + private boolean citySet; + + private String nullableStreet; + private boolean nullableStreetSet; + + public String getStreet() { + return street; + } + + public void setStreet(@NonNull String street) { + this.streetSet = true; + this.street = street; + } + + public boolean isStreetSet() { + return streetSet; + } + + public String getCity() { + return city; + } + + public void setCity(@NonNull String city) { + this.citySet = true; + this.city = city; + } + + public boolean isCitySet() { + return citySet; + } + + public String getNullableStreet() { + return nullableStreet; + } + + public void setNullableStreet(String nullableStreet) { + this.nullableStreetSet = true; + this.nullableStreet = nullableStreet; + } + + public boolean isNullableStreetSet() { + return nullableStreetSet; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java new file mode 100644 index 0000000000..d705652832 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyCollectionPropertyMapper { + + JSpecifyCollectionPropertyMapper INSTANCE = Mappers.getMapper( JSpecifyCollectionPropertyMapper.class ); + + NullMarkedCollectionTargetBean map(NullMarkedCollectionSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java new file mode 100644 index 0000000000..bf194fc08f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyCollectionPropertyTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedCollectionSourceBean.class, + NullMarkedCollectionTargetBean.class, + JSpecifyCollectionPropertyMapper.class + }) + void collectionPropertyWithNonNullSourceSkipsNullCheck() { + generatedSource.addComparisonToFixtureFor( JSpecifyCollectionPropertyMapper.class ); + + NullMarkedCollectionSourceBean source = new NullMarkedCollectionSourceBean(); + source.setValues( Arrays.asList( "a", "b", "c" ) ); + + NullMarkedCollectionTargetBean target = JSpecifyCollectionPropertyMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getValues() ).containsExactly( "a", "b", "c" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorDefaultValueMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorDefaultValueMapper.java new file mode 100644 index 0000000000..4b3956dccc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorDefaultValueMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * A {@code defaultValue} on the mapping of a {@code @Nullable} source to a {@code @NonNull} + * constructor parameter must suppress the "potentially nullable source" compile error. + */ +@Mapper +public interface JSpecifyConstructorDefaultValueMapper { + + JSpecifyConstructorDefaultValueMapper INSTANCE = + Mappers.getMapper( JSpecifyConstructorDefaultValueMapper.class ); + + @Mapping(target = "nonNullParam", source = "nullableValue", defaultValue = "fallback") + @Mapping(target = "nullableParam", source = "nullableValue") + @Mapping(target = "unannotatedParam", source = "nonNullValue") + ConstructorTargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorMapper.java new file mode 100644 index 0000000000..8791ebfaaa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper with constructor target to test JSpecify on constructor parameters. + * Only valid mappings: @NonNull source to @NonNull target, @Nullable to @Nullable. + */ +@Mapper +public interface JSpecifyConstructorMapper { + + JSpecifyConstructorMapper INSTANCE = Mappers.getMapper( JSpecifyConstructorMapper.class ); + + @Mapping(target = "nonNullParam", source = "nonNullValue") + @Mapping(target = "nullableParam", source = "nullableValue") + @Mapping(target = "unannotatedParam", source = "unannotatedValue") + ConstructorTargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorTest.java new file mode 100644 index 0000000000..9f1cd90fee --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyConstructorTest.java @@ -0,0 +1,124 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for JSpecify nullness annotations with constructor mappings. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithClasses({ + SourceBean.class, + ConstructorTargetBean.class +}) +@WithJSpecify +class JSpecifyConstructorTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses(JSpecifyConstructorMapper.class) + public void verifyGeneratedCode() { + generatedSource.addComparisonToFixtureFor( JSpecifyConstructorMapper.class ); + } + + @ProcessorTest + @WithClasses(JSpecifyConstructorMapper.class) + public void constructorMappingWithNonNullSource() { + SourceBean source = new SourceBean(); + source.setNonNullValue( "value" ); + source.setNullableValue( "nullable" ); + source.setUnannotatedValue( "unannotated" ); + + ConstructorTargetBean target = JSpecifyConstructorMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getNonNullParam() ).isEqualTo( "value" ); + assertThat( target.getNullableParam() ).isEqualTo( "nullable" ); + assertThat( target.getUnannotatedParam() ).isEqualTo( "unannotated" ); + } + + @ProcessorTest + @WithClasses(ErroneousJSpecifyConstructorMapper.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousJSpecifyConstructorMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + message = "Can't map potentially nullable source property \"nullableValue\" to @NonNull " + + "constructor parameter \"nonNullParam\". Consider adding a defaultValue or " + + "defaultExpression.") + }) + public void nullableSourceToNonNullConstructorParamShouldFail() { + } + + @ProcessorTest + @WithClasses(JSpecifyDirectParamConstructorMapper.class) + public void nonNullDirectParameterToNonNullConstructorParamCompiles() { + // A @NonNull method parameter (source has no property entries — it IS the parameter) + // must be accepted by a @NonNull constructor parameter without triggering the + // "potentially nullable source" error. + ConstructorTargetBean target = JSpecifyDirectParamConstructorMapper.INSTANCE.map( + "nn", "nullable", "plain" ); + + assertThat( target.getNonNullParam() ).isEqualTo( "nn" ); + assertThat( target.getNullableParam() ).isEqualTo( "nullable" ); + assertThat( target.getUnannotatedParam() ).isEqualTo( "plain" ); + } + + @ProcessorTest + @WithClasses(ErroneousJSpecifyDirectParamConstructorMapper.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousJSpecifyDirectParamConstructorMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + message = "Can't map potentially nullable source property \"nullableValue\" to @NonNull " + + "constructor parameter \"nonNullParam\". Consider adding a defaultValue or " + + "defaultExpression.") + }) + public void nullableDirectParameterToNonNullConstructorParamShouldFail() { + } + + @ProcessorTest + @WithClasses({ AddressBean.class, UnmappableConstructorTargetBean.class, ErroneousJSpecifyUnforgeableMapper.class }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousJSpecifyUnforgeableMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + message = "Can't map property \"String nullableValue\" to \"AddressBean payload\". " + + "Consider to declare/implement a mapping method: \"AddressBean map(String value)\".") + }) + public void failedAssignmentDoesNotAlsoTriggerNullableToNonNullConstructorParamError() { + // The JSpecify @NonNull constructor-param error must not fire when the assignment + // itself could not be resolved; otherwise users see two errors for one underlying issue. + } + + @ProcessorTest + @WithClasses(JSpecifyConstructorDefaultValueMapper.class) + public void defaultValueSuppressesNullableToNonNullConstructorParamError() { + SourceBean source = new SourceBean(); + source.setNonNullValue( "present" ); + // nullableValue left null on purpose — defaultValue "fallback" should kick in. + + ConstructorTargetBean target = JSpecifyConstructorDefaultValueMapper.INSTANCE.map( source ); + + assertThat( target.getNonNullParam() ).isEqualTo( "fallback" ); + assertThat( target.getUnannotatedParam() ).isEqualTo( "present" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDirectParamConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDirectParamConstructorMapper.java new file mode 100644 index 0000000000..4bb49a5f5d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDirectParamConstructorMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper that maps direct method parameters (no property entries) to constructor parameters. + * Verifies that a {@code @NonNull} source parameter is accepted by a {@code @NonNull} + * constructor parameter target. + */ +@Mapper +public interface JSpecifyDirectParamConstructorMapper { + + JSpecifyDirectParamConstructorMapper INSTANCE = Mappers.getMapper( JSpecifyDirectParamConstructorMapper.class ); + + @Mapping(target = "nonNullParam", source = "nonNullValue") + @Mapping(target = "nullableParam", source = "nullableValue") + @Mapping(target = "unannotatedParam", source = "unannotatedValue") + ConstructorTargetBean map(@NonNull String nonNullValue, + @Nullable String nullableValue, + String unannotatedValue); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java new file mode 100644 index 0000000000..2de32c4235 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Dedicated to {@link JSpecifyDisabledTest} so its generated implementation is not shared (and pre-loaded) by an + * enabled test in the same JVM. With JSpecify enabled the {@code @NonNull} source would skip the method-level guard. + */ +@NullMarked +@Mapper +public interface JSpecifyDisabledContainerSourceMapper { + + JSpecifyDisabledContainerSourceMapper INSTANCE = + Mappers.getMapper( JSpecifyDisabledContainerSourceMapper.class ); + + List mapAll(List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledMapper.java new file mode 100644 index 0000000000..08a5fe3c37 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * Dedicated mapper for {@link JSpecifyDisabledTest} so that the test's JSpecify-disabled + * compilation result is isolated from tests that share {@link JSpecifyNullCheckMapper} and + * would otherwise keep the JSpecify-enabled {@code INSTANCE} cached in the JVM. + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface JSpecifyDisabledMapper { + + JSpecifyDisabledMapper INSTANCE = Mappers.getMapper( JSpecifyDisabledMapper.class ); + + @Mapping(target = "nonNullTargetFromNullable", source = "nullableValue") + TargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java new file mode 100644 index 0000000000..bcdb0bc0ca --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Dedicated to {@link JSpecifyDisabledTest} so its generated implementation is not shared (and pre-loaded) by an + * enabled test in the same JVM. With JSpecify enabled the {@code @NonNull} return would force RETURN_DEFAULT. + */ +@NullMarked +@Mapper +public interface JSpecifyDisabledNonNullReturnMapper { + + JSpecifyDisabledNonNullReturnMapper INSTANCE = + Mappers.getMapper( JSpecifyDisabledNonNullReturnMapper.class ); + + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java new file mode 100644 index 0000000000..d669952737 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java @@ -0,0 +1,83 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@code -Amapstruct.disableJSpecify=true} fully opts out of JSpecify-based + * null-check inference. With the flag on, mappers that would normally be affected by + * JSpecify fall back to the pre-JSpecify baseline. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithJSpecify +@ProcessorOption(name = "mapstruct.disableJSpecify", value = "true") +class JSpecifyDisabledTest { + + @ProcessorTest + @WithClasses({ SourceBean.class, TargetBean.class, JSpecifyDisabledMapper.class }) + public void disabledFlagSkipsTargetNonNullInference() { + // With JSpecify enabled, mapping a @Nullable source onto a @NonNull target setter + // emits an "if (src != null)" guard. When disabled, the default NullValueCheckStrategy + // (ON_IMPLICIT_CONVERSION) applies and a direct String->String assignment gets no guard, + // so the setter is always invoked — even when the source is null. + SourceBean source = new SourceBean(); + // nullableValue left null + + TargetBean target = JSpecifyDisabledMapper.INSTANCE.map( source ); + + assertThat( target.isNonNullTargetFromNullableSet() ).isTrue(); + assertThat( target.getNonNullTargetFromNullable() ).isNull(); + } + + @ProcessorTest + @WithClasses({ + SourceBean.class, + ConstructorTargetBean.class, + ErroneousJSpecifyConstructorMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED) + public void disabledFlagSkipsConstructorNonNullHardError() { + // The JSpecify-driven hard error for a @Nullable source mapped to a @NonNull constructor + // parameter is purely a JSpecify signal; with the flag disabled it must not be raised. + } + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyDisabledContainerSourceMapper.class + }) + public void disabledFlagKeepsContainerMethodSourceGuard() { + // With JSpecify enabled the @NonNull source parameter skips the method-level null guard. When disabled, + // the resolver reports UNKNOWN, so the unconditional "if (sources == null) return null;" guard is kept and + // mapping a null source returns null rather than throwing an NPE. + assertThat( JSpecifyDisabledContainerSourceMapper.INSTANCE.mapAll( null ) ).isNull(); + } + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyDisabledNonNullReturnMapper.class + }) + public void disabledFlagSkipsNonNullReturnForcing() { + // With JSpecify enabled the @NonNull return forces RETURN_DEFAULT (empty list for a null source). When + // disabled, that forcing is suppressed and the default RETURN_NULL strategy applies, so a null source + // maps to null. + assertThat( JSpecifyDisabledNonNullReturnMapper.INSTANCE.mapAll( null ) ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java new file mode 100644 index 0000000000..5a1d0b0d68 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyIterableMethodMapper { + + JSpecifyIterableMethodMapper INSTANCE = Mappers.getMapper( JSpecifyIterableMethodMapper.class ); + + List mapAll(List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java new file mode 100644 index 0000000000..ceed2236f6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java @@ -0,0 +1,44 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyIterableMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyIterableMethodMapper.class + }) + void iterableMethodWithNonNullSourceSkipsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyIterableMethodMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List targets = JSpecifyIterableMethodMapper.INSTANCE.mapAll( Arrays.asList( source ) ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java new file mode 100644 index 0000000000..cdb844795b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Map; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyMapMethodMapper { + + JSpecifyMapMethodMapper INSTANCE = Mappers.getMapper( JSpecifyMapMethodMapper.class ); + + Map mapAll(Map sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java new file mode 100644 index 0000000000..4673841873 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyMapMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyMapMethodMapper.class + }) + void mapMethodWithNonNullSourceSkipsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyMapMethodMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + Map targets = + JSpecifyMapMethodMapper.INSTANCE.mapAll( Collections.singletonMap( "k", source ) ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( "k" ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNestedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNestedMapper.java new file mode 100644 index 0000000000..6d5e005446 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNestedMapper.java @@ -0,0 +1,28 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper with nested property chains and JSpecify annotations. + * Tests that the deepest property's annotation is used for the null check decision. + */ +@Mapper +public interface JSpecifyNestedMapper { + + JSpecifyNestedMapper INSTANCE = Mappers.getMapper( JSpecifyNestedMapper.class ); + + // nonNullAddress.street: leaf @NonNull -> target @NonNull -> skip null check (source @NonNull) + @Mapping(target = "street", source = "nonNullAddress.street") + // nonNullAddress.city: leaf @Nullable -> target @NonNull -> null check + @Mapping(target = "city", source = "nonNullAddress.city") + // nonNullAddress.street: leaf @NonNull -> target unannotated -> skip null check (source @NonNull) + @Mapping(target = "nullableStreet", source = "nonNullAddress.street") + FlatTargetBean map(NestedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNestedTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNestedTest.java new file mode 100644 index 0000000000..a17c882da1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNestedTest.java @@ -0,0 +1,83 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for JSpecify nullness annotations with nested property chains. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithClasses({ + AddressBean.class, + NestedSourceBean.class, + FlatTargetBean.class, + JSpecifyNestedMapper.class +}) +@WithJSpecify +class JSpecifyNestedTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void verifyGeneratedCode() { + generatedSource.addComparisonToFixtureFor( JSpecifyNestedMapper.class ); + } + + @ProcessorTest + public void nestedNonNullLeafSkipsNullCheck() { + AddressBean address = new AddressBean(); + address.setStreet( "Main St" ); + + NestedSourceBean source = new NestedSourceBean(); + source.setNonNullAddress( address ); + + FlatTargetBean target = JSpecifyNestedMapper.INSTANCE.map( source ); + + assertThat( target.isStreetSet() ).isTrue(); + assertThat( target.getStreet() ).isEqualTo( "Main St" ); + } + + @ProcessorTest + public void nestedNullableLeafToNonNullTargetAddsNullCheck() { + AddressBean address = new AddressBean(); + // city is null (@Nullable) + + NestedSourceBean source = new NestedSourceBean(); + source.setNonNullAddress( address ); + + FlatTargetBean target = JSpecifyNestedMapper.INSTANCE.map( source ); + + // city: source @Nullable -> target @NonNull -> null check -> setter not called + assertThat( target.isCitySet() ).isFalse(); + } + + @ProcessorTest + @WithClasses(JSpecifyNullableIntermediateMapper.class) + public void nullableIntermediateMustNotSkipNullCheckOnNonNullLeaf() { + generatedSource.addComparisonToFixtureFor( JSpecifyNullableIntermediateMapper.class ); + + // nullableAddress is @Nullable; its street is @NonNull; target setter is @NonNull. + // When the intermediate is null, the whole source chain yields null and the @NonNull + // target setter must NOT be invoked. + NestedSourceBean source = new NestedSourceBean(); + // nullableAddress stays null + + FlatTargetBean target = JSpecifyNullableIntermediateMapper.INSTANCE.map( source ); + + assertThat( target.isStreetSet() ).isFalse(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullParamMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullParamMapper.java new file mode 100644 index 0000000000..44be48f9a8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullParamMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper for testing JSpecify @NonNull on source parameter. + * The method-level null guard (if source == null return null) should be skipped. + */ +@Mapper +public interface JSpecifyNonNullParamMapper { + + JSpecifyNonNullParamMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullParamMapper.class ); + + @Mapping(target = "nonNullTarget", source = "nonNullValue") + @Mapping(target = "nullableTarget", source = "nullableValue") + @Mapping(target = "unannotatedTarget", source = "unannotatedValue") + @Mapping(target = "nonNullTargetFromNullable", source = "nullableValue") + TargetBean map(@NonNull SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java new file mode 100644 index 0000000000..8401e56682 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnBeanMapper { + + JSpecifyNonNullReturnBeanMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnBeanMapper.class ); + + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "nonNullByDefault", source = "value") + NullMarkedTargetBean map(@Nullable JSpecifyNonNullReturnBeanSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java new file mode 100644 index 0000000000..d4bdb926c5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java @@ -0,0 +1,22 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class JSpecifyNonNullReturnBeanSourceBean { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java new file mode 100644 index 0000000000..ccac0e0653 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnBeanTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + JSpecifyNonNullReturnBeanSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnBeanMapper.class + }) + void nonNullReturnForcesMapNullToDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnBeanMapper.class ); + + NullMarkedTargetBean fromNull = JSpecifyNonNullReturnBeanMapper.INSTANCE.map( null ); + + assertThat( fromNull ).isNotNull(); + assertThat( fromNull.getNonNullByDefault() ).isNull(); + + JSpecifyNonNullReturnBeanSourceBean source = new JSpecifyNonNullReturnBeanSourceBean(); + source.setValue( "value" ); + + NullMarkedTargetBean fromSource = JSpecifyNonNullReturnBeanMapper.INSTANCE.map( source ); + + assertThat( fromSource ).isNotNull(); + assertThat( fromSource.getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java new file mode 100644 index 0000000000..4f3fd1c0e2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnIterableMapper { + + JSpecifyNonNullReturnIterableMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnIterableMapper.class ); + + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java new file mode 100644 index 0000000000..f4ba29bb92 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java @@ -0,0 +1,49 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnIterableMapper.class + }) + void nonNullReturnIterableForcesEmptyDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnIterableMapper.class ); + + List fromNull = JSpecifyNonNullReturnIterableMapper.INSTANCE.mapAll( null ); + + assertThat( fromNull ).isEmpty(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List fromSource = + JSpecifyNonNullReturnIterableMapper.INSTANCE.mapAll( Arrays.asList( source ) ); + + assertThat( fromSource ).hasSize( 1 ); + assertThat( fromSource.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java new file mode 100644 index 0000000000..8e6a0cb885 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Map; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnMapMapper { + + JSpecifyNonNullReturnMapMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnMapMapper.class ); + + Map mapAll(@Nullable Map sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java new file mode 100644 index 0000000000..b96d3f3fc0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java @@ -0,0 +1,49 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnMapTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnMapMapper.class + }) + void nonNullReturnMapForcesEmptyDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnMapMapper.class ); + + Map fromNull = JSpecifyNonNullReturnMapMapper.INSTANCE.mapAll( null ); + + assertThat( fromNull ).isEmpty(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + Map fromSource = + JSpecifyNonNullReturnMapMapper.INSTANCE.mapAll( Collections.singletonMap( "k", source ) ); + + assertThat( fromSource ).hasSize( 1 ); + assertThat( fromSource.get( "k" ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java new file mode 100644 index 0000000000..62d061a001 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.stream.Stream; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnStreamMapper { + + JSpecifyNonNullReturnStreamMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnStreamMapper.class ); + + Stream mapAll(@Nullable Stream sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java new file mode 100644 index 0000000000..fa62973ebf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnStreamTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnStreamMapper.class + }) + void nonNullReturnStreamForcesEmptyDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnStreamMapper.class ); + + Stream fromNull = JSpecifyNonNullReturnStreamMapper.INSTANCE.mapAll( null ); + + assertThat( fromNull ).isNotNull(); + assertThat( fromNull.collect( Collectors.toList() ) ).isEmpty(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List fromSource = JSpecifyNonNullReturnStreamMapper.INSTANCE + .mapAll( Stream.of( source ) ) + .collect( Collectors.toList() ); + + assertThat( fromSource ).hasSize( 1 ); + assertThat( fromSource.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java new file mode 100644 index 0000000000..b79763d712 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnUpdateIterableMapper { + + JSpecifyNonNullReturnUpdateIterableMapper INSTANCE = + Mappers.getMapper( JSpecifyNonNullReturnUpdateIterableMapper.class ); + + List mapAll(@Nullable List sources, + @MappingTarget List target); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java new file mode 100644 index 0000000000..56e71e6995 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The @NonNull-return forcing is gated on {@code !isUpdateMethod()}. An update (existing-instance) method must not + * be forced to RETURN_DEFAULT: a null source returns the supplied target instance, not a fresh empty collection. + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnUpdateIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnUpdateIterableMapper.class + }) + void updateMethodWithNonNullReturnIsNotForced() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnUpdateIterableMapper.class ); + + List target = new ArrayList<>(); + + List result = JSpecifyNonNullReturnUpdateIterableMapper.INSTANCE.mapAll( null, target ); + + assertThat( result ).isSameAs( target ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullCheckMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullCheckMapper.java new file mode 100644 index 0000000000..8253aca89b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullCheckMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper for testing JSpecify nullness annotation support on property level. + */ +@Mapper +public interface JSpecifyNullCheckMapper { + + JSpecifyNullCheckMapper INSTANCE = Mappers.getMapper( JSpecifyNullCheckMapper.class ); + + @Mapping(target = "nonNullTarget", source = "nonNullValue") + @Mapping(target = "nullableTarget", source = "nullableValue") + @Mapping(target = "unannotatedTarget", source = "unannotatedValue") + @Mapping(target = "nonNullTargetFromNullable", source = "nullableValue") + TargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullCheckTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullCheckTest.java new file mode 100644 index 0000000000..4b3e11dbdd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullCheckTest.java @@ -0,0 +1,125 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +/** + * Tests for JSpecify nullness annotation support in MapStruct. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithClasses({ + SourceBean.class, + TargetBean.class +}) +@WithJSpecify +class JSpecifyNullCheckTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + // -- Property-level tests (JSpecifyNullCheckMapper) -- + + @ProcessorTest + @WithClasses(JSpecifyNullCheckMapper.class) + public void sourceNonNullGetterShouldSkipNullCheck() { + generatedSource.addComparisonToFixtureFor( JSpecifyNullCheckMapper.class ); + + // Source @NonNull getter -> no null check, setter always called + SourceBean source = new SourceBean(); + source.setNonNullValue( "value" ); + + TargetBean target = JSpecifyNullCheckMapper.INSTANCE.map( source ); + + assertThat( target.isNonNullTargetSet() ).isTrue(); + assertThat( target.getNonNullTarget() ).isEqualTo( "value" ); + } + + @ProcessorTest + @WithClasses(JSpecifyNullCheckMapper.class) + public void sourceNullableTargetNonNullShouldAddNullCheck() { + // Source @Nullable getter + Target @NonNull setter -> null check added + // When source value is null, setter should NOT be called + SourceBean source = new SourceBean(); + + TargetBean target = JSpecifyNullCheckMapper.INSTANCE.map( source ); + + assertThat( target.isNonNullTargetFromNullableSet() ).isFalse(); + } + + @ProcessorTest + @WithClasses(JSpecifyNullCheckMapper.class) + public void sourceNullableTargetNullableDefersToStrategy() { + // Source @Nullable + Target @Nullable -> defers to NullValueCheckStrategy + // Default NVCS is ON_IMPLICIT_CONVERSION, direct String->String has no null check + SourceBean source = new SourceBean(); + + TargetBean target = JSpecifyNullCheckMapper.INSTANCE.map( source ); + + // With ON_IMPLICIT_CONVERSION, no null check for direct assignment -> setter called + assertThat( target.isNullableTargetSet() ).isTrue(); + } + + // -- JSpecify overrides NullValueCheckStrategy -- + + @ProcessorTest + @WithClasses(JSpecifyOverridesStrategyMapper.class) + public void jspecifyOverridesNullValueCheckStrategyAlways() { + generatedSource.addComparisonToFixtureFor( JSpecifyOverridesStrategyMapper.class ); + + // Even with NullValueCheckStrategy.ALWAYS, source @NonNull should skip null check + SourceBean source = new SourceBean(); + source.setNonNullValue( "value" ); + + TargetBean target = JSpecifyOverridesStrategyMapper.INSTANCE.map( source ); + + assertThat( target.isNonNullTargetSet() ).isTrue(); + } + + // -- Method-level parameter null check -- + + @ProcessorTest + @WithClasses(JSpecifyNonNullParamMapper.class) + public void nonNullParamShouldSkipMethodLevelNullGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullParamMapper.class ); + + // Non-null input: mapper maps through normally. + SourceBean source = new SourceBean(); + source.setNonNullValue( "value" ); + source.setUnannotatedValue( "plain" ); + + TargetBean target = JSpecifyNonNullParamMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getNonNullTarget() ).isEqualTo( "value" ); + assertThat( target.getUnannotatedTarget() ).isEqualTo( "plain" ); + + // Null input: because the method-level guard was skipped, the mapper must dereference + // the source and throw NPE rather than silently returning null. + assertThatNullPointerException().isThrownBy( () -> JSpecifyNonNullParamMapper.INSTANCE.map( null ) ); + } + + @ProcessorTest + @WithClasses(JSpecifyNullCheckMapper.class) + public void unannotatedParamShouldHaveMethodLevelNullGuard() { + // Unannotated source parameter -> normal "if (source == null) return null" guard + SourceBean source = new SourceBean(); + + TargetBean target = JSpecifyNullCheckMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullMarkedTargetScopeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullMarkedTargetScopeMapper.java new file mode 100644 index 0000000000..cdbbdf7e82 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullMarkedTargetScopeMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Target is a {@code @NullMarked} bean with an unannotated setter parameter + * ({@code setNonNullByDefault}), which JSpecify semantics promote to {@code @NonNull}. + * The source getter is explicitly {@code @Nullable}, so a null check must be generated. + */ +@Mapper +public interface JSpecifyNullMarkedTargetScopeMapper { + + JSpecifyNullMarkedTargetScopeMapper INSTANCE = + Mappers.getMapper( JSpecifyNullMarkedTargetScopeMapper.class ); + + @Mapping(target = "nonNullByDefault", source = "nullableValue") + @Mapping(target = "explicitlyNullable", ignore = true) + NullMarkedTargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullMarkedTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullMarkedTest.java new file mode 100644 index 0000000000..e41aa5dece --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullMarkedTest.java @@ -0,0 +1,188 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage.PackageNullMarkedMapper; +import org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage.PackageNullMarkedSourceBean; +import org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage.PackageNullMarkedTargetBean; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.WithPackageInfo; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for JSpecify {@code @NullMarked} / {@code @NullUnmarked} support. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyNullMarkedTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ NullMarkedSourceBean.class, NullMarkedTargetBean.class, NullMarkedMapper.class }) + public void nullMarkedSourceAndTarget() { + generatedSource.addComparisonToFixtureFor( NullMarkedMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + NullMarkedTargetBean target = NullMarkedMapper.INSTANCE.map( source ); + + // nonNullByDefault: source effectively @NonNull -> skip null check -> setter called + assertThat( target.isNonNullByDefaultSet() ).isTrue(); + assertThat( target.getNonNullByDefault() ).isEqualTo( "value" ); + } + + @ProcessorTest + @WithClasses({ NullMarkedSourceBean.class, NullMarkedTargetBean.class, NullMarkedMapper.class }) + public void nullMarkedNullableSourceToNonNullTarget() { + // @Nullable getter -> target effectively @NonNull -> null check added + NullMarkedSourceBean source = new NullMarkedSourceBean(); + // explicitlyNullable is null + + NullMarkedTargetBean target = NullMarkedMapper.INSTANCE.map( source ); + + // Target's setter for explicitlyNullable is @Nullable -> defers to strategy + // Default NVCS = ON_IMPLICIT_CONVERSION, direct String -> String -> no null check + assertThat( target.isExplicitlyNullableSet() ).isTrue(); + } + + @ProcessorTest + @WithClasses({ NullMarkedSourceBean.class, TargetBean.class, NullMarkedSourceToPlainTargetMapper.class }) + public void nullMarkedSourceToPlainTarget() { + generatedSource.addComparisonToFixtureFor( NullMarkedSourceToPlainTargetMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + TargetBean target = NullMarkedSourceToPlainTargetMapper.INSTANCE.map( source ); + + // Source effectively @NonNull -> skip null check -> setter always called + assertThat( target.isUnannotatedTargetSet() ).isTrue(); + } + + @ProcessorTest + @WithClasses({ NullMarkedSourceBean.class, NullMarkedTargetBean.class, NullMarkedMapperWithParam.class }) + public void nullMarkedMapperSkipsMethodLevelNullGuard() { + generatedSource.addComparisonToFixtureFor( NullMarkedMapperWithParam.class ); + } + + @ProcessorTest + @WithClasses({ + PackageNullMarkedSourceBean.class, + PackageNullMarkedTargetBean.class, + PackageNullMarkedMapper.class + }) + @WithPackageInfo(PackageNullMarkedSourceBean.class) + public void packageLevelNullMarked() { + generatedSource.addComparisonToFixtureFor( PackageNullMarkedMapper.class ); + + // @NullMarked package: unannotated types are effectively @NonNull. + // Mapper uses NullValueCheckStrategy.ALWAYS but JSpecify wins -> no null checks. + PackageNullMarkedSourceBean source = new PackageNullMarkedSourceBean(); + // value is null + + PackageNullMarkedTargetBean target = PackageNullMarkedMapper.INSTANCE.map( source ); + + // Unannotated getter is effectively @NonNull -> no null check despite ALWAYS strategy + // -> setter is called even when source value is null + assertThat( target.isValueSet() ).isTrue(); + } + + @ProcessorTest + @WithClasses({ + SourceBean.class, + TargetBean.class, + JSpecifyNullUnmarkedMethodMapper.class + }) + public void nullUnmarkedOnMethodReversesEnclosingNullMarkedScope() { + // The mapper interface is @NullMarked but the method is @NullUnmarked. The source + // parameter is unannotated: with method-level scope, its nullability must be UNKNOWN + // (not promoted), so the method-level null guard is generated. Passing null must + // return null rather than NPE. + TargetBean target = JSpecifyNullUnmarkedMethodMapper.INSTANCE.map( null ); + + assertThat( target ).isNull(); + } + + @ProcessorTest + @WithClasses({ + NullUnmarkedSourceBean.class, + TargetBean.class, + JSpecifyNullUnmarkedMapper.class + }) + public void nullUnmarkedReversesEnclosingNullMarkedScope() { + // The source class is @NullUnmarked inside a @NullMarked outer class. + // Unannotated getter must be UNKNOWN, not promoted to @NonNull. + // With NVCS=ALWAYS, a null check is generated and the setter is NOT called when source is null. + NullUnmarkedSourceBean.Inner source = new NullUnmarkedSourceBean.Inner(); + // value is null + + TargetBean target = JSpecifyNullUnmarkedMapper.INSTANCE.map( source ); + + assertThat( target.isUnannotatedTargetSet() ).isFalse(); + } + + @ProcessorTest + @WithClasses({ + SourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNullMarkedTargetScopeMapper.class + }) + public void nullMarkedTargetPromotesUnannotatedSetterParamToNonNull() { + // Target bean is @NullMarked; setNonNullByDefault has an unannotated String parameter + // which must be treated as @NonNull by JSpecify semantics. Source getter is @Nullable, + // so a null check must be generated and the setter must NOT be called when source is null. + SourceBean source = new SourceBean(); + // nullableValue is null + + NullMarkedTargetBean target = JSpecifyNullMarkedTargetScopeMapper.INSTANCE.map( source ); + + assertThat( target.isNonNullByDefaultSet() ).isFalse(); + } + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + TargetBean.class, + JSpecifyNullableOverridesScopeMapper.class + }) + public void explicitNullableOverridesNullMarkedScope() { + // Source getter is explicitly @Nullable inside a @NullMarked class — the explicit + // annotation must keep the source NULLABLE instead of being promoted to @NonNull + // by the scope. Target setter is explicitly @NonNull, so a null check is required. + NullMarkedSourceBean source = new NullMarkedSourceBean(); + // explicitlyNullable is null + + TargetBean target = JSpecifyNullableOverridesScopeMapper.INSTANCE.map( source ); + + // Null check generated -> setter must NOT be called when source value is null. + assertThat( target.isNonNullTargetFromNullableSet() ).isFalse(); + } + + @ProcessorTest + @WithClasses({ + PackageNullMarkedSourceBean.class, + PackageNullMarkedTargetBean.class, + PackageNullMarkedMapper.class + }) + public void packageLevelNullMarkedWithoutPackageInfo() { + // Same classes and mapper as packageLevelNullMarked but WITHOUT @WithPackageInfo. + // Without the package-info.java being compiled, @NullMarked is not visible — + // unannotated types have unknown nullability and the ALWAYS strategy produces null checks. + generatedSource.addComparisonToFixtureFor( PackageNullMarkedMapper.class, "withoutPackageInfo" ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullUnmarkedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullUnmarkedMapper.java new file mode 100644 index 0000000000..defce89a61 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullUnmarkedMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * Source is a {@code @NullUnmarked} class nested inside a {@code @NullMarked} outer class. + * The unannotated getter must be treated as unknown nullability (not promoted to {@code @NonNull}). + * With {@code NullValueCheckStrategy.ALWAYS}, a null check must be generated. + */ +@Mapper( + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.IGNORE +) +public interface JSpecifyNullUnmarkedMapper { + + JSpecifyNullUnmarkedMapper INSTANCE = Mappers.getMapper( JSpecifyNullUnmarkedMapper.class ); + + @Mapping(target = "unannotatedTarget", source = "value") + TargetBean map(NullUnmarkedSourceBean.Inner source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullUnmarkedMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullUnmarkedMethodMapper.java new file mode 100644 index 0000000000..11f24a2934 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullUnmarkedMethodMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * Mapper interface annotated with {@code @NullMarked} but the mapping method itself is + * {@code @NullUnmarked}. The unannotated source parameter inside the {@code @NullUnmarked} + * method must be treated as unknown nullability, so with {@code NullValueCheckStrategy.ALWAYS} + * the method-level null guard must still be generated. + */ +@NullMarked +@Mapper( + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.IGNORE +) +public interface JSpecifyNullUnmarkedMethodMapper { + + JSpecifyNullUnmarkedMethodMapper INSTANCE = Mappers.getMapper( JSpecifyNullUnmarkedMethodMapper.class ); + + @NullUnmarked + @Mapping(target = "unannotatedTarget", source = "unannotatedValue") + TargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableIntermediateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableIntermediateMapper.java new file mode 100644 index 0000000000..1c6571a6a7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableIntermediateMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * Nested source chain where the intermediate accessor is {@code @Nullable} but the deepest + * accessor is {@code @NonNull}. The target setter is {@code @NonNull} — when the intermediate + * returns null, the setter must not be invoked. + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface JSpecifyNullableIntermediateMapper { + + JSpecifyNullableIntermediateMapper INSTANCE = Mappers.getMapper( JSpecifyNullableIntermediateMapper.class ); + + // nullableAddress is @Nullable; street is @NonNull; target setStreet is @NonNull. + // Because the intermediate can be null, the null check must not be skipped. + @Mapping(target = "street", source = "nullableAddress.street") + FlatTargetBean map(NestedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableOverridesScopeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableOverridesScopeMapper.java new file mode 100644 index 0000000000..08f8cba72e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableOverridesScopeMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +/** + * Maps an explicitly {@code @Nullable} source property inside a {@code @NullMarked} scope + * to a {@code @NonNull} setter parameter. A null check must be generated because the + * explicit {@code @Nullable} overrides the enclosing {@code @NullMarked} scope, keeping + * the source nullability as {@code NULLABLE} instead of being promoted to {@code NON_NULL}. + */ +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface JSpecifyNullableOverridesScopeMapper { + + JSpecifyNullableOverridesScopeMapper INSTANCE = + Mappers.getMapper( JSpecifyNullableOverridesScopeMapper.class ); + + @Mapping(target = "nonNullTargetFromNullable", source = "explicitlyNullable") + TargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java new file mode 100644 index 0000000000..026890dee8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNullableSourceIterableMapper { + + JSpecifyNullableSourceIterableMapper INSTANCE = Mappers.getMapper( JSpecifyNullableSourceIterableMapper.class ); + + @Nullable + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java new file mode 100644 index 0000000000..da77fe71e1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java @@ -0,0 +1,52 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Negative counterpart to {@link JSpecifyIterableMethodTest}: a {@code @Nullable} source parameter (and a + * {@code @Nullable} return, so the @NonNull-return forcing does not kick in) must keep the method-level + * {@code if ( sources == null ) return null;} guard. + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyNullableSourceIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNullableSourceIterableMapper.class + }) + void nullableSourceKeepsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyNullableSourceIterableMapper.class ); + + assertThat( JSpecifyNullableSourceIterableMapper.INSTANCE.mapAll( null ) ).isNull(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List targets = + JSpecifyNullableSourceIterableMapper.INSTANCE.mapAll( Arrays.asList( source ) ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyOverridesStrategyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyOverridesStrategyMapper.java new file mode 100644 index 0000000000..97303bc7ef --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyOverridesStrategyMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.factory.Mappers; + +/** + * Mapper with NullValueCheckStrategy.ALWAYS that should be overridden by JSpecify annotations. + * Source @NonNull getter -> should NOT have null check despite ALWAYS strategy. + */ +@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) +public interface JSpecifyOverridesStrategyMapper { + + JSpecifyOverridesStrategyMapper INSTANCE = Mappers.getMapper( JSpecifyOverridesStrategyMapper.class ); + + @Mapping(target = "nonNullTarget", source = "nonNullValue") + @Mapping(target = "nullableTarget", source = "nullableValue") + @Mapping(target = "unannotatedTarget", source = "unannotatedValue") + @Mapping(target = "nonNullTargetFromNullable", source = "nullableValue") + TargetBean map(SourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java new file mode 100644 index 0000000000..387103bae5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.IterableMapping; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyReturnNullOverrideIterableMapper { + + JSpecifyReturnNullOverrideIterableMapper INSTANCE = + Mappers.getMapper( JSpecifyReturnNullOverrideIterableMapper.class ); + + // Explicit RETURN_NULL, but the @NonNull return type (NullMarked scope) wins and forces RETURN_DEFAULT. + @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL) + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java new file mode 100644 index 0000000000..eaea578903 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that a JSpecify {@code @NonNull} return type wins over an explicitly configured + * {@code NullValueMappingStrategy.RETURN_NULL}: the generated method returns an empty collection rather + * than {@code null}. + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyReturnNullOverrideIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyReturnNullOverrideIterableMapper.class + }) + void nonNullReturnOverridesExplicitReturnNull() { + generatedSource.addComparisonToFixtureFor( JSpecifyReturnNullOverrideIterableMapper.class ); + + assertThat( JSpecifyReturnNullOverrideIterableMapper.INSTANCE.mapAll( null ) ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifySafetyGuardMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifySafetyGuardMapper.java new file mode 100644 index 0000000000..1be36485e5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifySafetyGuardMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/** + * Verifies that existing safety guards still trigger a null check when the source is + * {@code @Nullable}: + *
        + *
      • Unboxing ({@code Integer} -> {@code int})
      • + *
      • {@code defaultValue}
      • + *
      • {@link NullValuePropertyMappingStrategy#SET_TO_DEFAULT} on an update method
      • + *
      + */ +@Mapper +public interface JSpecifySafetyGuardMapper { + + JSpecifySafetyGuardMapper INSTANCE = Mappers.getMapper( JSpecifySafetyGuardMapper.class ); + + @Mapping(target = "unboxedNumber", source = "nullableNumber") + @Mapping(target = "textWithDefault", source = "nullableText", defaultValue = "default") + @Mapping(target = "textWithNvpms", ignore = true) + SafetyGuardTargetBean map(SafetyGuardSourceBean source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + @Mapping(target = "unboxedNumber", ignore = true) + @Mapping(target = "textWithDefault", ignore = true) + @Mapping(target = "textWithNvpms", source = "nullableText") + void update(SafetyGuardSourceBean source, @MappingTarget SafetyGuardTargetBean target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifySafetyGuardTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifySafetyGuardTest.java new file mode 100644 index 0000000000..affff4ad50 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifySafetyGuardTest.java @@ -0,0 +1,68 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that the pre-existing safety guards in {@code PropertyMapping} (unboxing, + * {@code defaultValue}, {@code NullValuePropertyMappingStrategy.SET_TO_DEFAULT}) still + * generate a null check when the source is {@code @Nullable}. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithClasses({ + SafetyGuardSourceBean.class, + SafetyGuardTargetBean.class, + JSpecifySafetyGuardMapper.class +}) +@WithJSpecify +class JSpecifySafetyGuardTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void nullableSourceToPrimitiveTargetKeepsUnboxingGuard() { + // Source @Nullable Integer is null; unboxing guard must prevent an NPE. + SafetyGuardSourceBean source = new SafetyGuardSourceBean(); + + SafetyGuardTargetBean target = JSpecifySafetyGuardMapper.INSTANCE.map( source ); + + assertThat( target.getUnboxedNumber() ).isZero(); + } + + @ProcessorTest + public void nullableSourceWithDefaultValueKeepsDefaultValueGuard() { + // Source @Nullable String is null; defaultValue must be applied. + SafetyGuardSourceBean source = new SafetyGuardSourceBean(); + + SafetyGuardTargetBean target = JSpecifySafetyGuardMapper.INSTANCE.map( source ); + + assertThat( target.getTextWithDefault() ).isEqualTo( "default" ); + } + + @ProcessorTest + public void nullableSourceWithSetToDefaultKeepsNvpmsGuard() { + // Source @Nullable String is null; NVPMS=SET_TO_DEFAULT must kick in on an update method + // (NVPMS is only honored for update methods) and reset the target to the String default. + SafetyGuardSourceBean source = new SafetyGuardSourceBean(); + SafetyGuardTargetBean target = new SafetyGuardTargetBean(); + target.setTextWithNvpms( "pre-existing" ); + + JSpecifySafetyGuardMapper.INSTANCE.update( source, target ); + + assertThat( target.getTextWithNvpms() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java new file mode 100644 index 0000000000..1b069af5b8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.stream.Stream; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyStreamMethodMapper { + + JSpecifyStreamMethodMapper INSTANCE = Mappers.getMapper( JSpecifyStreamMethodMapper.class ); + + Stream mapAll(Stream sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java new file mode 100644 index 0000000000..76c7af8792 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java @@ -0,0 +1,47 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyStreamMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyStreamMethodMapper.class + }) + void streamMethodWithNonNullSourceSkipsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyStreamMethodMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List targets = JSpecifyStreamMethodMapper.INSTANCE + .mapAll( Stream.of( source ) ) + .collect( Collectors.toList() ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyUpdateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyUpdateMapper.java new file mode 100644 index 0000000000..5eadd23ef1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyUpdateMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/** + * Mapper with update method and IGNORE strategy to test JSpecify interaction. + * NVPMS=IGNORE means: when source is null, leave target unchanged. + * Source @NonNull should still skip the null check (guaranteed non-null). + */ +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface JSpecifyUpdateMapper { + + JSpecifyUpdateMapper INSTANCE = Mappers.getMapper( JSpecifyUpdateMapper.class ); + + @Mapping(target = "nonNullTarget", source = "nonNullValue") + @Mapping(target = "nullableTarget", source = "nullableValue") + @Mapping(target = "unannotatedTarget", source = "unannotatedValue") + @Mapping(target = "nonNullTargetFromNullable", source = "nullableValue") + void update(SourceBean source, @MappingTarget TargetBean target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyUpdateTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyUpdateTest.java new file mode 100644 index 0000000000..e0e9c24d79 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyUpdateTest.java @@ -0,0 +1,82 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for JSpecify nullness annotations with update methods ({@code @MappingTarget}). + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithClasses({ + SourceBean.class, + TargetBean.class, + JSpecifyUpdateMapper.class +}) +@WithJSpecify +class JSpecifyUpdateTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + public void verifyGeneratedCode() { + generatedSource.addComparisonToFixtureFor( JSpecifyUpdateMapper.class ); + } + + @ProcessorTest + public void sourceNonNullSkipsNullCheckInUpdateMethod() { + // Source @NonNull getter -> no null check, setter always called + SourceBean source = new SourceBean(); + source.setNonNullValue( "updated" ); + + TargetBean target = new TargetBean(); + JSpecifyUpdateMapper.INSTANCE.update( source, target ); + + assertThat( target.isNonNullTargetSet() ).isTrue(); + assertThat( target.getNonNullTarget() ).isEqualTo( "updated" ); + } + + @ProcessorTest + public void sourceNullableWithIgnoreStrategyKeepsNullCheck() { + // Source @Nullable + NVPMS=IGNORE -> null check must be kept (safety guard) + // When source is null, target should be unchanged (IGNORE behavior) + SourceBean source = new SourceBean(); + // nullableValue is null + + TargetBean target = new TargetBean(); + target.setNullableTarget( "original" ); + + JSpecifyUpdateMapper.INSTANCE.update( source, target ); + + // IGNORE strategy: null source should not overwrite -> original value preserved + assertThat( target.getNullableTarget() ).isEqualTo( "original" ); + } + + @ProcessorTest + public void sourceNullableTargetNonNullWithIgnoreStrategyKeepsNullCheck() { + // Source @Nullable + Target @NonNull + NVPMS=IGNORE -> null check kept + SourceBean source = new SourceBean(); + // nullableValue is null + + TargetBean target = new TargetBean(); + target.setNonNullTargetFromNullable( "original" ); + + JSpecifyUpdateMapper.INSTANCE.update( source, target ); + + // null source should not overwrite (IGNORE + null check) + assertThat( target.getNonNullTargetFromNullable() ).isEqualTo( "original" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyVerboseNoteTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyVerboseNoteTest.java new file mode 100644 index 0000000000..46b3d0c869 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyVerboseNoteTest.java @@ -0,0 +1,43 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedNote; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; +import org.mapstruct.ap.testutil.runner.Compiler; + +/** + * Verifies that JSpecify-driven null-check decisions emit verbose diagnostic notes + * (visible with {@code -Amapstruct.verbose=true}) so they are no longer silent. + * + * @author Filip Hrisafov + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyVerboseNoteTest { + + @ProcessorTest(Compiler.JDK) + @ProcessorOption(name = "mapstruct.verbose", value = "true") + @WithClasses({ SourceBean.class, TargetBean.class, JSpecifyNullCheckMapper.class }) + @ExpectedNote("^-- MapStruct: JSpecify skipping null check for property \"nonNullTarget\": " + + "source is @NonNull\\.$") + @ExpectedNote("^-- MapStruct: JSpecify adding null check for property \"nonNullTargetFromNullable\": " + + "source=\\w+, target=\\w+\\.$") + public void emitsSetterDecisionNotes() { + } + + @ProcessorTest(Compiler.JDK) + @ProcessorOption(name = "mapstruct.verbose", value = "true") + @WithClasses({ SourceBean.class, TargetBean.class, JSpecifyNonNullParamMapper.class }) + @ExpectedNote("^-- MapStruct: JSpecify skipping method-level null guard for property \"source\": " + + "parameter is @NonNull\\.$") + public void emitsMethodLevelGuardSkipNote() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NestedSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NestedSourceBean.java new file mode 100644 index 0000000000..ed67655541 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NestedSourceBean.java @@ -0,0 +1,33 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class NestedSourceBean { + + private AddressBean nonNullAddress; + private AddressBean nullableAddress; + + @NonNull + public AddressBean getNonNullAddress() { + return nonNullAddress; + } + + public void setNonNullAddress(AddressBean nonNullAddress) { + this.nonNullAddress = nonNullAddress; + } + + @Nullable + public AddressBean getNullableAddress() { + return nullableAddress; + } + + public void setNullableAddress(AddressBean nullableAddress) { + this.nullableAddress = nullableAddress; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java new file mode 100644 index 0000000000..3c67457637 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; + +/** + * Source bean with a {@code @NonNull} collection getter (via @NullMarked scope). + */ +@NullMarked +public class NullMarkedCollectionSourceBean { + + private List values; + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java new file mode 100644 index 0000000000..eae3adc1a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class NullMarkedCollectionTargetBean { + + private List values; + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedMapper.java new file mode 100644 index 0000000000..e9d45171fc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper using @NullMarked source and target beans. + */ +@Mapper +public interface NullMarkedMapper { + + NullMarkedMapper INSTANCE = Mappers.getMapper( NullMarkedMapper.class ); + + @Mapping(target = "nonNullByDefault", source = "nonNullByDefault") + @Mapping(target = "explicitlyNullable", source = "explicitlyNullable") + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedMapperWithParam.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedMapperWithParam.java new file mode 100644 index 0000000000..62e111a143 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedMapperWithParam.java @@ -0,0 +1,26 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Mapper annotated with @NullMarked. + * Source parameter is effectively @NonNull -> method-level null guard should be skipped. + */ +@NullMarked +@Mapper +public interface NullMarkedMapperWithParam { + + NullMarkedMapperWithParam INSTANCE = Mappers.getMapper( NullMarkedMapperWithParam.class ); + + @Mapping(target = "nonNullByDefault", source = "nonNullByDefault") + @Mapping(target = "explicitlyNullable", source = "explicitlyNullable") + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedSourceBean.java new file mode 100644 index 0000000000..d2b1f6a054 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedSourceBean.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Source bean annotated with {@code @NullMarked}. + * All unannotated types are effectively {@code @NonNull}. + */ +@NullMarked +public class NullMarkedSourceBean { + + private String nonNullByDefault; + private String explicitlyNullable; + + public String getNonNullByDefault() { + return nonNullByDefault; + } + + public void setNonNullByDefault(String nonNullByDefault) { + this.nonNullByDefault = nonNullByDefault; + } + + @Nullable + public String getExplicitlyNullable() { + return explicitlyNullable; + } + + public void setExplicitlyNullable(@Nullable String explicitlyNullable) { + this.explicitlyNullable = explicitlyNullable; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedSourceToPlainTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedSourceToPlainTargetMapper.java new file mode 100644 index 0000000000..de8560d96f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedSourceToPlainTargetMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * Maps from a @NullMarked source to a plain (unannotated) target. + * Source unannotated getters are effectively @NonNull -> skip null check. + * Source @Nullable getter to unannotated target -> defer to strategy. + */ +@Mapper +public interface NullMarkedSourceToPlainTargetMapper { + + NullMarkedSourceToPlainTargetMapper INSTANCE = Mappers.getMapper( NullMarkedSourceToPlainTargetMapper.class ); + + @Mapping(target = "unannotatedTarget", source = "nonNullByDefault") + @Mapping(target = "nullableTarget", source = "explicitlyNullable") + @Mapping(target = "nonNullTarget", ignore = true) + @Mapping(target = "nonNullTargetFromNullable", ignore = true) + TargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedTargetBean.java new file mode 100644 index 0000000000..a1d0e67893 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedTargetBean.java @@ -0,0 +1,50 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Target bean annotated with {@code @NullMarked}. + * All unannotated setter parameters are effectively {@code @NonNull}. + */ +@NullMarked +public class NullMarkedTargetBean { + + private String nonNullByDefault; + private boolean nonNullByDefaultSet; + + private String explicitlyNullable; + private boolean explicitlyNullableSet; + + public String getNonNullByDefault() { + return nonNullByDefault; + } + + public void setNonNullByDefault(String nonNullByDefault) { + this.nonNullByDefaultSet = true; + this.nonNullByDefault = nonNullByDefault; + } + + public boolean isNonNullByDefaultSet() { + return nonNullByDefaultSet; + } + + @Nullable + public String getExplicitlyNullable() { + return explicitlyNullable; + } + + public void setExplicitlyNullable(@Nullable String explicitlyNullable) { + this.explicitlyNullableSet = true; + this.explicitlyNullable = explicitlyNullable; + } + + public boolean isExplicitlyNullableSet() { + return explicitlyNullableSet; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullUnmarkedSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullUnmarkedSourceBean.java new file mode 100644 index 0000000000..7b3ed11750 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullUnmarkedSourceBean.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; + +/** + * A {@code @NullMarked} outer class with a {@code @NullUnmarked} nested class. + * The nested class's unannotated getter must have unknown nullability — the closer + * {@code @NullUnmarked} takes precedence over the outer {@code @NullMarked} scope. + */ +@NullMarked +public final class NullUnmarkedSourceBean { + + private NullUnmarkedSourceBean() { + } + + @NullUnmarked + public static class Inner { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SafetyGuardSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SafetyGuardSourceBean.java new file mode 100644 index 0000000000..74bb8d162d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SafetyGuardSourceBean.java @@ -0,0 +1,36 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.Nullable; + +/** + * Source bean for verifying that existing safety guards (unboxing, defaultValue, NVPMS) + * still emit null checks when the source is {@code @Nullable}. + */ +public class SafetyGuardSourceBean { + + private Integer nullableNumber; + private String nullableText; + + @Nullable + public Integer getNullableNumber() { + return nullableNumber; + } + + public void setNullableNumber(@Nullable Integer nullableNumber) { + this.nullableNumber = nullableNumber; + } + + @Nullable + public String getNullableText() { + return nullableText; + } + + public void setNullableText(@Nullable String nullableText) { + this.nullableText = nullableText; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SafetyGuardTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SafetyGuardTargetBean.java new file mode 100644 index 0000000000..e45264bf49 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SafetyGuardTargetBean.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +public class SafetyGuardTargetBean { + + private int unboxedNumber; + private String textWithDefault; + private String textWithNvpms; + + public int getUnboxedNumber() { + return unboxedNumber; + } + + public void setUnboxedNumber(int unboxedNumber) { + this.unboxedNumber = unboxedNumber; + } + + public String getTextWithDefault() { + return textWithDefault; + } + + public void setTextWithDefault(String textWithDefault) { + this.textWithDefault = textWithDefault; + } + + public String getTextWithNvpms() { + return textWithNvpms; + } + + public void setTextWithNvpms(String textWithNvpms) { + this.textWithNvpms = textWithNvpms; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SourceBean.java new file mode 100644 index 0000000000..a40806ff6e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/SourceBean.java @@ -0,0 +1,42 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class SourceBean { + + private String nonNullValue; + private String nullableValue; + private String unannotatedValue; + + @NonNull + public String getNonNullValue() { + return nonNullValue; + } + + public void setNonNullValue(String nonNullValue) { + this.nonNullValue = nonNullValue; + } + + @Nullable + public String getNullableValue() { + return nullableValue; + } + + public void setNullableValue(String nullableValue) { + this.nullableValue = nullableValue; + } + + public String getUnannotatedValue() { + return unannotatedValue; + } + + public void setUnannotatedValue(String unannotatedValue) { + this.unannotatedValue = unannotatedValue; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/TargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/TargetBean.java new file mode 100644 index 0000000000..0e87b13eaf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/TargetBean.java @@ -0,0 +1,76 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class TargetBean { + + private String nonNullTarget; + private boolean nonNullTargetSet; + + private String nullableTarget; + private boolean nullableTargetSet; + + private String nonNullTargetFromNullable; + private boolean nonNullTargetFromNullableSet; + + private String unannotatedTarget; + private boolean unannotatedTargetSet; + + public String getNonNullTarget() { + return nonNullTarget; + } + + public void setNonNullTarget(@NonNull String nonNullTarget) { + this.nonNullTargetSet = true; + this.nonNullTarget = nonNullTarget; + } + + public boolean isNonNullTargetSet() { + return nonNullTargetSet; + } + + public String getNullableTarget() { + return nullableTarget; + } + + public void setNullableTarget(@Nullable String nullableTarget) { + this.nullableTargetSet = true; + this.nullableTarget = nullableTarget; + } + + public boolean isNullableTargetSet() { + return nullableTargetSet; + } + + public String getNonNullTargetFromNullable() { + return nonNullTargetFromNullable; + } + + public void setNonNullTargetFromNullable(@NonNull String nonNullTargetFromNullable) { + this.nonNullTargetFromNullableSet = true; + this.nonNullTargetFromNullable = nonNullTargetFromNullable; + } + + public boolean isNonNullTargetFromNullableSet() { + return nonNullTargetFromNullableSet; + } + + public String getUnannotatedTarget() { + return unannotatedTarget; + } + + public void setUnannotatedTarget(String unannotatedTarget) { + this.unannotatedTargetSet = true; + this.unannotatedTarget = unannotatedTarget; + } + + public boolean isUnannotatedTargetSet() { + return unannotatedTargetSet; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/UnmappableConstructorTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/UnmappableConstructorTargetBean.java new file mode 100644 index 0000000000..3253e3b390 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/UnmappableConstructorTargetBean.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import org.jspecify.annotations.NonNull; + +public class UnmappableConstructorTargetBean { + + private final AddressBean payload; + + public UnmappableConstructorTargetBean(@NonNull AddressBean payload) { + this.payload = payload; + } + + public AddressBean getPayload() { + return payload; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedMapper.java new file mode 100644 index 0000000000..f6f72ca243 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage; + +import org.mapstruct.Mapper; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.factory.Mappers; + +/** + * Mapper in a @NullMarked package with NullValueCheckStrategy.ALWAYS. + * Since the package is @NullMarked, unannotated types are effectively @NonNull + * and no null checks are produced despite the ALWAYS strategy. + */ +@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) +public interface PackageNullMarkedMapper { + + PackageNullMarkedMapper INSTANCE = Mappers.getMapper( PackageNullMarkedMapper.class ); + + PackageNullMarkedTargetBean map(PackageNullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedSourceBean.java new file mode 100644 index 0000000000..8b9deb00f2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedSourceBean.java @@ -0,0 +1,23 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage; + +/** + * Source bean in a @NullMarked package. + * Unannotated getter is effectively @NonNull. + */ +public class PackageNullMarkedSourceBean { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedTargetBean.java new file mode 100644 index 0000000000..e96af7ad04 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/PackageNullMarkedTargetBean.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage; + +/** + * Target bean in a @NullMarked package. + * Unannotated setter parameter is effectively @NonNull. + */ +public class PackageNullMarkedTargetBean { + + private String value; + private boolean valueSet; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.valueSet = true; + this.value = value; + } + + public boolean isValueSet() { + return valueSet; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/package-info.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/package-info.java new file mode 100644 index 0000000000..ee700639e0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/nullmarkedpackage/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +@NullMarked +package org.mapstruct.ap.test.nullcheck.jspecify.nullmarkedpackage; + +import org.jspecify.annotations.NullMarked; diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.java new file mode 100644 index 0000000000..d239f9ace7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CarListMapper { + + CarListMapper INSTANCE = Mappers.getMapper( CarListMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + List carsToCarDtoList(List cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java new file mode 100644 index 0000000000..860b17654a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarListMapperSettingOnMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_NULL) +public interface CarListMapperSettingOnMapper { + + CarListMapperSettingOnMapper INSTANCE = Mappers.getMapper( CarListMapperSettingOnMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + List carsToCarDtoList(List cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.java new file mode 100644 index 0000000000..0227949f4d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CarMapMapper { + + CarMapMapper INSTANCE = Mappers.getMapper( CarMapMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java new file mode 100644 index 0000000000..adb99887bd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapMapperSettingOnMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(nullValueMapMappingStrategy = NullValueMappingStrategy.RETURN_NULL) +public interface CarMapMapperSettingOnMapper { + + CarMapMapperSettingOnMapper INSTANCE = Mappers.getMapper( CarMapMapperSettingOnMapper.class ); + + @Mapping(target = "seatCount", ignore = true) + @Mapping(target = "model", ignore = true) + @Mapping(target = "catalogId", ignore = true) + CarDto map(Car car); + + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapper.java index d087d5ef4a..53f9e39cbd 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapper.java @@ -5,8 +5,6 @@ */ package org.mapstruct.ap.test.nullvaluemapping; -import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; - import java.util.List; import java.util.Map; import java.util.UUID; @@ -16,31 +14,28 @@ import org.mapstruct.MapMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; import org.mapstruct.ap.test.nullvaluemapping._target.DriverAndCarDto; import org.mapstruct.ap.test.nullvaluemapping.source.Car; import org.mapstruct.ap.test.nullvaluemapping.source.Driver; import org.mapstruct.factory.Mappers; +import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; + @Mapper(imports = UUID.class) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper( CarMapper.class ); @BeanMapping(nullValueMappingStrategy = RETURN_DEFAULT) - @Mappings({ - @Mapping(target = "seatCount", source = "numberOfSeats"), - @Mapping(target = "model", constant = "ModelT"), - @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") - }) + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") CarDto carToCarDto(Car car); @BeanMapping(nullValueMappingStrategy = RETURN_DEFAULT) - @Mappings({ - @Mapping(target = "seatCount", source = "car.numberOfSeats"), - @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") - }) + @Mapping(target = "seatCount", source = "car.numberOfSeats") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") CarDto carToCarDto(Car car, String model); @IterableMapping(nullValueMappingStrategy = RETURN_DEFAULT) diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperIterableSettingOnConfig.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperIterableSettingOnConfig.java new file mode 100644 index 0000000000..8fd9447073 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperIterableSettingOnConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.mapstruct.IterableMapping; +import org.mapstruct.MapMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +@Mapper(imports = UUID.class, config = CentralIterableMappingConfig.class) +public interface CarMapperIterableSettingOnConfig { + + CarMapperIterableSettingOnConfig INSTANCE = Mappers.getMapper( CarMapperIterableSettingOnConfig.class ); + + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") + CarDto carToCarDto(Car car); + + @IterableMapping(dateFormat = "dummy") + List carsToCarDtos(List cars); + + @MapMapping(valueDateFormat = "dummy") + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperIterableSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperIterableSettingOnMapper.java new file mode 100644 index 0000000000..0e1e123c52 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperIterableSettingOnMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.mapstruct.IterableMapping; +import org.mapstruct.MapMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +@Mapper( + imports = UUID.class, + nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT, + nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_NULL +) +public interface CarMapperIterableSettingOnMapper { + + CarMapperIterableSettingOnMapper INSTANCE = Mappers.getMapper( CarMapperIterableSettingOnMapper.class ); + + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") + CarDto carToCarDto(Car car); + + @IterableMapping(dateFormat = "dummy") + List carsToCarDtos(List cars); + + @MapMapping(valueDateFormat = "dummy") + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperMapSettingOnConfig.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperMapSettingOnConfig.java new file mode 100644 index 0000000000..47f0f34f63 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperMapSettingOnConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.mapstruct.IterableMapping; +import org.mapstruct.MapMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +@Mapper(imports = UUID.class, config = CentralMapMappingConfig.class) +public interface CarMapperMapSettingOnConfig { + + CarMapperMapSettingOnConfig INSTANCE = Mappers.getMapper( CarMapperMapSettingOnConfig.class ); + + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") + CarDto carToCarDto(Car car); + + @IterableMapping(dateFormat = "dummy") + List carsToCarDtos(List cars); + + @MapMapping(valueDateFormat = "dummy") + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperMapSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperMapSettingOnMapper.java new file mode 100644 index 0000000000..0caad062c5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperMapSettingOnMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.mapstruct.IterableMapping; +import org.mapstruct.MapMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.factory.Mappers; + +@Mapper( + imports = UUID.class, + nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT, + nullValueMapMappingStrategy = NullValueMappingStrategy.RETURN_NULL +) +public interface CarMapperMapSettingOnMapper { + + CarMapperMapSettingOnMapper INSTANCE = Mappers.getMapper( CarMapperMapSettingOnMapper.class ); + + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") + CarDto carToCarDto(Car car); + + @IterableMapping(dateFormat = "dummy") + List carsToCarDtos(List cars); + + @MapMapping(valueDateFormat = "dummy") + Map carsToCarDtoMap(Map cars); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnConfig.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnConfig.java index f8f15a3e77..8e4ca2dac6 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnConfig.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnConfig.java @@ -13,7 +13,6 @@ import org.mapstruct.MapMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import org.mapstruct.NullValueMappingStrategy; import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; import org.mapstruct.ap.test.nullvaluemapping.source.Car; @@ -24,11 +23,9 @@ public interface CarMapperSettingOnConfig { CarMapperSettingOnConfig INSTANCE = Mappers.getMapper( CarMapperSettingOnConfig.class ); - @Mappings({ - @Mapping(target = "seatCount", source = "numberOfSeats"), - @Mapping(target = "model", constant = "ModelT"), - @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") - }) + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") CarDto carToCarDto(Car car); @IterableMapping(dateFormat = "dummy") diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnMapper.java index c3f7d5f2c7..2306b3add4 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CarMapperSettingOnMapper.java @@ -13,7 +13,6 @@ import org.mapstruct.MapMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import org.mapstruct.NullValueMappingStrategy; import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; import org.mapstruct.ap.test.nullvaluemapping.source.Car; @@ -24,11 +23,9 @@ public interface CarMapperSettingOnMapper { CarMapperSettingOnMapper INSTANCE = Mappers.getMapper( CarMapperSettingOnMapper.class ); - @Mappings({ - @Mapping(target = "seatCount", source = "numberOfSeats"), - @Mapping(target = "model", constant = "ModelT"), - @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") - }) + @Mapping(target = "seatCount", source = "numberOfSeats") + @Mapping(target = "model", constant = "ModelT") + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") CarDto carToCarDto(Car car); @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CentralIterableMappingConfig.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CentralIterableMappingConfig.java new file mode 100644 index 0000000000..559c3520a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CentralIterableMappingConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import org.mapstruct.MapperConfig; +import org.mapstruct.NullValueMappingStrategy; + +@MapperConfig( + nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT, + nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_NULL +) +public class CentralIterableMappingConfig { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CentralMapMappingConfig.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CentralMapMappingConfig.java new file mode 100644 index 0000000000..7737e07de6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/CentralMapMappingConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import org.mapstruct.MapperConfig; +import org.mapstruct.NullValueMappingStrategy; + +@MapperConfig( + nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT, + nullValueMapMappingStrategy = NullValueMappingStrategy.RETURN_NULL +) +public class CentralMapMappingConfig { + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.java new file mode 100644 index 0000000000..c4acecd64f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueIterableMappingStrategyTest.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CarDto.class, + Car.class +}) +@IssueKey("2953") +public class NullValueIterableMappingStrategyTest { + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueIterableMappingStrategy", value = "return_default") + @WithClasses({ + CarListMapper.class + }) + void globalNullIterableMappingStrategy() { + assertThat( CarListMapper.INSTANCE.carsToCarDtoList( null ) ).isEmpty(); + } + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueIterableMappingStrategy", value = "return_default") + @WithClasses({ + CarListMapperSettingOnMapper.class + }) + void globalNullMapMappingStrategyWithOverrideInMapper() { + // Explicit definition in @Mapper should override global + assertThat( CarListMapperSettingOnMapper.INSTANCE.carsToCarDtoList( null ) ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.java new file mode 100644 index 0000000000..33b5942f85 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMapMappingStrategyTest.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluemapping; + +import org.mapstruct.ap.test.nullvaluemapping._target.CarDto; +import org.mapstruct.ap.test.nullvaluemapping.source.Car; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + CarDto.class, + Car.class +}) +@IssueKey("2953") +public class NullValueMapMappingStrategyTest { + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueMapMappingStrategy", value = "return_default") + @WithClasses({ + CarMapMapper.class + }) + void globalNullMapMappingStrategy() { + assertThat( CarMapMapper.INSTANCE.carsToCarDtoMap( null ) ).isEmpty(); + } + + @ProcessorTest + @ProcessorOption(name = "mapstruct.nullValueMapMappingStrategy", value = "return_default") + @WithClasses({ + CarMapMapperSettingOnMapper.class + }) + void globalNullMapMappingStrategyWithOverrideInMapper() { + // Explicit definition in @Mapper should override global + assertThat( CarMapMapperSettingOnMapper.INSTANCE.carsToCarDtoMap( null ) ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMappingTest.java index 4bd9324a9a..a027acaba3 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluemapping/NullValueMappingTest.java @@ -33,8 +33,14 @@ DriverAndCarDto.class, CarMapper.class, CarMapperSettingOnMapper.class, + CarMapperIterableSettingOnMapper.class, + CarMapperMapSettingOnMapper.class, CentralConfig.class, - CarMapperSettingOnConfig.class + CarMapperSettingOnConfig.class, + CentralIterableMappingConfig.class, + CarMapperIterableSettingOnConfig.class, + CentralMapMappingConfig.class, + CarMapperMapSettingOnConfig.class, }) public class NullValueMappingTest { @@ -161,8 +167,7 @@ public void shouldMapIterableWithNullArgOnMapper() { List carDtos = CarMapperSettingOnMapper.INSTANCE.carsToCarDtos( null ); //then - assertThat( carDtos ).isNotNull(); - assertThat( carDtos.isEmpty() ).isTrue(); + assertThat( carDtos ).isEmpty(); } @ProcessorTest @@ -175,6 +180,74 @@ public void shouldMapMapWithNullArgOnMapper() { assertThat( carDtoMap ).isNull(); } + @ProcessorTest + public void shouldMapExpressionAndConstantRegardlessOfIterableNullArgOnMapper() { + + //when + CarDto carDto = CarMapperIterableSettingOnMapper.INSTANCE.carToCarDto( null ); + + //then + assertThat( carDto ).isNotNull(); + assertThat( carDto.getMake() ).isNull(); + assertThat( carDto.getSeatCount() ).isEqualTo( 0 ); + assertThat( carDto.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto.getCatalogId() ).isNotEmpty(); + } + + @ProcessorTest + public void shouldMapIterableToNullWithIterableNullArgOnMapper() { + + //when + List carDtos = CarMapperIterableSettingOnMapper.INSTANCE.carsToCarDtos( null ); + + //then + assertThat( carDtos ).isNull(); + } + + @ProcessorTest + public void shouldMapMapRegardlessOfIterableNullArgOnMapper() { + + //when + Map carDtoMap = CarMapperIterableSettingOnMapper.INSTANCE.carsToCarDtoMap( null ); + + //then + assertThat( carDtoMap ).isEmpty(); + } + + @ProcessorTest + public void shouldMapExpressionAndConstantRegardlessMapNullArgOnMapper() { + + //when + CarDto carDto = CarMapperMapSettingOnMapper.INSTANCE.carToCarDto( null ); + + //then + assertThat( carDto ).isNotNull(); + assertThat( carDto.getMake() ).isNull(); + assertThat( carDto.getSeatCount() ).isEqualTo( 0 ); + assertThat( carDto.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto.getCatalogId() ).isNotEmpty(); + } + + @ProcessorTest + public void shouldMapIterableRegardlessOfMapNullArgOnMapper() { + + //when + List carDtos = CarMapperMapSettingOnMapper.INSTANCE.carsToCarDtos( null ); + + //then + assertThat( carDtos ).isEmpty(); + } + + @ProcessorTest + public void shouldMapMapToWithMapNullArgOnMapper() { + + //when + Map carDtoMap = CarMapperMapSettingOnMapper.INSTANCE.carsToCarDtoMap( null ); + + //then + assertThat( carDtoMap ).isNull(); + } + @ProcessorTest public void shouldMapExpressionAndConstantRegardlessNullArgOnConfig() { @@ -196,8 +269,7 @@ public void shouldMapIterableWithNullArgOnConfig() { List carDtos = CarMapperSettingOnConfig.INSTANCE.carsToCarDtos( null ); //then - assertThat( carDtos ).isNotNull(); - assertThat( carDtos.isEmpty() ).isTrue(); + assertThat( carDtos ).isEmpty(); } @ProcessorTest @@ -210,6 +282,74 @@ public void shouldMapMapWithNullArgOnConfig() { assertThat( carDtoMap ).isNull(); } + @ProcessorTest + public void shouldMapExpressionAndConstantRegardlessOfIterableNullArgOnConfig() { + + //when + CarDto carDto = CarMapperIterableSettingOnConfig.INSTANCE.carToCarDto( null ); + + //then + assertThat( carDto ).isNotNull(); + assertThat( carDto.getMake() ).isNull(); + assertThat( carDto.getSeatCount() ).isEqualTo( 0 ); + assertThat( carDto.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto.getCatalogId() ).isNotEmpty(); + } + + @ProcessorTest + public void shouldMapIterableToNullWithIterableNullArgOnConfig() { + + //when + List carDtos = CarMapperIterableSettingOnConfig.INSTANCE.carsToCarDtos( null ); + + //then + assertThat( carDtos ).isNull(); + } + + @ProcessorTest + public void shouldMapMapRegardlessOfIterableNullArgOnConfig() { + + //when + Map carDtoMap = CarMapperIterableSettingOnConfig.INSTANCE.carsToCarDtoMap( null ); + + //then + assertThat( carDtoMap ).isEmpty(); + } + + @ProcessorTest + public void shouldMapExpressionAndConstantRegardlessOfMapNullArgOnConfig() { + + //when + CarDto carDto = CarMapperMapSettingOnConfig.INSTANCE.carToCarDto( null ); + + //then + assertThat( carDto ).isNotNull(); + assertThat( carDto.getMake() ).isNull(); + assertThat( carDto.getSeatCount() ).isEqualTo( 0 ); + assertThat( carDto.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto.getCatalogId() ).isNotEmpty(); + } + + @ProcessorTest + public void shouldMapIterableRegardlessOfMapNullArgOnConfig() { + + //when + List carDtos = CarMapperMapSettingOnConfig.INSTANCE.carsToCarDtos( null ); + + //then + assertThat( carDtos ).isEmpty(); + } + + @ProcessorTest + public void shouldMapMapToNullWithMapNullArgOnConfig() { + + //when + Map carDtoMap = CarMapperMapSettingOnConfig.INSTANCE.carsToCarDtoMap( null ); + + //then + assertThat( carDtoMap ).isNull(); + } + @ProcessorTest public void shouldApplyConfiguredStrategyForMethodWithSeveralSourceParams() { //when diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/Bean.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/Bean.java new file mode 100644 index 0000000000..545ddffe8b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/Bean.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +import java.util.Collection; +import java.util.Map; + +public class Bean { + + private String id; + private Collection list; + private Map map; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Collection getList() { + return list; + } + + public void setList(Collection list) { + this.list = list; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanDTO.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanDTO.java new file mode 100644 index 0000000000..9529da8113 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanDTO.java @@ -0,0 +1,40 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +import java.util.Collection; +import java.util.Map; + +public class BeanDTO { + + private String id; + private Collection list; + private Map map; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Collection getList() { + return list; + } + + public void setList(Collection list) { + this.list = list; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanDTOWithId.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanDTOWithId.java new file mode 100644 index 0000000000..152a872d82 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanDTOWithId.java @@ -0,0 +1,19 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +public class BeanDTOWithId extends BeanDTO { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanMapper.java new file mode 100644 index 0000000000..4d453579ea --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface BeanMapper { + + BeanMapper INSTANCE = Mappers.getMapper( BeanMapper.class ); + + @Mapping(target = "list", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR) + @Mapping(target = "map", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR) + BeanDTO map(Bean source, @MappingTarget BeanDTO target); + + @Mapping(target = "id", source = "bean.id") + @Mapping(target = "list", source = "bean.list", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR) + @Mapping(target = "map", source = "bean.map", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR) + BeanDTO map(NestedBean source, @MappingTarget BeanDTO target); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR) + BeanDTO mapWithBeanMapping(Bean source, @MappingTarget BeanDTO target); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanMapperWithStrategyOnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanMapperWithStrategyOnMapper.java new file mode 100644 index 0000000000..0e26b91b5b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/BeanMapperWithStrategyOnMapper.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.CLEAR) +public interface BeanMapperWithStrategyOnMapper { + + BeanMapperWithStrategyOnMapper INSTANCE = Mappers.getMapper( BeanMapperWithStrategyOnMapper.class ); + + BeanDTO map(Bean source, @MappingTarget BeanDTO target); + + @Mapping(target = "id", source = "bean.id") + @Mapping(target = "list", source = "bean.list") + @Mapping(target = "map", source = "bean.map") + BeanDTO map(NestedBean source, @MappingTarget BeanDTO target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/NestedBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/NestedBean.java new file mode 100644 index 0000000000..448645c107 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/NestedBean.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +public class NestedBean { + private Bean bean; + + public Bean getBean() { + return bean; + } + + public void setBean(Bean bean) { + this.bean = bean; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/NullValuePropertyMappingClearTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/NullValuePropertyMappingClearTest.java new file mode 100644 index 0000000000..20cbcdc2b4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullvaluepropertymapping/clear/NullValuePropertyMappingClearTest.java @@ -0,0 +1,116 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullvaluepropertymapping.clear; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + Bean.class, + BeanDTO.class, + NestedBean.class, +}) +class NullValuePropertyMappingClearTest { + + @ProcessorTest + @WithClasses(BeanMapper.class) + void generatedMapperMethodsShouldCallClear() { + BeanDTO target = new BeanDTO(); + target.setId( "target" ); + List targetList = new ArrayList<>(); + targetList.add( "a" ); + targetList.add( "b" ); + target.setList( targetList ); + Map targetMap = new HashMap<>(); + targetMap.put( "a", "aValue" ); + target.setMap( targetMap ); + Bean source = new Bean(); + + BeanMapper.INSTANCE.map( source, target ); + assertThat( target.getId() ).isNull(); + assertThat( target.getList() ) + .isSameAs( targetList ) + .isEmpty(); + assertThat( target.getMap() ) + .isSameAs( targetMap ) + .isEmpty(); + + NestedBean nestedBean = new NestedBean(); + nestedBean.setBean( source ); + targetList.add( "a" ); + targetList.add( "b" ); + targetMap.put( "a", "aValue" ); + target.setId( "target" ); + BeanMapper.INSTANCE.map( nestedBean, target ); + assertThat( target.getId() ).isNull(); + assertThat( target.getList() ) + .isSameAs( targetList ) + .isEmpty(); + assertThat( target.getMap() ) + .isSameAs( targetMap ) + .isEmpty(); + + targetList.add( "a" ); + targetList.add( "b" ); + targetMap.put( "a", "aValue" ); + target.setId( "target" ); + BeanMapper.INSTANCE.mapWithBeanMapping( source, target ); + assertThat( target.getId() ).isNull(); + assertThat( target.getList() ) + .isSameAs( targetList ) + .isEmpty(); + assertThat( target.getMap() ) + .isSameAs( targetMap ) + .isEmpty(); + } + + @ProcessorTest + @WithClasses(BeanMapperWithStrategyOnMapper.class) + void generatedMapperWithMappingDefinedInConfigMethodsShouldCallClear() { + BeanDTO target = new BeanDTO(); + target.setId( "target" ); + List targetList = new ArrayList<>(); + targetList.add( "a" ); + targetList.add( "b" ); + target.setList( targetList ); + Map targetMap = new HashMap<>(); + targetMap.put( "a", "aValue" ); + target.setMap( targetMap ); + Bean source = new Bean(); + + BeanMapperWithStrategyOnMapper.INSTANCE.map( source, target ); + assertThat( target.getId() ).isNull(); + assertThat( target.getList() ) + .isSameAs( targetList ) + .isEmpty(); + assertThat( target.getMap() ) + .isSameAs( targetMap ) + .isEmpty(); + + NestedBean nestedBean = new NestedBean(); + nestedBean.setBean( source ); + targetList.add( "a" ); + targetList.add( "b" ); + targetMap.put( "a", "aValue" ); + target.setId( "target" ); + BeanMapperWithStrategyOnMapper.INSTANCE.map( nestedBean, target ); + assertThat( target.getId() ).isNull(); + assertThat( target.getList() ) + .isSameAs( targetList ) + .isEmpty(); + assertThat( target.getMap() ) + .isSameAs( targetMap ) + .isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/OptionalBeforeAfterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/OptionalBeforeAfterMapper.java new file mode 100644 index 0000000000..30dca2cf39 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/OptionalBeforeAfterMapper.java @@ -0,0 +1,76 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.beforeafter; + +import java.util.Optional; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetType; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OptionalBeforeAfterMapper { + + OptionalBeforeAfterMapper INSTANCE = Mappers.getMapper( OptionalBeforeAfterMapper.class ); + + Target toTarget(Source source); + + @BeforeMapping + default void beforeDeepOptionalSourceWithNoTargetType(Optional source) { + } + + @BeforeMapping + default void beforeDeepOptionalSourceWithNonOptionalTargetType(@TargetType Class targetType, + Optional source) { + } + + @AfterMapping + default void afterDeepOptionalSourceWithNoTarget(Optional source) { + + } + + @AfterMapping + default void afterDeepOptionalSourceWithNonOptionalTarget(@MappingTarget Target.SubType target, + Optional source) { + } + + @AfterMapping + default void afterDeepOptionalSourceWithOptionalTarget(@MappingTarget Optional target, + Optional source) { + } + + @AfterMapping + default void afterDeepNonOptionalSourceOptionalTarget(@MappingTarget Optional target, + Source.SubType source) { + } + + @BeforeMapping + default void beforeShallowOptionalSourceWithNoTargetType(Optional source) { + } + + @BeforeMapping + default void beforeShallowOptionalSourceWithNonOptionalTargetType(@TargetType Class targetType, + Optional source) { + } + + @AfterMapping + default void afterShallowOptionalSourceWithNoTarget(Optional source) { + + } + + @AfterMapping + default void afterShallowOptionalSourceWithNonOptionalTarget(@MappingTarget String target, + Optional source) { + } + + @AfterMapping + default void afterShallowNonOptionalSourceOptionalTarget(@MappingTarget Optional target, String source) { + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/OptionalBeforeAfterTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/OptionalBeforeAfterTest.java new file mode 100644 index 0000000000..ba1a7b09d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/OptionalBeforeAfterTest.java @@ -0,0 +1,25 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.beforeafter; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +@WithClasses({ + OptionalBeforeAfterMapper.class, Source.class, Target.class +}) +class OptionalBeforeAfterTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + void generatedCode() { + generatedSource.addComparisonToFixtureFor( OptionalBeforeAfterMapper.class ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/Source.java new file mode 100644 index 0000000000..1284bca7ea --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/Source.java @@ -0,0 +1,87 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.beforeafter; + +import java.util.Objects; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class Source { + + private final Optional deepOptionalToOptional; + private final Optional deepOptionalToNonOptional; + private final SubType deepNonOptionalToOptional; + + private final Optional shallowOptionalToOptional; + private final Optional shallowOptionalToNonOptional; + private final String shallowNonOptionalToOptional; + + public Source(Optional deepOptionalToOptional, Optional deepOptionalToNonOptional, + SubType deepNonOptionalToOptional, Optional shallowOptionalToOptional, + Optional shallowOptionalToNonOptional, String shallowNonOptionalToOptional) { + this.deepOptionalToOptional = deepOptionalToOptional; + this.deepOptionalToNonOptional = deepOptionalToNonOptional; + this.deepNonOptionalToOptional = deepNonOptionalToOptional; + this.shallowOptionalToOptional = shallowOptionalToOptional; + this.shallowOptionalToNonOptional = shallowOptionalToNonOptional; + this.shallowNonOptionalToOptional = shallowNonOptionalToOptional; + } + + public Optional getDeepOptionalToOptional() { + return deepOptionalToOptional; + } + + public Optional getDeepOptionalToNonOptional() { + return deepOptionalToNonOptional; + } + + public SubType getDeepNonOptionalToOptional() { + return deepNonOptionalToOptional; + } + + public Optional getShallowOptionalToOptional() { + return shallowOptionalToOptional; + } + + public Optional getShallowOptionalToNonOptional() { + return shallowOptionalToNonOptional; + } + + public String getShallowNonOptionalToOptional() { + return shallowNonOptionalToOptional; + } + + public static class SubType { + + private final String value; + + public SubType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + SubType subType = (SubType) o; + return Objects.equals( value, subType.value ); + } + + @Override + public int hashCode() { + return Objects.hash( value ); + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/Target.java new file mode 100644 index 0000000000..592e3338fe --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/beforeafter/Target.java @@ -0,0 +1,86 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.beforeafter; + +import java.util.Objects; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class Target { + + private final Optional deepOptionalToOptional; + private final SubType deepOptionalToNonOptional; + private final Optional deepNonOptionalToOptional; + + private final Optional shallowOptionalToOptional; + private final String shallowOptionalToNonOptional; + private final Optional shallowNonOptionalToOptional; + + public Target(Optional deepOptionalToOptional, SubType deepOptionalToNonOptional, + Optional deepNonOptionalToOptional, Optional shallowOptionalToOptional, + String shallowOptionalToNonOptional, Optional shallowNonOptionalToOptional) { + this.deepOptionalToOptional = deepOptionalToOptional; + this.deepOptionalToNonOptional = deepOptionalToNonOptional; + this.deepNonOptionalToOptional = deepNonOptionalToOptional; + this.shallowOptionalToOptional = shallowOptionalToOptional; + this.shallowOptionalToNonOptional = shallowOptionalToNonOptional; + this.shallowNonOptionalToOptional = shallowNonOptionalToOptional; + } + + public Optional getDeepOptionalToOptional() { + return deepOptionalToOptional; + } + + public SubType getDeepOptionalToNonOptional() { + return deepOptionalToNonOptional; + } + + public Optional getDeepNonOptionalToOptional() { + return deepNonOptionalToOptional; + } + + public Optional getShallowOptionalToOptional() { + return shallowOptionalToOptional; + } + + public String getShallowOptionalToNonOptional() { + return shallowOptionalToNonOptional; + } + + public Optional getShallowNonOptionalToOptional() { + return shallowNonOptionalToOptional; + } + + public static class SubType { + + private final String value; + + public SubType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + SubType subType = (SubType) o; + return Objects.equals( value, subType.value ); + } + + @Override + public int hashCode() { + return Objects.hash( value ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/OptionalBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/OptionalBuilderTest.java new file mode 100644 index 0000000000..9dd4b01256 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/OptionalBuilderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.builder; + +import java.util.Optional; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +class OptionalBuilderTest { + + @ProcessorTest + @WithClasses(SimpleOptionalBuilderMapper.class) + void simpleOptionalBuilder() { + Optional targetOpt = SimpleOptionalBuilderMapper.INSTANCE.map( null ); + assertThat( targetOpt ).isEmpty(); + + targetOpt = SimpleOptionalBuilderMapper.INSTANCE.map( new SimpleOptionalBuilderMapper.Source( "test" ) ); + assertThat( targetOpt ).isNotEmpty(); + SimpleOptionalBuilderMapper.Target target = targetOpt.get(); + assertThat( target.getValue() ).isEqualTo( "test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/SimpleOptionalBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/SimpleOptionalBuilderMapper.java new file mode 100644 index 0000000000..5f24aa0cae --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/SimpleOptionalBuilderMapper.java @@ -0,0 +1,67 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.builder; + +import java.util.Optional; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface SimpleOptionalBuilderMapper { + + SimpleOptionalBuilderMapper INSTANCE = Mappers.getMapper( SimpleOptionalBuilderMapper.class ); + + Optional map(Source source); + + class Source { + private final String value; + + public Source(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + class Target { + + private final String value; + + private Target(TargetBuilder builder) { + this.value = builder.value; + } + + public String getValue() { + return value; + } + + public static TargetBuilder builder() { + return new TargetBuilder(); + } + + } + + class TargetBuilder { + + private String value; + + public TargetBuilder value(String value) { + this.value = value; + return this; + } + + public Target build() { + return new Target(this); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java new file mode 100644 index 0000000000..bdff608ddc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.builder.crosspackage; + +import java.util.Optional; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.optional.builder.crosspackage.dto.PersonTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CrossPackageOptionalBuilderMapper { + + CrossPackageOptionalBuilderMapper INSTANCE = Mappers.getMapper( CrossPackageOptionalBuilderMapper.class ); + + Optional toTarget(PersonSource source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java new file mode 100644 index 0000000000..8919787dfb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java @@ -0,0 +1,29 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.builder.crosspackage; + +import java.util.Optional; + +import org.mapstruct.ap.test.optional.builder.crosspackage.dto.PersonTarget; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey( "4046" ) +@WithClasses({ CrossPackageOptionalBuilderMapper.class, PersonSource.class, PersonTarget.class }) +class CrossPackageOptionalBuilderTest { + + @ProcessorTest + void shouldUseStaticBuilderMethodForOptionalTargetAcrossPackages() { + PersonSource source = new PersonSource( "John" ); + Optional result = CrossPackageOptionalBuilderMapper.INSTANCE.toTarget( source ); + + assertThat( result ).isPresent(); + assertThat( result.get().getName() ).isEqualTo( "John" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java new file mode 100644 index 0000000000..dea0c2f2b8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.builder.crosspackage; + +public class PersonSource { + private final String name; + + public PersonSource(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java new file mode 100644 index 0000000000..6fb5ab770b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.builder.crosspackage.dto; + +public class PersonTarget { + + private final String name; + + private PersonTarget(PersonTargetBuilder builder) { + this.name = builder.name; + } + + public String getName() { + return name; + } + + public static PersonTargetBuilder builder() { + return new PersonTargetBuilder(); + } + + public static class PersonTargetBuilder { + + private String name; + + PersonTargetBuilder() { } + + public PersonTargetBuilder name(String name) { + this.name = name; + return this; + } + + public PersonTarget build() { + return new PersonTarget( this ); + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/custom/CustomOptionalMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/custom/CustomOptionalMapper.java new file mode 100644 index 0000000000..a6f8c155d4 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/custom/CustomOptionalMapper.java @@ -0,0 +1,63 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.custom; + +import java.util.Optional; + +import org.mapstruct.Condition; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +@SuppressWarnings("OptionalAssignedToNull") +public interface CustomOptionalMapper { + + CustomOptionalMapper INSTANCE = Mappers.getMapper( CustomOptionalMapper.class ); + + Target map(Source source); + + void update(@MappingTarget Target target, Source source); + + @Condition + default boolean isPresent(Optional optional) { + return optional != null; + } + + default E unwrapFromOptional(Optional optional) { + return optional == null + ? null + : optional.orElse( null ); + } + + class Target { + private String value = "initial"; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class Source { + private final Optional value; + + public Source(Optional value) { + this.value = value; + } + + public Optional getValue() { + return value; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/custom/CustomOptionalTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/custom/CustomOptionalTest.java new file mode 100644 index 0000000000..51991ac478 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/custom/CustomOptionalTest.java @@ -0,0 +1,60 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.custom; + +import java.util.Optional; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses(CustomOptionalMapper.class) +class CustomOptionalTest { + + @ProcessorTest + void shouldUseCustomMethodsWhenMapping() { + CustomOptionalMapper.Target target = CustomOptionalMapper.INSTANCE. + map( new CustomOptionalMapper.Source( null ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "initial" ); + + target = CustomOptionalMapper.INSTANCE.map( new CustomOptionalMapper.Source( Optional.empty() ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isNull(); + + target = CustomOptionalMapper.INSTANCE.map( new CustomOptionalMapper.Source( Optional.of( "test" ) ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "test" ); + } + + @ProcessorTest + void shouldUseCustomMethodsWhenUpdating() { + CustomOptionalMapper.Target target = new CustomOptionalMapper.Target(); + + CustomOptionalMapper.INSTANCE.update( target, new CustomOptionalMapper.Source( null ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "initial" ); + + CustomOptionalMapper.INSTANCE.update( target, new CustomOptionalMapper.Source( Optional.empty() ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isNull(); + + target = new CustomOptionalMapper.Target(); + CustomOptionalMapper.INSTANCE.update( target, new CustomOptionalMapper.Source( Optional.of( "test" ) ) ); + + assertThat( target ).isNotNull(); + assertThat( target.getValue() ).isEqualTo( "test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/OptionalDifferentTypesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/OptionalDifferentTypesMapper.java new file mode 100644 index 0000000000..5a276b4745 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/OptionalDifferentTypesMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.differenttypes; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OptionalDifferentTypesMapper { + + OptionalDifferentTypesMapper INSTANCE = Mappers.getMapper( OptionalDifferentTypesMapper.class ); + + Target toTarget(Source source); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/OptionalDifferentTypesTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/OptionalDifferentTypesTest.java new file mode 100644 index 0000000000..74ab85e174 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/OptionalDifferentTypesTest.java @@ -0,0 +1,204 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.differenttypes; + +import java.util.Optional; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + OptionalDifferentTypesMapper.class, Source.class, Target.class +}) +class OptionalDifferentTypesTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + void generatedCode() { + generatedSource.addComparisonToFixtureFor( OptionalDifferentTypesMapper.class ); + } + + @ProcessorTest + void constructorOptionalToOptionalWhenPresent() { + Source source = new Source( Optional.of( new Source.SubType( "some value" ) ), Optional.empty(), null ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getConstructorOptionalToOptional() ) + .hasValueSatisfying( subType -> + assertThat( subType.getValue() ).isEqualTo( "some value" ) ); + } + + @ProcessorTest + void constructorOptionalToOptionalWhenEmpty() { + Source source = new Source(); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getConstructorOptionalToOptional() ).isEmpty(); + } + + @ProcessorTest + void constructorOptionalToNonOptionalWhenPresent() { + Source source = new Source( Optional.empty(), Optional.of( new Source.SubType( "some value" ) ), null ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + Target.SubType subType = target.getConstructorOptionalToNonOptional(); + assertThat( subType ).isNotNull(); + assertThat( subType.getValue() ).isEqualTo( "some value" ); + } + + @ProcessorTest + void constructorOptionalToNonOptionalWhenEmpty() { + Source source = new Source(); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getConstructorOptionalToNonOptional() ).isNull(); + } + + @ProcessorTest + void constructorNonOptionalToOptionalWhenNotNull() { + Source source = new Source( Optional.empty(), Optional.empty(), new Source.SubType( "some value" ) ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getConstructorNonOptionalToOptional() ) + .hasValueSatisfying( subType -> + assertThat( subType.getValue() ).isEqualTo( "some value" ) ); + } + + @ProcessorTest + void constructorNonOptionalToOptionalWhenNull() { + Source source = new Source(); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getConstructorNonOptionalToOptional() ).isEmpty(); + } + + @ProcessorTest + void optionalToOptionalWhenPresent() { + Source source = new Source(); + source.setOptionalToOptional( Optional.of( new Source.SubType( "some value" ) ) ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getOptionalToOptional() ) + .hasValueSatisfying( subType -> + assertThat( subType.getValue() ).isEqualTo( "some value" ) ); + } + + @ProcessorTest + void optionalToOptionalWhenEmpty() { + Source source = new Source(); + source.setOptionalToOptional( Optional.empty() ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getOptionalToOptional() ).isEmpty(); + } + + @ProcessorTest + void optionalToNonOptionalWhenPresent() { + Source source = new Source(); + source.setOptionalToNonOptional( Optional.of( new Source.SubType( "some value" ) ) ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + Target.SubType subType = target.getOptionalToNonOptional(); + assertThat( subType ).isNotNull(); + assertThat( subType.getValue() ).isEqualTo( "some value" ); + } + + @ProcessorTest + void optionalToNonOptionalWhenEmpty() { + Source source = new Source(); + source.setOptionalToNonOptional( Optional.empty() ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getOptionalToNonOptional() ).isNull(); + } + + @ProcessorTest + void nonOptionalToOptionalWhenNotNull() { + Source source = new Source(); + source.setNonOptionalToOptional( new Source.SubType( "some value" ) ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getNonOptionalToOptional() ) + .hasValueSatisfying( subType -> + assertThat( subType.getValue() ).isEqualTo( "some value" ) ); + } + + @ProcessorTest + void nonOptionalToOptionalWhenNull() { + Source source = new Source(); + source.setNonOptionalToOptional( null ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.getNonOptionalToOptional() ).isEmpty(); + } + + @ProcessorTest + void publicOptionalToOptionalWhenPresent() { + Source source = new Source(); + source.publicOptionalToOptional = Optional.of( new Source.SubType( "some value" ) ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.publicOptionalToOptional ) + .hasValueSatisfying( subType -> + assertThat( subType.getValue() ).isEqualTo( "some value" ) ); + } + + @ProcessorTest + void publicOptionalToOptionalWhenEmpty() { + Source source = new Source(); + source.publicOptionalToOptional = Optional.empty(); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.publicOptionalToOptional ).isEmpty(); + } + + @ProcessorTest + void publicOptionalToNonOptionalWhenPresent() { + Source source = new Source(); + source.publicOptionalToNonOptional = Optional.of( new Source.SubType( "some value" ) ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + Target.SubType subType = target.publicOptionalToNonOptional; + assertThat( subType ).isNotNull(); + assertThat( subType.getValue() ).isEqualTo( "some value" ); + } + + @ProcessorTest + void publicOptionalToNonOptionalWhenEmpty() { + Source source = new Source(); + source.publicOptionalToNonOptional = Optional.empty(); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.publicOptionalToNonOptional ).isNull(); + } + + @ProcessorTest + void publicNonOptionalToOptionalWhenNotNull() { + Source source = new Source(); + source.publicNonOptionalToOptional = new Source.SubType( "some value" ); + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.publicNonOptionalToOptional ) + .hasValueSatisfying( subType -> + assertThat( subType.getValue() ).isEqualTo( "some value" ) ); + } + + @ProcessorTest + void publicNonOptionalToOptionalWhenNull() { + Source source = new Source(); + source.publicNonOptionalToOptional = null; + + Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source ); + assertThat( target.publicNonOptionalToOptional ).isEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/Source.java new file mode 100644 index 0000000000..0c0648000a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/Source.java @@ -0,0 +1,89 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.differenttypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class Source { + + private final Optional constructorOptionalToOptional; + private final Optional constructorOptionalToNonOptional; + private final SubType constructorNonOptionalToOptional; + + private Optional optionalToOptional = Optional.empty(); + private Optional optionalToNonOptional = Optional.empty(); + private SubType nonOptionalToOptional; + + @SuppressWarnings( "VisibilityModifier" ) + public Optional publicOptionalToOptional = Optional.empty(); + @SuppressWarnings( "VisibilityModifier" ) + public Optional publicOptionalToNonOptional = Optional.empty(); + @SuppressWarnings( "VisibilityModifier" ) + public SubType publicNonOptionalToOptional; + + public Source() { + this( Optional.empty(), Optional.empty(), null ); + } + + public Source(Optional constructorOptionalToOptional, Optional constructorOptionalToNonOptional, + SubType constructorNonOptionalToOptional) { + this.constructorOptionalToOptional = constructorOptionalToOptional; + this.constructorOptionalToNonOptional = constructorOptionalToNonOptional; + this.constructorNonOptionalToOptional = constructorNonOptionalToOptional; + } + + public Optional getConstructorOptionalToOptional() { + return constructorOptionalToOptional; + } + + public Optional getConstructorOptionalToNonOptional() { + return constructorOptionalToNonOptional; + } + + public SubType getConstructorNonOptionalToOptional() { + return constructorNonOptionalToOptional; + } + + public Optional getOptionalToOptional() { + return optionalToOptional; + } + + public void setOptionalToOptional(Optional optionalToOptional) { + this.optionalToOptional = optionalToOptional; + } + + public Optional getOptionalToNonOptional() { + return optionalToNonOptional; + } + + public void setOptionalToNonOptional(Optional optionalToNonOptional) { + this.optionalToNonOptional = optionalToNonOptional; + } + + public SubType getNonOptionalToOptional() { + return nonOptionalToOptional; + } + + public void setNonOptionalToOptional(SubType nonOptionalToOptional) { + this.nonOptionalToOptional = nonOptionalToOptional; + } + + public static class SubType { + + private final String value; + + public SubType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/Target.java new file mode 100644 index 0000000000..4f2b21c8a7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/differenttypes/Target.java @@ -0,0 +1,84 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.differenttypes; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class Target { + + private final Optional constructorOptionalToOptional; + private final SubType constructorOptionalToNonOptional; + private final Optional constructorNonOptionalToOptional; + + private Optional optionalToOptional = Optional.of( new SubType( "initial" ) ); + private SubType optionalToNonOptional; + private Optional nonOptionalToOptional = Optional.of( new SubType( "initial" ) ); + + @SuppressWarnings( "VisibilityModifier" ) + public Optional publicOptionalToOptional = Optional.of( new SubType( "initial" ) ); + @SuppressWarnings( "VisibilityModifier" ) + public SubType publicOptionalToNonOptional; + @SuppressWarnings( "VisibilityModifier" ) + public Optional publicNonOptionalToOptional = Optional.of( new SubType( "initial" ) ); + + public Target(Optional constructorOptionalToOptional, SubType constructorOptionalToNonOptional, + Optional constructorNonOptionalToOptional) { + this.constructorOptionalToOptional = constructorOptionalToOptional; + this.constructorOptionalToNonOptional = constructorOptionalToNonOptional; + this.constructorNonOptionalToOptional = constructorNonOptionalToOptional; + } + + public Optional getConstructorOptionalToOptional() { + return constructorOptionalToOptional; + } + + public SubType getConstructorOptionalToNonOptional() { + return constructorOptionalToNonOptional; + } + + public Optional getConstructorNonOptionalToOptional() { + return constructorNonOptionalToOptional; + } + + public Optional getOptionalToOptional() { + return optionalToOptional; + } + + public void setOptionalToOptional(Optional optionalToOptional) { + this.optionalToOptional = optionalToOptional; + } + + public SubType getOptionalToNonOptional() { + return optionalToNonOptional; + } + + public void setOptionalToNonOptional(SubType optionalToNonOptional) { + this.optionalToNonOptional = optionalToNonOptional; + } + + public Optional getNonOptionalToOptional() { + return nonOptionalToOptional; + } + + public void setNonOptionalToOptional(Optional nonOptionalToOptional) { + this.nonOptionalToOptional = nonOptionalToOptional; + } + + public static class SubType { + + private final String value; + + public SubType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/MappingContext.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/MappingContext.java new file mode 100644 index 0000000000..3624efb00e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/MappingContext.java @@ -0,0 +1,108 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetType; + +/** + * @author Filip Hrisafov + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class MappingContext { + + private final List invokedMethods = new ArrayList<>(); + + @BeforeMapping + public void beforeWithoutParameters() { + invokedMethods.add( "beforeWithoutParameters" ); + } + + @BeforeMapping + public void beforeWithOptionalSource(Optional source) { + invokedMethods.add( "beforeWithOptionalSource" ); + } + + @BeforeMapping + public void beforeWithSource(Source source) { + invokedMethods.add( "beforeWithSource" ); + } + + @BeforeMapping + public void beforeWithTargetType(@TargetType Class targetClass) { + invokedMethods.add( "beforeWithTargetType" ); + } + + @BeforeMapping + public void beforeWithBuilderTargetType(@TargetType Class targetBuilderClass) { + invokedMethods.add( "beforeWithBuilderTargetType" ); + } + + @BeforeMapping + public void beforeWithOptionalTarget(@MappingTarget Optional target) { + invokedMethods.add( "beforeWithOptionalTarget" ); + } + + @BeforeMapping + public void beforeWithTarget(@MappingTarget Target target) { + invokedMethods.add( "beforeWithTarget" ); + } + + @BeforeMapping + public void beforeWithTargetBuilder(@MappingTarget Target.Builder target) { + invokedMethods.add( "beforeWithTargetBuilder" ); + } + + @AfterMapping + public void afterWithoutParameters() { + invokedMethods.add( "afterWithoutParameters" ); + } + + @AfterMapping + public void afterWithOptionalSource(Optional source) { + invokedMethods.add( "afterWithOptionalSource" ); + } + + @AfterMapping + public void afterWithSource(Source source) { + invokedMethods.add( "afterWithSource" ); + } + + @AfterMapping + public void afterWithTargetType(@TargetType Class targetClass) { + invokedMethods.add( "afterWithTargetType" ); + } + + @AfterMapping + public void afterWithBuilderTargetType(@TargetType Class targetClass) { + invokedMethods.add( "afterWithBuilderTargetType" ); + } + + @AfterMapping + public void afterWithTarget(@MappingTarget Target target) { + invokedMethods.add( "afterWithTarget" ); + } + + @AfterMapping + public void afterWithTargetBuilder(@MappingTarget Target.Builder target) { + invokedMethods.add( "afterWithTargetBuilder" ); + } + + @AfterMapping + public void afterWithOptionalTarget(@MappingTarget Optional target) { + invokedMethods.add( "afterWithOptionalTarget" ); + } + + public List getInvokedMethods() { + return invokedMethods; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalLifecycleTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalLifecycleTest.java new file mode 100644 index 0000000000..c80dfcb6b6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalLifecycleTest.java @@ -0,0 +1,165 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +import java.util.Optional; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + MappingContext.class, + Source.class, + Target.class, +}) +public class OptionalLifecycleTest { + + @ProcessorTest + @WithClasses(OptionalToOptionalMapper.class) + void optionalToOptional() { + MappingContext context = new MappingContext(); + + OptionalToOptionalMapper.INSTANCE.map( Optional.empty(), context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType" + ); + + context = new MappingContext(); + OptionalToOptionalMapper.INSTANCE.map( Optional.of( new Source() ), context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType", + "beforeWithTarget", + "afterWithoutParameters", + "afterWithOptionalSource", + "afterWithTargetType", + "afterWithTarget", + "afterWithOptionalTarget" + ); + } + + @ProcessorTest + @WithClasses(OptionalToOptionalWithBuilderMapper.class) + void optionalToOptionalWithBuilder() { + MappingContext context = new MappingContext(); + + OptionalToOptionalWithBuilderMapper.INSTANCE.map( Optional.empty(), context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType", + "beforeWithBuilderTargetType" + ); + + context = new MappingContext(); + OptionalToOptionalWithBuilderMapper.INSTANCE.map( Optional.of( new Source() ), context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType", + "beforeWithBuilderTargetType", + "beforeWithTargetBuilder", + "afterWithoutParameters", + "afterWithOptionalSource", + "afterWithTargetType", + "afterWithBuilderTargetType", + "afterWithTarget", + "afterWithTargetBuilder", + "afterWithOptionalTarget" + ); + } + + @ProcessorTest + @WithClasses(OptionalToTypeMapper.class) + void optionalToType() { + MappingContext context = new MappingContext(); + + OptionalToTypeMapper.INSTANCE.map( Optional.empty(), context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType" + ); + + context = new MappingContext(); + OptionalToTypeMapper.INSTANCE.map( Optional.of( new Source() ), context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType", + "beforeWithTarget", + "afterWithoutParameters", + "afterWithOptionalSource", + "afterWithTargetType", + "afterWithTarget" + ); + } + + @ProcessorTest + @WithClasses(OptionalToTypeMultiSourceMapper.class) + void optionalToTypeMultiSource() { + MappingContext context = new MappingContext(); + + OptionalToTypeMultiSourceMapper.INSTANCE.map( Optional.empty(), null, context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType" + ); + + context = new MappingContext(); + OptionalToTypeMultiSourceMapper.INSTANCE.map( Optional.of( new Source() ), null, context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType", + "beforeWithTarget", + "afterWithoutParameters", + "afterWithOptionalSource", + "afterWithTargetType", + "afterWithTarget" + ); + + context = new MappingContext(); + OptionalToTypeMultiSourceMapper.INSTANCE.map( Optional.empty(), "Test", context ); + + assertThat( context.getInvokedMethods() ) + .containsExactlyInAnyOrder( + "beforeWithoutParameters", + "beforeWithOptionalSource", + "beforeWithTargetType", + "beforeWithTarget", + "afterWithoutParameters", + "afterWithOptionalSource", + "afterWithTargetType", + "afterWithTarget" + ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToOptionalMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToOptionalMapper.java new file mode 100644 index 0000000000..d7c5d08173 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToOptionalMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +import java.util.Optional; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Builder; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface OptionalToOptionalMapper { + + OptionalToOptionalMapper INSTANCE = Mappers.getMapper( OptionalToOptionalMapper.class ); + + @BeanMapping(builder = @Builder(disableBuilder = true)) + Optional map(Optional source, @Context MappingContext context); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToOptionalWithBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToOptionalWithBuilderMapper.java new file mode 100644 index 0000000000..8cfab7fd00 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToOptionalWithBuilderMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +import java.util.Optional; + +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface OptionalToOptionalWithBuilderMapper { + + OptionalToOptionalWithBuilderMapper INSTANCE = Mappers.getMapper( OptionalToOptionalWithBuilderMapper.class ); + + Optional map(Optional source, @Context MappingContext context); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToTypeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToTypeMapper.java new file mode 100644 index 0000000000..6a6f3e096e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToTypeMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +import java.util.Optional; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Builder; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface OptionalToTypeMapper { + + OptionalToTypeMapper INSTANCE = Mappers.getMapper( OptionalToTypeMapper.class ); + + @BeanMapping(builder = @Builder(disableBuilder = true)) + Target map(Optional source, @Context MappingContext context); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToTypeMultiSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToTypeMultiSourceMapper.java new file mode 100644 index 0000000000..d672f2ec05 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/OptionalToTypeMultiSourceMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +import java.util.Optional; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Builder; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface OptionalToTypeMultiSourceMapper { + + OptionalToTypeMultiSourceMapper INSTANCE = Mappers.getMapper( OptionalToTypeMultiSourceMapper.class ); + + @BeanMapping(builder = @Builder(disableBuilder = true)) + Target map(Optional source, String otherValue, @Context MappingContext context); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/Source.java new file mode 100644 index 0000000000..c7005c6ca6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/Source.java @@ -0,0 +1,21 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +/** + * @author Filip Hrisafov + */ +public class Source { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/Target.java new file mode 100644 index 0000000000..d5f68a1844 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/lifecycle/Target.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.lifecycle; + +/** + * @author Filip Hrisafov + */ +public class Target { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public static class Builder { + + private String value; + + public Builder value(String value) { + this.value = value; + return this; + } + + public Target build() { + Target target = new Target(); + target.setValue( value ); + return target; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/nested/Artist.java b/processor/src/test/java/org/mapstruct/ap/test/optional/nested/Artist.java new file mode 100644 index 0000000000..e6ce6faa65 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/nested/Artist.java @@ -0,0 +1,81 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.optional.nested; + +import java.util.Optional; + +/** + * @author Filip Hrisafov + */ +public class Artist { + private final String name; + private final Label label; + + public Artist(String name, Label label) { + this.name = name; + this.label = label; + } + + public boolean hasName() { + return name != null; + } + + public String getName() { + return name; + } + + public boolean hasLabel() { + return label != null; + } + + public Optional