diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1f63bf5f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..42003db2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,38 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '39 9 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 00000000..a94b10c2 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + - name: Build with Maven + run: mvn --batch-mode --no-transfer-progress package \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..c148fb38 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,42 @@ +name: Deploy static content to Pages + +on: + push: + branches: ["master"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + - name: Build with Maven + run: mvn --batch-mode --no-transfer-progress package site + - name: Setup Pages + uses: actions/configure-pages@v2 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: 'target/site' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bd3678f5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Publish package to the Maven Central Repository +on: + release: + types: [created] + workflow_dispatch: +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Maven Central Repository + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - id: install-secret-key + name: Install gpg secret key + run: | + cat <(echo -e "${{ secrets.GPG_SECRET_KEY }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG + - name: Publish package + run: mvn --batch-mode --no-transfer-progress clean deploy -Dgpg.passphrase=${{ secrets.GPG_SECRET_PASS }} + env: + MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_NAME }} + MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASS }} \ No newline at end of file diff --git a/.settings/.gitignore b/.settings/.gitignore deleted file mode 100644 index e2635f07..00000000 --- a/.settings/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/org.eclipse.jdt.core.prefs -/org.eclipse.m2e.core.prefs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index aa758d77..00000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -language: java -install: true -jdk: - - openjdk8 - -sudo: false - -before_install: - - chmod +x gradlew - -stages: - - name: build -# - name: snapshot -# only publish snapshots from "master" branch and not in pull requests -# if: branch = master AND type IN (push) - - name: release - # only publish releases from "release" branch and not in pull requests - if: branch = release AND type IN (push) - -jobs: - include: - - # run gradle build - - stage: build - script: - - ./gradlew build -Ddocx4j.version=6.1.2 - - ./gradlew build -Ddocx4j.version=6.0.1 - - # publish snapshot to oss.jfrog.org -# - stage: snapshot -# script: ./gradlew artifactoryPublish -x test -Dsnapshot=true -Dbintray.user=$BINTRAY_USER -Dbintray.key=$BINTRAY_KEY -Dbuild.number=$TRAVIS_BUILD_NUMBER - - # release a new stable version to bintray - - stage: release - script: ./gradlew bintrayUpload -x test -Dbintray.user=$BINTRAY_USER -Dbintray.key=$BINTRAY_KEY -Dbuild.number=$TRAVIS_BUILD_NUMBER diff --git a/README.md b/README.md index 00673cb7..2dd37f96 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,59 @@ # docx-stamper -[![Build Status](https://travis-ci.org/thombergs/docx-stamper.png?branch=master)](https://travis-ci.org/thombergs/docx-stamper) +[![Build Status](https://github.com/verronpro/docx-stamper/actions/workflows/maven.yml/badge.svg)](https://github.com/verronpro/docx-stamper/actions/workflows/maven.yml) -docx-stamper is a Java template engine for docx documents. You create a template .docx document with your favorite word processor +docx-stamper is a Java template engine for docx documents. You create a template .docx document with your favorite word +processor and feed it to a DocxStamper instance to create a document based on the template at runtime. Example code: + ```java -MyContext context = ...; // your own POJO against which expressions found in the template - // will be resolved -InputStream template = ...; // InputStream to your .docx template file -OutputStream out = ...; // OutputStream in which to write the resulting .docx document -DocxStamper stamper = new DocxStamperConfiguration() - .build(); -stamper.stamp(template, context, out); -out.close(); +MyContext context=...; // your own POJO against which expressions found in the template + // will be resolved + InputStream template=...; // InputStream to your .docx template file + OutputStream out=...; // OutputStream in which to write the resulting .docx document + DocxStamper stamper=new DocxStamperConfiguration() + .build(); + stamper.stamp(template,context,out); + out.close(); ``` ## Replacing Expressions in a .docx Template -The main feature of docx-stamper is **replacement of expressions** within the text of the template document. Simply add expressions like `${person.name}` or `${person.name.equals("Homer") ? "Duff" : "Budweiser"}` in the text of your .docx template and provide a context object against which the expression can be resolved. docx-stamper will try to keep the original formatting of the text in the template intact. You can use the full feature set of [Spring Expression Language](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html) (SpEL). + +The main feature of docx-stamper is **replacement of expressions** within the text of the template document. Simply add +expressions like `${person.name}` or `${person.name.equals("Homer") ? "Duff" : "Budweiser"}` in the text of your .docx +template and provide a context object against which the expression can be resolved. docx-stamper will try to keep the +original formatting of the text in the template intact. You can use the full feature set +of [Spring Expression Language](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html) ( +SpEL). The value an expression resolves to may be of the following types: -| Type of expression value | Effect | -| ---|---| -| java.lang.Object | The expression is replaced by the String representation of the object (`String.valueOf()`). -| java.lang.String | The expression is replaced with the String value.| -| java.util.Date | The expression is replaced by a formatted Date string (by default "dd.MM.yyyy"). You can change the format string by registering your own [DateResolver](src/main/java/org/wickedsource/docxstamper/replace/typeresolver/DateResolver.java).| -| [org.wickedsource.docxstamper...Image](src/main/java/org/wickedsource/docxstamper/replace/typeresolver/image/Image.java) | The expression is replaced with an inline image.| +| Type of expression value | Effect | +|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| java.lang.Object | The expression is replaced by the String representation of the object (`String.valueOf()`). | +| java.lang.String | The expression is replaced with the String value. | +| java.util.Date | The expression is replaced by a formatted Date string (by default "dd.MM.yyyy"). You can change the format string by registering your own [DateResolver](src/main/java/org/wickedsource/docxstamper/replace/typeresolver/DateResolver.java). | +| [org.wickedsource.docxstamper...Image](src/main/java/org/wickedsource/docxstamper/replace/typeresolver/image/Image.java) | The expression is replaced with an inline image. | -If an expression cannot be resolved successfully, it will be skipped (meaning the expression stays in the document as it was in the template). To support more than the above types you can implement your own [TypeResolver](src/main/java/org/wickedsource/docxstamper/api/typeresolver/ITypeResolver.java). To register your own TypeResolver with docx-stamper, use the following code: +If an expression cannot be resolved successfully, it will be skipped (meaning the expression stays in the document as it +was in the template). To support more than the above types you can implement your +own [TypeResolver](src/main/java/org/wickedsource/docxstamper/api/typeresolver/ITypeResolver.java). To register your own +TypeResolver with docx-stamper, use the following code: ```java -ITypeResolver typeResolver = ...; // instance of your own ITypeResolver implementation -Class type ...; // class of expression values your resolver handles -DocxStamper stamper = new DocxStamperConfiguration() - .addTypeResolver(type, typeResolver) - .build(); +ITypeResolver typeResolver=...; // instance of your own ITypeResolver implementation + Class type...; // class of expression values your resolver handles + DocxStamper stamper=new DocxStamperConfiguration() + .addTypeResolver(type,typeResolver) + .build(); ``` ## Customizing the SpEL Evaluation Context -If you want to take more control over the evaluation of expressions, you can implement a [EvaluationContextConfigurer](src/main/java/org/wickedsource/docxstamper/api/EvaluationContextConfigurer.java) -and customize Springs `StandardEvaluationContext` to your needs. You can register an `EvaluationContextConfigurer` like this: +If you want to take more control over the evaluation of expressions, you can implement +a [EvaluationContextConfigurer](src/main/java/org/wickedsource/docxstamper/api/EvaluationContextConfigurer.java) +and customize Springs `StandardEvaluationContext` to your needs. You can register an `EvaluationContextConfigurer` like +this: ```java EvaluationContextConfigurer configurer = ...; @@ -51,90 +64,113 @@ DocxStamper stamper = new DocxStamperConfiguration() ## Adding custom functions to the Expression Language -If you want to create custom functions (for different number formats or different date formats, for example), you can register functions +If you want to create custom functions (for different number formats or different date formats, for example), you can +register functions which can then be used in the expression language. The following code for example adds a function `toUppercase(String)` which can be used within the .docx document to uppercase a String: ```java -DocxStamper stamper = new DocxStamperConfiguration() - .exposeInterfaceToExpressionLanguage(UppercaseFunction.class, new UppercaseFunctionImpl()); - .build(); +DocxStamper stamper=new DocxStamperConfiguration() + .exposeInterfaceToExpressionLanguage(UppercaseFunction.class,new UppercaseFunctionImpl()); + .build(); public interface UppercaseFunction { - String toUppercase(String string); + String toUppercase(String string); } public static class UppercaseFunctionImpl implements UppercaseFunction { - @Override - public String toUppercase(String string) { - return string.toUpperCase(); - } + @Override + public String toUppercase(String string) { + return string.toUpperCase(); + } } ``` - ## Conditional Display and Repeating of Elements -Besides replacing expressions, docx-stamper can **process comments on paragraphs of text** in the template .docx document and do manipulations on the template based on these comments. By default, you can use the following expressions in comments: -| Expression in .docx comment | Effect | -| --------------------------------- |---------| -| `displayParagraphIf(boolean)` | The commented paragraph is only displayed in the resulting .docx document if the boolean condition resolves to `true`.| -| `displayTableRowIf(boolean)` | The table row surrounding the commented paragraph is only displayed in the resulting .docx document if the boolean condition resolves to `true`.| -| `displayTableIf(boolean)` | The whole table surrounding the commented paragraph is only displayed in the resulting .docx document if the boolean condition resolves to `true`.| -| `repeatTableRow(List)` | The table row surrounding the commented paragraph is copied once for each object in the passed-in list. Expressions found in the cells of the table row are evaluated against the object from the list. -| `repeatDocPart(List)` | Repeats the part of the document surrounded by the comment. The document part is copied once for each object in the passed-in list. Expressions found in the elements of the document part are evaluated against the object from the list. Can be used instead repeatTableRow and repeatParagraph if you want to repeat more than table rows and paragraphs. -| `replaceWordWith(expression)` | Replaces the commented word (must be a single word!) with the value of the given expression. | - -If a comment cannot be processed, by default an exception will be thrown. Successfully processed comments are removed from the document. You can add support to more expressions in comments by implementing your own [ICommentProcessor](src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java). To register you comment processor to docx-stamper, use the following code: +Besides replacing expressions, docx-stamper can **process comments on paragraphs of text** in the template .docx +document and do manipulations on the template based on these comments. By default, you can use the following expressions +in comments: + +| Expression in .docx comment | Effect | +|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `displayParagraphIf(boolean)` | The commented paragraph is only displayed in the resulting .docx document if the boolean condition resolves to `true`. | +| `displayTableRowIf(boolean)` | The table row surrounding the commented paragraph is only displayed in the resulting .docx document if the boolean condition resolves to `true`. | +| `displayTableIf(boolean)` | The whole table surrounding the commented paragraph is only displayed in the resulting .docx document if the boolean condition resolves to `true`. | +| `repeatTableRow(List)` | The table row surrounding the commented paragraph is copied once for each object in the passed-in list. Expressions found in the cells of the table row are evaluated against the object from the list. | +| `repeatDocPart(List)` | Repeats the part of the document surrounded by the comment. The document part is copied once for each object in the passed-in list. Expressions found in the elements of the document part are evaluated against the object from the list. Can be used instead repeatTableRow and repeatParagraph if you want to repeat more than table rows and paragraphs. | +| `replaceWordWith(expression)` | Replaces the commented word (must be a single word!) with the value of the given expression. | +| `resolveTable(StampTable)` | Replaces a table (must have 1 column and 2 rows) with the values given by the StampTable. The StampTable contains a list of headers for columns, and a 2 level list of rows containing values for each column. | + +If a comment cannot be processed, by default an exception will be thrown. Successfully processed comments are removed +from the document. You can add support to more expressions in comments by implementing your +own [ICommentProcessor](src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java). To +register you comment processor to docx-stamper, use the following code: ```java -ICommentProcessor commentProcessor = ...; // instance of your own ICommentProcessor implementation -Class interfaceClass = ...; // class of the interface that defines the methods that are - // exposed into the expression language -DocxStamper stamper = new DocxStamperConfiguration() - .addCommentProcessor(interfaceClass, commentProcessor) - .build(); +ICommentProcessor commentProcessor=...; // instance of your own ICommentProcessor implementation + Class interfaceClass=...; // class of the interface that defines the methods that are + // exposed into the expression language + DocxStamper stamper=new DocxStamperConfiguration() + .addCommentProcessor(interfaceClass,commentProcessor) + .build(); ``` -For an in-depth description of how to create a comment processor, see the javadoc of [ICommentProcessor](src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java). + +For an in-depth description of how to create a comment processor, see the javadoc +of [ICommentProcessor](src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java). ## Conditional Display and Repeating of Elements in Headers or Footers -The docx file format does not allow comments in Headers or Footers of a document. To be able to conditionally display content in a header or footer, simply surround the expression you would put in a comment with "#{}" and put it at the beginning of the paragraph you want to manipulate. The expression will be evaluated as it would be in a comment. + +The docx file format does not allow comments in Headers or Footers of a document. To be able to conditionally display +content in a header or footer, simply surround the expression you would put in a comment with "#{}" and put it at the +beginning of the paragraph you want to manipulate. The expression will be evaluated as it would be in a comment. ## Error Handling -By default DocxStamper fails with an UnresolvedExpressionException if an expression within the document or within the comments cannot be resolved successfully. If you want to change this behavior, you can do the following: + +By default, DocxStamper fails with an UnresolvedExpressionException if an expression within the document or within the +comments cannot be resolved successfully. If you want to change this behavior, you can do the following: ```java -DocxStamper stamper = new DocxStamperConfiguration() - .setFailOnUnresolvedExpression(false) - .build(); +DocxStamper stamper=new DocxStamperConfiguration() + .setFailOnUnresolvedExpression(false) + .build(); ``` ## Sample Code -The source code contains a set of tests show how to use the features. If you want to run them yourself, clone the repository and run [the tests in the main package](src/test/java/org/wickedsource/docxstamper) with the system property `-DkeepOutputFile=true` so that the resulting .docx documents will not be cleaned up so you can view them. The resulting files will be stored in your local temp folder (watch the logging output for the exact location of the files). -If you want to have a look at the .docx templates used in the tests, have a look at the [resources subfolder](src/test/resources/org/wickedsource/docxstamper) in the test folder. +The source code contains a set of tests show how to use the features. If you want to run them yourself, clone the +repository and run [the tests in the main package](src/test/java/org/wickedsource/docxstamper) with the system +property `-DkeepOutputFile=true` so that the resulting .docx documents will not be cleaned up so you can view them. The +resulting files will be stored in your local temp folder (watch the logging output for the exact location of the files). + +If you want to have a look at the .docx templates used in the tests, have a look at +the [resources subfolder](src/test/resources/org/wickedsource/docxstamper) in the test folder. ## Maven coordinates -To include docx-stamper in your project, you can use the following maven coordinates in your dependency management system: + +To include docx-stamper in your project, you can use the following maven coordinates in your dependency management +system: ```xml + - jcenter - https://jcenter.bintray.com/ + jcenter + https://jcenter.bintray.com/ - org.wickedsource.docx-stamper - docx-stamper - 1.4.0 +org.wickedsource.docx-stamper +docx-stamper +1.4.0 ``` Note that as of version 1.4.0 you have to provide the dependency to your version of Docx4J yourself: ```xml + org.docx4j docx4j @@ -142,40 +178,9 @@ Note that as of version 1.4.0 you have to provide the dependency to your version ``` -This way, you can choose which version of Docx4J you want to use instead having it dictated by docx-stamper. Look into -the [.travis.yml](.travis.yml) file to see against which versions of Docx4J docx-stamper has been tested. - -## Changelog -* 1.4.0 (2019-09-20) - feature release - * [issues](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.4.0+is%3Aclosed) - * [pull requests](https://github.com/thombergs/docx-stamper/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+milestone%3A1.4.0+) -* 1.3.0 (2018-08-11) - feature release - * [issues](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.3.0+is%3Aclosed) - * [pull requests](https://github.com/thombergs/docx-stamper/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+milestone%3A1.3.0+) -* 1.2.2 (2017-12-31) - [minor feature release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.2.2+is%3Aclosed) -* 1.2.1 (2017-10-18) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.2.1+is%3Aclosed) -* 1.2.0 (2017-09-26) - [feature release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.2.0+is%3Aclosed) -* 1.1.0 (2017-09-18) - [feature release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.1.0+is%3Aclosed) - * *API Break:* All methods that configure `DocxStamper` have been moved into `DocxStamperConfiguration`. - * *API Break:* Methods `getCommentProcessorRegistry()` and `getTypeResolverRegistry()` have been removed from `DocxStamper`. You can - configure CommentProcessors and TypeResolvers via `DocxStamperConfiguration` now. - * `DocxStamperConfiguration` can now be used as a Builder for `DocxStamper` objects. -* 1.0.12 (2017-09-08) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.12+is%3Aclosed) -* 1.0.11 (2017-06-09) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.11+is%3Aclosed) -* 1.0.10 (2017-04-03) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.10+is%3Aclosed) -* 1.0.9 (2017-03-18) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.9+is%3Aclosed) -* 1.0.8 (2017-02-24) - [minor feature release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.8+is%3Aclosed) -* 1.0.7 (2017-01-30) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.7+is%3Aclosed) -* 1.0.6 (2017-01-20) - [minor feature release](/issues?q=is%3Aissue+milestone%3A1.0.6+is%3Aclosed) -* 1.0.5 (2017-01-09) - bugfix release -* 1.0.4 (2016-11-20) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.4+is%3Aclosed) -* 1.0.3 (2016-11-05) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.3+is%3Aclosed) -* 1.0.2 (2016-10-02) - [bugfix release](https://github.com/thombergs/docx-stamper/issues?q=is%3Aissue+milestone%3A1.0.2+is%3Aclosed) +This way, you can choose which version of Docx4J you want to use instead having it dictated by docx-stamper. ## Contribute -If you have an issue or created a comment processor or type resolver that you think deserves to be part of the default distribution, feel free to open an issue or - even better - a pull request with your contribution. - -## Frequently Asked Questions -See the [Frequently Asked Questions](/wiki/Frequently-Asked-Questions) wiki page for some answers to recurring questions. - +If you have an issue or created a comment processor or type resolver that you think deserves to be part of the default +distribution, feel free to open an issue or - even better - a pull request with your contribution. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index e0b4d488..00000000 --- a/build.gradle +++ /dev/null @@ -1,146 +0,0 @@ -plugins { - id "java" - id "java-library" - id "com.jfrog.artifactory" version "4.5.4" - id "com.jfrog.bintray" version "1.8.1" - id "maven-publish" -} - -repositories { - mavenLocal() - mavenCentral() - jcenter() -} - -// run gradle with "-Dsnapshot=true" to automatically append "-SNAPSHOT" to the version -version = '1.4.0' + (Boolean.valueOf(System.getProperty("snapshot")) ? "-SNAPSHOT" : "") -sourceCompatibility = 1.8 - -ext{ - bintrayUser = System.getProperty("bintray.user") - bintrayKey = System.getProperty("bintray.key") - buildNumber = System.getProperty("build.number") - docx4JVersion = System.getProperty("docx4j.version") ? System.getProperty("docx4j.version") : '6.1.2' -} - -dependencies { - implementation("commons-io:commons-io:2.5") - implementation("org.javassist:javassist:3.21.0-GA") - implementation("org.springframework:spring-expression:4.3.6.RELEASE") - - // We want docx-stamper to be usable with any docx4j version, so the dependency to docx4J - // has to be provided by the client. - compileOnly("org.docx4j:docx4j:${docx4JVersion}") - - testImplementation("org.docx4j:docx4j:${docx4JVersion}") - testImplementation("junit:junit:4.4") -} - -task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource -} - -javadoc.failOnError = false -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -artifacts { - archives sourcesJar - archives javadocJar -} - -def pomConfig = { - licenses { - license { - name "MIT" - url "https://opensource.org/licenses/MIT" - distribution "repo" - } - } - developers { - developer { - id "thombergs" - name "Tom Hombergs" - email "tom.hombergs@gmail.com" - } - } - - scm { - url "https://github.com/thombergs/docx-stamper" - } -} - -publishing { - publications { - mavenPublication(MavenPublication) { - from components.java - artifact sourcesJar { - classifier "sources" - } - artifact javadocJar { - classifier "javadoc" - } - groupId 'org.wickedsource.docx-stamper' - artifactId 'docx-stamper' - version project.version - pom.withXml { - def root = asNode() - root.appendNode('description', 'Template engine for .docx documents.') - root.appendNode('name', 'docx-stamper') - root.appendNode('url', 'https://github.com/thombergs/docx-stamper') - root.children().last() + pomConfig - } - } - } -} - -artifactory { - contextUrl = 'http://oss.jfrog.org' - publish { - repository { - repoKey = 'oss-snapshot-local' - username = bintrayUser - password = bintrayKey - } - defaults { - publications('mavenPublication') - publishArtifacts = true - publishPom = true - properties = [ - 'build.number': buildNumber, - 'build.name': 'docx-stamper' - ] - } - } - resolve { - repoKey = 'jcenter' - } - clientConfig.info.setBuildNumber(buildNumber) - clientConfig.info.setBuildName('docx-stamper') -} - -bintray { - user = bintrayUser - key = bintrayKey - publications = ['mavenPublication'] - - pkg { - repo = 'maven-releases' - name = 'docx-stamper' - userOrg = 'reflectoring' - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/thombergs/docx-stamper' - version { - name = project.version - desc = "build ${buildNumber}" - released = new Date() - } - } - - publish = true -} - - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 1a958be6..00000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 663c4485..00000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip diff --git a/gradlew b/gradlew deleted file mode 100644 index 4453ccea..00000000 --- a/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -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 -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -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 - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save ( ) { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index f9553162..00000000 --- a/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..ac3d8828 --- /dev/null +++ b/pom.xml @@ -0,0 +1,285 @@ + + + 4.0.0 + pro.verron + docx-stamper + 1.6.0-javax + jar + docx-stamper + + Docx-stamper is a Java template engine for docx documents. This is a maintenance fork from + https://github.com/thombergs/docx-stamper. + + https://github.com/verronpro/docx-stamper + + scm:git:https://github.com/verronpro/docx-stamper + scm:git:https://github.com/verronpro/docx-stamper.git + scm:git:https://github.com/verronpro/docx-stamper.git + + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + ynaciri@gmail.com + Youssouf Naciri + https://youssouf.naciri.fr + + + joseph@verron.pro + Joseph Verron + https://verron.pro + + + tom.hombergs@gmail.com + Tom Hombergs + http://wickedsource.org + + + bertrand@florat.net + Bertrand Florat + https://florat.net + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.0 + + UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + UTF-8 + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + UTF-8 + false + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + + + --pinentry-mode + loopback + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + @{argLine} --add-opens java.base/java.lang=ALL-UNNAMED + + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + + org.pitest + pitest-maven + 1.9.5 + + + --add-opens + java.base/java.lang=ALL-UNNAMED + + + + + pre-site + + mutationCoverage + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.0.0-M7 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.4.1 + + UTF-8 + UTF-8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + UTF-8 + false + + + + + javadoc + test-javadoc + + + + + + org.jacoco + jacoco-maven-plugin + + + + report + + + + + + org.pitest + pitest-maven + 1.9.5 + + + + + + + commons-io + commons-io + 2.11.0 + + + org.javassist + javassist + 3.29.2-GA + + + junit + junit + 4.13.2 + test + + + org.springframework + spring-expression + 5.3.23 + + + + org.docx4j + docx4j-JAXB-MOXy + 11.2.9 + + + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.2 + compile + + + org.springframework + spring-context + 5.3.14 + test + + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/src/main/java/org/wickedsource/docxstamper/DocxStamper.java b/src/main/java/org/wickedsource/docxstamper/DocxStamper.java index a0d5345e..683dca5c 100644 --- a/src/main/java/org/wickedsource/docxstamper/DocxStamper.java +++ b/src/main/java/org/wickedsource/docxstamper/DocxStamper.java @@ -7,12 +7,6 @@ import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; import org.wickedsource.docxstamper.el.ExpressionResolver; import org.wickedsource.docxstamper.processor.CommentProcessorRegistry; -import org.wickedsource.docxstamper.processor.displayif.DisplayIfProcessor; -import org.wickedsource.docxstamper.processor.displayif.IDisplayIfProcessor; -import org.wickedsource.docxstamper.processor.repeat.*; -import org.wickedsource.docxstamper.processor.replaceExpression.IReplaceWithProcessor; -import org.wickedsource.docxstamper.processor.replaceExpression.ReplaceWithProcessor; -import org.wickedsource.docxstamper.proxy.ProxyBuilder; import org.wickedsource.docxstamper.replace.PlaceholderReplacer; import org.wickedsource.docxstamper.replace.typeresolver.DateResolver; import org.wickedsource.docxstamper.replace.typeresolver.FallbackResolver; @@ -21,6 +15,8 @@ import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.Date; import java.util.Map; @@ -34,138 +30,133 @@ */ public class DocxStamper { - private PlaceholderReplacer placeholderReplacer; + private PlaceholderReplacer placeholderReplacer; - private CommentProcessorRegistry commentProcessorRegistry; + private CommentProcessorRegistry commentProcessorRegistry; - private TypeResolverRegistry typeResolverRegistry; + private TypeResolverRegistry typeResolverRegistry; - private DocxStamperConfiguration config = new DocxStamperConfiguration(); + private DocxStamperConfiguration config = new DocxStamperConfiguration(); - public DocxStamper() { - initFields(); - } - - public DocxStamper(DocxStamperConfiguration config) { - this.config = config; - initFields(); - } - - private void initFields() { - typeResolverRegistry = new TypeResolverRegistry(new FallbackResolver()); - typeResolverRegistry.registerTypeResolver(Image.class, new ImageResolver()); - typeResolverRegistry.registerTypeResolver(Date.class, new DateResolver("dd.MM.yyyy")); - for (Map.Entry, ITypeResolver> entry : config.getTypeResolvers().entrySet()) { - typeResolverRegistry.registerTypeResolver(entry.getKey(), entry.getValue()); + public DocxStamper() { + initFields(); } - ExpressionResolver expressionResolver = new ExpressionResolver(config.getEvaluationContextConfigurer()); - placeholderReplacer = new PlaceholderReplacer<>(typeResolverRegistry, config.getLineBreakPlaceholder()); - placeholderReplacer.setExpressionResolver(expressionResolver); - placeholderReplacer.setLeaveEmptyOnExpressionError(config.isLeaveEmptyOnExpressionError()); - placeholderReplacer.setReplaceNullValues(config.isReplaceNullValues()); - - commentProcessorRegistry = new CommentProcessorRegistry(placeholderReplacer); - commentProcessorRegistry.setExpressionResolver(expressionResolver); - commentProcessorRegistry.setFailOnInvalidExpression(config.isFailOnUnresolvedExpression()); - commentProcessorRegistry.registerCommentProcessor(IRepeatProcessor.class, new RepeatProcessor(typeResolverRegistry, expressionResolver)); - commentProcessorRegistry.registerCommentProcessor(IParagraphRepeatProcessor.class, new ParagraphRepeatProcessor(typeResolverRegistry)); - commentProcessorRegistry.registerCommentProcessor(IRepeatDocPartProcessor.class, new RepeatDocPartProcessor(typeResolverRegistry)); - commentProcessorRegistry.registerCommentProcessor(IDisplayIfProcessor.class, new DisplayIfProcessor()); - commentProcessorRegistry.registerCommentProcessor(IReplaceWithProcessor.class, - new ReplaceWithProcessor()); - for (Map.Entry, ICommentProcessor> entry : config.getCommentProcessors().entrySet()) { - commentProcessorRegistry.registerCommentProcessor(entry.getKey(), entry.getValue()); + public DocxStamper(DocxStamperConfiguration config) { + this.config = config; + initFields(); } - } - - /** - *

- * Reads in a .docx template and "stamps" it into the given OutputStream, using the specified context object to - * fill out any expressions it finds. - *

- *

- * In the .docx template you have the following options to influence the "stamping" process: - *

- *
    - *
  • Use expressions like ${name} or ${person.isOlderThan(18)} in the template's text. These expressions are resolved - * against the contextRoot object you pass into this method and are replaced by the results.
  • - *
  • Use comments within the .docx template to mark certain paragraphs to be manipulated.
  • - *
- *

- * Within comments, you can put expressions in which you can use the following methods by default: - *

- *
    - *
  • displayParagraphIf(boolean) to conditionally display paragraphs or not
  • - *
  • displayTableRowIf(boolean) to conditionally display table rows or not
  • - *
  • displayTableIf(boolean) to conditionally display whole tables or not
  • - *
  • repeatTableRow(List<Object>) to create a new table row for each object in the list and resolve expressions - * within the table cells against one of the objects within the list.
  • - *
- *

- * If you need a wider vocabulary of methods available in the comments, you can create your own ICommentProcessor - * and register it via getCommentProcessorRegistry().addCommentProcessor(). - *

- * - * @param template the .docx template. - * @param contextRoot the context root object against which all expressions found in the template are evaluated. - * @param out the output stream in which to write the resulting .docx document. - * @throws DocxStamperException in case of an error. - */ - public void stamp(InputStream template, T contextRoot, OutputStream out) throws DocxStamperException { - try { - WordprocessingMLPackage document = WordprocessingMLPackage.load(template); - stamp(document, contextRoot, out); - } catch (DocxStamperException e) { - throw e; - } catch (Exception e) { - throw new DocxStamperException(e); + + private void initFields() { + typeResolverRegistry = new TypeResolverRegistry(new FallbackResolver()); + typeResolverRegistry.registerTypeResolver(Image.class, new ImageResolver()); + typeResolverRegistry.registerTypeResolver(Date.class, new DateResolver("dd.MM.yyyy")); + for (Map.Entry, ITypeResolver> entry : config.getTypeResolvers().entrySet()) { + typeResolverRegistry.registerTypeResolver(entry.getKey(), entry.getValue()); + } + + ExpressionResolver expressionResolver = new ExpressionResolver(config); + placeholderReplacer = new PlaceholderReplacer(typeResolverRegistry, config); + + config.getCommentProcessorsToUse().entrySet().forEach(entry -> { + try { + Class processorImpl = entry.getValue(); + Constructor constructor = processorImpl.getDeclaredConstructor(DocxStamperConfiguration.class, TypeResolverRegistry.class); + Object processorInstance = constructor.newInstance(config, typeResolverRegistry); + config.getCommentProcessors().put(entry.getKey(), processorInstance); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + + commentProcessorRegistry = new CommentProcessorRegistry(placeholderReplacer, config); + commentProcessorRegistry.setExpressionResolver(expressionResolver); } - } - - /** - * Same as stamp(InputStream, T, OutputStream) except that you may pass in a DOCX4J document as a template instead - * of an InputStream. - * - * @param document the .docx template. - * @param contextRoot the context root object against which all expressions found in the template are evaluated. - * @param out the output stream in which to write the resulting .docx document. - * @throws DocxStamperException in case of an error. - */ - public void stamp(WordprocessingMLPackage document, T contextRoot, OutputStream out) throws DocxStamperException { - try { - ProxyBuilder proxyBuilder = addCustomInterfacesToContextRoot(contextRoot, this.config.getExpressionFunctions()); - processComments(document, proxyBuilder); - replaceExpressions(document, proxyBuilder); - document.save(out); - commentProcessorRegistry.reset(); - } catch (DocxStamperException e) { - throw e; - } catch (Exception e) { - throw new DocxStamperException(e); + + /** + *

+ * Reads in a .docx template and "stamps" it into the given OutputStream, using the specified context object to + * fill out any expressions it finds. + *

+ *

+ * In the .docx template you have the following options to influence the "stamping" process: + *

+ *
    + *
  • Use expressions like ${name} or ${person.isOlderThan(18)} in the template's text. These expressions are resolved + * against the contextRoot object you pass into this method and are replaced by the results.
  • + *
  • Use comments within the .docx template to mark certain paragraphs to be manipulated.
  • + *
+ *

+ * Within comments, you can put expressions in which you can use the following methods by default: + *

+ *
    + *
  • displayParagraphIf(boolean) to conditionally display paragraphs or not
  • + *
  • displayTableRowIf(boolean) to conditionally display table rows or not
  • + *
  • displayTableIf(boolean) to conditionally display whole tables or not
  • + *
  • repeatTableRow(List<Object>) to create a new table row for each object in the list and resolve expressions + * within the table cells against one of the objects within the list.
  • + *
+ *

+ * If you need a wider vocabulary of methods available in the comments, you can create your own ICommentProcessor + * and register it via getCommentProcessorRegistry().addCommentProcessor(). + *

+ * + * @param template the .docx template. + * @param contextRoot the context root object against which all expressions found in the template are evaluated. + * @param out the output stream in which to write the resulting .docx document. + * @throws DocxStamperException in case of an error. + */ + public void stamp(InputStream template, T contextRoot, OutputStream out) throws DocxStamperException { + try { + WordprocessingMLPackage document = WordprocessingMLPackage.load(template); + stamp(document, contextRoot, out); + } catch (DocxStamperException e) { + throw e; + } catch (Exception e) { + throw new DocxStamperException(e); + } } - } - private ProxyBuilder addCustomInterfacesToContextRoot(T contextRoot, Map, Object> interfacesToImplementations) { - ProxyBuilder proxyBuilder = new ProxyBuilder() - .withRoot(contextRoot); - if (interfacesToImplementations.isEmpty()) { - return proxyBuilder; + /** + * Same as stamp(InputStream, T, OutputStream) except that you may pass in a DOCX4J document as a template instead + * of an InputStream. + * + * @param document the .docx template. + * @param contextRoot the context root object against which all expressions found in the template are evaluated. + * @param out the output stream in which to write the resulting .docx document. + * @throws DocxStamperException in case of an error. + */ + public void stamp(WordprocessingMLPackage document, T contextRoot, OutputStream out) throws DocxStamperException { + try { + processComments(document, contextRoot); + replaceExpressions(document, contextRoot); + document.save(out); + commentProcessorRegistry.reset(); + } catch (DocxStamperException e) { + throw e; + } catch (Exception e) { + throw new DocxStamperException(e); + } } - for (Map.Entry, Object> entry : interfacesToImplementations.entrySet()) { - Class interfaceClass = entry.getKey(); - Object implementation = entry.getValue(); - proxyBuilder.withInterface(interfaceClass, implementation); + + /** + * This method allows getting comment processors instances in use to access their internal state. Useful for + * testing purposes. + * + * @param interfaceToGet ICommentProcessor interface to lookup. + * @return ICommentProcessor implementation instance or null if not used. + */ + public ICommentProcessor getCommentProcessorInstance(Class interfaceToGet) { + return (ICommentProcessor) config.getCommentProcessors().get(interfaceToGet); } - return proxyBuilder; - } - private void replaceExpressions(WordprocessingMLPackage document, ProxyBuilder proxyBuilder) { - placeholderReplacer.resolveExpressions(document, proxyBuilder); - } + private void replaceExpressions(WordprocessingMLPackage document, T contextObject) { + placeholderReplacer.resolveExpressions(document, contextObject); + } - private void processComments(final WordprocessingMLPackage document, ProxyBuilder proxyBuilder) { - commentProcessorRegistry.runProcessors(document, proxyBuilder); - } + private void processComments(final WordprocessingMLPackage document, T contextObject) { + commentProcessorRegistry.runProcessors(document, contextObject); + } } diff --git a/src/main/java/org/wickedsource/docxstamper/DocxStamperConfiguration.java b/src/main/java/org/wickedsource/docxstamper/DocxStamperConfiguration.java index 1dfe448a..e64bf651 100644 --- a/src/main/java/org/wickedsource/docxstamper/DocxStamperConfiguration.java +++ b/src/main/java/org/wickedsource/docxstamper/DocxStamperConfiguration.java @@ -1,177 +1,278 @@ package org.wickedsource.docxstamper; -import java.util.HashMap; -import java.util.Map; - import org.wickedsource.docxstamper.api.EvaluationContextConfigurer; -import org.wickedsource.docxstamper.api.commentprocessor.ICommentProcessor; import org.wickedsource.docxstamper.api.typeresolver.ITypeResolver; import org.wickedsource.docxstamper.el.NoOpEvaluationContextConfigurer; +import org.wickedsource.docxstamper.processor.displayif.DisplayIfProcessor; +import org.wickedsource.docxstamper.processor.displayif.IDisplayIfProcessor; +import org.wickedsource.docxstamper.processor.repeat.*; +import org.wickedsource.docxstamper.processor.replaceExpression.IReplaceWithProcessor; +import org.wickedsource.docxstamper.processor.replaceExpression.ReplaceWithProcessor; +import org.wickedsource.docxstamper.processor.table.ITableResolver; +import org.wickedsource.docxstamper.processor.table.TableResolver; import org.wickedsource.docxstamper.replace.typeresolver.FallbackResolver; +import java.util.HashMap; +import java.util.Map; + /** * Provides configuration parameters for DocxStamper. */ public class DocxStamperConfiguration { - private String lineBreakPlaceholder; - - private EvaluationContextConfigurer evaluationContextConfigurer = new NoOpEvaluationContextConfigurer(); - - private boolean failOnUnresolvedExpression = true; - - private boolean leaveEmptyOnExpressionError = false; - - private boolean replaceNullValues = false; - - private Map, ICommentProcessor> commentProcessors = new HashMap<>(); - - private Map, ITypeResolver> typeResolvers = new HashMap<>(); - - private ITypeResolver defaultTypeResolver = new FallbackResolver(); - - private Map, Object> expressionFunctions = new HashMap<>(); - - /** - * The String provided as lineBreakPlaceholder will be replaces with a line break - * when stamping a document. If no lineBreakPlaceholder is provided, no replacement - * will take place. - * - * @param lineBreakPlaceholder the String that should be replaced with line breaks during stamping. - * @return the configuration object for chaining. - */ - public DocxStamperConfiguration setLineBreakPlaceholder(String lineBreakPlaceholder) { - this.lineBreakPlaceholder = lineBreakPlaceholder; - return this; - } - - /** - * Provides an {@link EvaluationContextConfigurer} which may change the configuration of a Spring - * {@link org.springframework.expression.EvaluationContext} which is used for evaluating expressions - * in comments and text. - * @param evaluationContextConfigurer the configurer to use. - */ - public DocxStamperConfiguration setEvaluationContextConfigurer(EvaluationContextConfigurer evaluationContextConfigurer) { - this.evaluationContextConfigurer = evaluationContextConfigurer; - return this; - } - - /** - * If set to true, stamper will throw an {@link org.wickedsource.docxstamper.api.UnresolvedExpressionException} - * if a variable expression or processor expression within the document or within the comments is encountered that cannot be resolved. Is set to true by default. - */ - public DocxStamperConfiguration setFailOnUnresolvedExpression(boolean failOnUnresolvedExpression) { - this.failOnUnresolvedExpression = failOnUnresolvedExpression; - return this; - } - - /** - * Registers the specified ICommentProcessor as an implementation of the - * specified interface. - * - * @param interfaceClass the Interface which is implemented by the commentProcessor. - * @param commentProcessor the commentProcessor implementing the specified interface. - */ - public DocxStamperConfiguration addCommentProcessor(Class interfaceClass, - ICommentProcessor commentProcessor) { - this.commentProcessors.put(interfaceClass, commentProcessor); - return this; - } - - /** - *

- * Registers the given ITypeResolver for the given class. The registered ITypeResolver's resolve() method will only - * be called with objects of the specified class. - *

- *

- * Note that each type can only be resolved by ONE ITypeResolver implementation. Multiple calls to addTypeResolver() - * with the same resolvedType parameter will override earlier calls. - *

- * - * @param resolvedType the class whose objects are to be passed to the given ITypeResolver. - * @param resolver the resolver to resolve objects of the given type. - * @param the type resolved by the ITypeResolver. - */ - public DocxStamperConfiguration addTypeResolver(Class resolvedType, ITypeResolver resolver) { - this.typeResolvers.put(resolvedType, resolver); - return this; - } - - /** - * Exposes all methods of a given interface to the expression language. - * @param interfaceClass the interface whose methods should be exposed in the expression language. - * @param implementation the implementation that should be called to evaluate invocations of the interface methods - * within the expression language. Must implement the interface above. - */ - public DocxStamperConfiguration exposeInterfaceToExpressionLanguage(Class interfaceClass, Object implementation) { - this.expressionFunctions.put(interfaceClass, implementation); - return this; - } - - /** - * If an error is caught while evaluating an expression the expression will be replaced with an empty string instead - * of leaving the original expression in the document. - * @param leaveEmpty true to replace expressions with empty string when an error is caught while evaluating - */ - public DocxStamperConfiguration leaveEmptyOnExpressionError(boolean leaveEmpty) { - this.leaveEmptyOnExpressionError = leaveEmpty; - return this; - } - - /** - * Indicates if expressions that resolve to null should be processed. - * @param replaceNullValues true to replace null value expression with resolved value (which is null), false to leave the expression as is - */ - public DocxStamperConfiguration replaceNullValues(boolean replaceNullValues) { - this.replaceNullValues = replaceNullValues; - return this; - } - - /** - * Creates a {@link DocxStamper} instance configured with this configuration. - */ - public DocxStamper build() { - return new DocxStamper(this); - } - - EvaluationContextConfigurer getEvaluationContextConfigurer() { - return evaluationContextConfigurer; - } - - boolean isFailOnUnresolvedExpression() { - return failOnUnresolvedExpression; - } - - Map, ICommentProcessor> getCommentProcessors() { - return commentProcessors; - } - - Map, ITypeResolver> getTypeResolvers() { - return typeResolvers; - } - - ITypeResolver getDefaultTypeResolver() { - return defaultTypeResolver; - } - - public DocxStamperConfiguration setDefaultTypeResolver(ITypeResolver defaultTypeResolver) { - this.defaultTypeResolver = defaultTypeResolver; - return this; - } - - public boolean isLeaveEmptyOnExpressionError() { - return leaveEmptyOnExpressionError; - } - - public boolean isReplaceNullValues() { - return replaceNullValues; - } - - String getLineBreakPlaceholder() { - return lineBreakPlaceholder; - } - - public Map, Object> getExpressionFunctions() { - return expressionFunctions; - } + private String lineBreakPlaceholder; + + private EvaluationContextConfigurer evaluationContextConfigurer = new NoOpEvaluationContextConfigurer(); + + private boolean failOnUnresolvedExpression = true; + + private boolean leaveEmptyOnExpressionError = false; + + private boolean replaceUnresolvedExpressions = false; + + private String unresolvedExpressionsDefaultValue = null; + + private boolean replaceNullValues = false; + + private String nullValuesDefault = null; + + private final Map, Class> commentProcessorsToUse = new HashMap<>(); + + private final Map, Object> commentProcessors = new HashMap<>(); + + private final Map, ITypeResolver> typeResolvers = new HashMap<>(); + + private ITypeResolver defaultTypeResolver = new FallbackResolver(); + + private final Map, Object> expressionFunctions = new HashMap<>(); + + public DocxStamperConfiguration() { + commentProcessorsToUse.put(IRepeatProcessor.class, RepeatProcessor.class); + commentProcessorsToUse.put(IParagraphRepeatProcessor.class, ParagraphRepeatProcessor.class); + commentProcessorsToUse.put(IRepeatDocPartProcessor.class, RepeatDocPartProcessor.class); + commentProcessorsToUse.put(ITableResolver.class, TableResolver.class); + commentProcessorsToUse.put(IDisplayIfProcessor.class, DisplayIfProcessor.class); + commentProcessorsToUse.put(IReplaceWithProcessor.class, ReplaceWithProcessor.class); + } + + /** + * Copy operator for the whole DocxStamperConfiguration, including creating self comment processors instances + * to avoid unexpected resets, since comment processors are stateful their instances cannot be shared over + * multiple stampings. + * + * @return copied DocxStamperConfiguration. + */ + public DocxStamperConfiguration copy() { + DocxStamperConfiguration newConfig = new DocxStamperConfiguration() + .setLineBreakPlaceholder(lineBreakPlaceholder) + .setEvaluationContextConfigurer(evaluationContextConfigurer) + .setFailOnUnresolvedExpression(failOnUnresolvedExpression) + .leaveEmptyOnExpressionError(leaveEmptyOnExpressionError) + .replaceUnresolvedExpressions(replaceUnresolvedExpressions) + .unresolvedExpressionsDefaultValue(unresolvedExpressionsDefaultValue) + .replaceNullValues(replaceNullValues) + .nullValuesDefault(nullValuesDefault) + .setDefaultTypeResolver(defaultTypeResolver); + + typeResolvers.entrySet().forEach(entry -> newConfig.addTypeResolver(entry.getKey(), entry.getValue())); + expressionFunctions.entrySet().forEach(entry -> newConfig.exposeInterfaceToExpressionLanguage(entry.getKey(), entry.getValue())); + commentProcessorsToUse.entrySet().forEach(entry -> newConfig.addCommentProcessor(entry.getKey(), entry.getValue())); + + return newConfig; + } + + /** + * The String provided as lineBreakPlaceholder will be replaces with a line break + * when stamping a document. If no lineBreakPlaceholder is provided, no replacement + * will take place. + * + * @param lineBreakPlaceholder the String that should be replaced with line breaks during stamping. + * @return the configuration object for chaining. + */ + public DocxStamperConfiguration setLineBreakPlaceholder(String lineBreakPlaceholder) { + this.lineBreakPlaceholder = lineBreakPlaceholder; + return this; + } + + /** + * Provides an {@link EvaluationContextConfigurer} which may change the configuration of a Spring + * {@link org.springframework.expression.EvaluationContext} which is used for evaluating expressions + * in comments and text. + * + * @param evaluationContextConfigurer the configurer to use. + */ + public DocxStamperConfiguration setEvaluationContextConfigurer(EvaluationContextConfigurer evaluationContextConfigurer) { + this.evaluationContextConfigurer = evaluationContextConfigurer; + return this; + } + + /** + * If set to true, stamper will throw an {@link org.wickedsource.docxstamper.api.UnresolvedExpressionException} + * if a variable expression or processor expression within the document or within the comments is encountered that cannot be resolved. Is set to true by default. + */ + public DocxStamperConfiguration setFailOnUnresolvedExpression(boolean failOnUnresolvedExpression) { + this.failOnUnresolvedExpression = failOnUnresolvedExpression; + return this; + } + + /** + * Registers the specified ICommentProcessor as an implementation of the + * specified interface. + * + * @param interfaceClass the Interface which is implemented by the commentProcessor. + * @param commentProcessorImplClass the commentProcessor class implementing the specified interface. + */ + public DocxStamperConfiguration addCommentProcessor(Class interfaceClass, + Class commentProcessorImplClass) { + this.commentProcessorsToUse.put(interfaceClass, commentProcessorImplClass); + return this; + } + + /** + *

+ * Registers the given ITypeResolver for the given class. The registered ITypeResolver's resolve() method will only + * be called with objects of the specified class. + *

+ *

+ * Note that each type can only be resolved by ONE ITypeResolver implementation. Multiple calls to addTypeResolver() + * with the same resolvedType parameter will override earlier calls. + *

+ * + * @param resolvedType the class whose objects are to be passed to the given ITypeResolver. + * @param resolver the resolver to resolve objects of the given type. + * @param the type resolved by the ITypeResolver. + */ + public DocxStamperConfiguration addTypeResolver(Class resolvedType, ITypeResolver resolver) { + this.typeResolvers.put(resolvedType, resolver); + return this; + } + + /** + * Exposes all methods of a given interface to the expression language. + * + * @param interfaceClass the interface whose methods should be exposed in the expression language. + * @param implementation the implementation that should be called to evaluate invocations of the interface methods + * within the expression language. Must implement the interface above. + */ + public DocxStamperConfiguration exposeInterfaceToExpressionLanguage(Class interfaceClass, Object implementation) { + this.expressionFunctions.put(interfaceClass, implementation); + return this; + } + + /** + * If an error is caught while evaluating an expression the expression will be replaced with an empty string instead + * of leaving the original expression in the document. + * + * @param leaveEmpty true to replace expressions with empty string when an error is caught while evaluating + */ + public DocxStamperConfiguration leaveEmptyOnExpressionError(boolean leaveEmpty) { + this.leaveEmptyOnExpressionError = leaveEmpty; + return this; + } + + /** + * Indicates if expressions that resolve to null should be processed. + * + * @param replaceNullValues true to replace null value expression with resolved value (which is null), false to leave the expression as is + */ + public DocxStamperConfiguration replaceNullValues(boolean replaceNullValues) { + this.replaceNullValues = replaceNullValues; + return this; + } + + /** + * Indicates if expressions that resolve to null should be replaced by a global default value. + * + * @param nullValuesDefault value to use instead for expression resolving to null + * @see DocxStamperConfiguration#replaceNullValues + */ + public DocxStamperConfiguration nullValuesDefault(String nullValuesDefault) { + this.nullValuesDefault = nullValuesDefault; + return this; + } + + /** + * Indicates if expressions that doesn't resolve should be replaced by a default value. + * + * @param replaceUnresolvedExpressions true to replace null value expression with resolved value (which is null), false to leave the expression as is + */ + public DocxStamperConfiguration replaceUnresolvedExpressions(boolean replaceUnresolvedExpressions) { + this.replaceUnresolvedExpressions = replaceUnresolvedExpressions; + return this; + } + + /** + * Indicates the default value to use for expressions that doesn't resolve. + * + * @param unresolvedExpressionsDefaultValue value to use instead for expression that doesn't resolve + * @see DocxStamperConfiguration#replaceUnresolvedExpressions + */ + public DocxStamperConfiguration unresolvedExpressionsDefaultValue(String unresolvedExpressionsDefaultValue) { + this.unresolvedExpressionsDefaultValue = unresolvedExpressionsDefaultValue; + return this; + } + + /** + * Creates a {@link DocxStamper} instance configured with this configuration. + */ + public DocxStamper build() { + return new DocxStamper(this); + } + + public EvaluationContextConfigurer getEvaluationContextConfigurer() { + return evaluationContextConfigurer; + } + + public boolean isFailOnUnresolvedExpression() { + return failOnUnresolvedExpression; + } + + public Map, Object> getCommentProcessors() { + return commentProcessors; + } + + public Map, Class> getCommentProcessorsToUse() { + return commentProcessorsToUse; + } + + Map, ITypeResolver> getTypeResolvers() { + return typeResolvers; + } + + ITypeResolver getDefaultTypeResolver() { + return defaultTypeResolver; + } + + public DocxStamperConfiguration setDefaultTypeResolver(ITypeResolver defaultTypeResolver) { + this.defaultTypeResolver = defaultTypeResolver; + return this; + } + + public boolean isLeaveEmptyOnExpressionError() { + return leaveEmptyOnExpressionError; + } + + public boolean isReplaceNullValues() { + return replaceNullValues; + } + + public String getNullValuesDefault() { + return nullValuesDefault; + } + + public boolean isReplaceUnresolvedExpressions() { + return replaceUnresolvedExpressions; + } + + public String getUnresolvedExpressionsDefaultValue() { + return unresolvedExpressionsDefaultValue; + } + + public String getLineBreakPlaceholder() { + return lineBreakPlaceholder; + } + + public Map, Object> getExpressionFunctions() { + return expressionFunctions; + } } diff --git a/src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java b/src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java index 3ac338e8..6d3c3882 100644 --- a/src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java @@ -75,13 +75,20 @@ public interface ICommentProcessor { * This method is always called BEFORE the custom methods of the custom comment * processor interface are called. * - * @param commentWrapper of the currently processed comment within the template. + * @param commentWrapper of the currently processed comment within the template. */ void setCurrentCommentWrapper(CommentWrapper commentWrapper); + /** + * Passes the processed document, in order to make all linked data (images, etc) available + * to processors that need it (example : repeatDocPart) + * + * @param document DocX template being processed. + */ + void setDocument(WordprocessingMLPackage document); + /** * Resets all state in the comment processor so that it can be re-used in another stamping process. */ void reset(); - } diff --git a/src/main/java/org/wickedsource/docxstamper/el/ExpressionResolver.java b/src/main/java/org/wickedsource/docxstamper/el/ExpressionResolver.java index 4749a9a5..729960f0 100644 --- a/src/main/java/org/wickedsource/docxstamper/el/ExpressionResolver.java +++ b/src/main/java/org/wickedsource/docxstamper/el/ExpressionResolver.java @@ -4,20 +4,15 @@ import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.wickedsource.docxstamper.api.EvaluationContextConfigurer; +import org.wickedsource.docxstamper.DocxStamperConfiguration; public class ExpressionResolver { private static final ExpressionUtil expressionUtil = new ExpressionUtil(); + private final DocxStamperConfiguration configuration; - private final EvaluationContextConfigurer evaluationContextConfigurer; - - public ExpressionResolver() { - this.evaluationContextConfigurer = new NoOpEvaluationContextConfigurer(); - } - - public ExpressionResolver(EvaluationContextConfigurer evaluationContextConfigurer) { - this.evaluationContextConfigurer = evaluationContextConfigurer; + public ExpressionResolver(DocxStamperConfiguration configuration) { + this.configuration = configuration; } /** @@ -33,7 +28,8 @@ public Object resolveExpression(String expressionString, Object contextRoot) { } ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext evaluationContext = new StandardEvaluationContext(contextRoot); - evaluationContextConfigurer.configureEvaluationContext(evaluationContext); + evaluationContext.addMethodResolver(new StandardMethodResolver(configuration)); + configuration.getEvaluationContextConfigurer().configureEvaluationContext(evaluationContext); Expression expression = parser.parseExpression(expressionString); return expression.getValue(evaluationContext); } diff --git a/src/main/java/org/wickedsource/docxstamper/el/StandardMethodExecutor.java b/src/main/java/org/wickedsource/docxstamper/el/StandardMethodExecutor.java new file mode 100644 index 00000000..3399afc8 --- /dev/null +++ b/src/main/java/org/wickedsource/docxstamper/el/StandardMethodExecutor.java @@ -0,0 +1,35 @@ +package org.wickedsource.docxstamper.el; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.TypedValue; +import org.wickedsource.docxstamper.DocxStamperConfiguration; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class StandardMethodExecutor implements MethodExecutor { + private final DocxStamperConfiguration configuration; + private final Method method; + private final Object implementation; + + public StandardMethodExecutor(DocxStamperConfiguration configuration, Method method, Object implementation) { + this.configuration = configuration; + this.method = method; + this.implementation = implementation; + } + + @Override + public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { + try { + return new TypedValue(method.invoke(implementation, arguments)); + } catch (InvocationTargetException | IllegalAccessException e) { + if (configuration.isFailOnUnresolvedExpression()) { + throw new AccessException(String.format("Error calling method %s", method.getName()), e); + } else { + return new TypedValue(null); + } + } + } +} diff --git a/src/main/java/org/wickedsource/docxstamper/el/StandardMethodResolver.java b/src/main/java/org/wickedsource/docxstamper/el/StandardMethodResolver.java new file mode 100644 index 00000000..d0223ecb --- /dev/null +++ b/src/main/java/org/wickedsource/docxstamper/el/StandardMethodResolver.java @@ -0,0 +1,72 @@ +package org.wickedsource.docxstamper.el; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.wickedsource.docxstamper.DocxStamperConfiguration; + +import java.lang.reflect.Method; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; + +public class StandardMethodResolver implements MethodResolver { + private final DocxStamperConfiguration configuration; + + public StandardMethodResolver(DocxStamperConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List argumentTypes) throws AccessException { + Map.Entry methodEntry = findCommentProcessorMethod(name, argumentTypes); + + if (methodEntry == null) { + methodEntry = findExpressionContextMethod(name, argumentTypes); + } + + if (methodEntry == null) return null; + + return new StandardMethodExecutor(configuration, methodEntry.getKey(), methodEntry.getValue()); + } + + private Map.Entry findCommentProcessorMethod(String expectedName, List expectedArguments) { + return findMethodInMap(configuration.getCommentProcessors(), expectedName, expectedArguments); + } + + private Map.Entry findExpressionContextMethod(String expectedName, List expectedArguments) { + return findMethodInMap(configuration.getExpressionFunctions(), expectedName, expectedArguments); + } + + private Map.Entry findMethodInMap(Map, Object> methodMap, String expectedName, List expectedArguments) { + for (Map.Entry, Object> entry : methodMap.entrySet()) { + Class iface = entry.getKey(); + + for (Method actualMethod : iface.getDeclaredMethods()) { + if (methodEquals(actualMethod, expectedName, expectedArguments)) { + return new AbstractMap.SimpleEntry<>(actualMethod, entry.getValue()); + } + } + } + + return null; + } + + private boolean methodEquals(Method actualMethod, String expectedName, List expectedArguments) { + if (!actualMethod.getName().equals(expectedName)) return false; + if (actualMethod.getParameterTypes().length != expectedArguments.size()) return false; + + for (int i = 0; i < expectedArguments.size(); i++) { + Class expectedType = expectedArguments.get(i) != null ? expectedArguments.get(i).getType() : null; + Class actualType = actualMethod.getParameterTypes()[i]; + // null is allowed in place of any type of argument + if (expectedType != null && !actualType.isAssignableFrom(expectedType)) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/org/wickedsource/docxstamper/processor/BaseCommentProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/BaseCommentProcessor.java index b209d798..0c74b3d9 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/BaseCommentProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/BaseCommentProcessor.java @@ -1,46 +1,70 @@ package org.wickedsource.docxstamper.processor; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.wickedsource.docxstamper.DocxStamperConfiguration; import org.wickedsource.docxstamper.api.commentprocessor.ICommentProcessor; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.coordinates.RunCoordinates; +import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; +import org.wickedsource.docxstamper.replace.PlaceholderReplacer; import org.wickedsource.docxstamper.util.CommentWrapper; import java.util.Objects; public abstract class BaseCommentProcessor implements ICommentProcessor { + protected final TypeResolverRegistry typeResolverRegistry; - private ParagraphCoordinates currentParagraphCoordinates; + protected final DocxStamperConfiguration configuration; + protected final PlaceholderReplacer placeholderReplacer; + private ParagraphCoordinates currentParagraphCoordinates; - private RunCoordinates currentRunCoordinates; + private RunCoordinates currentRunCoordinates; - private CommentWrapper currentCommentWrapper; + private CommentWrapper currentCommentWrapper; - public RunCoordinates getCurrentRunCoordinates() { - return currentRunCoordinates; - } + private WordprocessingMLPackage document; - @Override - public void setCurrentRunCoordinates(RunCoordinates currentRunCoordinates) { - this.currentRunCoordinates = currentRunCoordinates; - } + public BaseCommentProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + this.configuration = config; + this.typeResolverRegistry = typeResolverRegistry; + this.placeholderReplacer = new PlaceholderReplacer(typeResolverRegistry, configuration); + } - @Override - public void setCurrentParagraphCoordinates(ParagraphCoordinates coordinates) { - this.currentParagraphCoordinates = coordinates; - } + public RunCoordinates getCurrentRunCoordinates() { + return currentRunCoordinates; + } - public ParagraphCoordinates getCurrentParagraphCoordinates() { - return currentParagraphCoordinates; - } + @Override + public void setCurrentRunCoordinates(RunCoordinates currentRunCoordinates) { + this.currentRunCoordinates = currentRunCoordinates; + } - @Override - public void setCurrentCommentWrapper(CommentWrapper currentCommentWrapper) { - Objects.requireNonNull(currentCommentWrapper.getCommentRangeStart()); - Objects.requireNonNull(currentCommentWrapper.getCommentRangeEnd()); - this.currentCommentWrapper = currentCommentWrapper; - } + @Override + public void setCurrentParagraphCoordinates(ParagraphCoordinates coordinates) { + this.currentParagraphCoordinates = coordinates; + } - public CommentWrapper getCurrentCommentWrapper() { - return currentCommentWrapper; - } + public ParagraphCoordinates getCurrentParagraphCoordinates() { + return currentParagraphCoordinates; + } + + @Override + public void setCurrentCommentWrapper(CommentWrapper currentCommentWrapper) { + Objects.requireNonNull(currentCommentWrapper.getCommentRangeStart()); + Objects.requireNonNull(currentCommentWrapper.getCommentRangeEnd()); + this.currentCommentWrapper = currentCommentWrapper; + } + + public CommentWrapper getCurrentCommentWrapper() { + return currentCommentWrapper; + } + + @Override + public void setDocument(WordprocessingMLPackage document) { + this.document = document; + } + + public WordprocessingMLPackage getDocument() { + return document; + } } diff --git a/src/main/java/org/wickedsource/docxstamper/processor/CommentProcessorRegistry.java b/src/main/java/org/wickedsource/docxstamper/processor/CommentProcessorRegistry.java index 201b3c4e..83b84a30 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/CommentProcessorRegistry.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/CommentProcessorRegistry.java @@ -6,15 +6,13 @@ import org.slf4j.LoggerFactory; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelParseException; -import org.wickedsource.docxstamper.api.DocxStamperException; +import org.wickedsource.docxstamper.DocxStamperConfiguration; import org.wickedsource.docxstamper.api.UnresolvedExpressionException; import org.wickedsource.docxstamper.api.commentprocessor.ICommentProcessor; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.coordinates.RunCoordinates; import org.wickedsource.docxstamper.el.ExpressionResolver; import org.wickedsource.docxstamper.el.ExpressionUtil; -import org.wickedsource.docxstamper.proxy.ProxyBuilder; -import org.wickedsource.docxstamper.proxy.ProxyException; import org.wickedsource.docxstamper.replace.PlaceholderReplacer; import org.wickedsource.docxstamper.util.CommentUtil; import org.wickedsource.docxstamper.util.CommentWrapper; @@ -23,12 +21,7 @@ import org.wickedsource.docxstamper.util.walk.CoordinatesWalker; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; /** * Allows registration of ICommentProcessor objects. Each registered @@ -38,207 +31,179 @@ */ public class CommentProcessorRegistry { - private Logger logger = LoggerFactory.getLogger(CommentProcessorRegistry.class); + private final Logger logger = LoggerFactory.getLogger(CommentProcessorRegistry.class); + private final DocxStamperConfiguration configuration; - private Map> commentProcessorInterfaces = new HashMap<>(); + private ExpressionResolver expressionResolver; - private List commentProcessors = new ArrayList<>(); + private final ExpressionUtil expressionUtil = new ExpressionUtil(); - private ExpressionResolver expressionResolver = new ExpressionResolver(); + private final PlaceholderReplacer placeholderReplacer; - private ExpressionUtil expressionUtil = new ExpressionUtil(); - - private PlaceholderReplacer placeholderReplacer; + public CommentProcessorRegistry(PlaceholderReplacer placeholderReplacer, DocxStamperConfiguration configuration) { + this.placeholderReplacer = placeholderReplacer; + this.configuration = configuration; + this.expressionResolver = new ExpressionResolver(configuration); + } - private boolean failOnInvalidExpression = true; + public void setExpressionResolver(ExpressionResolver expressionResolver) { + this.expressionResolver = expressionResolver; + } - public CommentProcessorRegistry(PlaceholderReplacer placeholderReplacer) { - this.placeholderReplacer = placeholderReplacer; - } + /** + * Lets each registered ICommentProcessor have a run on the specified docx + * document. At the end of the document the commit method is called for each + * ICommentProcessor. The ICommentProcessors are run in the order they were + * registered. + * + * @param document the docx document over which to run the registered ICommentProcessors. + * @param expressionContext the context root object + */ + public void runProcessors(final WordprocessingMLPackage document, final T expressionContext) { + final Map comments = CommentUtil.getComments(document); + final List proceedComments = new ArrayList<>(); + + CoordinatesWalker walker = new BaseCoordinatesWalker(document) { + + @Override + protected void onParagraph(ParagraphCoordinates paragraphCoordinates) { + runProcessorsOnParagraphComment(document, comments, expressionContext, paragraphCoordinates) + .ifPresent(proceedComments::add); + runProcessorsOnInlineContent(expressionContext, paragraphCoordinates); + } + + @Override + protected void onRun(RunCoordinates runCoordinates, + ParagraphCoordinates paragraphCoordinates) { + runProcessorsOnRunComment(document, comments, expressionContext, paragraphCoordinates, runCoordinates) + .ifPresent(proceedComments::add); + } + + }; + walker.walk(); + + for (Object processor : configuration.getCommentProcessors().values()) { + ((ICommentProcessor) processor).commitChanges(document); + } + for (CommentWrapper commentWrapper : proceedComments) { + CommentUtil.deleteComment(commentWrapper); + } - public void setExpressionResolver(ExpressionResolver expressionResolver) { - this.expressionResolver = expressionResolver; - } + } - public void registerCommentProcessor(Class interfaceClass, - ICommentProcessor commentProcessor) { - this.commentProcessorInterfaces.put(commentProcessor, interfaceClass); - this.commentProcessors.add(commentProcessor); - } + /** + * Finds all processor expressions within the specified paragraph and tries + * to evaluate it against all registered {@link ICommentProcessor}s. + * + * @param expressionContext a builder for a proxy around the context root object to customize its interface + * @param paragraphCoordinates the paragraph to process. + * @param type of the context root object + */ + private void runProcessorsOnInlineContent(T expressionContext, + ParagraphCoordinates paragraphCoordinates) { + + ParagraphWrapper paragraph = new ParagraphWrapper(paragraphCoordinates.getParagraph()); + List processorExpressions = expressionUtil + .findProcessorExpressions(paragraph.getText()); + + for (String processorExpression : processorExpressions) { + String strippedExpression = expressionUtil.stripExpression(processorExpression); + + for (final Object processor : configuration.getCommentProcessors().values()) { + ((ICommentProcessor) processor).setCurrentParagraphCoordinates(paragraphCoordinates); + } + + try { + expressionResolver.resolveExpression(strippedExpression, expressionContext); + placeholderReplacer.replace(paragraph, processorExpression, null); + logger.debug(String.format( + "Processor expression '%s' has been successfully processed by a comment processor.", + processorExpression)); + } catch (SpelEvaluationException | SpelParseException e) { + if (configuration.isFailOnUnresolvedExpression()) { + throw new UnresolvedExpressionException(strippedExpression, e); + } else { + logger.warn(String.format( + "Skipping processor expression '%s' because it can not be resolved by any comment processor. Reason: %s. Set log level to TRACE to view Stacktrace.", + processorExpression, e.getMessage())); + logger.trace("Reason for skipping processor expression: ", e); + } + } + } + } - /** - * Lets each registered ICommentProcessor have a run on the specified docx - * document. At the end of the document the commit method is called for each - * ICommentProcessor. The ICommentProcessors are run in the order they were - * registered. - * - * @param document the docx document over which to run the registered ICommentProcessors. - * @param proxyBuilder a builder for a proxy around the context root object to customize its interface - * @param type of the contextRoot object. - */ - public void runProcessors(final WordprocessingMLPackage document, final ProxyBuilder proxyBuilder) { - final Map comments = CommentUtil.getComments(document); - final List proceedComments = new ArrayList<>(); - CoordinatesWalker walker = new BaseCoordinatesWalker(document) { + /** + * Takes the first comment on the specified paragraph and tries to evaluate + * the string within the comment against all registered + * {@link ICommentProcessor}s. + * + * @param document the word document. + * @param comments the comments within the document. + * @param expressionContext the context root object + * @param paragraphCoordinates the paragraph whose comments to evaluate. + * @param the type of the context root object. + */ + private Optional runProcessorsOnParagraphComment(final WordprocessingMLPackage document, + final Map comments, T expressionContext, + ParagraphCoordinates paragraphCoordinates) { + Comments.Comment comment = CommentUtil.getCommentFor(paragraphCoordinates.getParagraph(), document); + return runCommentProcessors(comments, expressionContext, comment, paragraphCoordinates, null, document); + } - @Override - protected void onParagraph(ParagraphCoordinates paragraphCoordinates) { - runProcessorsOnParagraphComment(document, comments, proxyBuilder, paragraphCoordinates) - .ifPresent(proceedComments::add); - runProcessorsOnInlineContent(proxyBuilder, paragraphCoordinates); - } + private Optional runProcessorsOnRunComment(final WordprocessingMLPackage document, + final Map comments, T expressionContext, + ParagraphCoordinates paragraphCoordinates, RunCoordinates runCoordinates) { + Comments.Comment comment = CommentUtil.getCommentAround(runCoordinates.getRun(), document); + return runCommentProcessors(comments, expressionContext, comment, paragraphCoordinates, runCoordinates, document); + } - @Override - protected void onRun(RunCoordinates runCoordinates, - ParagraphCoordinates paragraphCoordinates) { - runProcessorsOnRunComment(document, comments, proxyBuilder, paragraphCoordinates, runCoordinates) - .ifPresent(proceedComments::add); - } + private Optional runCommentProcessors(final Map comments, T expressionContext, + Comments.Comment comment, ParagraphCoordinates paragraphCoordinates, + RunCoordinates runCoordinates, WordprocessingMLPackage document) { - }; - walker.walk(); + CommentWrapper commentWrapper = Optional.ofNullable(comment) + .map(Comments.Comment::getId) + .map(comments::get) + .orElse(null); - for (ICommentProcessor processor : commentProcessors) { - processor.commitChanges(document); - } - for (CommentWrapper commentWrapper : proceedComments) { - CommentUtil.deleteComment(commentWrapper); - } - - } - - /** - * Finds all processor expressions within the specified paragraph and tries - * to evaluate it against all registered {@link ICommentProcessor}s. - * - * @param proxyBuilder a builder for a proxy around the context root object to customize its interface - * @param paragraphCoordinates the paragraph to process. - * @param type of the context root object - */ - private void runProcessorsOnInlineContent(ProxyBuilder proxyBuilder, - ParagraphCoordinates paragraphCoordinates) { - - ParagraphWrapper paragraph = new ParagraphWrapper(paragraphCoordinates.getParagraph()); - List processorExpressions = expressionUtil - .findProcessorExpressions(paragraph.getText()); - - for (String processorExpression : processorExpressions) { - String strippedExpression = expressionUtil.stripExpression(processorExpression); - - for (final ICommentProcessor processor : commentProcessors) { - Class commentProcessorInterface = commentProcessorInterfaces.get(processor); - proxyBuilder.withInterface(commentProcessorInterface, processor); - processor.setCurrentParagraphCoordinates(paragraphCoordinates); - } - - try { - T contextRootProxy = proxyBuilder.build(); - expressionResolver.resolveExpression(strippedExpression, contextRootProxy); - placeholderReplacer.replace(paragraph, processorExpression, null); - logger.debug(String.format( - "Processor expression '%s' has been successfully processed by a comment processor.", - processorExpression)); - } catch (SpelEvaluationException | SpelParseException e) { - if (failOnInvalidExpression) { - throw new UnresolvedExpressionException(strippedExpression, e); - } else { - logger.warn(String.format( - "Skipping processor expression '%s' because it can not be resolved by any comment processor. Reason: %s. Set log level to TRACE to view Stacktrace.", - processorExpression, e.getMessage())); - logger.trace("Reason for skipping processor expression: ", e); + if (Objects.isNull(comment) || Objects.isNull(commentWrapper)) { + // no comment to process + return Optional.empty(); } - } catch (ProxyException e) { - throw new DocxStamperException("Could not create a proxy around context root object", e); - } - } - } - - - /** - * Takes the first comment on the specified paragraph and tries to evaluate - * the string within the comment against all registered - * {@link ICommentProcessor}s. - * - * @param document the word document. - * @param comments the comments within the document. - * @param proxyBuilder a builder for a proxy around the context root object to customize its interface - * @param paragraphCoordinates the paragraph whose comments to evaluate. - * @param the type of the context root object. - */ - private Optional runProcessorsOnParagraphComment(final WordprocessingMLPackage document, - final Map comments, ProxyBuilder proxyBuilder, - ParagraphCoordinates paragraphCoordinates) { - Comments.Comment comment = CommentUtil.getCommentFor(paragraphCoordinates.getParagraph(), document); - return runCommentProcessors(document, comments, proxyBuilder, comment, paragraphCoordinates, null); - } - - private Optional runProcessorsOnRunComment(final WordprocessingMLPackage document, - final Map comments, ProxyBuilder proxyBuilder, - ParagraphCoordinates paragraphCoordinates, RunCoordinates runCoordinates) { - Comments.Comment comment = CommentUtil.getCommentAround(runCoordinates.getRun(), document); - return runCommentProcessors(document, comments, proxyBuilder, comment, paragraphCoordinates, runCoordinates); - } - - private Optional runCommentProcessors(final WordprocessingMLPackage document, - final Map comments, ProxyBuilder proxyBuilder, - Comments.Comment comment, ParagraphCoordinates paragraphCoordinates, - RunCoordinates runCoordinates) { - - CommentWrapper commentWrapper = Optional.ofNullable(comment) - .map(Comments.Comment::getId) - .map(comments::get) - .orElse(null); - - if (Objects.isNull(comment) || Objects.isNull(commentWrapper)) { - // no comment to process - return Optional.empty(); - } - String commentString = CommentUtil.getCommentString(comment); + String commentString = CommentUtil.getCommentString(comment); - for (final ICommentProcessor processor : commentProcessors) { - Class commentProcessorInterface = commentProcessorInterfaces.get(processor); - proxyBuilder.withInterface(commentProcessorInterface, processor); - processor.setCurrentParagraphCoordinates(paragraphCoordinates); - processor.setCurrentRunCoordinates(runCoordinates); - processor.setCurrentCommentWrapper(commentWrapper); - } + for (final Object processor : configuration.getCommentProcessors().values()) { + ((ICommentProcessor) processor).setCurrentParagraphCoordinates(paragraphCoordinates); + ((ICommentProcessor) processor).setCurrentRunCoordinates(runCoordinates); + ((ICommentProcessor) processor).setCurrentCommentWrapper(commentWrapper); + ((ICommentProcessor) processor).setDocument(document); + } - try { - T contextRootProxy = proxyBuilder.build(); - expressionResolver.resolveExpression(commentString, contextRootProxy); - comments.remove(comment.getId()); // guarantee one-time processing - logger.debug( - String.format("Comment '%s' has been successfully processed by a comment processor.", - commentString)); - return Optional.of(commentWrapper); - } catch (SpelEvaluationException | SpelParseException e) { - if (failOnInvalidExpression) { - throw new UnresolvedExpressionException(commentString, e); - } else { - logger.warn(String.format( - "Skipping comment expression '%s' because it can not be resolved by any comment processor. Reason: %s. Set log level to TRACE to view Stacktrace.", - commentString, e.getMessage())); - logger.trace("Reason for skipping comment: ", e); - } - } catch (ProxyException e) { - throw new DocxStamperException("Could not create a proxy around context root object", e); + try { + expressionResolver.resolveExpression(commentString, expressionContext); + comments.remove(comment.getId()); + logger.debug( + String.format("Comment '%s' has been successfully processed by a comment processor.", + commentString)); + return Optional.of(commentWrapper); + } catch (SpelEvaluationException | SpelParseException e) { + if (configuration.isFailOnUnresolvedExpression()) { + throw new UnresolvedExpressionException(commentString, e); + } else { + logger.warn(String.format( + "Skipping comment expression '%s' because it can not be resolved by any comment processor. Reason: %s. Set log level to TRACE to view Stacktrace.", + commentString, e.getMessage())); + logger.trace("Reason for skipping comment: ", e); + } + } + return Optional.empty(); } - return Optional.empty(); - } - - public boolean isFailOnInvalidExpression() { - return failOnInvalidExpression; - } - public void setFailOnInvalidExpression(boolean failOnInvalidExpression) { - this.failOnInvalidExpression = failOnInvalidExpression; - } - - public void reset() { - for (ICommentProcessor processor : commentProcessors) { - processor.reset(); + public void reset() { + for (Object processor : configuration.getCommentProcessors().values()) { + ((ICommentProcessor) processor).reset(); + } } - } } diff --git a/src/main/java/org/wickedsource/docxstamper/processor/displayif/DisplayIfProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/displayif/DisplayIfProcessor.java index 3c00e664..17ec3320 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/displayif/DisplayIfProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/displayif/DisplayIfProcessor.java @@ -1,9 +1,11 @@ package org.wickedsource.docxstamper.processor.displayif; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.wickedsource.docxstamper.DocxStamperConfiguration; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableRowCoordinates; +import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; import org.wickedsource.docxstamper.processor.BaseCommentProcessor; import org.wickedsource.docxstamper.processor.CommentProcessingException; import org.wickedsource.docxstamper.util.ObjectDeleter; @@ -13,75 +15,84 @@ public class DisplayIfProcessor extends BaseCommentProcessor implements IDisplayIfProcessor { - private List paragraphsToBeRemoved = new ArrayList<>(); - - private List tablesToBeRemoved = new ArrayList<>(); - - private List tableRowsToBeRemoved = new ArrayList<>(); - - @Override - public void commitChanges(WordprocessingMLPackage document) { - ObjectDeleter deleter = new ObjectDeleter(document); - removeParagraphs(deleter); - removeTables(deleter); - removeTableRows(deleter); - } - - @Override - public void reset() { - paragraphsToBeRemoved = new ArrayList<>(); - tablesToBeRemoved = new ArrayList<>(); - tableRowsToBeRemoved = new ArrayList<>(); - } - - private void removeParagraphs(ObjectDeleter deleter) { - for (ParagraphCoordinates pCoords : paragraphsToBeRemoved) { - deleter.deleteParagraph(pCoords); - } - } - - private void removeTables(ObjectDeleter deleter) { - for (TableCoordinates tCoords : tablesToBeRemoved) { - deleter.deleteTable(tCoords); - } - } - - private void removeTableRows(ObjectDeleter deleter) { - for (TableRowCoordinates rCoords : tableRowsToBeRemoved) { - deleter.deleteTableRow(rCoords); - } - } - - @Override - public void displayParagraphIf(Boolean condition) { - if (!condition) { - ParagraphCoordinates coords = getCurrentParagraphCoordinates(); - paragraphsToBeRemoved.add(coords); - } - } - - @Override - public void displayTableIf(Boolean condition) { - if (!condition) { - ParagraphCoordinates pCoords = getCurrentParagraphCoordinates(); - if (pCoords.getParentTableCellCoordinates() == null || - pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates() == null || - pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates().getParentTableCoordinates() == null) { - throw new CommentProcessingException("Paragraph is not within a table!", pCoords); - } - tablesToBeRemoved.add(pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates().getParentTableCoordinates()); - } - } - - @Override - public void displayTableRowIf(Boolean condition) { - if (!condition) { - ParagraphCoordinates pCoords = getCurrentParagraphCoordinates(); - if (pCoords.getParentTableCellCoordinates() == null || - pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates() == null) { - throw new CommentProcessingException("Paragraph is not within a table!", pCoords); - } - tableRowsToBeRemoved.add(pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates()); - } - } + private List paragraphsToBeRemoved = new ArrayList<>(); + + private List tablesToBeRemoved = new ArrayList<>(); + + private List tableRowsToBeRemoved = new ArrayList<>(); + + public DisplayIfProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); + } + + @Override + public void commitChanges(WordprocessingMLPackage document) { + ObjectDeleter deleter = new ObjectDeleter(document); + removeParagraphs(deleter); + removeTables(deleter); + removeTableRows(deleter); + } + + @Override + public void reset() { + paragraphsToBeRemoved = new ArrayList<>(); + tablesToBeRemoved = new ArrayList<>(); + tableRowsToBeRemoved = new ArrayList<>(); + } + + private void removeParagraphs(ObjectDeleter deleter) { + for (ParagraphCoordinates pCoords : paragraphsToBeRemoved) { + deleter.deleteParagraph(pCoords); + } + } + + private void removeTables(ObjectDeleter deleter) { + for (TableCoordinates tCoords : tablesToBeRemoved) { + deleter.deleteTable(tCoords); + } + } + + private void removeTableRows(ObjectDeleter deleter) { + for (TableRowCoordinates rCoords : tableRowsToBeRemoved) { + deleter.deleteTableRow(rCoords); + } + } + + @Override + public void displayParagraphIf(Boolean condition) { + if (!condition) { + ParagraphCoordinates coords = getCurrentParagraphCoordinates(); + paragraphsToBeRemoved.add(coords); + } + } + + @Override + public void displayParagraphIfPresent(Object condition) { + displayParagraphIf(condition != null); + } + + @Override + public void displayTableIf(Boolean condition) { + if (!condition) { + ParagraphCoordinates pCoords = getCurrentParagraphCoordinates(); + if (pCoords.getParentTableCellCoordinates() == null || + pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates() == null || + pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates().getParentTableCoordinates() == null) { + throw new CommentProcessingException("Paragraph is not within a table!", pCoords); + } + tablesToBeRemoved.add(pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates().getParentTableCoordinates()); + } + } + + @Override + public void displayTableRowIf(Boolean condition) { + if (!condition) { + ParagraphCoordinates pCoords = getCurrentParagraphCoordinates(); + if (pCoords.getParentTableCellCoordinates() == null || + pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates() == null) { + throw new CommentProcessingException("Paragraph is not within a table!", pCoords); + } + tableRowsToBeRemoved.add(pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates()); + } + } } diff --git a/src/main/java/org/wickedsource/docxstamper/processor/displayif/IDisplayIfProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/displayif/IDisplayIfProcessor.java index 30f3a591..e9b97088 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/displayif/IDisplayIfProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/displayif/IDisplayIfProcessor.java @@ -10,6 +10,14 @@ public interface IDisplayIfProcessor { */ void displayParagraphIf(Boolean condition); + /** + * May be called to delete the commented paragraph or not, depending on the presence of the given data. + * + * @param condition if non null, the commented paragraph will remain in the document. If null, the commented paragraph + * will be deleted at stamping. + */ + void displayParagraphIfPresent(Object condition); + /** * May be called to delete the table surrounding the commented paragraph, depending on the given boolean condition. * diff --git a/src/main/java/org/wickedsource/docxstamper/processor/repeat/IRepeatDocPartProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/repeat/IRepeatDocPartProcessor.java index 52d5b0b5..29f13546 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/repeat/IRepeatDocPartProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/repeat/IRepeatDocPartProcessor.java @@ -10,5 +10,5 @@ public interface IRepeatDocPartProcessor { * * @param objects the objects which serve as context root for expressions found in the template table row. */ - void repeatDocPart(List objects); + void repeatDocPart(List objects) throws Exception; } diff --git a/src/main/java/org/wickedsource/docxstamper/processor/repeat/ParagraphRepeatProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/repeat/ParagraphRepeatProcessor.java index 032341db..4f900e98 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/repeat/ParagraphRepeatProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/repeat/ParagraphRepeatProcessor.java @@ -6,11 +6,13 @@ import org.docx4j.wml.CommentRangeStart; import org.docx4j.wml.ContentAccessor; import org.docx4j.wml.P; +import org.wickedsource.docxstamper.DocxStamperConfiguration; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; import org.wickedsource.docxstamper.processor.BaseCommentProcessor; -import org.wickedsource.docxstamper.replace.PlaceholderReplacer; import org.wickedsource.docxstamper.util.CommentUtil; +import org.wickedsource.docxstamper.util.CommentWrapper; +import org.wickedsource.docxstamper.util.ParagraphUtil; import java.math.BigInteger; import java.util.ArrayList; @@ -21,16 +23,16 @@ public class ParagraphRepeatProcessor extends BaseCommentProcessor implements IParagraphRepeatProcessor { private static class ParagraphsToRepeat { + CommentWrapper commentWrapper; List data; List

paragraphs; } private Map pToRepeat = new HashMap<>(); - private PlaceholderReplacer placeholderReplacer; - public ParagraphRepeatProcessor(TypeResolverRegistry typeResolverRegistry) { - this.placeholderReplacer = new PlaceholderReplacer<>(typeResolverRegistry); + public ParagraphRepeatProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); } @Override @@ -41,11 +43,11 @@ public void repeatParagraph(List objects) { List

paragraphs = getParagraphsInsideComment(paragraph); ParagraphsToRepeat toRepeat = new ParagraphsToRepeat(); + toRepeat.commentWrapper = getCurrentCommentWrapper(); toRepeat.data = objects; toRepeat.paragraphs = paragraphs; pToRepeat.put(paragraphCoordinates, toRepeat); - CommentUtil.deleteComment(getCurrentCommentWrapper()); } @Override @@ -54,15 +56,20 @@ public void commitChanges(WordprocessingMLPackage document) { ParagraphsToRepeat paragraphsToRepeat = pToRepeat.get(rCoords); List expressionContexts = paragraphsToRepeat.data; - List

paragraphsToAdd = new ArrayList<>(); - for (final Object expressionContext : expressionContexts) { - for (P paragraphToClone : paragraphsToRepeat.paragraphs) { - P pClone = XmlUtils.deepCopy(paragraphToClone); - placeholderReplacer.resolveExpressionsForParagraph(pClone, expressionContext, document); - paragraphsToAdd.add(pClone); + if (expressionContexts != null) { + for (final Object expressionContext : expressionContexts) { + for (P paragraphToClone : paragraphsToRepeat.paragraphs) { + P pClone = XmlUtils.deepCopy(paragraphToClone); + CommentUtil.deleteCommentFromElement(pClone, paragraphsToRepeat.commentWrapper.getComment().getId()); + placeholderReplacer.resolveExpressionsForParagraph(pClone, expressionContext, document); + + paragraphsToAdd.add(pClone); + } } + } else if (configuration.isReplaceNullValues() && configuration.getNullValuesDefault() != null) { + paragraphsToAdd.add(ParagraphUtil.create(configuration.getNullValuesDefault())); } Object parent = rCoords.getParagraph().getParent(); @@ -103,7 +110,7 @@ public static List

getParagraphsInsideComment(P paragraph) { if (parent instanceof ContentAccessor) { ContentAccessor contentAccessor = (ContentAccessor) parent; int index = contentAccessor.getContent().indexOf(paragraph); - for (int i = index + 1; i < contentAccessor.getContent().size() && !foundEnd; i ++) { + for (int i = index + 1; i < contentAccessor.getContent().size() && !foundEnd; i++) { Object next = contentAccessor.getContent().get(i); if (next instanceof CommentRangeEnd && ((CommentRangeEnd) next).getId().equals(commentId)) { diff --git a/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatDocPartProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatDocPartProcessor.java index d2d18aa9..705b0520 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatDocPartProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatDocPartProcessor.java @@ -1,86 +1,195 @@ package org.wickedsource.docxstamper.processor.repeat; import org.docx4j.XmlUtils; +import org.docx4j.jaxb.Context; +import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart; import org.docx4j.wml.*; import org.jvnet.jaxb2_commons.ppp.Child; +import org.springframework.util.CollectionUtils; +import org.wickedsource.docxstamper.DocxStamper; +import org.wickedsource.docxstamper.DocxStamperConfiguration; import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; import org.wickedsource.docxstamper.processor.BaseCommentProcessor; -import org.wickedsource.docxstamper.replace.PlaceholderReplacer; -import org.wickedsource.docxstamper.util.CommentUtil; import org.wickedsource.docxstamper.util.CommentWrapper; +import org.wickedsource.docxstamper.util.DocumentUtil; +import org.wickedsource.docxstamper.util.ParagraphUtil; import org.wickedsource.docxstamper.util.walk.BaseDocumentWalker; -import org.wickedsource.docxstamper.util.walk.DocumentWalker; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.*; +import java.util.stream.Collectors; public class RepeatDocPartProcessor extends BaseCommentProcessor implements IRepeatDocPartProcessor { - private Map> partsToRepeat = new HashMap<>(); - private PlaceholderReplacer placeholderReplacer; + private Map> subContexts = new HashMap<>(); + private Map> repeatElementsMap = new HashMap<>(); + private Map subTemplates = new HashMap<>(); + private Map gcpMap = new HashMap<>(); - public RepeatDocPartProcessor(TypeResolverRegistry typeResolverRegistry) { - this.placeholderReplacer = new PlaceholderReplacer<>(typeResolverRegistry); + private static ObjectFactory objectFactory = null; + + public RepeatDocPartProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); } + @Override - public void repeatDocPart(List objects) { - partsToRepeat.put(getCurrentCommentWrapper(), objects); + public void repeatDocPart(List contexts) throws Exception { + if (contexts == null) { + contexts = Collections.emptyList(); + } + + CommentWrapper currentCommentWrapper = getCurrentCommentWrapper(); + ContentAccessor gcp = findGreatestCommonParent( + currentCommentWrapper.getCommentRangeEnd().getParent(), + (ContentAccessor) currentCommentWrapper.getCommentRangeStart().getParent() + ); + List repeatElements = getRepeatElements(currentCommentWrapper, gcp); + + if (!repeatElements.isEmpty()) { + subTemplates.put(currentCommentWrapper, extractSubTemplate(currentCommentWrapper, repeatElements, getOrCreateObjectFactory())); + subContexts.put(currentCommentWrapper, contexts); + gcpMap.put(currentCommentWrapper, gcp); + repeatElementsMap.put(currentCommentWrapper, repeatElements); + } + } + + private static ObjectFactory getOrCreateObjectFactory() { + if (objectFactory == null) { + objectFactory = Context.getWmlObjectFactory(); + } + return objectFactory; + } + + private WordprocessingMLPackage copyTemplate(WordprocessingMLPackage doc) throws Docx4JException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + doc.save(baos); + return WordprocessingMLPackage.load(new ByteArrayInputStream(baos.toByteArray())); } @Override public void commitChanges(WordprocessingMLPackage document) { - for (CommentWrapper commentWrapper : partsToRepeat.keySet()) { - List expressionContexts = partsToRepeat.get(commentWrapper); - - CommentRangeStart start = commentWrapper.getCommentRangeStart(); - - ContentAccessor gcp = findGreatestCommonParent(commentWrapper.getCommentRangeEnd(), (ContentAccessor) start.getParent()); - List repeatElements = getRepeatElements(commentWrapper, gcp); - int insertIndex = gcp.getContent().indexOf(repeatElements.stream().findFirst().orElse(null)); - - CommentUtil.deleteComment(commentWrapper); // for deep copy without comment - - for (final Object expressionContext : expressionContexts) { - for (final Object element : repeatElements) { - Object elClone = XmlUtils.unwrap(XmlUtils.deepCopy(element)); - if (elClone instanceof P) { - placeholderReplacer.resolveExpressionsForParagraph((P) elClone, expressionContext, document); - } else if (elClone instanceof ContentAccessor) { - DocumentWalker walker = new BaseDocumentWalker((ContentAccessor)elClone) { - @Override - protected void onParagraph(P paragraph) { - placeholderReplacer.resolveExpressionsForParagraph(paragraph, expressionContext, document); - } - }; - walker.walk(); + for (CommentWrapper commentWrapper : subContexts.keySet()) { + List expressionContexts = subContexts.get(commentWrapper); + + // index changes after each replacement so we need to get the insert index at the right moment. + ContentAccessor insertParentContentAccessor = gcpMap.get(commentWrapper); + Integer index = insertParentContentAccessor.getContent().indexOf(repeatElementsMap.get(commentWrapper).get(0)); + + if (expressionContexts != null) { + for (Object subContext : expressionContexts) { + try { + WordprocessingMLPackage subTemplate = copyTemplate(subTemplates.get(commentWrapper)); + DocxStamper stamper = new DocxStamper<>(configuration.copy()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + stamper.stamp(subTemplate, subContext, output); + WordprocessingMLPackage subDocument = WordprocessingMLPackage.load(new ByteArrayInputStream(output.toByteArray())); + try { + List changes = DocumentUtil.prepareDocumentForInsert(subDocument, document); + insertParentContentAccessor.getContent().addAll(index, changes); + index += changes.size(); + } catch (Exception e) { + throw new RuntimeException("Unexpected error occured ! Skipping this comment", e); + } + } catch (Docx4JException e) { + throw new RuntimeException(e); } - gcp.getContent().add(insertIndex++, elClone); } + } else if (configuration.isReplaceNullValues() && configuration.getNullValuesDefault() != null) { + insertParentContentAccessor.getContent().add(index, ParagraphUtil.create(configuration.getNullValuesDefault())); } - gcp.getContent().removeAll(repeatElements); + + insertParentContentAccessor.getContent().removeAll(repeatElementsMap.get(commentWrapper)); } } @Override public void reset() { - partsToRepeat = new HashMap<>(); + subContexts = new HashMap<>(); + subTemplates = new HashMap<>(); + gcpMap = new HashMap<>(); + repeatElementsMap = new HashMap<>(); + } + + private WordprocessingMLPackage extractSubTemplate(CommentWrapper commentWrapper, List repeatElements, ObjectFactory objectFactory) throws Exception { + WordprocessingMLPackage document = getDocument(); + WordprocessingMLPackage subDocument = WordprocessingMLPackage.createPackage(); + + CommentsPart commentsPart = new CommentsPart(); + subDocument.getMainDocumentPart().addTargetPart(commentsPart); + + // copy the elements to repeat without comment range anchors + List finalRepeatElements = repeatElements.stream().map(XmlUtils::deepCopy).collect(Collectors.toList()); + removeCommentAnchorsFromFinalElements(commentWrapper, finalRepeatElements); + subDocument.getMainDocumentPart().getContent().addAll(finalRepeatElements); + + // copy the images from parent document using the original repeat elements + ContentAccessor fakeBody = getOrCreateObjectFactory().createBody(); + fakeBody.getContent().addAll(repeatElements); + DocumentUtil.walkObjectsAndImportImages(fakeBody, document, subDocument); + + Comments comments = objectFactory.createComments(); + extractedSubComments(commentWrapper, comments); + commentsPart.setContents(comments); + + return subDocument; + } + + private void extractedSubComments(CommentWrapper commentWrapper, Comments comments) { + for (CommentWrapper child : commentWrapper.getChildren()) { + comments.getComment().add(child.getComment()); + if (CollectionUtils.isEmpty(child.getChildren())) { + continue; + } + extractedSubComments(child, comments); + } + } + + private static void removeCommentAnchorsFromFinalElements(CommentWrapper commentWrapper, List finalRepeatElements) { + List commentsToRemove = new ArrayList<>(); + + new BaseDocumentWalker(() -> finalRepeatElements) { + @Override + protected void onCommentRangeStart(CommentRangeStart commentRangeStart) { + if (commentRangeStart.getId().equals(commentWrapper.getComment().getId())) { + commentsToRemove.add(commentRangeStart); + } + } + + @Override + protected void onCommentRangeEnd(CommentRangeEnd commentRangeEnd) { + if (commentRangeEnd.getId().equals(commentWrapper.getComment().getId())) { + commentsToRemove.add(commentRangeEnd); + } + } + }.walk(); + + for (Object commentAnchorToRemove : commentsToRemove) { + if (commentAnchorToRemove instanceof CommentRangeStart) { + ContentAccessor parent = ((ContentAccessor) ((CommentRangeStart) commentAnchorToRemove).getParent()); + parent.getContent().removeAll(parent.getContent().subList(0, parent.getContent().indexOf(commentAnchorToRemove) + 1)); + } else if (commentAnchorToRemove instanceof CommentRangeEnd) { + ContentAccessor parent = ((ContentAccessor) ((CommentRangeEnd) commentAnchorToRemove).getParent()); + parent.getContent().removeAll(parent.getContent().subList(parent.getContent().indexOf(commentAnchorToRemove), parent.getContent().size())); + } else { + throw new RuntimeException("Unknown comment anchor type given to remove !"); + } + } } private static List getRepeatElements(CommentWrapper commentWrapper, ContentAccessor greatestCommonParent) { List repeatElements = new ArrayList<>(); boolean startFound = false; - for (Object element : greatestCommonParent.getContent()){ + for (Object element : greatestCommonParent.getContent()) { if (!startFound && depthElementSearch(commentWrapper.getCommentRangeStart(), element)) { startFound = true; } if (startFound) { repeatElements.add(element); - if (depthElementSearch(commentWrapper.getCommentRangeEnd(), element)) { break; } @@ -91,22 +200,24 @@ && depthElementSearch(commentWrapper.getCommentRangeStart(), element)) { private static ContentAccessor findGreatestCommonParent(Object targetSearch, ContentAccessor searchFrom) { if (depthElementSearch(targetSearch, searchFrom)) { - if (searchFrom instanceof Tr) { // if it's Tr - need add new line to table - return (ContentAccessor) ((Tr) searchFrom).getParent(); - } else if (searchFrom instanceof Tc) { // if it's Tc - need add new cell to row - return (ContentAccessor) ((Tc) searchFrom).getParent(); - } - return searchFrom; + return findInsertableParent(searchFrom); } return findGreatestCommonParent(targetSearch, (ContentAccessor) ((Child) searchFrom).getParent()); } + private static ContentAccessor findInsertableParent(ContentAccessor searchFrom) { + if (!(searchFrom instanceof Tc || searchFrom instanceof Body)) { + return findInsertableParent((ContentAccessor) ((Child) searchFrom).getParent()); + } + return searchFrom; + } + private static boolean depthElementSearch(Object searchTarget, Object content) { content = XmlUtils.unwrap(content); if (searchTarget.equals(content)) { return true; } else if (content instanceof ContentAccessor) { - for (Object object : ((ContentAccessor)content).getContent()) { + for (Object object : ((ContentAccessor) content).getContent()) { Object unwrappedObject = XmlUtils.unwrap(object); if (searchTarget.equals(unwrappedObject) || depthElementSearch(searchTarget, unwrappedObject)) { diff --git a/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatProcessor.java index 28c7e168..9cff662c 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/repeat/RepeatProcessor.java @@ -4,14 +4,14 @@ import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.P; import org.docx4j.wml.Tr; +import org.wickedsource.docxstamper.DocxStamperConfiguration; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableRowCoordinates; import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; -import org.wickedsource.docxstamper.el.ExpressionResolver; import org.wickedsource.docxstamper.processor.BaseCommentProcessor; import org.wickedsource.docxstamper.processor.CommentProcessingException; -import org.wickedsource.docxstamper.replace.PlaceholderReplacer; import org.wickedsource.docxstamper.util.CommentUtil; +import org.wickedsource.docxstamper.util.CommentWrapper; import org.wickedsource.docxstamper.util.walk.BaseDocumentWalker; import org.wickedsource.docxstamper.util.walk.DocumentWalker; @@ -22,14 +22,13 @@ public class RepeatProcessor extends BaseCommentProcessor implements IRepeatProcessor { private Map> tableRowsToRepeat = new HashMap<>(); + private Map tableRowsCommentsToRemove = new HashMap<>(); - private PlaceholderReplacer placeholderReplacer; - - public RepeatProcessor(TypeResolverRegistry typeResolverRegistry, ExpressionResolver expressionResolver) { - this.placeholderReplacer = new PlaceholderReplacer<>(typeResolverRegistry); - this.placeholderReplacer.setExpressionResolver(expressionResolver); + public RepeatProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); } + @Override public void commitChanges(WordprocessingMLPackage document) { repeatRows(document); @@ -38,28 +37,35 @@ public void commitChanges(WordprocessingMLPackage document) { @Override public void reset() { this.tableRowsToRepeat = new HashMap<>(); + this.tableRowsCommentsToRemove = new HashMap<>(); } private void repeatRows(final WordprocessingMLPackage document) { for (TableRowCoordinates rCoords : tableRowsToRepeat.keySet()) { List expressionContexts = tableRowsToRepeat.get(rCoords); int index = rCoords.getIndex(); - for (final Object expressionContext : expressionContexts) { - Tr rowClone = XmlUtils.deepCopy(rCoords.getRow()); - DocumentWalker walker = new BaseDocumentWalker(rowClone) { - @Override - protected void onParagraph(P paragraph) { - placeholderReplacer.resolveExpressionsForParagraph(paragraph, expressionContext, document); - } - }; - walker.walk(); - rCoords.getParentTableCoordinates().getTable().getContent().add(++index, rowClone); + + if (expressionContexts != null) { + for (final Object expressionContext : expressionContexts) { + Tr rowClone = XmlUtils.deepCopy(rCoords.getRow()); + CommentUtil.deleteCommentFromElement(rowClone, tableRowsCommentsToRemove.get(rCoords).getComment().getId()); + + DocumentWalker walker = new BaseDocumentWalker(rowClone) { + @Override + protected void onParagraph(P paragraph) { + placeholderReplacer.resolveExpressionsForParagraph(paragraph, expressionContext, document); + } + }; + walker.walk(); + rCoords.getParentTableCoordinates().getTable().getContent().add(++index, rowClone); + } } + + // TODO : how to replace null values here ? rCoords.getParentTableCoordinates().getTable().getContent().remove(rCoords.getRow()); } } - @Override public void repeatTableRow(List objects) { ParagraphCoordinates pCoords = getCurrentParagraphCoordinates(); @@ -67,8 +73,8 @@ public void repeatTableRow(List objects) { pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates() == null) { throw new CommentProcessingException("Paragraph is not within a table!", pCoords); } - tableRowsToRepeat.put(getCurrentParagraphCoordinates().getParentTableCellCoordinates().getParentTableRowCoordinates(), objects); - CommentUtil.deleteComment(getCurrentCommentWrapper()); + TableRowCoordinates parentTableRowCoordinates = getCurrentParagraphCoordinates().getParentTableCellCoordinates().getParentTableRowCoordinates(); + tableRowsToRepeat.put(parentTableRowCoordinates, objects); + tableRowsCommentsToRemove.put(parentTableRowCoordinates, getCurrentCommentWrapper()); } - } diff --git a/src/main/java/org/wickedsource/docxstamper/processor/replaceExpression/ReplaceWithProcessor.java b/src/main/java/org/wickedsource/docxstamper/processor/replaceExpression/ReplaceWithProcessor.java index 892b59ad..cee5d7e4 100644 --- a/src/main/java/org/wickedsource/docxstamper/processor/replaceExpression/ReplaceWithProcessor.java +++ b/src/main/java/org/wickedsource/docxstamper/processor/replaceExpression/ReplaceWithProcessor.java @@ -1,31 +1,43 @@ package org.wickedsource.docxstamper.processor.replaceExpression; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wickedsource.docxstamper.DocxStamperConfiguration; +import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; import org.wickedsource.docxstamper.processor.BaseCommentProcessor; import org.wickedsource.docxstamper.util.RunUtil; +/** + * @deprecated + */ +@Deprecated public class ReplaceWithProcessor extends BaseCommentProcessor - implements IReplaceWithProcessor { + implements IReplaceWithProcessor { - public ReplaceWithProcessor() { + private final Logger logger = LoggerFactory.getLogger(ReplaceWithProcessor.class); - } + public ReplaceWithProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); + } - @Override - public void commitChanges(WordprocessingMLPackage document) { - } - @Override - public void reset() { - // nothing to rest - } + @Override + public void commitChanges(WordprocessingMLPackage document) { + } - @Override - public void replaceWordWith(String expression) { - if (this.getCurrentRunCoordinates() != null) { - RunUtil.setText(this.getCurrentRunCoordinates().getRun(), expression); - } - - } + @Override + public void reset() { + // nothing to rest + } + @Override + public void replaceWordWith(String expression) { + logger.warn("replaceWordWith has been deprecated in favor of inplace placeholders (${expression})"); + if (expression != null && this.getCurrentRunCoordinates() != null) { + RunUtil.setText(this.getCurrentRunCoordinates().getRun(), expression); + } else if (configuration.isReplaceNullValues() && configuration.getNullValuesDefault() != null) { + RunUtil.setText(this.getCurrentRunCoordinates().getRun(), configuration.getNullValuesDefault()); + } + } } diff --git a/src/main/java/org/wickedsource/docxstamper/processor/table/ITableResolver.java b/src/main/java/org/wickedsource/docxstamper/processor/table/ITableResolver.java new file mode 100644 index 00000000..48dc2eeb --- /dev/null +++ b/src/main/java/org/wickedsource/docxstamper/processor/table/ITableResolver.java @@ -0,0 +1,5 @@ +package org.wickedsource.docxstamper.processor.table; + +public interface ITableResolver { + void resolveTable(StampTable givenTable); +} diff --git a/src/main/java/org/wickedsource/docxstamper/processor/table/StampTable.java b/src/main/java/org/wickedsource/docxstamper/processor/table/StampTable.java new file mode 100644 index 00000000..39a873dd --- /dev/null +++ b/src/main/java/org/wickedsource/docxstamper/processor/table/StampTable.java @@ -0,0 +1,36 @@ +package org.wickedsource.docxstamper.processor.table; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.singletonList; + +public class StampTable { + private final List headers; + private final List> records; + + public StampTable() { + this.headers = new ArrayList<>(); + this.records = new ArrayList<>(); + } + + public StampTable( + List headers, + List> records + ) { + this.headers = headers; + this.records = records; + } + + public List headers() { + return headers; + } + + public List> records() { + return records; + } + + public static StampTable empty() { + return new StampTable(singletonList("placeholder"), singletonList(singletonList("placeholder"))); + } +} diff --git a/src/main/java/org/wickedsource/docxstamper/processor/table/TableResolver.java b/src/main/java/org/wickedsource/docxstamper/processor/table/TableResolver.java new file mode 100644 index 00000000..a782f723 --- /dev/null +++ b/src/main/java/org/wickedsource/docxstamper/processor/table/TableResolver.java @@ -0,0 +1,135 @@ +package org.wickedsource.docxstamper.processor.table; + +import org.docx4j.XmlUtils; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.ContentAccessor; +import org.docx4j.wml.Tbl; +import org.docx4j.wml.Tc; +import org.docx4j.wml.Tr; +import org.wickedsource.docxstamper.DocxStamperConfiguration; +import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; +import org.wickedsource.docxstamper.api.coordinates.TableCellCoordinates; +import org.wickedsource.docxstamper.api.coordinates.TableCoordinates; +import org.wickedsource.docxstamper.api.coordinates.TableRowCoordinates; +import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; +import org.wickedsource.docxstamper.processor.BaseCommentProcessor; +import org.wickedsource.docxstamper.processor.CommentProcessingException; +import org.wickedsource.docxstamper.util.ParagraphUtil; + +import javax.xml.bind.JAXBElement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TableResolver extends BaseCommentProcessor implements ITableResolver { + private final Map cols = new HashMap<>(); + + public TableResolver(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); + } + + @Override + public void resolveTable(StampTable givenTable) { + ParagraphCoordinates pCoords = getCurrentParagraphCoordinates(); + if (pCoords.getParentTableCellCoordinates() == null || + pCoords.getParentTableCellCoordinates().getParentTableRowCoordinates() == null) { + throw new CommentProcessingException("Paragraph is not within a table!", pCoords); + } + + TableCellCoordinates parentTableCellCoordinates = pCoords.getParentTableCellCoordinates(); + TableRowCoordinates parentTableRowCoordinates = parentTableCellCoordinates.getParentTableRowCoordinates(); + TableCoordinates parentTableCoordinates = parentTableRowCoordinates.getParentTableCoordinates(); + Tbl table = parentTableCoordinates.getTable(); + + cols.put(table, givenTable); + } + + @Override + public void commitChanges(WordprocessingMLPackage document) { + for (Map.Entry entry : cols.entrySet()) { + Tbl wordTable = entry.getKey(); + + StampTable stampedTable = entry.getValue(); + + if (stampedTable != null) { + replaceTableInplace(wordTable, stampedTable); + } else if (configuration.isReplaceNullValues() && configuration.getNullValuesDefault() != null) { + replaceTableWithNullDefault(wordTable); + } else { + removeTableFromDocument(wordTable); + } + } + } + + private static void removeTableFromDocument(Tbl wordTable) { + ((ContentAccessor) wordTable.getParent()).getContent().remove(wordTable); + } + + private void replaceTableWithNullDefault(Tbl wordTable) { + ContentAccessor accessor = ((ContentAccessor) wordTable.getParent()); + int tablePosition = accessor.getContent().indexOf(wordTable); + if (tablePosition >= 0) { + accessor.getContent().set(tablePosition, ParagraphUtil.create(configuration.getNullValuesDefault())); + } + } + + private void replaceTableInplace(Tbl wordTable, StampTable stampedTable) { + List stampedHeaders = stampedTable.headers(); + List> stampedRecords = stampedTable.records(); + + List rows = wordTable.getContent(); + Tr headerRow = (Tr) rows.get(0); + Tr firstDataRow = (Tr) rows.get(1); + + growAndFillRow(headerRow, stampedHeaders); + + if (!stampedRecords.isEmpty()) { + growAndFillRow(firstDataRow, stampedRecords.get(0)); + + for (List rowContent : stampedRecords.subList(1, stampedRecords.size())) { + rows.add(copyRowFromTemplate(firstDataRow, rowContent)); + } + } else { + rows.remove(firstDataRow); + } + } + + private Tr copyRowFromTemplate(Tr firstDataRow, List rowContent) { + Tr newXmlRow = XmlUtils.deepCopy(firstDataRow); + List xmlRow = newXmlRow.getContent(); + for (int i = 0; i < rowContent.size(); i++) { + String cellContent = rowContent.get(i); + Tc xmlCell = ((JAXBElement) xmlRow.get(i)).getValue(); + setCellText(xmlCell, cellContent); + } + return newXmlRow; + } + + private void growAndFillRow(Tr row, List values) { + List cellRowContent = row.getContent(); + + //Replace text in first cell + JAXBElement cell0 = (JAXBElement) cellRowContent.get(0); + Tc cell0tc = cell0.getValue(); + setCellText(cell0tc, values.isEmpty() ? "" : values.get(0)); + + if (values.size() > 1) { + //Copy first cell and replace content for each remaining values + for (String cellContent : values.subList(1, values.size())) { + JAXBElement xmlCell = XmlUtils.deepCopy(cell0); + setCellText(xmlCell.getValue(), cellContent); + cellRowContent.add(xmlCell); + } + } + } + + private void setCellText(Tc tableCell, String content) { + tableCell.getContent().clear(); + tableCell.getContent().add(ParagraphUtil.create(content)); + } + + @Override + public void reset() { + cols.clear(); + } +} diff --git a/src/main/java/org/wickedsource/docxstamper/proxy/ProxyBuilder.java b/src/main/java/org/wickedsource/docxstamper/proxy/ProxyBuilder.java deleted file mode 100644 index ca167321..00000000 --- a/src/main/java/org/wickedsource/docxstamper/proxy/ProxyBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.wickedsource.docxstamper.proxy; - -import java.util.HashMap; -import java.util.Map; - -import javassist.util.proxy.ProxyFactory; - -/** - * Allows an object to be wrapped by a proxy so that it will implement additional interfaces. - * - * @param the type of the root object. - */ -public class ProxyBuilder { - - private T root; - - private Map, Object> interfacesToImplementations = new HashMap<>(); - - /** - * Specifies the root object for the proxy that shall be enhanced. - * - * @param rootObject the root object. - * @return this builder for chaining. - */ - public ProxyBuilder withRoot(T rootObject) { - this.root = rootObject; - return this; - } - - /** - * Specifies an interfaces and an implementation of an interface by which the root object - * shall be extended. - * - * @param interfaceClass the class of the interface - * @param interfaceImpl an implementation of the interface - * @return this builder for chaining. - */ - public ProxyBuilder withInterface(Class interfaceClass, Object interfaceImpl) { - this.interfacesToImplementations.put(interfaceClass, interfaceImpl); - return this; - } - - /** - * Creates a proxy object out of the specified root object and the specified interfaces - * and implementations. - * - * @return a proxy object that is still of type T but additionally implements all specified - * interfaces. - * @throws ProxyException if the proxy could not be created. - */ - public T build() throws ProxyException { - - if (this.root == null) { - throw new IllegalArgumentException("root must not be null!"); - } - - if (this.interfacesToImplementations.isEmpty()) { - // nothing to proxy - return this.root; - } - - try { - ProxyMethodHandler methodHandler = new ProxyMethodHandler(root, - interfacesToImplementations); - ProxyFactory proxyFactory = new ProxyFactory(); - proxyFactory.setSuperclass(root.getClass()); - proxyFactory.setInterfaces(interfacesToImplementations.keySet().toArray(new Class[]{})); - return (T) proxyFactory.create(new Class[0], new Object[0], methodHandler); - } catch (Exception e) { - throw new ProxyException(e); - } - } - - -} \ No newline at end of file diff --git a/src/main/java/org/wickedsource/docxstamper/proxy/ProxyException.java b/src/main/java/org/wickedsource/docxstamper/proxy/ProxyException.java deleted file mode 100644 index 176df66e..00000000 --- a/src/main/java/org/wickedsource/docxstamper/proxy/ProxyException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.wickedsource.docxstamper.proxy; - -public class ProxyException extends Exception { - - public ProxyException(Throwable cause) { - super(cause); - } - -} diff --git a/src/main/java/org/wickedsource/docxstamper/proxy/ProxyMethodHandler.java b/src/main/java/org/wickedsource/docxstamper/proxy/ProxyMethodHandler.java deleted file mode 100644 index 789ec410..00000000 --- a/src/main/java/org/wickedsource/docxstamper/proxy/ProxyMethodHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.wickedsource.docxstamper.proxy; - -import java.lang.reflect.Method; -import java.util.Map; - -import javassist.util.proxy.MethodHandler; - -public class ProxyMethodHandler implements MethodHandler { - - private final Object contextRoot; - - private final Map, Object> interfacesWithImplementations; - - public ProxyMethodHandler(Object root, - Map, Object> interfacesWithImplementations) { - this.contextRoot = root; - this.interfacesWithImplementations = interfacesWithImplementations; - for (Map.Entry, Object> entry : interfacesWithImplementations.entrySet()) { - Class interfaceClass = entry.getKey(); - Object implementation = entry.getValue(); - if (!interfaceClass.isAssignableFrom(implementation.getClass())) { - throw new IllegalArgumentException( - String.format("%s does not implement %s!", implementation, interfaceClass)); - } - } - } - - @Override - public Object invoke(Object o, Method method, Method method2, Object[] args) throws Throwable { - for (Map.Entry, Object> entry : interfacesWithImplementations.entrySet()) { - Class interfaceClass = entry.getKey(); - Object implementation = entry.getValue(); - if (methodCanBeHandledByInterface(method, interfaceClass)) { - return method.invoke(implementation, args); - } - } - - return method.invoke(contextRoot, args); - } - - public boolean methodCanBeHandledByInterface(Method method, Class interfaceClass) { - try { - interfaceClass.getMethod(method.getName(), method.getParameterTypes()); - // no exception, so we found the method we're looking for - return true; - } catch (NoSuchMethodException e) { - return false; - } - } - -} diff --git a/src/main/java/org/wickedsource/docxstamper/replace/PlaceholderReplacer.java b/src/main/java/org/wickedsource/docxstamper/replace/PlaceholderReplacer.java index c1752218..8aa72400 100644 --- a/src/main/java/org/wickedsource/docxstamper/replace/PlaceholderReplacer.java +++ b/src/main/java/org/wickedsource/docxstamper/replace/PlaceholderReplacer.java @@ -1,7 +1,5 @@ package org.wickedsource.docxstamper.replace; -import java.util.List; - import org.docx4j.jaxb.Context; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.Br; @@ -11,132 +9,116 @@ import org.slf4j.LoggerFactory; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelParseException; -import org.wickedsource.docxstamper.api.DocxStamperException; +import org.wickedsource.docxstamper.DocxStamperConfiguration; +import org.wickedsource.docxstamper.api.UnresolvedExpressionException; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.typeresolver.ITypeResolver; import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; import org.wickedsource.docxstamper.el.ExpressionResolver; import org.wickedsource.docxstamper.el.ExpressionUtil; -import org.wickedsource.docxstamper.proxy.ProxyBuilder; -import org.wickedsource.docxstamper.proxy.ProxyException; import org.wickedsource.docxstamper.util.ParagraphWrapper; import org.wickedsource.docxstamper.util.RunUtil; import org.wickedsource.docxstamper.util.walk.BaseCoordinatesWalker; import org.wickedsource.docxstamper.util.walk.CoordinatesWalker; -public class PlaceholderReplacer { - - private Logger logger = LoggerFactory.getLogger(PlaceholderReplacer.class); - - private ExpressionUtil expressionUtil = new ExpressionUtil(); - - private ExpressionResolver expressionResolver = new ExpressionResolver(); - - private TypeResolverRegistry typeResolverRegistry; - - private String lineBreakPlaceholder; +import java.util.List; - private boolean leaveEmptyOnExpressionError = false; +public class PlaceholderReplacer { - private boolean replaceNullValues = false; + private final Logger logger = LoggerFactory.getLogger(PlaceholderReplacer.class); - public PlaceholderReplacer(TypeResolverRegistry typeResolverRegistry) { - this.typeResolverRegistry = typeResolverRegistry; - } + private final ExpressionUtil expressionUtil = new ExpressionUtil(); - public PlaceholderReplacer(TypeResolverRegistry typeResolverRegistry, String lineBreakPlaceholder) { - this.typeResolverRegistry = typeResolverRegistry; - this.lineBreakPlaceholder = lineBreakPlaceholder; - } + private final DocxStamperConfiguration configuration; - public boolean isLeaveEmptyOnExpressionError() { - return leaveEmptyOnExpressionError; - } + private final ExpressionResolver expressionResolver; - public void setLeaveEmptyOnExpressionError(boolean leaveEmptyOnExpressionError) { - this.leaveEmptyOnExpressionError = leaveEmptyOnExpressionError; - } + private final TypeResolverRegistry typeResolverRegistry; - public void setReplaceNullValues(boolean replaceNullValues) { - this.replaceNullValues = replaceNullValues; - } - - public void setExpressionResolver(ExpressionResolver expressionResolver) { - this.expressionResolver = expressionResolver; - } + public PlaceholderReplacer(TypeResolverRegistry typeResolverRegistry, DocxStamperConfiguration configuration) { + this.typeResolverRegistry = typeResolverRegistry; + this.configuration = configuration; + this.expressionResolver = new ExpressionResolver(configuration); + } - /** - * Finds expressions in a document and resolves them against the specified context object. The expressions in the - * document are then replaced by the resolved values. - * - * @param document the document in which to replace all expressions. - * @param proxyBuilder builder for a proxy around the context root to customize its interface - */ - public void resolveExpressions(final WordprocessingMLPackage document, ProxyBuilder proxyBuilder) { - try { - final T expressionContext = proxyBuilder.build(); - CoordinatesWalker walker = new BaseCoordinatesWalker(document) { - @Override - protected void onParagraph(ParagraphCoordinates paragraphCoordinates) { - resolveExpressionsForParagraph(paragraphCoordinates.getParagraph(), expressionContext, document); - } - }; - walker.walk(); - } catch (ProxyException e) { - throw new DocxStamperException("could not create proxy around context root!", e); + /** + * Finds expressions in a document and resolves them against the specified context object. The expressions in the + * document are then replaced by the resolved values. + * + * @param document the document in which to replace all expressions. + * @param expressionContext the context root + */ + public void resolveExpressions(final WordprocessingMLPackage document, Object expressionContext) { + CoordinatesWalker walker = new BaseCoordinatesWalker(document) { + @Override + protected void onParagraph(ParagraphCoordinates paragraphCoordinates) { + resolveExpressionsForParagraph(paragraphCoordinates.getParagraph(), expressionContext, document); + } + }; + walker.walk(); } - } - @SuppressWarnings("unchecked") - public void resolveExpressionsForParagraph(P p, T expressionContext, WordprocessingMLPackage document) { - ParagraphWrapper paragraphWrapper = new ParagraphWrapper(p); - List placeholders = expressionUtil.findVariableExpressions(paragraphWrapper.getText()); - for (String placeholder : placeholders) { - try { - Object replacement = expressionResolver.resolveExpression(placeholder, expressionContext); - if (replacement != null) { - ITypeResolver resolver = typeResolverRegistry.getResolverForType(replacement.getClass()); - Object replacementObject = resolver.resolve(document, replacement); - replace(paragraphWrapper, placeholder, replacementObject); - logger.debug(String.format("Replaced expression '%s' with value provided by TypeResolver %s", placeholder, resolver.getClass())); - } else if(replaceNullValues) { - ITypeResolver resolver = typeResolverRegistry.getDefaultResolver(); - Object replacementObject = resolver.resolve(document, replacement); - replace(paragraphWrapper, placeholder, replacementObject); - logger.debug(String.format("Replaced expression '%s' with value provided by TypeResolver %s", placeholder, resolver.getClass())); + @SuppressWarnings("unchecked") + public void resolveExpressionsForParagraph(P p, Object expressionContext, WordprocessingMLPackage document) { + ParagraphWrapper paragraphWrapper = new ParagraphWrapper(p); + List placeholders = expressionUtil.findVariableExpressions(paragraphWrapper.getText()); + for (String placeholder : placeholders) { + try { + Object replacement = expressionResolver.resolveExpression(placeholder, expressionContext); + if (replacement != null) { + ITypeResolver resolver = typeResolverRegistry.getResolverForType(replacement.getClass()); + Object replacementObject = resolver.resolve(document, replacement); + replace(paragraphWrapper, placeholder, replacementObject); + logger.debug(String.format("Replaced expression '%s' with value provided by TypeResolver %s", placeholder, resolver.getClass())); + } else if (configuration.isReplaceNullValues()) { + ITypeResolver resolver = typeResolverRegistry.getDefaultResolver(); + Object replacementObject = resolver.resolve(document, configuration.getNullValuesDefault()); + replace(paragraphWrapper, placeholder, replacementObject); + logger.debug(String.format("Replaced expression '%s' with value provided by TypeResolver %s", placeholder, resolver.getClass())); + } + } catch (SpelEvaluationException | SpelParseException e) { + if (configuration.isFailOnUnresolvedExpression()) { + throw new UnresolvedExpressionException( + String.format("Expression %s could not be resolved against context root of type %s. Reason: %s. Set log level to TRACE to view Stacktrace.", placeholder, expressionContext.getClass(), e.getMessage()), + e); + } else { + logger.warn(String.format( + "Expression %s could not be resolved against context root of type %s. Reason: %s. Set log level to TRACE to view Stacktrace.", + placeholder, expressionContext.getClass(), e.getMessage())); + logger.trace("Reason for skipping expression:", e); + + if (configuration.isLeaveEmptyOnExpressionError()) { + replace(paragraphWrapper, placeholder, null); + } else if (configuration.isReplaceUnresolvedExpressions()) { + replace(paragraphWrapper, placeholder, configuration.getUnresolvedExpressionsDefaultValue()); + } + } + } } - } catch (SpelEvaluationException | SpelParseException e) { - logger.warn(String.format( - "Expression %s could not be resolved against context root of type %s. Reason: %s. Set log level to TRACE to view Stacktrace.", - placeholder, expressionContext.getClass(), e.getMessage())); - logger.trace("Reason for skipping expression:", e); - - if(isLeaveEmptyOnExpressionError()) { - replace(paragraphWrapper,placeholder,null); + if (configuration.getLineBreakPlaceholder() != null) { + replaceLineBreaks(paragraphWrapper); } - } } - if (this.lineBreakPlaceholder != null) { - replaceLineBreaks(paragraphWrapper); - } - } - private void replaceLineBreaks(ParagraphWrapper paragraphWrapper) { - Br lineBreak = Context.getWmlObjectFactory().createBr(); - R run = RunUtil.create(lineBreak); - while (paragraphWrapper.getText().contains(this.lineBreakPlaceholder)) { - replace(paragraphWrapper, this.lineBreakPlaceholder, run); + private void replaceLineBreaks(ParagraphWrapper paragraphWrapper) { + Br lineBreak = Context.getWmlObjectFactory().createBr(); + R run = RunUtil.create(lineBreak); + while (paragraphWrapper.getText().contains(configuration.getLineBreakPlaceholder())) { + replace(paragraphWrapper, configuration.getLineBreakPlaceholder(), run); + } } - } - public void replace(ParagraphWrapper p, String placeholder, Object replacementObject) { - if (replacementObject == null) { - replacementObject = RunUtil.create(""); - } - if (replacementObject instanceof R) { - RunUtil.applyParagraphStyle(p.getParagraph(), (R) replacementObject); + public void replace(ParagraphWrapper p, String placeholder, Object replacementObject) { + if (replacementObject == null) { + replacementObject = RunUtil.create(""); + } + if (replacementObject instanceof String) { + replacementObject = RunUtil.create((String) replacementObject, p.getParagraph()); + } + if (replacementObject instanceof R) { + RunUtil.applyParagraphStyle(p.getParagraph(), (R) replacementObject); + } + p.replace(placeholder, replacementObject); } - p.replace(placeholder, replacementObject); - } } diff --git a/src/main/java/org/wickedsource/docxstamper/replace/typeresolver/image/ImageResolver.java b/src/main/java/org/wickedsource/docxstamper/replace/typeresolver/image/ImageResolver.java index 456c3773..aff5fa70 100644 --- a/src/main/java/org/wickedsource/docxstamper/replace/typeresolver/image/ImageResolver.java +++ b/src/main/java/org/wickedsource/docxstamper/replace/typeresolver/image/ImageResolver.java @@ -16,7 +16,7 @@ */ public class ImageResolver implements ITypeResolver { - private static Random random = new Random(); + private static final Random random = new Random(); @Override public R resolve(WordprocessingMLPackage document, Object image) { diff --git a/src/main/java/org/wickedsource/docxstamper/util/CommentUtil.java b/src/main/java/org/wickedsource/docxstamper/util/CommentUtil.java index 73deffc6..169d92a8 100644 --- a/src/main/java/org/wickedsource/docxstamper/util/CommentUtil.java +++ b/src/main/java/org/wickedsource/docxstamper/util/CommentUtil.java @@ -1,5 +1,6 @@ package org.wickedsource.docxstamper.util; +import org.docx4j.TextUtils; import org.docx4j.XmlUtils; import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.exceptions.InvalidFormatException; @@ -15,237 +16,320 @@ import org.wickedsource.docxstamper.util.walk.DocumentWalker; import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; public class CommentUtil { - private static Logger logger = LoggerFactory.getLogger(CommentUtil.class); - - private CommentUtil() { - - } - - /** - * Returns the comment the given DOCX4J run is commented with. - * @param run the DOCX4J run whose comment to retrieve. - * @param document the document that contains the run. - * @return the comment, if found, null otherwise. - */ - public static Comments.Comment getCommentAround(R run, - WordprocessingMLPackage document) { - try { - if (run instanceof Child) { - Child child = (Child) run; - ContentAccessor parent = (ContentAccessor) child.getParent(); - if (parent == null) - return null; - CommentRangeStart possibleComment = null; - boolean foundChild = false; - for (Object contentElement : parent.getContent()) { - - // so first we look for the start of the comment - if (XmlUtils.unwrap(contentElement) instanceof CommentRangeStart) { - possibleComment = (CommentRangeStart) contentElement; - } - // then we check if the child we are looking for is ours - else if (possibleComment != null && child.equals(contentElement)) { - foundChild = true; - } - // and then if we have an end of a comment we are good! - else if (possibleComment != null && foundChild && XmlUtils - .unwrap(contentElement) instanceof CommentRangeEnd) { - try { - BigInteger id = possibleComment.getId(); - CommentsPart commentsPart = (CommentsPart) document.getParts() - .get(new PartName("/word/comments.xml")); - Comments comments = commentsPart.getContents(); - for (Comments.Comment comment : comments.getComment()) { - if (comment.getId().equals(id)) { - return comment; - } - } - } - catch (InvalidFormatException e) { - logger.warn(String.format( - "Error while searching comment. Skipping run %s.", - run), e); - } - } - // else restart - else { - possibleComment = null; - foundChild = false; - } - } - } - return null; - } - catch (Docx4JException e) { - throw new DocxStamperException( - "error accessing the comments of the document!", e); - } - } - - /** - * Returns the first comment found for the given docx object. Note that an object is - * only considered commented if the comment STARTS within the object. Comments - * spanning several objects are not supported by this method. - * - * @param object the object whose comment to load. - * @param document the document in which the object is embedded (needed to load the - * comment from the comments.xml part). - * @return the concatenated string of all paragraphs of text within the comment or - * null if the specified object is not commented. - * @throws Docx4JException in case of a Docx4J processing error. - */ - public static Comments.Comment getCommentFor(ContentAccessor object, - WordprocessingMLPackage document) { - try { - for (Object contentObject : object.getContent()) { - if (contentObject instanceof CommentRangeStart) { - try { - BigInteger id = ((CommentRangeStart) contentObject).getId(); - CommentsPart commentsPart = (CommentsPart) document.getParts() - .get(new PartName("/word/comments.xml")); - Comments comments = commentsPart.getContents(); - for (Comments.Comment comment : comments.getComment()) { - if (comment.getId().equals(id)) { - return comment; - } - } - } - catch (InvalidFormatException e) { - logger.warn(String.format( - "Error while searching comment. Skipping object %s.", - object), e); - } - } - } - return null; - } - catch (Docx4JException e) { - throw new DocxStamperException( - "error accessing the comments of the document!", e); - } - } - - public static String getCommentStringFor(ContentAccessor object, - WordprocessingMLPackage document) throws Docx4JException { - Comments.Comment comment = getCommentFor(object, document); - return getCommentString(comment); - } - - /** - * Returns the string value of the specified comment object. - */ - public static String getCommentString(Comments.Comment comment) { - StringBuilder builder = new StringBuilder(); - for (Object commentChildObject : comment.getContent()) { - if (commentChildObject instanceof P) { - builder.append(new ParagraphWrapper((P) commentChildObject).getText()); - } - } - return builder.toString(); - } - - public static void deleteComment(CommentWrapper comment) { - if (comment.getCommentRangeEnd() != null) { - ContentAccessor commentRangeEndParent = (ContentAccessor) comment - .getCommentRangeEnd().getParent(); - commentRangeEndParent.getContent().remove(comment.getCommentRangeEnd()); - deleteCommentReference(commentRangeEndParent, - comment.getCommentRangeEnd().getId()); - } - if (comment.getCommentRangeStart() != null) { - ContentAccessor commentRangeStartParent = (ContentAccessor) comment - .getCommentRangeStart().getParent(); - commentRangeStartParent.getContent().remove(comment.getCommentRangeStart()); - deleteCommentReference(commentRangeStartParent, - comment.getCommentRangeStart().getId()); - } - // TODO: also delete comment from comments.xml - } - - private static boolean deleteCommentReference(ContentAccessor parent, - BigInteger commentId) { - for (int i = 0; i < parent.getContent().size(); i++) { - Object contentObject = XmlUtils.unwrap(parent.getContent().get(i)); - if (contentObject instanceof ContentAccessor) { - if (deleteCommentReference((ContentAccessor) contentObject, commentId)) { - return true; - } - } - if (contentObject instanceof R) { - for (Object runContentObject : ((R) contentObject).getContent()) { - Object unwrapped = XmlUtils.unwrap(runContentObject); - if (unwrapped instanceof R.CommentReference) { - BigInteger foundCommentId = ((R.CommentReference) unwrapped) - .getId(); - if (foundCommentId.equals(commentId)) { - parent.getContent().remove(i); - return true; - } - } - } - } - } - return false; - } - - public static Map getComments( - WordprocessingMLPackage document) { - Map comments = new HashMap<>(); - collectCommentRanges(comments, document); - collectComments(comments, document); - return comments; - } - - private static void collectCommentRanges( - final Map comments, - WordprocessingMLPackage document) { - DocumentWalker documentWalker = new BaseDocumentWalker( - document.getMainDocumentPart()) { - @Override - protected void onCommentRangeStart(CommentRangeStart commentRangeStart) { - CommentWrapper commentWrapper = comments.get(commentRangeStart.getId()); - if (commentWrapper == null) { - commentWrapper = new CommentWrapper(); - comments.put(commentRangeStart.getId(), commentWrapper); - } - commentWrapper.setCommentRangeStart(commentRangeStart); - } - - @Override - protected void onCommentRangeEnd(CommentRangeEnd commentRangeEnd) { - CommentWrapper commentWrapper = comments.get(commentRangeEnd.getId()); - if (commentWrapper == null) { - commentWrapper = new CommentWrapper(); - comments.put(commentRangeEnd.getId(), commentWrapper); - } - commentWrapper.setCommentRangeEnd(commentRangeEnd); - } - }; - documentWalker.walk(); - } - - private static void collectComments(final Map comments, - WordprocessingMLPackage document) { - try { - CommentsPart commentsPart = (CommentsPart) document.getParts() - .get(new PartName("/word/comments.xml")); - if (commentsPart != null) { - for (Comments.Comment comment : commentsPart.getContents().getComment()) { - CommentWrapper commentWrapper = comments.get(comment.getId()); - if (commentWrapper != null) { - commentWrapper.setComment(comment); - } - } - } - } - catch (Docx4JException e) { - throw new IllegalStateException(e); - } - } + private static final Logger logger = LoggerFactory.getLogger(CommentUtil.class); + + private CommentUtil() { + + } + + /** + * Returns the comment the given DOCX4J run is commented with. + * + * @param run the DOCX4J run whose comment to retrieve. + * @param document the document that contains the run. + * @return the comment, if found, null otherwise. + */ + public static Comments.Comment getCommentAround(R run, + WordprocessingMLPackage document) { + try { + if (run instanceof Child) { + Child child = run; + ContentAccessor parent = (ContentAccessor) child.getParent(); + if (parent == null) + return null; + CommentRangeStart possibleComment = null; + boolean foundChild = false; + for (Object contentElement : parent.getContent()) { + + // so first we look for the start of the comment + if (XmlUtils.unwrap(contentElement) instanceof CommentRangeStart) { + possibleComment = (CommentRangeStart) contentElement; + } + // then we check if the child we are looking for is ours + else if (possibleComment != null && child.equals(contentElement)) { + foundChild = true; + } + // and then if we have an end of a comment we are good! + else if (possibleComment != null && foundChild && XmlUtils + .unwrap(contentElement) instanceof CommentRangeEnd) { + try { + BigInteger id = possibleComment.getId(); + CommentsPart commentsPart = (CommentsPart) document.getParts() + .get(new PartName("/word/comments.xml")); + Comments comments = commentsPart.getContents(); + for (Comments.Comment comment : comments.getComment()) { + if (comment.getId().equals(id)) { + return comment; + } + } + } catch (InvalidFormatException e) { + logger.warn(String.format( + "Error while searching comment. Skipping run %s.", + run), e); + } + } + // else restart + else { + possibleComment = null; + foundChild = false; + } + } + } + return null; + } catch (Docx4JException e) { + throw new DocxStamperException( + "error accessing the comments of the document!", e); + } + } + + /** + * Returns the first comment found for the given docx object. Note that an object is + * only considered commented if the comment STARTS within the object. Comments + * spanning several objects are not supported by this method. + * + * @param object the object whose comment to load. + * @param document the document in which the object is embedded (needed to load the + * comment from the comments.xml part). + * @return the concatenated string of all paragraphs of text within the comment or + * null if the specified object is not commented. + * @throws Docx4JException in case of a Docx4J processing error. + */ + public static Comments.Comment getCommentFor(ContentAccessor object, + WordprocessingMLPackage document) { + try { + for (Object contentObject : object.getContent()) { + if (contentObject instanceof CommentRangeStart) { + try { + BigInteger id = ((CommentRangeStart) contentObject).getId(); + CommentsPart commentsPart = (CommentsPart) document.getParts() + .get(new PartName("/word/comments.xml")); + Comments comments = commentsPart.getContents(); + for (Comments.Comment comment : comments.getComment()) { + if (comment.getId().equals(id)) { + return comment; + } + } + } catch (InvalidFormatException e) { + logger.warn(String.format( + "Error while searching comment. Skipping object %s.", + object), e); + } + } + } + return null; + } catch (Docx4JException e) { + throw new DocxStamperException( + "error accessing the comments of the document!", e); + } + } + + public static String getCommentStringFor(ContentAccessor object, + WordprocessingMLPackage document) throws Docx4JException { + Comments.Comment comment = getCommentFor(object, document); + return getCommentString(comment); + } + + /** + * Returns the string value of the specified comment object. + */ + public static String getCommentString(Comments.Comment comment) { + StringBuilder builder = new StringBuilder(); + for (Object commentChildObject : comment.getContent()) { + if (commentChildObject instanceof P) { + builder.append(new ParagraphWrapper((P) commentChildObject).getText()); + } + } + return builder.toString(); + } + + public static void deleteComment(CommentWrapper comment) { + if (comment.getCommentRangeEnd() != null) { + ContentAccessor commentRangeEndParent = (ContentAccessor) comment + .getCommentRangeEnd().getParent(); + commentRangeEndParent.getContent().remove(comment.getCommentRangeEnd()); + deleteCommentReference(commentRangeEndParent, + comment.getCommentRangeEnd().getId()); + } + if (comment.getCommentRangeStart() != null) { + ContentAccessor commentRangeStartParent = (ContentAccessor) comment + .getCommentRangeStart().getParent(); + commentRangeStartParent.getContent().remove(comment.getCommentRangeStart()); + deleteCommentReference(commentRangeStartParent, + comment.getCommentRangeStart().getId()); + } + } + + public static void deleteCommentFromElement(ContentAccessor element, BigInteger commentId) { + List elementsToRemove = new ArrayList<>(); + + for (Object obj : element.getContent()) { + Object unwrapped = XmlUtils.unwrap(obj); + if (unwrapped instanceof CommentRangeStart) { + if (((CommentRangeStart) unwrapped).getId().equals(commentId)) { + elementsToRemove.add(obj); + } + } else if (unwrapped instanceof CommentRangeEnd) { + if (((CommentRangeEnd) unwrapped).getId().equals(commentId)) { + elementsToRemove.add(obj); + } + } else if (unwrapped instanceof R.CommentReference) { + if (((R.CommentReference) unwrapped).getId().equals(commentId)) { + elementsToRemove.add(obj); + } + } else if (unwrapped instanceof ContentAccessor) { + deleteCommentFromElement((ContentAccessor) unwrapped, commentId); + } + } + + element.getContent().removeAll(elementsToRemove); + } + + + private static boolean deleteCommentReference(ContentAccessor parent, + BigInteger commentId) { + for (int i = 0; i < parent.getContent().size(); i++) { + Object contentObject = XmlUtils.unwrap(parent.getContent().get(i)); + if (contentObject instanceof ContentAccessor) { + if (deleteCommentReference((ContentAccessor) contentObject, commentId)) { + return true; + } + } else if (contentObject instanceof R) { + for (Object runContentObject : ((R) contentObject).getContent()) { + Object unwrapped = XmlUtils.unwrap(runContentObject); + if (unwrapped instanceof R.CommentReference && removeCommentReference(parent, commentId, i, (R.CommentReference) unwrapped)) + return true; + } + } else if (contentObject instanceof R.CommentReference) { + if (removeCommentReference(parent, commentId, i, (R.CommentReference) contentObject)) + return true; + } + } + return false; + } + + private static boolean removeCommentReference(ContentAccessor parent, BigInteger commentId, int i, R.CommentReference contentObject) { + BigInteger foundCommentId = contentObject + .getId(); + if (foundCommentId.equals(commentId)) { + parent.getContent().remove(i); + return true; + } + return false; + } + + public static Map getComments( + WordprocessingMLPackage document) { + Map rootComments = new HashMap<>(); + Map allComments = new HashMap<>(); + collectCommentRanges(rootComments, allComments, document); + collectComments(rootComments, allComments, document); + return cleanMalformedComments(rootComments); + } + + private static Map cleanMalformedComments(Map rootComments) { + Map filteredCommentEntries = new HashMap<>(); + + rootComments.forEach((key, comment) -> { + if (isCommentMalformed(comment)) { + logger.error( + "Skipping malformed comment, missing range start and/or range end : {}", + getCommentContent(comment) + ); + } else { + filteredCommentEntries.put(key, comment); + comment.setChildren(cleanMalformedComments(comment.getChildren())); + } + }); + + return filteredCommentEntries; + } + + private static Set cleanMalformedComments(Set children) { + return children.stream().filter(comment -> { + if (isCommentMalformed(comment)) { + logger.error( + "Skipping malformed comment, missing range start and/or range end : {}", + getCommentContent(comment) + ); + return false; + } + comment.setChildren(cleanMalformedComments(comment.getChildren())); + return true; + }).collect(Collectors.toSet()); + } + + private static String getCommentContent(CommentWrapper comment) { + return comment.getComment() != null + ? comment.getComment().getContent().stream().map(TextUtils::getText).collect(Collectors.joining("")) + : ""; + } + + private static boolean isCommentMalformed(CommentWrapper comment) { + return comment.getCommentRangeStart() == null || comment.getCommentRangeEnd() == null || comment.getComment() == null; + } + + private static void collectCommentRanges( + Map rootComments, final Map allComments, + WordprocessingMLPackage document) { + Stack stack = new Stack<>(); + DocumentWalker documentWalker = new BaseDocumentWalker( + document.getMainDocumentPart()) { + @Override + protected void onCommentRangeStart(CommentRangeStart commentRangeStart) { + CommentWrapper commentWrapper = allComments.get(commentRangeStart.getId()); + if (commentWrapper == null) { + commentWrapper = new CommentWrapper(); + allComments.put(commentRangeStart.getId(), commentWrapper); + if (stack.isEmpty()) { + rootComments.put(commentRangeStart.getId(), commentWrapper); + } else { + stack.peek().getChildren().add(commentWrapper); + } + } + commentWrapper.setCommentRangeStart(commentRangeStart); + stack.push(commentWrapper); + } + + @Override + protected void onCommentRangeEnd(CommentRangeEnd commentRangeEnd) { + CommentWrapper commentWrapper = allComments.get(commentRangeEnd.getId()); + if (commentWrapper == null) { + throw new RuntimeException("Found a comment range end before the comment range start !"); + } + commentWrapper.setCommentRangeEnd(commentRangeEnd); + if (!stack.isEmpty()) { + if (stack.peek().equals(commentWrapper)) { + stack.pop(); + } else { + throw new RuntimeException("Cannot figure which comment contains the other !"); + } + } + } + }; + documentWalker.walk(); + } + + private static void collectComments(final Map rootComments, + Map allComments, WordprocessingMLPackage document) { + try { + CommentsPart commentsPart = (CommentsPart) document.getParts() + .get(new PartName("/word/comments.xml")); + if (commentsPart != null) { + for (Comments.Comment comment : commentsPart.getContents().getComment()) { + CommentWrapper commentWrapper = allComments.get(comment.getId()); + if (commentWrapper != null) { + commentWrapper.setComment(comment); + } + } + } + } catch (Docx4JException e) { + throw new IllegalStateException(e); + } + } } diff --git a/src/main/java/org/wickedsource/docxstamper/util/CommentWrapper.java b/src/main/java/org/wickedsource/docxstamper/util/CommentWrapper.java index 50d0e8fb..3bfde03c 100644 --- a/src/main/java/org/wickedsource/docxstamper/util/CommentWrapper.java +++ b/src/main/java/org/wickedsource/docxstamper/util/CommentWrapper.java @@ -4,6 +4,9 @@ import org.docx4j.wml.CommentRangeStart; import org.docx4j.wml.Comments; +import java.util.HashSet; +import java.util.Set; + public class CommentWrapper { private Comments.Comment comment; @@ -12,6 +15,8 @@ public class CommentWrapper { private CommentRangeEnd commentRangeEnd; + private Set children = new HashSet(); + public CommentWrapper(Comments.Comment comment, CommentRangeStart commentRangeStart, CommentRangeEnd commentRangeEnd) { this.comment = comment; this.commentRangeStart = commentRangeStart; @@ -33,6 +38,10 @@ public CommentRangeEnd getCommentRangeEnd() { return commentRangeEnd; } + public Set getChildren() { + return children; + } + void setComment(Comments.Comment comment) { this.comment = comment; } @@ -44,4 +53,8 @@ void setCommentRangeStart(CommentRangeStart commentRangeStart) { void setCommentRangeEnd(CommentRangeEnd commentRangeEnd) { this.commentRangeEnd = commentRangeEnd; } + + void setChildren(Set children) { + this.children = children; + } } diff --git a/src/main/java/org/wickedsource/docxstamper/util/DocumentUtil.java b/src/main/java/org/wickedsource/docxstamper/util/DocumentUtil.java new file mode 100644 index 00000000..5b2c49a2 --- /dev/null +++ b/src/main/java/org/wickedsource/docxstamper/util/DocumentUtil.java @@ -0,0 +1,170 @@ +package org.wickedsource.docxstamper.util; + +import org.docx4j.dml.Graphic; +import org.docx4j.dml.wordprocessingDrawing.Inline; +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.Part; +import org.docx4j.wml.ContentAccessor; +import org.docx4j.wml.Drawing; +import org.docx4j.wml.R; +import org.wickedsource.docxstamper.replace.typeresolver.image.ImageResolver; + +import javax.xml.bind.JAXBElement; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class DocumentUtil { + /** + * Recursively walk through the content accessor to replace embedded images and import the matching + * files to the destination document before importing content. + * + * @param sourceDocument document to import. + * @param destDocument document to add the source document content to. + * @return the whole content of the source document with imported images replaced. + * @throws Exception + */ + public static List prepareDocumentForInsert(WordprocessingMLPackage sourceDocument, WordprocessingMLPackage destDocument) throws Exception { + return walkObjectsAndImportImages(sourceDocument.getMainDocumentPart(), sourceDocument, destDocument); + } + + /** + * Recursively walk through the content accessor to replace embedded images and import the matching + * files to the destination document. + * + * @param sourceContainer source container to walk. + * @param sourceDocument source document containing image files. + * @param destDocument destination document to add image files to. + * @return the list of imported objects from the source container. + * @throws Exception + */ + public static List walkObjectsAndImportImages(ContentAccessor sourceContainer, WordprocessingMLPackage sourceDocument, WordprocessingMLPackage destDocument) throws Exception { + List result = new ArrayList<>(); + for (Object obj : sourceContainer.getContent()) { + if (obj instanceof R && isImageRun((R) obj)) { + byte[] imageData = getRunDrawingData((R) obj, sourceDocument); + // TODO : retrieve filename, altText and width from source document + result.add(ImageResolver.createRunWithImage(destDocument, imageData, null, null, null)); + } else if (obj instanceof ContentAccessor) { + List importedChildren = walkObjectsAndImportImages((ContentAccessor) obj, sourceDocument, destDocument); + ((ContentAccessor) obj).getContent().clear(); + ((ContentAccessor) obj).getContent().addAll(importedChildren); + result.add(obj); + } else { + result.add(obj); + } + } + return result; + } + + /** + * Extract an image bytes from an embedded image run. + * + * @param run run containing the embedded drawing. + * @param document document to read the file from. + * @return + * @throws Docx4JException + * @throws IOException + */ + private static byte[] getRunDrawingData(R run, WordprocessingMLPackage document) throws Docx4JException, IOException { + for (Object runElement : run.getContent()) { + if (runElement instanceof JAXBElement && ((JAXBElement) runElement).getValue() instanceof Drawing) { + Drawing drawing = (Drawing) ((JAXBElement) runElement).getValue(); + byte[] imageData = getImageData(document, drawing); + return imageData; + } + } + throw new RuntimeException("Run drawing not found !"); + } + + /** + * Retrieve an image bytes from the document part store. + * + * @param document document to read the file from. + * @param drawing drawing embedding the image. + * @return + * @throws IOException + * @throws Docx4JException + */ + private static byte[] getImageData(WordprocessingMLPackage document, Drawing drawing) throws IOException, Docx4JException { + String imageRelId = getImageRelationshipId(drawing); + Part imageRelPart = document.getMainDocumentPart().getRelationshipsPart().getPart(imageRelId); + // TODO : find a better way to find image rel part name in source part store + String imageRelPartName = imageRelPart.getPartName().getName().substring(1); + byte[] imageData = streamToByteArray( + document.getSourcePartStore().getPartSize(imageRelPartName), + document.getSourcePartStore().loadPart(imageRelPartName) + ); + return imageData; + } + + /** + * Check if a run contains an embedded image. + * + * @param run + * @return true if the run contains an image, false otherwise. + */ + private static boolean isImageRun(R run) { + for (Object runElement : run.getContent()) { + if (runElement instanceof JAXBElement && ((JAXBElement) runElement).getValue() instanceof Drawing) { + return true; + } + } + return false; + } + + /** + * Retrieve an embedded drawing relationship id. + * + * @param drawing the drawing to get the relationship id. + * @return + */ + public static String getImageRelationshipId(Drawing drawing) { + Graphic graphic = getInlineGraphic(drawing); + return graphic.getGraphicData().getPic().getBlipFill().getBlip().getEmbed(); + } + + /** + * Extract an inline graphic from a drawing. + * + * @param drawing the drawing containing the graphic. + * @return + */ + private static Graphic getInlineGraphic(Drawing drawing) { + if (drawing.getAnchorOrInline().isEmpty()) { + throw new RuntimeException("Anchor or Inline is empty !"); + } + Object anchorOrInline = drawing.getAnchorOrInline().get(0); + if (anchorOrInline instanceof Inline) { + Inline inline = ((Inline) anchorOrInline); + return inline.getGraphic(); + } else { + throw new RuntimeException("Don't know how to process anchor !"); + } + } + + /** + * Converts an InputStream to byte array. + * + * @param size expected size of the byte array. + * @param is input stream to read data from. + * @return the data from the input stream. + * @throws IOException + */ + private static byte[] streamToByteArray(long size, InputStream is) throws IOException { + if (size > Integer.MAX_VALUE) { + throw new RuntimeException("Image size exceeds maximum allowed (2GB)"); + } + int intSize = (int) size; + byte[] data = new byte[intSize]; + int offset = 0; + int numRead; + while ((numRead = is.read(data, offset, intSize - offset)) > 0) { + offset += numRead; + } + is.close(); + return data; + } +} diff --git a/src/main/java/org/wickedsource/docxstamper/util/ObjectDeleter.java b/src/main/java/org/wickedsource/docxstamper/util/ObjectDeleter.java index a70faf81..47deedcd 100644 --- a/src/main/java/org/wickedsource/docxstamper/util/ObjectDeleter.java +++ b/src/main/java/org/wickedsource/docxstamper/util/ObjectDeleter.java @@ -7,15 +7,16 @@ import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableRowCoordinates; - +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ObjectDeleter { private WordprocessingMLPackage document; - private int objectsDeletedFromMainDocument = 0; + private List deletedObjectsIndexes = new ArrayList<>(10); private Map deletedObjectsPerParent = new HashMap<>(); @@ -26,9 +27,9 @@ public ObjectDeleter(WordprocessingMLPackage document) { public void deleteParagraph(ParagraphCoordinates paragraphCoordinates) { if (paragraphCoordinates.getParentTableCellCoordinates() == null) { // global paragraph - int indexToDelete = paragraphCoordinates.getIndex() - objectsDeletedFromMainDocument; + int indexToDelete = getOffset(paragraphCoordinates.getIndex()); document.getMainDocumentPart().getContent().remove(indexToDelete); - objectsDeletedFromMainDocument++; + deletedObjectsIndexes.add(paragraphCoordinates.getIndex()); } else { // paragraph within a table cell Tc parentCell = paragraphCoordinates.getParentTableCellCoordinates().getCell(); @@ -47,15 +48,33 @@ private void deleteFromCell(Tc cell, int index) { TableCellUtil.addEmptyParagraph(cell); } deletedObjectsPerParent.put(cell, objectsDeletedFromParent + 1); - // TODO: find out why border lines are removed in some cells after having deleted a paragraph + // TODO: find out why border lines are removed in some cells after having + // deleted a paragraph + } + + /** + * Get new index of element to be deleted, taking into account previously + * deleted elements + * + * @param initialIndex initial index of the element to be deleted + * @return the index of the item to be removed + */ + private int getOffset(final int initialIndex) { + int newIndex = initialIndex; + for (Integer deletedIndex : this.deletedObjectsIndexes) { + if (initialIndex > deletedIndex) { + newIndex--; + } + } + return newIndex; } public void deleteTable(TableCoordinates tableCoordinates) { if (tableCoordinates.getParentTableCellCoordinates() == null) { // global table - int indexToDelete = tableCoordinates.getIndex() - objectsDeletedFromMainDocument; + int indexToDelete = getOffset(tableCoordinates.getIndex()); document.getMainDocumentPart().getContent().remove(indexToDelete); - objectsDeletedFromMainDocument++; + deletedObjectsIndexes.add(tableCoordinates.getIndex()); } else { // nested table within an table cell Tc parentCell = tableCoordinates.getParentTableCellCoordinates().getCell(); diff --git a/src/main/java/org/wickedsource/docxstamper/util/ParagraphUtil.java b/src/main/java/org/wickedsource/docxstamper/util/ParagraphUtil.java index fb06994a..b17f66cd 100644 --- a/src/main/java/org/wickedsource/docxstamper/util/ParagraphUtil.java +++ b/src/main/java/org/wickedsource/docxstamper/util/ParagraphUtil.java @@ -1,6 +1,12 @@ package org.wickedsource.docxstamper.util; +import java.util.ArrayList; +import java.util.List; +import org.docx4j.TraversalUtil; +import org.docx4j.finders.ClassFinder; import org.docx4j.jaxb.Context; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.CTTxbxContent; import org.docx4j.wml.ObjectFactory; import org.docx4j.wml.P; import org.docx4j.wml.R; @@ -28,5 +34,22 @@ public static P create(String... texts) { return p; } + /** + * Finds all Paragraphs in a Document which are in a TextBox + * @param document + * @return + */ + public static List getAllTextBoxes(WordprocessingMLPackage document) { + ClassFinder finder = new ClassFinder(P.class); // docx4j class + //necessary even if not used + new TraversalUtil(document.getMainDocumentPart().getContent(),finder); // docx4j class + ArrayList result = new ArrayList<>(finder.results.size()); + for (Object o : finder.results) { + if (o instanceof P && ((P) o).getParent() instanceof CTTxbxContent) { + result.add(o); + } + } + return result; + } } diff --git a/src/main/java/org/wickedsource/docxstamper/util/walk/CoordinatesWalker.java b/src/main/java/org/wickedsource/docxstamper/util/walk/CoordinatesWalker.java index 30b95bb2..34e0b4d7 100644 --- a/src/main/java/org/wickedsource/docxstamper/util/walk/CoordinatesWalker.java +++ b/src/main/java/org/wickedsource/docxstamper/util/walk/CoordinatesWalker.java @@ -13,6 +13,7 @@ import javax.xml.bind.JAXBElement; import java.util.ArrayList; import java.util.List; +import org.wickedsource.docxstamper.util.ParagraphUtil; public abstract class CoordinatesWalker { @@ -35,6 +36,9 @@ public void walk() { // walk through elements in main document part walkContent(document.getMainDocumentPart().getContent()); + + // walk through textboxes in main document + walkContent(ParagraphUtil.getAllTextBoxes(document)); // walk through elements in headers List footerRelationships = getRelationshipsOfType(document, Namespaces.FOOTER); diff --git a/src/main/java/org/wickedsource/docxstamper/util/walk/DocumentWalker.java b/src/main/java/org/wickedsource/docxstamper/util/walk/DocumentWalker.java index 1f1bb102..6a2cb939 100644 --- a/src/main/java/org/wickedsource/docxstamper/util/walk/DocumentWalker.java +++ b/src/main/java/org/wickedsource/docxstamper/util/walk/DocumentWalker.java @@ -41,8 +41,9 @@ public void walk() { private void walkTable(Tbl table) { onTable(table); for (Object contentElement : table.getContent()) { - if (XmlUtils.unwrap(contentElement) instanceof Tr) { - Tr row = (Tr) contentElement; + Object unwrappedObject = XmlUtils.unwrap(contentElement); + if (unwrappedObject instanceof Tr) { + Tr row = (Tr) unwrappedObject; walkTableRow(row); } } @@ -52,8 +53,9 @@ private void walkTable(Tbl table) { private void walkTableRow(Tr row) { onTableRow(row); for (Object rowContentElement : row.getContent()) { - if (XmlUtils.unwrap(rowContentElement) instanceof Tc) { - Tc cell = rowContentElement instanceof Tc ? (Tc) rowContentElement : (Tc) ((JAXBElement) rowContentElement).getValue(); + Object unwrappedObject = XmlUtils.unwrap(rowContentElement); + if (unwrappedObject instanceof Tc) { + Tc cell = (Tc) unwrappedObject; walkTableCell(cell); } } @@ -62,12 +64,19 @@ private void walkTableRow(Tr row) { private void walkTableCell(Tc cell) { onTableCell(cell); for (Object cellContentElement : cell.getContent()) { - if (XmlUtils.unwrap(cellContentElement) instanceof P) { + Object unwrappedObject = XmlUtils.unwrap(cellContentElement); + if (unwrappedObject instanceof P) { P p = (P) cellContentElement; walkParagraph(p); - } else if (XmlUtils.unwrap(cellContentElement) instanceof Tbl) { - Tbl nestedTable = (Tbl) ((JAXBElement) cellContentElement).getValue(); + } else if (unwrappedObject instanceof Tbl) { + Tbl nestedTable = (Tbl) unwrappedObject; walkTable(nestedTable); + } else if (unwrappedObject instanceof CommentRangeStart) { + CommentRangeStart commentRangeStart = (CommentRangeStart) unwrappedObject; + onCommentRangeStart(commentRangeStart); + } else if (unwrappedObject instanceof CommentRangeEnd) { + CommentRangeEnd commentRangeEnd = (CommentRangeEnd) unwrappedObject; + onCommentRangeEnd(commentRangeEnd); } } } @@ -75,11 +84,12 @@ private void walkTableCell(Tc cell) { private void walkParagraph(P p) { onParagraph(p); for (Object element : p.getContent()) { - if (XmlUtils.unwrap(element) instanceof CommentRangeStart) { - CommentRangeStart commentRangeStart = (CommentRangeStart) element; + Object unwrappedObject = XmlUtils.unwrap(element); + if (unwrappedObject instanceof CommentRangeStart) { + CommentRangeStart commentRangeStart = (CommentRangeStart) unwrappedObject; onCommentRangeStart(commentRangeStart); - } else if (XmlUtils.unwrap(element) instanceof CommentRangeEnd) { - CommentRangeEnd commentRangeEnd = (CommentRangeEnd) element; + } else if (unwrappedObject instanceof CommentRangeEnd) { + CommentRangeEnd commentRangeEnd = (CommentRangeEnd) unwrappedObject; onCommentRangeEnd(commentRangeEnd); } } diff --git a/src/test/java/org/wickedsource/docxstamper/AbstractDocx4jTest.java b/src/test/java/org/wickedsource/docxstamper/AbstractDocx4jTest.java index 4588f329..6fc89a57 100644 --- a/src/test/java/org/wickedsource/docxstamper/AbstractDocx4jTest.java +++ b/src/test/java/org/wickedsource/docxstamper/AbstractDocx4jTest.java @@ -12,7 +12,7 @@ */ public abstract class AbstractDocx4jTest { - private Logger logger = LoggerFactory.getLogger(AbstractDocx4jTest.class); + private final Logger logger = LoggerFactory.getLogger(AbstractDocx4jTest.class); private File tempFile; @@ -41,7 +41,7 @@ protected WordprocessingMLPackage saveAndLoadDocument(WordprocessingMLPackage do * object structure were really transported into the XML of the .docx file. */ protected WordprocessingMLPackage stampAndLoad(InputStream template, T contextRoot) throws IOException, Docx4JException { - return stampAndLoad(template, contextRoot, new DocxStamperConfiguration()); + return stampAndLoad(template, contextRoot, new DocxStamperConfiguration().setFailOnUnresolvedExpression(false)); } protected WordprocessingMLPackage stampAndLoad(InputStream template, T contextRoot, DocxStamperConfiguration config) throws IOException, Docx4JException { diff --git a/src/test/java/org/wickedsource/docxstamper/ConditionalDisplayOfTablesBug32Test.java b/src/test/java/org/wickedsource/docxstamper/ConditionalDisplayOfTablesBug32Test.java new file mode 100644 index 00000000..60df53a7 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/ConditionalDisplayOfTablesBug32Test.java @@ -0,0 +1,49 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.P; +import org.docx4j.wml.Tbl; +import org.docx4j.wml.Tc; +import org.docx4j.wml.Tr; +import org.junit.Assert; +import org.junit.Test; +import org.wickedsource.docxstamper.context.NameContext; +import org.wickedsource.docxstamper.util.ParagraphWrapper; + +import javax.xml.bind.JAXBElement; +import java.io.IOException; +import java.io.InputStream; + +public class ConditionalDisplayOfTablesBug32Test extends AbstractDocx4jTest { + + @Test + public void test() throws Docx4JException, IOException { + NameContext context = new NameContext(); + context.setName("Homer"); + InputStream template = getClass().getResourceAsStream("ConditionalDisplayOfTablesBug32Test.docx"); + WordprocessingMLPackage document = stampAndLoad(template, context); + globalTablesAreRemoved(document); + nestedTablesAreRemoved(document); + } + + private void globalTablesAreRemoved(WordprocessingMLPackage document) { + Assert.assertTrue(document.getMainDocumentPart().getContent().get(1) instanceof P); + P p1 = (P) document.getMainDocumentPart().getContent().get(1); + Tbl table2 = (Tbl) ((JAXBElement) document.getMainDocumentPart().getContent().get(3)).getValue(); + Tbl table3 = (Tbl) ((JAXBElement) document.getMainDocumentPart().getContent().get(5)).getValue(); + P p4 = (P) document.getMainDocumentPart().getContent().get(7); + + Assert.assertEquals("This paragraph stays untouched.", new ParagraphWrapper(p1).getText()); + Assert.assertNotNull(table2); + Assert.assertNotNull(table3); + Assert.assertEquals("This paragraph stays untouched.", new ParagraphWrapper(p4).getText()); + } + + private void nestedTablesAreRemoved(WordprocessingMLPackage document) { + Tbl outerTable = (Tbl) ((JAXBElement) document.getMainDocumentPart().getContent().get(3)).getValue(); + Tc cell = (Tc) ((JAXBElement) ((Tr) outerTable.getContent().get(1)).getContent().get(1)).getValue(); + Assert.assertEquals("", new ParagraphWrapper((P) cell.getContent().get(0)).getText()); // empty paragraph, since the last element inside the cell was removed + } + +} \ No newline at end of file diff --git a/src/test/java/org/wickedsource/docxstamper/CustomCommentProcessorTest.java b/src/test/java/org/wickedsource/docxstamper/CustomCommentProcessorTest.java index 96e3194a..499eb07e 100644 --- a/src/test/java/org/wickedsource/docxstamper/CustomCommentProcessorTest.java +++ b/src/test/java/org/wickedsource/docxstamper/CustomCommentProcessorTest.java @@ -7,10 +7,13 @@ import org.wickedsource.docxstamper.api.commentprocessor.ICommentProcessor; import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates; import org.wickedsource.docxstamper.api.coordinates.RunCoordinates; +import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; +import org.wickedsource.docxstamper.processor.BaseCommentProcessor; import org.wickedsource.docxstamper.util.CommentWrapper; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; @@ -18,30 +21,36 @@ public class CustomCommentProcessorTest extends AbstractDocx4jTest { @Test public void test() throws Docx4JException, IOException { - CustomCommentProcessor processor = new CustomCommentProcessor(); DocxStamperConfiguration config = new DocxStamperConfiguration() - .addCommentProcessor(ICustomCommentProcessor.class, processor); + .addCommentProcessor(ICustomCommentProcessor.class, CustomCommentProcessor.class); InputStream template = getClass().getResourceAsStream("CustomCommentProcessorTest.docx"); - stampAndLoad(template, new EmptyContext(), config); + OutputStream out = getOutputStream(); + DocxStamper stamper = new DocxStamper(config); + stamper.stamp(template, new EmptyContext(), out); + CustomCommentProcessor processor = (CustomCommentProcessor) stamper.getCommentProcessorInstance(ICustomCommentProcessor.class); Assert.assertEquals(2, processor.getVisitedParagraphs().size()); } - static class EmptyContext{ + static class EmptyContext { } - public interface ICustomCommentProcessor { + public interface ICustomCommentProcessor extends ICommentProcessor { void visitParagraph(); } - public static class CustomCommentProcessor implements ICommentProcessor, ICustomCommentProcessor{ + public static class CustomCommentProcessor extends BaseCommentProcessor implements ICustomCommentProcessor { - private List visitedParagraphs = new ArrayList<>(); + private final List visitedParagraphs = new ArrayList<>(); private ParagraphCoordinates currentParagraph; + public CustomCommentProcessor(DocxStamperConfiguration config, TypeResolverRegistry typeResolverRegistry) { + super(config, typeResolverRegistry); + } + @Override public void commitChanges(WordprocessingMLPackage document) { @@ -62,6 +71,11 @@ public void setCurrentCommentWrapper(CommentWrapper commentWrapper) { } + @Override + public void setDocument(WordprocessingMLPackage document) { + + } + @Override public void reset() { diff --git a/src/test/java/org/wickedsource/docxstamper/ExpressionReplacementInTextBoxesTest.java b/src/test/java/org/wickedsource/docxstamper/ExpressionReplacementInTextBoxesTest.java new file mode 100644 index 00000000..d45ed342 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/ExpressionReplacementInTextBoxesTest.java @@ -0,0 +1,37 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.P; +import org.junit.Assert; +import org.junit.Test; +import org.wickedsource.docxstamper.context.NameContext; +import org.wickedsource.docxstamper.util.ParagraphWrapper; + +import java.io.IOException; +import java.io.InputStream; +import org.wickedsource.docxstamper.util.ParagraphUtil; + +public class ExpressionReplacementInTextBoxesTest extends AbstractDocx4jTest { + + @Test + public void test() throws Docx4JException, IOException { + NameContext context = new NameContext(); + context.setName("Bart Simpson"); + InputStream template = getClass().getResourceAsStream("ExpressionReplacementInTextBoxesTest.docx"); + WordprocessingMLPackage document = stampAndLoad(template, context); + resolvedExpressionsAreReplacedInFirstLevelTextBox(document); + unresolvedExpressionsAreNotReplacedInFirstTextBox(document); + } + + private void resolvedExpressionsAreReplacedInFirstLevelTextBox(WordprocessingMLPackage document) { + P nameParagraph = (P) ParagraphUtil.getAllTextBoxes(document).get(0); + Assert.assertEquals("Bart Simpson", new ParagraphWrapper(nameParagraph).getText()); + } + + private void unresolvedExpressionsAreNotReplacedInFirstTextBox(WordprocessingMLPackage document) { + P nameParagraph = (P) ParagraphUtil.getAllTextBoxes(document).get(2); + Assert.assertEquals("${foo}", new ParagraphWrapper(nameParagraph).getText()); + } + +} diff --git a/src/test/java/org/wickedsource/docxstamper/LeaveEmptyOnExpressionErrorTest.java b/src/test/java/org/wickedsource/docxstamper/LeaveEmptyOnExpressionErrorTest.java index ab83eada..63c7c2ae 100644 --- a/src/test/java/org/wickedsource/docxstamper/LeaveEmptyOnExpressionErrorTest.java +++ b/src/test/java/org/wickedsource/docxstamper/LeaveEmptyOnExpressionErrorTest.java @@ -1,9 +1,5 @@ package org.wickedsource.docxstamper; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.P; @@ -12,25 +8,30 @@ import org.wickedsource.docxstamper.context.NameContext; import org.wickedsource.docxstamper.util.ParagraphWrapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + public class LeaveEmptyOnExpressionErrorTest extends AbstractDocx4jTest { - @Test - public void test() throws Docx4JException, IOException { - NameContext context = new NameContext(); - context.setName("Homer Simpson"); - InputStream template = getClass().getResourceAsStream("LeaveEmptyOnExpressionErrorTest.docx"); - OutputStream out = getOutputStream(); - DocxStamper stamper = new DocxStamperConfiguration() - .leaveEmptyOnExpressionError(true) - .build(); - stamper.stamp(template, context, out); - InputStream in = getInputStream(out); - WordprocessingMLPackage document = WordprocessingMLPackage.load(in); - resolvedExpressionsAreReplaced(document); - } + @Test + public void test() throws Docx4JException, IOException { + NameContext context = new NameContext(); + context.setName("Homer Simpson"); + InputStream template = getClass().getResourceAsStream("LeaveEmptyOnExpressionErrorTest.docx"); + OutputStream out = getOutputStream(); + DocxStamper stamper = new DocxStamperConfiguration() + .setFailOnUnresolvedExpression(false) + .leaveEmptyOnExpressionError(true) + .build(); + stamper.stamp(template, context, out); + InputStream in = getInputStream(out); + WordprocessingMLPackage document = WordprocessingMLPackage.load(in); + resolvedExpressionsAreReplaced(document); + } - private void resolvedExpressionsAreReplaced(WordprocessingMLPackage document) { - P nameParagraph = (P) document.getMainDocumentPart().getContent().get(0); - Assert.assertEquals("Leave me empty .", new ParagraphWrapper(nameParagraph).getText()); - } + private void resolvedExpressionsAreReplaced(WordprocessingMLPackage document) { + P nameParagraph = (P) document.getMainDocumentPart().getContent().get(0); + Assert.assertEquals("Leave me empty .", new ParagraphWrapper(nameParagraph).getText()); + } } diff --git a/src/test/java/org/wickedsource/docxstamper/MapAccessorAndReflectivePropertyAccessorTest.java b/src/test/java/org/wickedsource/docxstamper/MapAccessorAndReflectivePropertyAccessorTest.java new file mode 100644 index 00000000..d3a11e78 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/MapAccessorAndReflectivePropertyAccessorTest.java @@ -0,0 +1,58 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.P; +import org.docx4j.wml.R; +import org.docx4j.wml.Tbl; +import org.docx4j.wml.Text; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.context.expression.MapAccessor; + +import javax.xml.bind.JAXBElement; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MapAccessorAndReflectivePropertyAccessorTest extends AbstractDocx4jTest { + class Container { + public String value; + + public Container(String value) { + this.value = value; + } + } + + @Test + public void shouldResolveMapAndPropertyPlaceholders() throws Docx4JException, IOException { + List listProp = new ArrayList<>(); + listProp.add(new Container("first value")); + listProp.add(new Container("second value")); + + Map context = new HashMap<>(); + context.put("FLAT_STRING", "Flat string has been resolved"); + context.put("OBJECT_LIST_PROP", listProp); + + DocxStamperConfiguration config = new DocxStamperConfiguration() + .setFailOnUnresolvedExpression(false) + .setLineBreakPlaceholder("\n") + .replaceNullValues(true) + .nullValuesDefault("N/C") + .replaceUnresolvedExpressions(true) + .unresolvedExpressionsDefaultValue("N/C") + .setEvaluationContextConfigurer(ctx -> ctx.addPropertyAccessor(new MapAccessor())); + + InputStream template = getClass().getResourceAsStream("MapAccessorAndReflectivePropertyAccessorTest.docx"); + WordprocessingMLPackage result = stampAndLoad(template, context, config); + List contents = result.getMainDocumentPart().getContent(); + + Assert.assertEquals("Flat string has been resolved", ((Text) ((JAXBElement) ((R) ((P) contents.get(0)).getContent().get(1)).getContent().get(0)).getValue()).getValue()); + Assert.assertEquals(3, ((Tbl) ((JAXBElement) contents.get(2)).getValue()).getContent().size()); + Assert.assertEquals("first value", ((Text) ((JAXBElement) ((R) ((P) contents.get(6)).getContent().get(0)).getContent().get(0)).getValue()).getValue()); + Assert.assertEquals("second value", ((Text) ((JAXBElement) ((R) ((P) contents.get(9)).getContent().get(0)).getContent().get(0)).getValue()).getValue()); + } +} diff --git a/src/test/java/org/wickedsource/docxstamper/MultiStampTest.java b/src/test/java/org/wickedsource/docxstamper/MultiStampTest.java index 288db23e..68bc1d50 100644 --- a/src/test/java/org/wickedsource/docxstamper/MultiStampTest.java +++ b/src/test/java/org/wickedsource/docxstamper/MultiStampTest.java @@ -1,11 +1,5 @@ package org.wickedsource.docxstamper; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.junit.Assert; @@ -14,59 +8,65 @@ import org.wickedsource.docxstamper.util.walk.BaseCoordinatesWalker; import org.wickedsource.docxstamper.util.walk.CoordinatesWalker; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + public class MultiStampTest extends AbstractDocx4jTest { - @Test - public void expressionsAreResolvedOnMultiStamp() throws Docx4JException, IOException { - DocxStamper stamper = new DocxStamper(new DocxStamperConfiguration()); - NamesContext context = new NamesContext(); + @Test + public void expressionsAreResolvedOnMultiStamp() throws Docx4JException, IOException { + DocxStamper stamper = new DocxStamper(new DocxStamperConfiguration().setFailOnUnresolvedExpression(false)); + NamesContext context = new NamesContext(); - InputStream template = getClass().getResourceAsStream("MultiStampTest.docx"); - OutputStream out = getOutputStream(); - stamper.stamp(template, context, out); - InputStream in = getInputStream(out); - WordprocessingMLPackage document = WordprocessingMLPackage.load(in); - assertTableRows(document); + InputStream template = getClass().getResourceAsStream("MultiStampTest.docx"); + OutputStream out = getOutputStream(); + stamper.stamp(template, context, out); + InputStream in = getInputStream(out); + WordprocessingMLPackage document = WordprocessingMLPackage.load(in); + assertTableRows(document); - template = getClass().getResourceAsStream("MultiStampTest.docx"); - out = getOutputStream(); - stamper.stamp(template, context, out); - in = getInputStream(out); - document = WordprocessingMLPackage.load(in); - assertTableRows(document); - } + template = getClass().getResourceAsStream("MultiStampTest.docx"); + out = getOutputStream(); + stamper.stamp(template, context, out); + in = getInputStream(out); + document = WordprocessingMLPackage.load(in); + assertTableRows(document); + } - private void assertTableRows(WordprocessingMLPackage document) { - final List cells = new ArrayList<>(); - CoordinatesWalker cellWalker = new BaseCoordinatesWalker(document) { - @Override - protected void onTableCell(TableCellCoordinates tableCellCoordinates) { - cells.add(tableCellCoordinates); - } - }; - cellWalker.walk(); + private void assertTableRows(WordprocessingMLPackage document) { + final List cells = new ArrayList<>(); + CoordinatesWalker cellWalker = new BaseCoordinatesWalker(document) { + @Override + protected void onTableCell(TableCellCoordinates tableCellCoordinates) { + cells.add(tableCellCoordinates); + } + }; + cellWalker.walk(); - Assert.assertEquals(5, cells.size()); - } + Assert.assertEquals(5, cells.size()); + } - public static class NamesContext { - private List names = new ArrayList<>(); + public static class NamesContext { + private List names = new ArrayList<>(); - public NamesContext() { - this.names.add("Homer"); - this.names.add("Marge"); - this.names.add("Bart"); - this.names.add("Lisa"); - this.names.add("Maggie"); - } + public NamesContext() { + this.names.add("Homer"); + this.names.add("Marge"); + this.names.add("Bart"); + this.names.add("Lisa"); + this.names.add("Maggie"); + } - public List getNames() { - return names; - } + public List getNames() { + return names; + } - public void setNames(List names) { - this.names = names; - } - } + public void setNames(List names) { + this.names = names; + } + } } diff --git a/src/test/java/org/wickedsource/docxstamper/RepeatDocPartAndCommentProcessorsIsolationTest.java b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartAndCommentProcessorsIsolationTest.java new file mode 100644 index 00000000..48a82d41 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartAndCommentProcessorsIsolationTest.java @@ -0,0 +1,96 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.XmlUtils; +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.Tbl; +import org.docx4j.wml.Tc; +import org.docx4j.wml.Tr; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.context.expression.MapAccessor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RepeatDocPartAndCommentProcessorsIsolationTest extends AbstractDocx4jTest { + + static class TableValue { + public String value; + + TableValue(String value) { + this.value = value; + } + } + + @Test + public void repeatDocPartShouldNotUseSameCommentProcessorInstancesForSubtemplate() throws Docx4JException, IOException { + Map context = new HashMap<>(); + + List firstTable = new ArrayList<>(); + firstTable.add(new TableValue("firstTable value1")); + firstTable.add(new TableValue("firstTable value2")); + + List secondTable = new ArrayList<>(); + secondTable.add(new TableValue("repeatDocPart value1")); + secondTable.add(new TableValue("repeatDocPart value2")); + secondTable.add(new TableValue("repeatDocPart value3")); + + List thirdTable = new ArrayList<>(); + thirdTable.add(new TableValue("secondTable value1")); + thirdTable.add(new TableValue("secondTable value2")); + thirdTable.add(new TableValue("secondTable value3")); + thirdTable.add(new TableValue("secondTable value4")); + + context.put("firstTable", firstTable); + context.put("secondTable", secondTable); + context.put("thirdTable", thirdTable); + + InputStream template = getClass().getResourceAsStream("RepeatDocPartAndCommentProcessorsIsolationTest.docx"); + DocxStamperConfiguration config = new DocxStamperConfiguration(); + config.setEvaluationContextConfigurer((ctx) -> ctx.addPropertyAccessor(new MapAccessor())); + WordprocessingMLPackage document = stampAndLoad(template, context, config); + + List documentContent = document.getMainDocumentPart().getContent(); + + Assert.assertEquals(19, documentContent.size()); + + Assert.assertEquals("This will stay untouched.", documentContent.get(0).toString()); + Assert.assertEquals("This will also stay untouched.", documentContent.get(4).toString()); + Assert.assertEquals("This will stay untouched too.", documentContent.get(18).toString()); + + // checking table before repeating paragraph + Tbl table1 = (Tbl) XmlUtils.unwrap(documentContent.get(2)); + checkTableAgainstContextValues(firstTable, table1); + + // checking repeating paragraph + Assert.assertEquals("Repeating paragraph :", documentContent.get(6).toString()); + Assert.assertEquals("Repeating paragraph :", documentContent.get(9).toString()); + Assert.assertEquals("Repeating paragraph :", documentContent.get(12).toString()); + + Assert.assertEquals("repeatDocPart value1", documentContent.get(8).toString()); + Assert.assertEquals("repeatDocPart value2", documentContent.get(11).toString()); + Assert.assertEquals("repeatDocPart value3", documentContent.get(14).toString()); + + // checking table after repeating paragraph + Tbl table2 = (Tbl) XmlUtils.unwrap(documentContent.get(16)); + checkTableAgainstContextValues(thirdTable, table2); + } + + private static void checkTableAgainstContextValues(List tableValues, Tbl docxTable) { + Assert.assertEquals(tableValues.size(), docxTable.getContent().size()); + for (int i = 0; i < tableValues.size(); i++) { + Tr row = (Tr) docxTable.getContent().get(i); + Assert.assertEquals(1, row.getContent().size()); + + Tc cell = (Tc) XmlUtils.unwrap(row.getContent().get(0)); + String expected = tableValues.get(i).value; + Assert.assertEquals(1, cell.getContent().size()); + Assert.assertEquals(expected, cell.getContent().get(0).toString()); + } + } +} diff --git a/src/test/java/org/wickedsource/docxstamper/RepeatDocPartNestingTest.java b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartNestingTest.java new file mode 100644 index 00000000..75a82609 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartNestingTest.java @@ -0,0 +1,118 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.XmlUtils; +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.ContentAccessor; +import org.docx4j.wml.P; +import org.docx4j.wml.Tc; +import org.junit.Assert; +import org.junit.Test; +import org.wickedsource.docxstamper.context.AClass; +import org.wickedsource.docxstamper.context.Grade; +import org.wickedsource.docxstamper.context.SchoolContext; +import org.wickedsource.docxstamper.context.Student; +import org.wickedsource.docxstamper.util.ParagraphWrapper; +import org.wickedsource.docxstamper.util.walk.BaseDocumentWalker; +import org.wickedsource.docxstamper.util.walk.DocumentWalker; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class RepeatDocPartNestingTest extends AbstractDocx4jTest { + + int initParagraphsNumber = 2; + int schoolNameTitle = 1; + int numberOfGrades = 3; + int gradeTitle = 1; + int numberOfClasses = 3; + int classTitle = 1; + int numberOfStudents = 5; + int lastParagraphTitle = 1; + + private List documentContent = null; + + @Test + public void test() throws Docx4JException, IOException { + SchoolContext schoolContext = new SchoolContext("South Park Primary School"); + for (int i = 0; i < numberOfGrades; i++) { + schoolContext.getGrades().add(createOneGrade(i)); + } + InputStream template = getClass().getResourceAsStream("RepeatDocPartNestingTest.docx"); + WordprocessingMLPackage document = stampAndLoad(template, schoolContext); + + documentContent = document.getMainDocumentPart().getContent(); + // check object's num + int expectObjects = initParagraphsNumber + schoolNameTitle + numberOfGrades * (gradeTitle + numberOfClasses * (classTitle + numberOfStudents)) + lastParagraphTitle; + Assert.assertEquals(expectObjects, documentContent.size()); + + int index = 2; // skip init paragraphs + // check school name + checkParagraphContent(schoolContext.getSchoolName(), index++); + for (Grade grade : schoolContext.getGrades()) { + // check grade name + String expected = String.format("Grade No.%d there are %d classes", grade.getNumber(), grade.getClasses().size()); + checkParagraphContent(expected, index++); + for (AClass aClass : grade.getClasses()) { + // check class name + expected = String.format("Class No.%d there are %d students", aClass.getNumber(), aClass.getStudents() + .size()); + checkParagraphContent(expected, index++); + // check the student's list + for (Student s : aClass.getStudents()) { + Object object = XmlUtils.unwrap(documentContent.get(index++)); + final List cells = new ArrayList<>(); + DocumentWalker cellWalker = new BaseDocumentWalker((ContentAccessor) object) { + @Override + protected void onTableCell(Tc tableCell) { + cells.add(tableCell); + } + }; + cellWalker.walk(); + Assert.assertEquals(3, cells.size()); + Assert.assertEquals(String.valueOf(s.getNumber()), new ParagraphWrapper((P) cells.get(0) + .getContent() + .get(0)).getText()); + Assert.assertEquals(s.getName(), new ParagraphWrapper((P) cells.get(1).getContent() + .get(0)).getText()); + Assert.assertEquals(String.valueOf(s.getAge()), new ParagraphWrapper((P) cells.get(2) + .getContent() + .get(0)).getText()); + + } + } + } + checkParagraphContent(String.format("There are %d grades.", numberOfGrades), index); + + } + + private void checkParagraphContent(String expected, int index) { + Object object = XmlUtils.unwrap(documentContent.get(index)); + P paragraph = (P) object; + Assert.assertEquals(expected, new ParagraphWrapper(paragraph).getText()); + } + + private Grade createOneGrade(int number) { + + Grade grade = new Grade(number); + for (int i = 0; i < numberOfClasses; i++) { + grade.getClasses().add(createOneClass(i)); + } + return grade; + } + + private AClass createOneClass(int classNumber) { + AClass aClass = new AClass(classNumber); + for (int i = 0; i < 5; i++) { + aClass.getStudents().add(findOneStudent(i)); + } + return aClass; + } + + private Student findOneStudent(int i) { + return new Student(i, "Bruce·No" + i, 1 + i); + } + +} diff --git a/src/test/java/org/wickedsource/docxstamper/RepeatDocPartTest.java b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartTest.java index cb5fb608..aa93c4d7 100644 --- a/src/test/java/org/wickedsource/docxstamper/RepeatDocPartTest.java +++ b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartTest.java @@ -36,7 +36,7 @@ public void test() throws Docx4JException, IOException { int index = 2; // skip init paragraphs for (Character character : context.getCharacters()) { - for (int j=0; j < 3; j++) { // 3 elements should be repeated + for (int j = 0; j < 3; j++) { // 3 elements should be repeated Object object = XmlUtils.unwrap(documentContent.get(index++)); switch (j) { case 0: { @@ -65,7 +65,7 @@ protected void onTableCell(Tc tableCell) { List runs = paragraph.getContent(); Assert.assertEquals(2, runs.size()); - List targetRunContent = ((R)runs.get(1)).getContent(); + List targetRunContent = ((R) runs.get(1)).getContent(); Assert.assertEquals(1, targetRunContent.size()); Assert.assertTrue(targetRunContent.get(0) instanceof Br); break; diff --git a/src/test/java/org/wickedsource/docxstamper/RepeatDocPartWithImageTest.java b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartWithImageTest.java new file mode 100644 index 00000000..ea35e9a2 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartWithImageTest.java @@ -0,0 +1,48 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.wickedsource.docxstamper.replace.typeresolver.image.Image; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RepeatDocPartWithImageTest extends AbstractDocx4jTest { + @Test + public void shouldImportImageDataInTheMainDocument() throws Docx4JException, IOException { + List images = new ArrayList<>(); + images.add(new Image(getClass().getResourceAsStream("butterfly.png"))); + images.add(new Image(getClass().getResourceAsStream("map.jpg"))); + + Map context = new HashMap<>(); + ArrayList units = new ArrayList<>(); + + images.forEach(image -> { + Map unit = new HashMap<>(); + Map productionFacility = new HashMap<>(); + unit.put("productionFacility", productionFacility); + productionFacility.put("coverImage", image); + units.add(unit); + }); + + context.put("units", units); + + DocxStamperConfiguration config = new DocxStamperConfiguration() + .setEvaluationContextConfigurer((StandardEvaluationContext ctx) -> ctx.addPropertyAccessor(new MapAccessor())); + + InputStream template = getClass().getResourceAsStream("RepeatDocPartWithImageTest.docx"); + WordprocessingMLPackage document = stampAndLoad(template, context, config); + + Assert.assertEquals(images.get(0).getImageBytes().length, document.getSourcePartStore().getPartSize("word/media/document_image_rId11.png")); + Assert.assertEquals(images.get(1).getImageBytes().length, document.getSourcePartStore().getPartSize("word/media/document_image_rId12.jpeg")); + Assert.assertEquals(images.get(0).getImageBytes().length, document.getSourcePartStore().getPartSize("word/media/document_image_rId13.png")); + } +} diff --git a/src/test/java/org/wickedsource/docxstamper/RepeatDocPartWithImagesInSourceTest.java b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartWithImagesInSourceTest.java new file mode 100644 index 00000000..3b3073d0 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/RepeatDocPartWithImagesInSourceTest.java @@ -0,0 +1,67 @@ +package org.wickedsource.docxstamper; + +import org.docx4j.XmlUtils; +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.Part; +import org.docx4j.wml.ContentAccessor; +import org.docx4j.wml.Drawing; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.wickedsource.docxstamper.util.DocumentUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class RepeatDocPartWithImagesInSourceTest extends AbstractDocx4jTest { + @Test + public void shouldReplicateImageFromTheMainDocumentInTheSubTemplate() throws Docx4JException, IOException { + Map context = new HashMap<>(); + ArrayList> subDocParts = new ArrayList<>(); + + Map firstPart = new HashMap<>(); + firstPart.put("name", "first doc part"); + subDocParts.add(firstPart); + + Map secondPart = new HashMap<>(); + secondPart.put("name", "second doc part"); + subDocParts.add(secondPart); + + context.put("subDocParts", subDocParts); + + DocxStamperConfiguration config = new DocxStamperConfiguration() + .setEvaluationContextConfigurer((StandardEvaluationContext ctx) -> ctx.addPropertyAccessor(new MapAccessor())); + + InputStream template = getClass().getResourceAsStream("RepeatDocPartWithImagesInSourceTest.docx"); + WordprocessingMLPackage document = stampAndLoad(template, context, config); + + Assert.assertEquals(document.getMainDocumentPart().getContent().size(), 11); + + Assert.assertEquals(document.getMainDocumentPart().getContent().get(0).toString(), "This is not repeated"); + Assert.assertEquals(document.getMainDocumentPart().getContent().get(2).toString(), "This should be repeated : first doc part"); + Assert.assertEquals(document.getMainDocumentPart().getContent().get(4).toString(), "This should be repeated too"); + Assert.assertEquals(document.getMainDocumentPart().getContent().get(5).toString(), "This should be repeated : second doc part"); + Assert.assertEquals(document.getMainDocumentPart().getContent().get(7).toString(), "This should be repeated too"); + Assert.assertEquals(document.getMainDocumentPart().getContent().get(9).toString(), "Not this"); + + Drawing image1 = (Drawing) XmlUtils.unwrap(((ContentAccessor) ((ContentAccessor) document.getMainDocumentPart().getContent().get(3)).getContent().get(0)).getContent().get(0)); + Drawing image2 = (Drawing) XmlUtils.unwrap(((ContentAccessor) ((ContentAccessor) document.getMainDocumentPart().getContent().get(6)).getContent().get(0)).getContent().get(0)); + + String image1RelId = DocumentUtil.getImageRelationshipId(image1); + String image2RelId = DocumentUtil.getImageRelationshipId(image2); + + Part image1RelPart = document.getMainDocumentPart().getRelationshipsPart().getPart(image1RelId); + Part image2RelPart = document.getMainDocumentPart().getRelationshipsPart().getPart(image2RelId); + + Assert.assertNotNull(image1RelPart); + Assert.assertNotNull(image2RelPart); + + Assert.assertNotEquals(document.getSourcePartStore().getPartSize(image1RelPart.getPartName().getName().substring(1)), 0); + Assert.assertNotEquals(document.getSourcePartStore().getPartSize(image2RelPart.getPartName().getName().substring(1)), 0); + } +} diff --git a/src/test/java/org/wickedsource/docxstamper/context/AClass.java b/src/test/java/org/wickedsource/docxstamper/context/AClass.java new file mode 100644 index 00000000..0b2382c8 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/context/AClass.java @@ -0,0 +1,25 @@ +package org.wickedsource.docxstamper.context; + +import java.util.ArrayList; +import java.util.List; + + +public class AClass { + public AClass() { + } + + int number; + List students = new ArrayList<>(); + + public int getNumber() { + return number; + } + + public List getStudents() { + return students; + } + + public AClass(int number) { + this.number = number; + } +} \ No newline at end of file diff --git a/src/test/java/org/wickedsource/docxstamper/context/Grade.java b/src/test/java/org/wickedsource/docxstamper/context/Grade.java new file mode 100644 index 00000000..03ac8d68 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/context/Grade.java @@ -0,0 +1,25 @@ +package org.wickedsource.docxstamper.context; + +import java.util.ArrayList; +import java.util.List; + + +public class Grade { + public Grade() { + } + + int number; + List classes = new ArrayList<>(); + + public int getNumber() { + return number; + } + + public Grade(int number) { + this.number = number; + } + + public List getClasses() { + return classes; + } +} \ No newline at end of file diff --git a/src/test/java/org/wickedsource/docxstamper/context/SchoolContext.java b/src/test/java/org/wickedsource/docxstamper/context/SchoolContext.java new file mode 100644 index 00000000..985076c6 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/context/SchoolContext.java @@ -0,0 +1,25 @@ +package org.wickedsource.docxstamper.context; + +import java.util.ArrayList; +import java.util.List; + +public class SchoolContext { + public SchoolContext() { + } + + String schoolName; + List grades = new ArrayList<>(); + + public SchoolContext(String schoolName) { + this.schoolName = schoolName; + } + + public String getSchoolName() { + return schoolName; + } + + public List getGrades() { + return grades; + } + +} \ No newline at end of file diff --git a/src/test/java/org/wickedsource/docxstamper/context/Student.java b/src/test/java/org/wickedsource/docxstamper/context/Student.java new file mode 100644 index 00000000..6d0ef855 --- /dev/null +++ b/src/test/java/org/wickedsource/docxstamper/context/Student.java @@ -0,0 +1,32 @@ +package org.wickedsource.docxstamper.context; + +public class Student { + public Student() { + } + + public int getNumber() { + return number; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + int number; + String name; + int age; + + public Student(int number, String name, int age) { + this.number = number; + this.name = name; + this.age = age; + } +} \ No newline at end of file diff --git a/src/test/java/org/wickedsource/docxstamper/proxy/ProxyMethodHandlerTest.java b/src/test/java/org/wickedsource/docxstamper/proxy/ProxyMethodHandlerTest.java deleted file mode 100644 index b03ef46b..00000000 --- a/src/test/java/org/wickedsource/docxstamper/proxy/ProxyMethodHandlerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.wickedsource.docxstamper.proxy; - -import org.junit.Assert; -import org.junit.Test; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.wickedsource.docxstamper.api.typeresolver.TypeResolverRegistry; -import org.wickedsource.docxstamper.context.NameContext; -import org.wickedsource.docxstamper.processor.CommentProcessorRegistry; -import org.wickedsource.docxstamper.replace.PlaceholderReplacer; -import org.wickedsource.docxstamper.replace.typeresolver.FallbackResolver; - -public class ProxyMethodHandlerTest { - - private PlaceholderReplacer placeholderReplacer = new PlaceholderReplacer(new TypeResolverRegistry(new FallbackResolver())); - - @Test - public void proxyDelegatesToRegisteredCommentProcessors() throws Exception { - - CommentProcessorRegistry processorRegistry = new CommentProcessorRegistry(placeholderReplacer); - processorRegistry.registerCommentProcessor(ITestInterface.class, new TestImpl()); - - NameContext contextRoot = new NameContext(); - contextRoot.setName("Tom"); - NameContext context = new ProxyBuilder() - .withRoot(contextRoot) - .withInterface(ITestInterface.class, new TestImpl()) - .build(); - - ExpressionParser parser = new SpelExpressionParser(); - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(context); - - Expression nameExpression = parser.parseExpression("name"); - String name = (String) nameExpression.getValue(evaluationContext); - - Expression methodExpression = parser.parseExpression("returnString(name)"); - String returnStringResult = (String) methodExpression.getValue(evaluationContext); - - Assert.assertEquals("Tom", returnStringResult); - Assert.assertEquals("Tom", name); - } - -} \ No newline at end of file diff --git a/src/test/java/org/wickedsource/docxstamper/proxy/TestImpl.java b/src/test/java/org/wickedsource/docxstamper/proxy/TestImpl.java index d5f64b45..6f31d765 100644 --- a/src/test/java/org/wickedsource/docxstamper/proxy/TestImpl.java +++ b/src/test/java/org/wickedsource/docxstamper/proxy/TestImpl.java @@ -8,35 +8,40 @@ public class TestImpl implements ITestInterface, ICommentProcessor { - @Override - public String returnString(String string) { - return string; - } + @Override + public String returnString(String string) { + return string; + } - @Override - public void commitChanges(WordprocessingMLPackage document) { + @Override + public void commitChanges(WordprocessingMLPackage document) { - } + } - @Override - public void setCurrentParagraphCoordinates(ParagraphCoordinates coordinates) { + @Override + public void setCurrentParagraphCoordinates(ParagraphCoordinates coordinates) { - } + } - @Override - public void setCurrentRunCoordinates(RunCoordinates coordinates) { - // TODO Auto-generated method stub + @Override + public void setCurrentRunCoordinates(RunCoordinates coordinates) { + // TODO Auto-generated method stub - } + } - @Override - public void setCurrentCommentWrapper(CommentWrapper commentWrapper) { + @Override + public void setCurrentCommentWrapper(CommentWrapper commentWrapper) { - } + } - @Override - public void reset() { + @Override + public void setDocument(WordprocessingMLPackage document) { - } + } + + @Override + public void reset() { + + } } diff --git a/src/test/java/org/wickedsource/docxstamper/util/ObjectDeleterTest.java b/src/test/java/org/wickedsource/docxstamper/util/ObjectDeleterTest.java index 469716ff..19095c8d 100644 --- a/src/test/java/org/wickedsource/docxstamper/util/ObjectDeleterTest.java +++ b/src/test/java/org/wickedsource/docxstamper/util/ObjectDeleterTest.java @@ -10,9 +10,8 @@ import org.wickedsource.docxstamper.api.coordinates.TableCellCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableCoordinates; import org.wickedsource.docxstamper.api.coordinates.TableRowCoordinates; -import org.wickedsource.docxstamper.util.ParagraphWrapper; -import org.wickedsource.docxstamper.util.walk.*; - +import org.wickedsource.docxstamper.util.walk.BaseCoordinatesWalker; +import org.wickedsource.docxstamper.util.walk.CoordinatesWalker; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; diff --git a/src/test/resources/org/wickedsource/docxstamper/ConditionalDisplayOfTablesBug32Test.docx b/src/test/resources/org/wickedsource/docxstamper/ConditionalDisplayOfTablesBug32Test.docx new file mode 100644 index 00000000..fea0c23c Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/ConditionalDisplayOfTablesBug32Test.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/ExpressionReplacementInTextBoxesTest.docx b/src/test/resources/org/wickedsource/docxstamper/ExpressionReplacementInTextBoxesTest.docx new file mode 100644 index 00000000..83227cf8 Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/ExpressionReplacementInTextBoxesTest.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/MapAccessorAndReflectivePropertyAccessorTest.docx b/src/test/resources/org/wickedsource/docxstamper/MapAccessorAndReflectivePropertyAccessorTest.docx new file mode 100644 index 00000000..de2c92d5 Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/MapAccessorAndReflectivePropertyAccessorTest.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartAndCommentProcessorsIsolationTest.docx b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartAndCommentProcessorsIsolationTest.docx new file mode 100644 index 00000000..eadfc561 Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartAndCommentProcessorsIsolationTest.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartNestingTest.docx b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartNestingTest.docx new file mode 100644 index 00000000..fd51b14a Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartNestingTest.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartWithImageTest.docx b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartWithImageTest.docx new file mode 100644 index 00000000..034743c4 Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartWithImageTest.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartWithImagesInSourceTest.docx b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartWithImagesInSourceTest.docx new file mode 100644 index 00000000..a0a9364a Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/RepeatDocPartWithImagesInSourceTest.docx differ diff --git a/src/test/resources/org/wickedsource/docxstamper/butterfly.png b/src/test/resources/org/wickedsource/docxstamper/butterfly.png new file mode 100644 index 00000000..134b9ef6 Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/butterfly.png differ diff --git a/src/test/resources/org/wickedsource/docxstamper/map.jpg b/src/test/resources/org/wickedsource/docxstamper/map.jpg new file mode 100644 index 00000000..e986c9a2 Binary files /dev/null and b/src/test/resources/org/wickedsource/docxstamper/map.jpg differ