├── docker-run.sh ├── docker-build.sh ├── src ├── main │ ├── resources │ │ ├── invalid-keystore-pw-peppol.jks │ │ ├── META-INF │ │ │ └── services │ │ │ │ └── com.helger.phase4.peppol.servlet.IPhase4PeppolIncomingSBDHandlerSPI │ │ ├── NOTICE │ │ ├── logback.xml │ │ ├── static │ │ │ └── index.html │ │ ├── application.properties │ │ └── LICENSE │ └── java │ │ └── com │ │ └── helger │ │ └── phase4 │ │ └── peppolstandalone │ │ ├── controller │ │ ├── HttpNotFoundException.java │ │ ├── HttpForbiddenException.java │ │ ├── HttpInternalServerErrorException.java │ │ ├── PeppolReportingController.java │ │ ├── PeppolSenderController.java │ │ └── PeppolSender.java │ │ ├── Phase4PeppolStandaloneApplication.java │ │ ├── APConfig.java │ │ ├── spi │ │ └── CustomPeppolIncomingSBDHandlerSPI.java │ │ ├── servlet │ │ ├── SpringBootAS4Servlet.java │ │ └── ServletConfig.java │ │ └── reporting │ │ └── AppReportingHelper.java ├── etc │ ├── license-template.txt │ └── javadoc.css └── test │ ├── java │ └── com │ │ └── helger │ │ └── phase4 │ │ └── peppolstandalone │ │ ├── Phase4PeppolStandaloneApplicationTests.java │ │ └── SPITest.java │ └── resources │ └── external │ └── example-invoice.xml ├── .gitignore ├── docker-build-multistage.sh ├── docker-run.cmd ├── Dockerfile ├── findbugs-exclude.xml ├── docker-build.cmd ├── docker-build-multistage.cmd ├── .github └── workflows │ └── maven.yml ├── Dockerfile.multistage ├── CODE_OF_CONDUCT.md ├── docs ├── example-sending-report-error.json └── example-sending-report-success.json ├── pom.xml ├── README.md └── LICENSE.txt /docker-run.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | docker run -p 8080:8080 phelger/phase4-peppol-standalone 3 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker build --build-arg JAR_FILE=target/*.jar -t phelger/phase4-peppol-standalone . 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/invalid-keystore-pw-peppol.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phax/phase4-peppol-standalone/HEAD/src/main/resources/invalid-keystore-pw-peppol.jks -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings/ 4 | 5 | bin/ 6 | target/ 7 | generated/ 8 | 9 | private-*.properties 10 | *.p12 11 | *.jks 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.helger.phase4.peppol.servlet.IPhase4PeppolIncomingSBDHandlerSPI: -------------------------------------------------------------------------------- 1 | com.helger.phase4.peppolstandalone.spi.CustomPeppolIncomingSBDHandlerSPI 2 | -------------------------------------------------------------------------------- /src/main/resources/NOTICE: -------------------------------------------------------------------------------- 1 | ============================================================================= 2 | = NOTICE file corresponding to section 4d of the Apache License Version 2.0 = 3 | ============================================================================= 4 | This product includes Open Source Software developed by 5 | Philip Helger - https://www.helger.com/ 6 | -------------------------------------------------------------------------------- /docker-build-multistage.sh: -------------------------------------------------------------------------------- 1 | #!/bin sh 2 | 3 | # Multi-stage Docker build script 4 | # This builds the Java application inside Docker, so you don't need local Java/Maven 5 | 6 | echo Building phase4-peppol-standalone with multi-stage Docker build... 7 | docker build -f Dockerfile.multistage -t phelger/phase4-peppol-standalone . 8 | echo Build complete! 9 | 10 | # To run the resulting image: 11 | # docker run -d -p 8080:8080 phelger/phase4-peppol-standalone 12 | # Access the application at http://localhost:8080 13 | docker run -p 8080:8080 phelger/phase4-peppol-standalone 14 | echo Application is running at http://localhost:8080 15 | -------------------------------------------------------------------------------- /src/etc/license-template.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023-2025 Philip Helger (www.helger.com) 2 | philip[at]helger[dot]com 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. -------------------------------------------------------------------------------- /docker-run.cmd: -------------------------------------------------------------------------------- 1 | @REM 2 | @REM Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | @REM philip[at]helger[dot]com 4 | @REM 5 | @REM Licensed under the Apache License, Version 2.0 (the "License"); 6 | @REM you may not use this file except in compliance with the License. 7 | @REM You may obtain a copy of the License at 8 | @REM 9 | @REM http://www.apache.org/licenses/LICENSE-2.0 10 | @REM 11 | @REM Unless required by applicable law or agreed to in writing, software 12 | @REM distributed under the License is distributed on an "AS IS" BASIS, 13 | @REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | @REM See the License for the specific language governing permissions and 15 | @REM limitations under the License. 16 | @REM 17 | 18 | @echo off 19 | docker run -p 8080:8080 phelger/phase4-peppol-standalone 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | # philip[at]helger[dot]com 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | FROM eclipse-temurin:21-alpine 19 | VOLUME /tmp 20 | ARG JAR_FILE 21 | COPY ${JAR_FILE} app.jar 22 | ENTRYPOINT ["java","-jar","/app.jar"] 23 | -------------------------------------------------------------------------------- /findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-build.cmd: -------------------------------------------------------------------------------- 1 | @REM 2 | @REM Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | @REM philip[at]helger[dot]com 4 | @REM 5 | @REM Licensed under the Apache License, Version 2.0 (the "License"); 6 | @REM you may not use this file except in compliance with the License. 7 | @REM You may obtain a copy of the License at 8 | @REM 9 | @REM http://www.apache.org/licenses/LICENSE-2.0 10 | @REM 11 | @REM Unless required by applicable law or agreed to in writing, software 12 | @REM distributed under the License is distributed on an "AS IS" BASIS, 13 | @REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | @REM See the License for the specific language governing permissions and 15 | @REM limitations under the License. 16 | @REM 17 | 18 | @echo off 19 | docker build --build-arg JAR_FILE=target/*.jar -t phelger/phase4-peppol-standalone . 20 | -------------------------------------------------------------------------------- /src/test/java/com/helger/phase4/peppolstandalone/Phase4PeppolStandaloneApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone; 18 | 19 | import org.junit.jupiter.api.Test; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | 22 | @SpringBootTest 23 | final class Phase4PeppolStandaloneApplicationTests 24 | { 25 | @Test 26 | void testContextLoads () 27 | {} 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/helger/phase4/peppolstandalone/SPITest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import com.helger.unittestext.SPITestHelper; 22 | 23 | /** 24 | * Test SPI definitions 25 | * 26 | * @author Philip Helger 27 | */ 28 | public final class SPITest 29 | { 30 | @Test 31 | public void testBasic () throws Exception 32 | { 33 | SPITestHelper.testIfAllSPIImplementationsAreValid (); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 23 | 24 | [%date{ISO8601}] [phase4] [%thread] %-5level %logger{35} -%kvp- %msg %n 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/controller/HttpNotFoundException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.controller; 18 | 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.web.bind.annotation.ResponseStatus; 21 | 22 | /** 23 | * REST Controller exception mapping to HTTP 404 (Not Found) 24 | * 25 | * @author Philip Helger 26 | */ 27 | @ResponseStatus (HttpStatus.NOT_FOUND) 28 | public class HttpNotFoundException extends RuntimeException 29 | { 30 | public HttpNotFoundException () 31 | {} 32 | 33 | public HttpNotFoundException (final String sMsg) 34 | { 35 | super (sMsg); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/controller/HttpForbiddenException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.controller; 18 | 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.web.bind.annotation.ResponseStatus; 21 | 22 | /** 23 | * REST Controller exception mapping to HTTP 403 (Forbidden) 24 | * 25 | * @author Philip Helger 26 | */ 27 | @ResponseStatus (HttpStatus.FORBIDDEN) 28 | public class HttpForbiddenException extends RuntimeException 29 | { 30 | public HttpForbiddenException () 31 | {} 32 | 33 | public HttpForbiddenException (final String sMsg) 34 | { 35 | super (sMsg); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/controller/HttpInternalServerErrorException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.controller; 18 | 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.web.bind.annotation.ResponseStatus; 21 | 22 | /** 23 | * REST Controller exception mapping to HTTP 500 (Internal Server Error) 24 | * 25 | * @author Philip Helger 26 | */ 27 | @ResponseStatus (HttpStatus.INTERNAL_SERVER_ERROR) 28 | public class HttpInternalServerErrorException extends RuntimeException 29 | { 30 | public HttpInternalServerErrorException (final String sMsg) 31 | { 32 | super (sMsg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docker-build-multistage.cmd: -------------------------------------------------------------------------------- 1 | @REM 2 | @REM Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | @REM philip[at]helger[dot]com 4 | @REM 5 | @REM Licensed under the Apache License, Version 2.0 (the "License"); 6 | @REM you may not use this file except in compliance with the License. 7 | @REM You may obtain a copy of the License at 8 | @REM 9 | @REM http://www.apache.org/licenses/LICENSE-2.0 10 | @REM 11 | @REM Unless required by applicable law or agreed to in writing, software 12 | @REM distributed under the License is distributed on an "AS IS" BASIS, 13 | @REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | @REM See the License for the specific language governing permissions and 15 | @REM limitations under the License. 16 | @REM 17 | 18 | @REM 19 | @REM Multi-stage Docker build script 20 | @REM This builds the Java application inside Docker, so you don't need local Java/Maven 21 | @REM 22 | 23 | @echo off 24 | echo Building phase4-peppol-standalone with multi-stage Docker build... 25 | docker build -f Dockerfile.multistage -t phelger/phase4-peppol-standalone . 26 | echo Build complete! 27 | 28 | @REM To run the resulting image: 29 | @REM docker run -d -p 8080:8080 phelger/phase4-peppol-standalone 30 | @REM Access the application at http://localhost:8080 31 | docker run -p 8080:8080 phelger/phase4-peppol-standalone 32 | echo Application is running at http://localhost:8080 33 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | java: [ '17', '21' ] 23 | name: Java ${{ matrix.Java }} build 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up JDK ${{ matrix.Java }} 28 | uses: actions/setup-java@v4 29 | with: 30 | java-version: ${{ matrix.Java }} 31 | distribution: 'adopt' 32 | 33 | - name: Cache local Maven repository 34 | uses: actions/cache@v4 35 | with: 36 | path: ~/.m2/repository 37 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-maven- 40 | 41 | - name: Maven Build 42 | run: mvn --batch-mode --update-snapshots install 43 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/Phase4PeppolStandaloneApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.scheduling.annotation.EnableScheduling; 22 | 23 | /** 24 | * This is the application entrypoint. 25 | * 26 | * @author Philip Helger 27 | */ 28 | @SpringBootApplication 29 | @EnableScheduling 30 | public class Phase4PeppolStandaloneApplication 31 | { 32 | public static void main (final String [] args) 33 | { 34 | SpringApplication.run (Phase4PeppolStandaloneApplication.class, args); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Dockerfile.multistage: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | # philip[at]helger[dot]com 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Multi-stage build: Build stage 19 | FROM eclipse-temurin:21-alpine AS builder 20 | 21 | # Install Maven 22 | RUN apk add --no-cache maven 23 | 24 | # Set working directory 25 | WORKDIR /app 26 | 27 | # Copy pom.xml first for better Docker layer caching 28 | COPY pom.xml . 29 | 30 | # Download dependencies 31 | RUN mvn dependency:go-offline -B 32 | 33 | # Copy source code 34 | COPY src ./src 35 | 36 | # Build the application 37 | RUN mvn clean install -DskipTests 38 | 39 | 40 | # Runtime stage 41 | FROM eclipse-temurin:21-alpine 42 | 43 | VOLUME /tmp 44 | 45 | # Copy the built jar from builder stage 46 | COPY --from=builder /app/target/*.jar app.jar 47 | 48 | ENTRYPOINT ["java","-jar","/app.jar"] 49 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | phase4-peppol-standalone 23 | 28 | 29 | 30 |

phase4-peppol-standalone is working

31 |
See https://github.com/phax/phase4-peppol-standalone/ for details.
32 | 33 |

Sending Peppol messages

34 |
Send Peppol AS4 messages via /sendas4/{senderId}/{receiverId}/{docTypeId}/{processId}/{countryC1} (will create SBDH)
35 |
Send Peppol SBDH messages via /sendsbdh
36 | 37 |

Receiving Peppol messages

38 |
Peppol AS4 messages are received via HTTP POST at /as4
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/APConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone; 18 | 19 | import org.jspecify.annotations.NonNull; 20 | import org.jspecify.annotations.Nullable; 21 | 22 | import com.helger.annotation.concurrent.Immutable; 23 | import com.helger.config.fallback.IConfigWithFallback; 24 | import com.helger.peppol.servicedomain.EPeppolNetwork; 25 | import com.helger.phase4.config.AS4Configuration; 26 | 27 | @Immutable 28 | public final class APConfig 29 | { 30 | private APConfig () 31 | {} 32 | 33 | @NonNull 34 | public static IConfigWithFallback getConfig () 35 | { 36 | return AS4Configuration.getConfig (); 37 | } 38 | 39 | @NonNull 40 | public static EPeppolNetwork getPeppolStage () 41 | { 42 | final String sStageID = getConfig ().getAsString ("peppol.stage"); 43 | final EPeppolNetwork ret = EPeppolNetwork.getFromIDOrNull (sStageID); 44 | if (ret == null) 45 | throw new IllegalStateException ("Failed to determine peppol stage from value '" + sStageID + "'"); 46 | return ret; 47 | } 48 | 49 | @Nullable 50 | public static String getMyPeppolSeatID () 51 | { 52 | return getConfig ().getAsString ("peppol.seatid"); 53 | } 54 | 55 | @Nullable 56 | public static String getMySmpUrl () 57 | { 58 | return getConfig ().getAsString ("smp.url"); 59 | } 60 | 61 | @Nullable 62 | public static String getPhase4ApiRequiredToken () 63 | { 64 | return getConfig ().getAsString ("phase4.api.requiredtoken"); 65 | } 66 | 67 | @Nullable 68 | public static String getMyPeppolCountryCode () 69 | { 70 | return getConfig ().getAsString ("peppol.owner.countrycode"); 71 | } 72 | 73 | @Nullable 74 | public static String getMyPeppolReportingSenderID () 75 | { 76 | return getConfig ().getAsString ("peppol.reporting.senderid"); 77 | } 78 | 79 | public static boolean isSchedulePeppolReporting () 80 | { 81 | return getConfig ().getAsBoolean ("peppol.reporting.scheduled", true); 82 | } 83 | 84 | public static boolean isSendingEnabled () 85 | { 86 | return getConfig ().getAsBoolean ("peppol.sending.enabled", true); 87 | } 88 | 89 | public static boolean isReceivingEnabled () 90 | { 91 | return getConfig ().getAsBoolean ("peppol.receiving.enabled", true); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at codeofconduct@helger.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | # philip[at]helger[dot]com 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # [CHANGEME] Set to "false" when deployed on a server 19 | global.debug=true 20 | # [CHANGEME] Set to "true" when deployed on a server 21 | global.production=false 22 | global.nostartupinfo=true 23 | 24 | # [CHANGEME] Where to store runtime data 25 | #global.datapath=/var/www/as4/data 26 | global.datapath=generated/ 27 | 28 | # [CHANGEME] Use this to switch between "prod" (production) and "test" (test) 29 | peppol.stage=test 30 | 31 | # [CHANGEME] Your Peppol Seat ID taken from your AP/SMP certificate 32 | peppol.seatid=POP000000 33 | 34 | # [CHANGEME] Use your Peppol organisation's country code 35 | peppol.owner.countrycode=AT 36 | 37 | # [CHANGEME] Use the Peppol SPID number (without the Pxx prefix!) 38 | peppol.reporting.senderid=0242:000000 39 | 40 | # Enable or disable the scheduling of Peppol Reporting create, validate, store and send to run monthly 41 | peppol.reporting.scheduled=true 42 | 43 | # [CHANGEME] Public endpoint of this AP 44 | phase4.endpoint.address=http://localhost:8080/as4 45 | 46 | # [CHANGEME] Public URL of your SMP to check for valid inbound requests 47 | #smp.url=http://smp.helger.com 48 | 49 | # [CHANGEME] The mandatory value required in the "X-Token" HTTP header for the sending APIs 50 | phase4.api.requiredtoken=NjIh9tIx3Rgzme19mGIy 51 | 52 | # [CHANGEME] AS4 dump directory 53 | phase4.dump.path=${global.datapath}phase4-dumps/ 54 | 55 | # [CHANGEME] put your keystore details here 56 | org.apache.wss4j.crypto.merlin.keystore.type=JKS 57 | org.apache.wss4j.crypto.merlin.keystore.file=invalid-keystore-pw-peppol.jks 58 | org.apache.wss4j.crypto.merlin.keystore.password=peppol 59 | org.apache.wss4j.crypto.merlin.keystore.alias=1 60 | org.apache.wss4j.crypto.merlin.keystore.private.password=peppol 61 | 62 | # This is a default Peppol Truststore - should be refined for production 63 | org.apache.wss4j.crypto.merlin.truststore.type=PKCS12 64 | # All these truststores are predefined, and are part of the peppol-commons library 65 | # See https://github.com/phax/peppol-commons/tree/master/peppol-commons/src/main/resources/truststore 66 | # 67 | # For Test only use: truststore/2025/ap-test-truststore.p12 68 | # For Production only use: truststore/2025/ap-prod-truststore.p12 69 | org.apache.wss4j.crypto.merlin.truststore.file=truststore/2025/ap-test-truststore.p12 70 | org.apache.wss4j.crypto.merlin.truststore.password=peppol 71 | 72 | # SMP Client 73 | smpclient.truststore.type=PKCS12 74 | # All these truststores are predefined, and are part of the peppol-commons library 75 | # See https://github.com/phax/peppol-commons/tree/master/peppol-commons/src/main/resources/truststore 76 | # 77 | # For Test only use: truststore/2025/smp-test-truststore.p12 78 | # For Production only use: truststore/2025/smp-prod-truststore.p12 79 | smpclient.truststore.path=truststore/2025/smp-test-truststore.p12 80 | smpclient.truststore.password=peppol 81 | 82 | # Outbound Proxy (if needed) 83 | #http.proxy.host= 84 | #http.proxy.port= 85 | #http.proxy.nonProxyHosts 86 | 87 | # [CHANGEME] SpringBoot port 88 | server.port=8080 89 | 90 | # SpringBoot - /actuator/shutdown 91 | management.endpoints.web.exposure.include=* 92 | management.endpoint.shutdown.enabled=true 93 | endpoints.shutdown.enabled=true 94 | 95 | # SpringBoot - Max file size for large tests 96 | spring.servlet.multipart.max-file-size=100MB 97 | spring.servlet.multipart.max-request-size=100MB 98 | 99 | # When running behind a load balancer, this might help: 100 | #server.forward-headers-strategy = native 101 | #server.tomcat.remoteip.remote-ip-header = x-forwarded-for 102 | #server.tomcat.remoteip.protocol-header = x-forwarded-proto 103 | #server.tomcat.remoteip.port-header = x-forwarded-port 104 | #server.tomcat.remoteip.host-header = x-forwarded-host 105 | -------------------------------------------------------------------------------- /docs/example-sending-report-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentDateTimeUTC": "2025-05-19T16:49:32.878Z", 3 | "phase4Version": "3.1.1", 4 | "smlDnsZone": "acc.edelivery.tech.ec.europa.eu.", 5 | "senderId": "iso6523-actorid-upis::9915:helger", 6 | "receiverId": "iso6523-actorid-upis::9922:optbcntrlacc1001", 7 | "docTypeId": "busdox-docid-qns::urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1", 8 | "docTypeIdInCodeList": true, 9 | "processId": "cenbii-procid-ubl::urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", 10 | "processIdInCodeList": true, 11 | "countryC1": "AT", 12 | "senderPartyId": "POP000000", 13 | "transportProfileId": "peppol-transport-as4-v2_0", 14 | "sbdhInstanceIdentifier": "1dcd5b3e-ba5a-4306-9233-614064249b1a", 15 | "c3EndpointUrl": "https://phase4-controller.testbedng.acc.peppol.org/rcv1", 16 | "c3Cert": "-----BEGIN CERTIFICATE-----\nMIIFwjCCA6qgAwIBAgIQdcvHyRuilYPuwtZo7up+HjANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQT3BlblBFUFBPTCBBSVNCTDEWMBQGA1UECxMNRk9SIFRFU1QgT05MWTEpMCcGA1UEAxMgUEVQUE9MIEFDQ0VTUyBQT0lOVCBURVNUIENBIC0gRzIwHhcNMjUwMzA2MDAwMDAwWhcNMjcwMjI0MjM1OTU5WjBPMQswCQYDVQQGEwJCRTETMBEGA1UECgwKT3BlblBlcHBvbDEXMBUGA1UECwwOUEVQUE9MIFRFU1QgQVAxEjAQBgNVBAMMCVBHRDAwMDAwNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKmr2tbgNaSjUpmv5lxcu7kjnTJEuK11qzf0TlTOBbAXj/HKwPwq/ckEeF2HOVI9hF7rnKwINNqUDjS70cOaqB0lGiIDk1XcGL3fr2lJzlbhvRZ7YWAeEMRzQpKxepxhZXlzsdgxD98dcHPbas5SdbzNMpwL2KSEaaGs9V50hN7un2hkyJ46mM0wbhb3H4ch+pYBZgH7YSBrRf4EP0sARBs5jIPA9VBixuEDKa/qnjTOr/bNbhZUXFKiMMW0Lg1HzJ8jZXKAaNSeYmttFmk0ZEtehNl5uaZhI+jTk9S7f6p/nrKJEvmOOjFNEuVpJprMldpK9OhOBiSxxjazEHR3m98CAwEAAaOCAXwwggF4MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgOoMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBTyRxjum21BsLnMqObPLNnaHQ8opTBdBgNVHR8EVjBUMFKgUKBOhkxodHRwOi8vcGtpLWNybC5zeW1hdXRoLmNvbS9jYV82YTkzNzczNGEzOTNhMDgwNWJmMzNjZGE4YjMzMTA5My9MYXRlc3RDUkwuY3JsMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL3BraS1vY3NwLnN5bWF1dGguY29tMB8GA1UdIwQYMBaAFGtvS7bxN7orPH8Yzborsrl8KjfrMC0GCmCGSAGG+EUBEAMEHzAdBhNghkgBhvhFARABAgMBAYGpkOEDFgY5NTc2MDgwOQYKYIZIAYb4RQEQBQQrMCkCAQAWJGFIUjBjSE02THk5d2Eya3RjbUV1YzNsdFlYVjBhQzVqYjIwPTANBgkqhkiG9w0BAQsFAAOCAgEAg1iaEwiUjcEyFqq0g2/BKUlTN9zmCzfBAr7FOFnGRexykQK4Bk5dSmwRhhPUiLwpUr2oFCY+61tDu/uzIS/J07WzqLgL0O3yECimsXvAV7JZMbUnswBneufgNsgipi5hD7VNxrStP8wh8n8wZgN6YDDUq6a7ya6/RWMYJclENcDwiE2gQ8wOKgcT/Z2kKdywJLPSiuUzj2VjyKbWxwCOPyzdSPlL+AnyawjzSllkLL5ao8zXGfzXDkIIxo0YaPebDwvlrqkCByxdH86kxKYUSvoSCNQwLmhWrLeZp981/LKYBEZBFem9k6iQrF0phvw7KnkKooxomIDnx7nKlTvCKrKYsSdtephG6ru7U6iVoQj98cdGD06aM4rBIY/kayXak+DE2choEWllLIvB1VEnRFU0PMW0E+vpgbB7anzzr7THzREjDOa6kIGcJdNC/FlTuFA6s2pJDBt6eK0RHIZqJsC7fN8+wA49RUgg6CCBwI9zyweaaZSj43mzYTqNYlEd7sJSglgc05ZdQqxmfPLSh1mhK4ghSb7Rxj7d9RSETr6tb32nVHYKP/HCUObREe0uf6RwwsohjXE7dV9XQWypLEYft8Y97Dn9uvbKkk6QopexGQjkqC4O12eMsKD+Nub/HmynOflc0JHnNU28dZLLuUrsYC1/HvimLnYVqT7BsHg=\n-----END CERTIFICATE-----", 17 | "c3CertSubjectCN": "PGD000005", 18 | "c3CertSubjectO": "OpenPeppol", 19 | "c3CertCheckDT": "2025-05-19T18:49:33.15+02:00", 20 | "c3CertCheckResult": "VALID", 21 | "as4MessageId": "43fa2a88-92d2-49c6-b18b-cd913cf1b355@phase4", 22 | "as4ConversationId": "phase4@Conv2886911484538149295", 23 | "as4SendingDateTime": "2025-05-19T18:49:33.171+02:00", 24 | "sendingResult": "AS4_ERROR_MESSAGE_RECEIVED", 25 | "as4ReceivedSignalMsg": "2025-05-19T16:49:34.067Zd0be855c-51c5-4302-8d57-de35cf409496@phase4.openpeppol.playground43fa2a88-92d2-49c6-b18b-cd913cf1b355@phase4An undefined error occurred.The incoming Peppol message could not be processed.", 26 | "as4ResponseError": true, 27 | "as4ResponseErrors": [ 28 | { 29 | "description": "An undefined error occurred.", 30 | "errorDetails": "The incoming Peppol message could not be processed.", 31 | "category": "Content", 32 | "refToMessageInError": "43fa2a88-92d2-49c6-b18b-cd913cf1b355@phase4", 33 | "errorCode": "EBMS:0004", 34 | "severity": "failure", 35 | "shortDescription": "Other" 36 | } 37 | ], 38 | "overallDurationMillis": 1382, 39 | "sendingSuccess": false, 40 | "overallSuccess": false 41 | } -------------------------------------------------------------------------------- /docs/example-sending-report-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentDateTimeUTC": "2025-05-19T16:49:18.693Z", 3 | "phase4Version": "3.1.1", 4 | "smlDnsZone": "acc.edelivery.tech.ec.europa.eu.", 5 | "senderId": "iso6523-actorid-upis::9915:phase4-test-sender", 6 | "receiverId": "iso6523-actorid-upis::9915:helger", 7 | "docTypeId": "busdox-docid-qns::urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1", 8 | "docTypeIdInCodeList": true, 9 | "processId": "cenbii-procid-ubl::urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", 10 | "processIdInCodeList": true, 11 | "countryC1": "AT", 12 | "senderPartyId": "POP000000", 13 | "transportProfileId": "peppol-transport-as4-v2_0", 14 | "sbdhInstanceIdentifier": "51b10163-a32d-417b-b03f-ab3a7cf8f41a", 15 | "c3EndpointUrl": "https://www.helger.com/phase4/as4", 16 | "c3Cert": "-----BEGIN CERTIFICATE-----\nMIIF0TCCA7mgAwIBAgIQcyxSArntXaqTdp2N6B0D2DANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQT3BlblBFUFBPTCBBSVNCTDEWMBQGA1UECxMNRk9SIFRFU1QgT05MWTEpMCcGA1UEAxMgUEVQUE9MIEFDQ0VTUyBQT0lOVCBURVNUIENBIC0gRzIwHhcNMjUwMjI1MDAwMDAwWhcNMjcwMjE1MjM1OTU5WjBeMQswCQYDVQQGEwJBVDEiMCAGA1UECgwZSGVsZ2VyIElUIENvbnN1bHRpbmcgR21iSDEXMBUGA1UECwwOUEVQUE9MIFRFU1QgQVAxEjAQBgNVBAMMCVBPUDAwMDMwNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK7YW/30et1kZK/l3hKrxJEr0NkCf/mzjkQPUh8jKyd4YZrgiod/Ry/xnp2eHcHV2Aiukk9kMkg8Ptf5W8jMgvlKeN58dHp890vupeh4iOPdq0sJ9B3HJhXQHgxhe90CZIsJi8fn7fFawMHPuVDmwvrnzYWlc0qF/xXFqM/NwBWiqKikp5lvVvZUehzJiRmEY0c1uFoXZClqUmcmmWGOBWzj8nW6IeIsZ9GurNG+9zlT6L3JRJoJCluzTjjbk4XKqEQFiP4aiDAa1nuIzMea3DkB2nx40L8TwZEO2d8Xecr3xTfkyq92eHyStyIlEW1459bOSa56Yp6Mlu7JFKmTgLkCAwEAAaOCAXwwggF4MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgOoMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBSBn6GQ+OAyiDWmqpU18M9DwzYRwTBdBgNVHR8EVjBUMFKgUKBOhkxodHRwOi8vcGtpLWNybC5zeW1hdXRoLmNvbS9jYV82YTkzNzczNGEzOTNhMDgwNWJmMzNjZGE4YjMzMTA5My9MYXRlc3RDUkwuY3JsMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL3BraS1vY3NwLnN5bWF1dGguY29tMB8GA1UdIwQYMBaAFGtvS7bxN7orPH8Yzborsrl8KjfrMC0GCmCGSAGG+EUBEAMEHzAdBhNghkgBhvhFARABAgMBAYGpkOEDFgY5NTc2MDgwOQYKYIZIAYb4RQEQBQQrMCkCAQAWJGFIUjBjSE02THk5d2Eya3RjbUV1YzNsdFlYVjBhQzVqYjIwPTANBgkqhkiG9w0BAQsFAAOCAgEAIWvpuipkFN2cSIIntNeoKfne7q9dFzJIqVTay7ZeODtcoNqEsawMzGrAAgOzyzudq+rdF0FMaywTHHvtPfHWuK96UZVIPZs1CFcOlKYkQD0k47YxHc9VJUwoCB4PLgQk5pqdfbIigLd5oFXZmgI786Pkouu0LBHsH0Im2OPH6a9EdFBECBYnS+w2PTycF/mxEru0btz4i8ZIOj2pHRBAoBCItykIJwTbqknHH8CAm2mmEnnSxyE1qDui++c811Qn8H0NtQg9x2E57XkNQTrEDMOgNw5dyrp5izUmvNdLfxFvPmvadNWVRq52MD23jU2QM1byWoYyBynlvxII829ZshjUGlycnpc7NyuQVbbPlb9Ku07ILaBDrI7qXJ3+Y8x7HOJ9qmX9nK4s5smWx6tsOUov9ZYlMvAqKipkycBe6fTVwylzfZWNKPw/6hqMM0vOz49Yv5yxUvGHEvjyTUBrEgLJytrP4jvlvY1SORISzCOn5IgZuHXYzTfd03+Z+uc0VgQHjyfNlTx/tMmrA4gCp4J6+G/mo5XXlSWFfAExypSh97GCTm+BKN5KkpyR7+1WevpyFEKK0ug+9Dr8KkSKnGSuVJ7XVhDbV5oPeTOT4HmMktEynedS61JX9We6Ilex07ak4tYouBdjwfyQmOJIgTrtchKmi5okoZeFZShLhVM=\n-----END CERTIFICATE-----", 17 | "c3CertSubjectCN": "POP000306", 18 | "c3CertSubjectO": "Helger IT Consulting GmbH", 19 | "c3CertCheckDT": "2025-05-19T18:49:18.78+02:00", 20 | "c3CertCheckResult": "VALID", 21 | "as4MessageId": "72bc09b0-76ef-46e4-a438-12edb2184dad@phase4", 22 | "as4ConversationId": "phase4@Conv2660433053088152967", 23 | "as4SendingDateTime": "2025-05-19T18:49:18.793+02:00", 24 | "sendingResult": "SUCCESS", 25 | "as4ReceivedSignalMsg": "2025-05-19T18:49:19.108+02:002e965220-e358-43f3-822f-c9273fa0da07@phase472bc09b0-76ef-46e4-a438-12edb2184dad@phase4BK+jzgUF/IUdpwRf+bsN5t2lefZ74G9O9J9EVS43Usc=9w/AGupof7aakLRBVDClfS5oMbjeFWWqV6Fu27K6l7U=W4EcSaDZL1TM2bKgPQwszDW+KB5cAFU0r8NMkO2mcYE=", 26 | "as4ResponseError": false, 27 | "overallDurationMillis": 446, 28 | "sendingSuccess": true, 29 | "overallSuccess": true 30 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 23 | 4.0.0 24 | 25 | com.helger.phase4 26 | phase4-parent-pom 27 | 4.2.2 28 | 29 | phase4-peppol-standalone 30 | phase4-peppol-standalone 31 | phase4 Peppol demo application 32 | https://github.com/phax/phase4-peppol-standalone 33 | 2023 34 | 35 | 36 | Philip Helger 37 | https://www.helger.com 38 | 39 | 40 | 41 | 42 | philip 43 | Philip Helger 44 | ph(at)helger.com 45 | https://www.helger.com 46 | 47 | 48 | 49 | 50 | 51 | Apache 2 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | repo 54 | 55 | 56 | 57 | 58 | 73 | 74 | 75 | 2.1.0 76 | 4.1.0 77 | 4.0.0 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-dependencies 86 | ${spring-boot.version} 87 | pom 88 | import 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-starter 97 | 98 | 99 | org.springframework.boot 100 | spring-boot-starter-web 101 | 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-starter-actuator 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-devtools 111 | runtime 112 | true 113 | 114 | 115 | 116 | com.helger.phase4 117 | phase4-lib 118 | 119 | 120 | com.helger.phase4 121 | phase4-profile-peppol 122 | 123 | 124 | com.helger.phase4 125 | phase4-peppol-servlet 126 | 127 | 128 | com.helger.phase4 129 | phase4-peppol-client 130 | 131 | 132 | com.helger.peppol 133 | peppol-reporting 134 | ${peppol-reporting.version} 135 | 136 | 137 | 138 | com.helger.peppol 139 | peppol-reporting-backend-inmemory 140 | ${peppol-reporting.version} 141 | 142 | 143 | com.helger.peppol 144 | peppol-reporting-support 145 | ${peppol-ap-support.version} 146 | 147 | 148 | 149 | com.sun.xml.bind 150 | jaxb-impl 151 | 152 | 153 | 154 | org.springframework.boot 155 | spring-boot-starter-test 156 | test 157 | 158 | 159 | com.helger.commons 160 | ph-unittest-support-ext 161 | test 162 | 163 | 164 | junit 165 | junit 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-compiler-plugin 176 | 177 | 17 178 | 179 | true 180 | 181 | 182 | 183 | org.springframework.boot 184 | spring-boot-maven-plugin 185 | ${spring-boot.version} 186 | 187 | 188 | 189 | repackage 190 | 191 | 192 | 193 | 194 | 195 | com.helger.phase4.peppolstandalone.Phase4PeppolStandaloneApplication 196 | ZIP 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/spi/CustomPeppolIncomingSBDHandlerSPI.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.spi; 18 | 19 | import org.jspecify.annotations.NonNull; 20 | import org.slf4j.Logger; 21 | import org.unece.cefact.namespaces.sbdh.StandardBusinessDocument; 22 | import org.w3c.dom.Element; 23 | 24 | import com.helger.annotation.style.IsSPIImplementation; 25 | import com.helger.http.header.HttpHeaderMap; 26 | import com.helger.peppol.reporting.api.PeppolReportingItem; 27 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackend; 28 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackendException; 29 | import com.helger.peppol.sbdh.PeppolSBDHData; 30 | import com.helger.peppol.sbdh.payload.PeppolSBDHPayloadBinaryMarshaller; 31 | import com.helger.peppol.sbdh.spec12.BinaryContentType; 32 | import com.helger.peppol.sbdh.spec12.ObjectFactory; 33 | import com.helger.phase4.ebms3header.Ebms3UserMessage; 34 | import com.helger.phase4.error.AS4ErrorList; 35 | import com.helger.phase4.incoming.IAS4IncomingMessageMetadata; 36 | import com.helger.phase4.incoming.IAS4IncomingMessageState; 37 | import com.helger.phase4.logging.Phase4LoggerFactory; 38 | import com.helger.phase4.peppol.servlet.IPhase4PeppolIncomingSBDHandlerSPI; 39 | import com.helger.phase4.peppol.servlet.Phase4PeppolServletMessageProcessorSPI; 40 | import com.helger.phase4.peppolstandalone.APConfig; 41 | import com.helger.phase4.util.Phase4Exception; 42 | import com.helger.security.certificate.CertificateHelper; 43 | 44 | /** 45 | * This is a way of handling incoming Peppol messages 46 | * 47 | * @author Philip Helger 48 | */ 49 | @IsSPIImplementation 50 | public class CustomPeppolIncomingSBDHandlerSPI implements IPhase4PeppolIncomingSBDHandlerSPI 51 | { 52 | private static final Logger LOGGER = Phase4LoggerFactory.getLogger (CustomPeppolIncomingSBDHandlerSPI.class); 53 | 54 | public void handleIncomingSBD (@NonNull final IAS4IncomingMessageMetadata aMessageMetadata, 55 | @NonNull final HttpHeaderMap aHeaders, 56 | @NonNull final Ebms3UserMessage aUserMessage, 57 | @NonNull final byte [] aSBDBytes, 58 | @NonNull final StandardBusinessDocument aSBD, 59 | @NonNull final PeppolSBDHData aPeppolSBD, 60 | @NonNull final IAS4IncomingMessageState aIncomingState, 61 | @NonNull final AS4ErrorList aProcessingErrorMessages) throws Exception 62 | { 63 | if (!APConfig.isReceivingEnabled ()) 64 | { 65 | LOGGER.info ("Peppol AP receiving is disabled"); 66 | throw new Phase4Exception ("Peppol AP receiving is disabled"); 67 | } 68 | 69 | final String sMyPeppolSeatID = APConfig.getMyPeppolSeatID (); 70 | 71 | // Example code snippets how to get data 72 | LOGGER.info ("Received a new Peppol Message"); 73 | LOGGER.info (" C1 = " + aPeppolSBD.getSenderAsIdentifier ().getURIEncoded ()); 74 | LOGGER.info (" C2 = " + CertificateHelper.getSubjectCN (aIncomingState.getSigningCertificate ())); 75 | LOGGER.info (" C3 = " + sMyPeppolSeatID); 76 | LOGGER.info (" C4 = " + aPeppolSBD.getReceiverAsIdentifier ().getURIEncoded ()); 77 | LOGGER.info (" DocType = " + aPeppolSBD.getDocumentTypeAsIdentifier ().getURIEncoded ()); 78 | LOGGER.info (" Process = " + aPeppolSBD.getProcessAsIdentifier ().getURIEncoded ()); 79 | LOGGER.info (" CountryC1 = " + aPeppolSBD.getCountryC1 ()); 80 | 81 | // TODO add your code here 82 | // E.g. write to disk, write to S3, write to database, write to queue... 83 | LOGGER.error ("You need to implement handleIncomingSBD to deal with incoming messages"); 84 | 85 | if (false) 86 | { 87 | // TODO example code on how to identify Factur-X payloads 88 | final Element aXMLPayload = aPeppolSBD.getBusinessMessageNoClone (); 89 | if (ObjectFactory._BinaryContent_QNAME.getLocalPart ().equals (aXMLPayload.getLocalName ()) && 90 | ObjectFactory._BinaryContent_QNAME.getNamespaceURI ().equals (aXMLPayload.getNamespaceURI ())) 91 | { 92 | if ("urn:peppol:doctype:pdf+xml".equals (aPeppolSBD.getStandard ()) && 93 | "0".equals (aPeppolSBD.getTypeVersion ()) && 94 | "factur-x".equals (aPeppolSBD.getType ())) 95 | { 96 | // Handle as Factur-X 97 | BinaryContentType aBinaryContent = new PeppolSBDHPayloadBinaryMarshaller ().read (aXMLPayload); 98 | byte [] aPDFBytes = aBinaryContent.getValue (); 99 | // TODO do something with the PDF bytes 100 | } 101 | } 102 | } 103 | 104 | // In case there is an error, throw any Exception -> will lead to an AS4 105 | // Error Message to the sender 106 | 107 | // Last action in this method 108 | new Thread ( () -> { 109 | // TODO If you have a way to determine the real end user of the message 110 | // here, this might be a good opportunity to store the data for Peppol 111 | // Reporting (do this asynchronously as the last activity) 112 | // Note: this is a separate thread so that it does not block the sending 113 | // of the positive receipt message 114 | 115 | // TODO Peppol Reporting - enable if possible to be done in here 116 | if (false) 117 | try 118 | { 119 | LOGGER.info ("Creating Peppol Reporting Item and storing it"); 120 | 121 | // TODO determine correct values for the next three fields 122 | final String sC3ID = sMyPeppolSeatID; 123 | final String sC4CountryCode = "AT"; 124 | final String sEndUserID = aPeppolSBD.getReceiverAsIdentifier ().getURIEncoded (); 125 | 126 | // Create the reporting item 127 | final PeppolReportingItem aReportingItem = Phase4PeppolServletMessageProcessorSPI.createPeppolReportingItemForReceivedMessage (aUserMessage, 128 | aPeppolSBD, 129 | aIncomingState, 130 | sC3ID, 131 | sC4CountryCode, 132 | sEndUserID); 133 | PeppolReportingBackend.withBackendDo (APConfig.getConfig (), 134 | aBackend -> aBackend.storeReportingItem (aReportingItem)); 135 | } 136 | catch (final PeppolReportingBackendException ex) 137 | { 138 | LOGGER.error ("Failed to store Peppol Reporting Item", ex); 139 | // TODO improve error handling 140 | } 141 | }).start (); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/servlet/SpringBootAS4Servlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.servlet; 18 | 19 | import org.jspecify.annotations.NonNull; 20 | 21 | import com.helger.base.string.StringHelper; 22 | import com.helger.base.url.URLHelper; 23 | import com.helger.http.EHttpMethod; 24 | import com.helger.phase4.crypto.AS4CryptoFactoryInMemoryKeyStore; 25 | import com.helger.phase4.incoming.AS4IncomingProfileSelectorConstant; 26 | import com.helger.phase4.incoming.AS4RequestHandler; 27 | import com.helger.phase4.incoming.mgr.AS4ProfileSelector; 28 | import com.helger.phase4.model.pmode.resolve.AS4DefaultPModeResolver; 29 | import com.helger.phase4.peppol.servlet.Phase4PeppolDefaultReceiverConfiguration; 30 | import com.helger.phase4.peppol.servlet.Phase4PeppolReceiverConfiguration; 31 | import com.helger.phase4.peppol.servlet.Phase4PeppolServletMessageProcessorSPI; 32 | import com.helger.phase4.servlet.AS4UnifiedResponse; 33 | import com.helger.phase4.servlet.AS4XServletHandler; 34 | import com.helger.phase4.servlet.IAS4ServletRequestHandlerCustomizer; 35 | import com.helger.security.certificate.CertificateDecodeHelper; 36 | import com.helger.smpclient.peppol.SMPClientReadOnly; 37 | import com.helger.web.scope.IRequestWebScopeWithoutResponse; 38 | import com.helger.xservlet.AbstractXServlet; 39 | 40 | public class SpringBootAS4Servlet extends AbstractXServlet 41 | { 42 | public SpringBootAS4Servlet () 43 | { 44 | // Multipart is handled specifically inside 45 | settings ().setMultipartEnabled (false); 46 | 47 | // The main XServlet handler to handle the inbound request 48 | final AS4XServletHandler hdl = new AS4XServletHandler (); 49 | hdl.setRequestHandlerCustomizer (new IAS4ServletRequestHandlerCustomizer () 50 | { 51 | public void customizeBeforeHandling (@NonNull final IRequestWebScopeWithoutResponse aRequestScope, 52 | @NonNull final AS4UnifiedResponse aUnifiedResponse, 53 | @NonNull final AS4RequestHandler aRequestHandler) 54 | { 55 | final AS4CryptoFactoryInMemoryKeyStore aCryptoFactory = ServletConfig.getCryptoFactoryToUse (); 56 | 57 | // This method refers to the outer static method 58 | aRequestHandler.setCryptoFactory (aCryptoFactory); 59 | 60 | // Specific setters, dependent on a specific AS4 profile ID 61 | // This example code only uses the global one (if any) 62 | final String sAS4ProfileID = AS4ProfileSelector.getDefaultAS4ProfileID (); 63 | if (StringHelper.isNotEmpty (sAS4ProfileID)) 64 | { 65 | aRequestHandler.setPModeResolver (new AS4DefaultPModeResolver (sAS4ProfileID)); 66 | aRequestHandler.setIncomingProfileSelector (new AS4IncomingProfileSelectorConstant (sAS4ProfileID)); 67 | 68 | // TODO Example code to disable PMode validation 69 | // Delete the block if you don't need it 70 | if (false) 71 | { 72 | final boolean bValidateAgainstProfile = false; 73 | aRequestHandler.setIncomingProfileSelector (new AS4IncomingProfileSelectorConstant (sAS4ProfileID, 74 | bValidateAgainstProfile)); 75 | } 76 | } 77 | 78 | // TODO Example code for changing the Peppol receiver data based on the 79 | // source URL 80 | // Delete the block if you don't need it 81 | if (false) 82 | { 83 | final String sUrl = aRequestScope.getURLDecoded (); 84 | 85 | // The receiver check data you want to set 86 | final Phase4PeppolReceiverConfiguration aReceiverCheckData; 87 | if (sUrl != null && sUrl.startsWith ("https://ap-prod.example.org/as4")) 88 | { 89 | aReceiverCheckData = Phase4PeppolReceiverConfiguration.builder () 90 | .receiverCheckEnabled (true) 91 | .serviceMetadataProvider (new SMPClientReadOnly (URLHelper.getAsURI ("http://smp-prod.example.org"))) 92 | .as4EndpointUrl ("https://ap-prod.example.org/as4") 93 | .apCertificate (new CertificateDecodeHelper ().source ("....Public Prod AP Cert....") 94 | .pemEncoded (true) 95 | .getDecodedOrNull ()) 96 | .sbdhIdentifierFactoryPeppol () 97 | .performSBDHValueChecks (Phase4PeppolDefaultReceiverConfiguration.isPerformSBDHValueChecks ()) 98 | .checkSBDHForMandatoryCountryC1 (Phase4PeppolDefaultReceiverConfiguration.isCheckSBDHForMandatoryCountryC1 ()) 99 | .checkSigningCertificateRevocation (Phase4PeppolDefaultReceiverConfiguration.isCheckSigningCertificateRevocation ()) 100 | .build (); 101 | } 102 | else 103 | { 104 | aReceiverCheckData = Phase4PeppolReceiverConfiguration.builder () 105 | .receiverCheckEnabled (true) 106 | .serviceMetadataProvider (new SMPClientReadOnly (URLHelper.getAsURI ("http://smp-test.example.org"))) 107 | .as4EndpointUrl ("https://ap-test.example.org/as4") 108 | .apCertificate (new CertificateDecodeHelper ().source ("....Public Test AP Cert....") 109 | .pemEncoded (true) 110 | .getDecodedOrNull ()) 111 | .sbdhIdentifierFactoryPeppol () 112 | .performSBDHValueChecks (Phase4PeppolDefaultReceiverConfiguration.isPerformSBDHValueChecks ()) 113 | .checkSBDHForMandatoryCountryC1 (Phase4PeppolDefaultReceiverConfiguration.isCheckSBDHForMandatoryCountryC1 ()) 114 | .checkSigningCertificateRevocation (Phase4PeppolDefaultReceiverConfiguration.isCheckSigningCertificateRevocation ()) 115 | .build (); 116 | } 117 | 118 | // Find the right SPI handler 119 | aRequestHandler.getProcessorOfType (Phase4PeppolServletMessageProcessorSPI.class) 120 | .setReceiverCheckData (aReceiverCheckData); 121 | } 122 | } 123 | 124 | public void customizeAfterHandling (@NonNull final IRequestWebScopeWithoutResponse aRequestScope, 125 | @NonNull final AS4UnifiedResponse aUnifiedResponse, 126 | @NonNull final AS4RequestHandler aRequestHandler) 127 | { 128 | // empty 129 | } 130 | }); 131 | 132 | // HTTP POST only 133 | handlerRegistry ().registerHandler (EHttpMethod.POST, hdl); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Standalone Peppol phase4 2 | 3 | This an example standalone implementation of [phase4](https://github.com/phax/phase4) for the Peppol Network. 4 | 5 | This is a template application and NOT ready for production use, because you need to take decisions and add some code. 6 | Of course phase4 itself is ready for production use - see a list of [known phase4 users](https://github.com/phax/phase4/wiki/Known-Users) that have agreed to be publicly listed. 7 | 8 | **Note:** because it is a template application, no releases are created - you have to modify it anyway. 9 | 10 | Contact me via email for *commercial support* (see `pom.xml` for the address). 11 | 12 | This project is part of my Peppol solution stack. See https://github.com/phax/peppol for other components and libraries in that area. 13 | 14 | # Functionality 15 | 16 | ## Functionality Receiving 17 | 18 | Based on the Servlet technology, the application takes AS4 messages via HTTP POST to `/as4`. 19 | 20 | By default, all valid incoming messages are handled by class `com.helger.phase4.peppolstandalone.spi.CustomPeppolIncomingSBDHandlerSPI`. 21 | This class contains a `TODO` where you need to implement the stuff you want to do with incoming messages. 22 | It also contains a lot of boilerplate code to show how certain things can be achieved (e.g. intergration with `peppol-reporting`). 23 | 24 | ## Functionality Sending 25 | 26 | Sending is triggered via an HTTP POST request. 27 | 28 | All the sending APIs mentioned below also require the HTTP Header `X-Token` to be present and have a specific value. 29 | What value that is, depends on the configuration property `phase4.api.requiredtoken`. 30 | The pre-configured value is `NjIh9tIx3Rgzme19mGIy` and should be changed in your own setup. 31 | 32 | The actual Peppol Network choice (test or production network) is done based on the `peppol.stage` configuration parameter. 33 | 34 | To send to an AS4 endpoint use this URL (the SBDH is built inside): 35 | ``` 36 | /sendas4/{senderId}/{receiverId}/{docTypeId}/{processId}/{countryC1} 37 | ``` 38 | 39 | To send to an AS4 endpoint use this URL when the SBDH is already available (especially for Peppol Testbed): 40 | ``` 41 | /sendsbdh 42 | ``` 43 | 44 | In both cases, the payload to send must be the XML business document (like the UBL Invoice). 45 | The outcome is a JSON document that contains most of the relevant details on sending. 46 | 47 | Test call using the file `src\test\resources\external\example-invoice.xml` as the request body (note the URL escaping of special chars via the `%` sign): 48 | `http://localhost:8080/sendas4/9915:phase4-test-sender/9915:helger/urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice%23%23urn:cen.eu:en16931:2017%23compliant%23urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1/urn:fdc:peppol.eu:2017:poacc:billing:01:1.0/GB` 49 | 50 | **Note:** Documents are NOT validated internally. They need to be validated externally. See https://github.com/phax/phive and https://github.com/phax/phive-rules for this. 51 | 52 | ## Peppol Reporting 53 | 54 | Was added on 2025-02-16 as an example. On 2025-04-12 extended with the `do-peppol-reporting` API and the automatic scheduling. 55 | 56 | By default every 2nd of the month, at 5:00am the scheduled job to create, validate, store and send the Peppol Reports is executed. The 2nd was chosen to definitively not run in timezone issues. 57 | 58 | Via `GET` on `/create-tsr/{year}/{month}` a Peppol Reporting Transaction Statistics Report (TSR) will be created. This does not validate or send the report. 59 | The `year` parameter must be ≥ 2024 and the `month` parameter must be between `1` and `12`. 60 | The response is a TSR XML in UTF-8 encoding. 61 | 62 | Via `GET` on `/create-eusr/{year}/{month}` a Peppol Reporting End User Statistics Report (EUSR) will be created. This does not validate or send the report. 63 | The `year` parameter must be ≥ 2024 and the `month` parameter must be between `1` and `12`. 64 | The response is an EUSR XML in UTF-8 encoding. 65 | 66 | Via `GET` on `/do-peppol-reporting/{year}/{month}` it will create TSR and EUSR reports, validate them, store them, send them to OpenPeppol and stores the sending reports of those. 67 | The `year` parameter must be ≥ 2024 and the `month` parameter must be between `1` and `12`. 68 | The response is a constant text showing that it was done. 69 | 70 | 71 | ## What is not included 72 | 73 | The following list contains the elements not considered for this demo application: 74 | 75 | * You need your own Peppol certificate to make it work - the contained keystore is a dummy one only 76 | * Document validation is not included 77 | * See https://github.com/phax/phive and https://github.com/phax/phive-rules for this. 78 | * Peppol Reporting is included, but disabled by default, as no reporting backend is present. 79 | * You need to pick a backend (like MySQL or PostgreSQL) from https://github.com/phax/peppol-reporting and add to your `pom.xml` 80 | * The calls for storing Peppol Reporting information is part of the code, but disabled by default, as relevant parameters cannot be determined automatically 81 | * The default storage of created Peppol Reports is the file system - you should choose something else here as well (SQL, MongoDB etc.) 82 | 83 | # Get it up and running 84 | 85 | ## Tasks 86 | 87 | 1. Prepare your Peppol Access Point Key Store according to the rules described at https://github.com/phax/phoss-smp/wiki/Certificate-setup 88 | 1. Set the correct value of `peppol.stage` in the `application.properties` file 89 | 1. Configure your Key Store in the `application.properties` file 90 | 1. Choose the correct Trust Store based on the Peppol Network stage (see above). Don't touch the Trust Store contents - they are part of the deployment. 91 | 1. Set the correct value of `peppol.seatid` in the `application.properties` file 92 | 1. Once the Peppol Certificate is configured, change the code snippet with `TODO` in file `ServletConfig` according to the comment (approx. line 215) 93 | 1. Note that incoming Peppol messages are only logged and discarded. Edit the code in class `CustomPeppolIncomingSBDHandlerSPI` to fix it. 94 | 1. Build and start the application (see below) 95 | 96 | ## Building 97 | 98 | This application is based on Spring Boot 3.x and uses Apache 3.x and Java 17 (or higher) to build. 99 | 100 | ``` 101 | mvn clean install 102 | ``` 103 | 104 | The resulting Spring Boot application is afterwards available as `target/phase4-peppol-standalone-x.y.z.jar` (`x.y.z` is the version number). 105 | 106 | An example Docker file is also present - see `docker-build.cmd` and `docker-run.cmd` for details. 107 | 108 | ## Configuration 109 | 110 | The main configuration is done via the file `src/main/resources/application.properties`. 111 | You may need to rebuild the application to have an effect. 112 | 113 | The following configuration properties are contained by default: 114 | * **`peppol.stage`** - defines the stage of the Peppol Network that should be used. Allowed values are `test` 115 | (for the test/pilot Peppol Network) and `prod` (for the production Peppol Network). It defines e.g. 116 | the SML to be used and the CAs against which checks are performed 117 | * **`peppol.seatid`** - defines your Peppol Seat ID. It could be taken from your AP certificate as well, 118 | but this way it is a bit easier. 119 | * **`peppol.owner.countrycode`** - defines the country code of you as a Peppol Service Provider. Use the 120 | 2-letter country code (as in `AT` for Austria). This is required to send the Peppol Reports to 121 | OpenPeppol. 122 | * **`peppol.reporting.senderid`** - the sending Peppol Participant ID. For now, this can be e.g. the VAT 123 | number or organisational number of you as a Service Provider. In **the future** this will most likely need 124 | to be an SPID (using the `0242` participant scheme ID). Example value: `9915:TestReportSender`. This will be used 125 | as the sending Participant ID for sending Peppol Reports to OpenPeppol. 126 | * **`peppol.reporting.scheduled`** - a boolean value to indicate, if the Peppol TSR and EUSR reports should 127 | automatically sent be towards OpenPeppol on a monthly basis. The cron rule is place is `0 0 5 2 * *`. 128 | 129 | ## Running 130 | 131 | If you run it with `java -jar target/phase4-peppol-standalone-x.y.z.jar` it will spawn a local Tomcat at port `8080` and you can access it via `http://localhost:8080`. 132 | It should show a small introduction page. The `/as4` servlet itself has no user interface. 133 | 134 | In case you run the application behind an HTTP proxy, modify the settings in the configuration file (`http.proxy.*`) and check the code for respective `TODO` comments. 135 | 136 | In case you don't like port 8080, also change it in the configuration file. 137 | 138 | --- 139 | 140 | My personal [Coding Styleguide](https://github.com/phax/meta/blob/master/CodingStyleguide.md) | 141 | It is appreciated if you star the GitHub project if you like it. 142 | -------------------------------------------------------------------------------- /src/test/resources/external/example-invoice.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 6 | urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 7 | test-invoice-phase4 8 | 2021-10-13 9 | 2021-12-01 10 | 380 11 | EUR 12 | 4025:123:4343 13 | 0150abc 14 | 15 | 16 | phase4-test-sender 17 | 18 | SupplierTradingName Ltd. 19 | 20 | 21 | Main street 1 22 | Postbox 123 23 | London 24 | GB 123 EW 25 | 26 | GB 27 | 28 | 29 | 30 | GB1232434 31 | 32 | VAT 33 | 34 | 35 | 36 | SupplierOfficialName Ltd 37 | GB983294 38 | 39 | 40 | 41 | 42 | 43 | helger 44 | 45 | BuyerTradingName AS 46 | 47 | 48 | Hovedgatan 32 49 | Po box 878 50 | Stockholm 51 | 456 34 52 | 53 | SE 54 | 55 | 56 | 57 | SE4598375937 58 | 59 | VAT 60 | 61 | 62 | 63 | Buyer Official Name 64 | 39937423947 65 | 66 | 67 | Lisa Johnson 68 | 23434234 69 | lj@buyer.se 70 | 71 | 72 | 73 | 74 | 2021-10-01 75 | 76 | 9483759475923478 77 | 78 | Delivery street 2 79 | Building 56 80 | Stockholm 81 | 21234 82 | 83 | SE 84 | 85 | 86 | 87 | 88 | 89 | Delivery party Name 90 | 91 | 92 | 93 | 94 | 30 95 | Snippet1 96 | 97 | IBAN32423940 98 | AccountName 99 | 100 | BIC324098 101 | 102 | 103 | 104 | 105 | Payment within 10 days, 2% discount 106 | 107 | 108 | true 109 | Insurance 110 | 25 111 | 112 | S 113 | 25.0 114 | 115 | VAT 116 | 117 | 118 | 119 | 120 | 331.25 121 | 122 | 1325 123 | 331.25 124 | 125 | S 126 | 25.0 127 | 128 | VAT 129 | 130 | 131 | 132 | 133 | 134 | 1300 135 | 1325 136 | 1656.25 137 | 25 138 | 1656.25 139 | 140 | 141 | 142 | 1 143 | 7 144 | 2800 145 | Konteringsstreng 146 | 147 | 123 148 | 149 | 150 | Description of item 151 | item name 152 | 153 | 21382183120983 154 | 155 | 156 | NO 157 | 158 | 159 | 09348023 160 | 161 | 162 | S 163 | 25.0 164 | 165 | VAT 166 | 167 | 168 | 169 | 170 | 400 171 | 172 | 173 | 174 | 2 175 | -3 176 | -1500 177 | 178 | 123 179 | 180 | 181 | Description 2 182 | item name 2 183 | 184 | 21382183120983 185 | 186 | 187 | NO 188 | 189 | 190 | 09348023 191 | 192 | 193 | S 194 | 25.0 195 | 196 | VAT 197 | 198 | 199 | 200 | 201 | 500 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/controller/PeppolReportingController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.controller; 18 | 19 | import java.time.YearMonth; 20 | 21 | import org.slf4j.Logger; 22 | import org.springframework.http.MediaType; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.PathVariable; 25 | import org.springframework.web.bind.annotation.RequestHeader; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import com.helger.base.string.StringHelper; 29 | import com.helger.collection.commons.CommonsArrayList; 30 | import com.helger.collection.commons.ICommonsList; 31 | import com.helger.peppol.reporting.api.PeppolReportingItem; 32 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackend; 33 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackendException; 34 | import com.helger.peppol.reporting.eusr.EndUserStatisticsReport; 35 | import com.helger.peppol.reporting.jaxb.eusr.EndUserStatisticsReport110Marshaller; 36 | import com.helger.peppol.reporting.jaxb.eusr.v110.EndUserStatisticsReportType; 37 | import com.helger.peppol.reporting.jaxb.tsr.TransactionStatisticsReport101Marshaller; 38 | import com.helger.peppol.reporting.jaxb.tsr.v101.TransactionStatisticsReportType; 39 | import com.helger.peppol.reporting.tsr.TransactionStatisticsReport; 40 | import com.helger.phase4.logging.Phase4LoggerFactory; 41 | import com.helger.phase4.peppolstandalone.APConfig; 42 | import com.helger.phase4.peppolstandalone.reporting.AppReportingHelper; 43 | 44 | /** 45 | * This is the primary REST controller for the APIs to create Peppol Reports TSR and EUSR.
46 | * IMPORTANT: this API will only work, if you configure a Peppol Reporting backend in your pom.xml. 47 | * 48 | * @author Philip Helger 49 | */ 50 | @RestController 51 | public class PeppolReportingController 52 | { 53 | private static final Logger LOGGER = Phase4LoggerFactory.getLogger (PeppolReportingController.class); 54 | 55 | /** 56 | * This API creates a TSR report from the provided year and month 57 | * 58 | * @param xtoken 59 | * The X-Token header 60 | * @param nYear 61 | * The year to use. Must be ≥ 2024 62 | * @param nMonth 63 | * The month to use. Must be ≥ 1 and ≤ 12 64 | * @return The created TSR reporting in XML in UTF-8 encoding 65 | */ 66 | @GetMapping (path = "/create-tsr/{year}/{month}", produces = MediaType.APPLICATION_XML_VALUE) 67 | public String createPeppolReportingTSR (@RequestHeader (name = PeppolSenderController.HEADER_X_TOKEN, 68 | required = true) final String xtoken, 69 | @PathVariable (name = "year", required = true) final int nYear, 70 | @PathVariable (name = "month", required = true) final int nMonth) 71 | { 72 | if (StringHelper.isEmpty (xtoken)) 73 | { 74 | LOGGER.error ("The specific token header is missing"); 75 | throw new HttpForbiddenException (); 76 | } 77 | if (!xtoken.equals (APConfig.getPhase4ApiRequiredToken ())) 78 | { 79 | LOGGER.error ("The specified token value does not match the configured required token"); 80 | throw new HttpForbiddenException (); 81 | } 82 | 83 | // Check parameters 84 | final YearMonth aYearMonth = AppReportingHelper.getValidYearMonthInAPI (nYear, nMonth); 85 | 86 | LOGGER.info ("Trying to create Peppol Reporting TSR for " + aYearMonth); 87 | 88 | try 89 | { 90 | // Now get all items from data storage and store them in a list (we start 91 | // with an initial size of 1K to avoid too many copy operations) 92 | final ICommonsList aReportingItems = new CommonsArrayList <> (1024); 93 | if (PeppolReportingBackend.withBackendDo (APConfig.getConfig (), 94 | aBackend -> aBackend.forEachReportingItem (aYearMonth, 95 | aReportingItems::add)) 96 | .isSuccess ()) 97 | { 98 | // Create report with the read transactions 99 | final TransactionStatisticsReportType aReport = TransactionStatisticsReport.builder () 100 | .monthOf (aYearMonth) 101 | .reportingServiceProviderID (APConfig.getMyPeppolSeatID ()) 102 | .reportingItemList (aReportingItems) 103 | .build (); 104 | return new TransactionStatisticsReport101Marshaller ().getAsString (aReport); 105 | } 106 | throw new HttpInternalServerErrorException ("Failed to read Peppol Reporting backend data"); 107 | } 108 | catch (final PeppolReportingBackendException ex) 109 | { 110 | LOGGER.error ("Failed to read Peppol Reporting Items", ex); 111 | throw new HttpInternalServerErrorException ("Failed to read Peppol Reporting backend data: " + ex.getMessage ()); 112 | } 113 | } 114 | 115 | /** 116 | * This API creates an EUSR report from the provided year and month 117 | * 118 | * @param xtoken 119 | * The X-Token header 120 | * @param nYear 121 | * The year to use. Must be ≥ 2024 122 | * @param nMonth 123 | * The month to use. Must be ≥ 1 and ≤ 12 124 | * @return The created EUSR reporting in XML in UTF-8 encoding 125 | */ 126 | @GetMapping (path = "/create-eusr/{year}/{month}", produces = MediaType.APPLICATION_XML_VALUE) 127 | public String createPeppolReportingEUSR (@RequestHeader (name = PeppolSenderController.HEADER_X_TOKEN, 128 | required = true) final String xtoken, 129 | @PathVariable (name = "year", required = true) final int nYear, 130 | @PathVariable (name = "month", required = true) final int nMonth) 131 | { 132 | if (StringHelper.isEmpty (xtoken)) 133 | { 134 | LOGGER.error ("The specific token header is missing"); 135 | throw new HttpForbiddenException (); 136 | } 137 | if (!xtoken.equals (APConfig.getPhase4ApiRequiredToken ())) 138 | { 139 | LOGGER.error ("The specified token value does not match the configured required token"); 140 | throw new HttpForbiddenException (); 141 | } 142 | 143 | // Check parameters 144 | final YearMonth aYearMonth = AppReportingHelper.getValidYearMonthInAPI (nYear, nMonth); 145 | 146 | LOGGER.info ("Trying to create Peppol Reporting EUSR for " + aYearMonth); 147 | 148 | try 149 | { 150 | // Now get all items from data storage and store them in a list (we start 151 | // with an initial size of 1K to avoid too many copy operations) 152 | final ICommonsList aReportingItems = new CommonsArrayList <> (1024); 153 | if (PeppolReportingBackend.withBackendDo (APConfig.getConfig (), 154 | aBackend -> aBackend.forEachReportingItem (aYearMonth, 155 | aReportingItems::add)) 156 | .isSuccess ()) 157 | { 158 | // Create report with the read transactions 159 | final EndUserStatisticsReportType aReport = EndUserStatisticsReport.builder () 160 | .monthOf (aYearMonth) 161 | .reportingServiceProviderID (APConfig.getMyPeppolSeatID ()) 162 | .reportingItemList (aReportingItems) 163 | .build (); 164 | return new EndUserStatisticsReport110Marshaller ().getAsString (aReport); 165 | } 166 | throw new HttpInternalServerErrorException ("Failed to read Peppol Reporting backend data"); 167 | } 168 | catch (final PeppolReportingBackendException ex) 169 | { 170 | LOGGER.error ("Failed to read Peppol Reporting Items", ex); 171 | throw new HttpInternalServerErrorException ("Failed to read Peppol Reporting backend data: " + ex.getMessage ()); 172 | } 173 | } 174 | 175 | /** 176 | * This API creates a TSR and EUSR report for the provided year and month, validate them, store 177 | * them and send them to the dedicated receiver. 178 | * 179 | * @param xtoken 180 | * The X-Token header 181 | * @param nYear 182 | * The year to use. Must be ≥ 2024 183 | * @param nMonth 184 | * The month to use. Must be ≥ 1 and ≤ 12 185 | * @return A constant string 186 | */ 187 | @GetMapping (path = "/do-peppol-reporting/{year}/{month}", produces = MediaType.APPLICATION_XML_VALUE) 188 | public String createValidateStoreAndSend (@RequestHeader (name = PeppolSenderController.HEADER_X_TOKEN, 189 | required = true) final String xtoken, 190 | @PathVariable (name = "year", required = true) final int nYear, 191 | @PathVariable (name = "month", required = true) final int nMonth) 192 | { 193 | if (StringHelper.isEmpty (xtoken)) 194 | { 195 | LOGGER.error ("The specific token header is missing"); 196 | throw new HttpForbiddenException (); 197 | } 198 | if (!xtoken.equals (APConfig.getPhase4ApiRequiredToken ())) 199 | { 200 | LOGGER.error ("The specified token value does not match the configured required token"); 201 | throw new HttpForbiddenException (); 202 | } 203 | 204 | // Check parameters 205 | final YearMonth aYearMonth = AppReportingHelper.getValidYearMonthInAPI (nYear, nMonth); 206 | AppReportingHelper.createAndSendPeppolReports (aYearMonth); 207 | 208 | return "Done - check report storage"; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/controller/PeppolSenderController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.controller; 18 | 19 | import org.slf4j.Logger; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.PostMapping; 24 | import org.springframework.web.bind.annotation.RequestBody; 25 | import org.springframework.web.bind.annotation.RequestHeader; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import com.helger.base.io.nonblocking.NonBlockingByteArrayInputStream; 29 | import com.helger.base.string.StringHelper; 30 | import com.helger.peppol.sbdh.PeppolSBDHData; 31 | import com.helger.peppol.sbdh.PeppolSBDHDataReadException; 32 | import com.helger.peppol.sbdh.PeppolSBDHDataReader; 33 | import com.helger.peppol.security.PeppolTrustedCA; 34 | import com.helger.peppol.servicedomain.EPeppolNetwork; 35 | import com.helger.peppol.sml.ESML; 36 | import com.helger.peppolid.factory.PeppolIdentifierFactory; 37 | import com.helger.phase4.logging.Phase4LoggerFactory; 38 | import com.helger.phase4.peppol.Phase4PeppolSendingReport; 39 | import com.helger.phase4.peppolstandalone.APConfig; 40 | import com.helger.security.certificate.TrustedCAChecker; 41 | 42 | /** 43 | * This is the primary REST controller for the APIs to send messages over Peppol. 44 | * 45 | * @author Philip Helger 46 | */ 47 | @RestController 48 | public class PeppolSenderController 49 | { 50 | static final String HEADER_X_TOKEN = "X-Token"; 51 | private static final Logger LOGGER = Phase4LoggerFactory.getLogger (PeppolSenderController.class); 52 | 53 | @GetMapping (path = "/phase4ping", produces = MediaType.TEXT_PLAIN_VALUE) 54 | public String ping () 55 | { 56 | return "pong"; 57 | } 58 | 59 | @PostMapping (path = "/sendas4/{senderId}/{receiverId}/{docTypeId}/{processId}/{countryC1}", 60 | produces = MediaType.APPLICATION_JSON_VALUE) 61 | public String sendPeppolMessage (@RequestHeader (name = HEADER_X_TOKEN, required = true) final String xtoken, 62 | @RequestBody final byte [] aPayloadBytes, 63 | @PathVariable final String senderId, 64 | @PathVariable final String receiverId, 65 | @PathVariable final String docTypeId, 66 | @PathVariable final String processId, 67 | @PathVariable final String countryC1) 68 | { 69 | if (!APConfig.isSendingEnabled ()) 70 | { 71 | LOGGER.info ("Peppol AP sending is disabled"); 72 | throw new HttpNotFoundException (); 73 | } 74 | 75 | if (StringHelper.isEmpty (xtoken)) 76 | { 77 | LOGGER.error ("The specific token header is missing"); 78 | throw new HttpForbiddenException (); 79 | } 80 | if (!xtoken.equals (APConfig.getPhase4ApiRequiredToken ())) 81 | { 82 | LOGGER.error ("The specified token value does not match the configured required token"); 83 | throw new HttpForbiddenException (); 84 | } 85 | 86 | final EPeppolNetwork eStage = APConfig.getPeppolStage (); 87 | final ESML eSML = eStage.isProduction () ? ESML.DIGIT_PRODUCTION : ESML.DIGIT_TEST; 88 | final TrustedCAChecker aAPCA = eStage.isProduction () ? PeppolTrustedCA.peppolProductionAP () 89 | : PeppolTrustedCA.peppolTestAP (); 90 | LOGGER.info ("Trying to send Peppol " + 91 | eStage.name () + 92 | " message from '" + 93 | senderId + 94 | "' to '" + 95 | receiverId + 96 | "' using '" + 97 | docTypeId + 98 | "' and '" + 99 | processId + 100 | "' for '" + 101 | countryC1 + 102 | "'"); 103 | final Phase4PeppolSendingReport aSendingReport = PeppolSender.sendPeppolMessageCreatingSbdh (eSML, 104 | aAPCA, 105 | aPayloadBytes, 106 | senderId, 107 | receiverId, 108 | docTypeId, 109 | processId, 110 | countryC1); 111 | 112 | // Return as JSON 113 | return aSendingReport.getAsJsonString (); 114 | } 115 | 116 | @PostMapping (path = "/sendas4-facturx/{senderId}/{receiverId}/{countryC1}", 117 | produces = MediaType.APPLICATION_JSON_VALUE) 118 | public String sendPeppolFacturX (@RequestHeader (name = HEADER_X_TOKEN, required = true) final String xtoken, 119 | @RequestBody final byte [] aPayloadBytes, 120 | @PathVariable final String senderId, 121 | @PathVariable final String receiverId, 122 | @PathVariable final String countryC1) 123 | { 124 | if (!APConfig.isSendingEnabled ()) 125 | { 126 | LOGGER.info ("Peppol AP sending is disabled"); 127 | throw new HttpNotFoundException (); 128 | } 129 | 130 | if (StringHelper.isEmpty (xtoken)) 131 | { 132 | LOGGER.error ("The specific token header is missing"); 133 | throw new HttpForbiddenException (); 134 | } 135 | if (!xtoken.equals (APConfig.getPhase4ApiRequiredToken ())) 136 | { 137 | LOGGER.error ("The specified token value does not match the configured required token"); 138 | throw new HttpForbiddenException (); 139 | } 140 | 141 | final EPeppolNetwork eStage = APConfig.getPeppolStage (); 142 | final ESML eSML = eStage.isProduction () ? ESML.DIGIT_PRODUCTION : ESML.DIGIT_TEST; 143 | final TrustedCAChecker aAPCA = eStage.isProduction () ? PeppolTrustedCA.peppolProductionAP () 144 | : PeppolTrustedCA.peppolTestAP (); 145 | LOGGER.info ("Trying to send Peppol " + 146 | eStage.name () + 147 | " message from '" + 148 | senderId + 149 | "' to '" + 150 | receiverId + 151 | "' using Factur-X for '" + 152 | countryC1 + 153 | "'"); 154 | final Phase4PeppolSendingReport aSendingReport = PeppolSender.sendPeppolFacturXMessageCreatingSbdh (eSML, 155 | aAPCA, 156 | aPayloadBytes, 157 | senderId, 158 | receiverId, 159 | countryC1); 160 | 161 | // Return as JSON 162 | return aSendingReport.getAsJsonString (); 163 | } 164 | 165 | @PostMapping (path = "/sendsbdh", produces = MediaType.APPLICATION_JSON_VALUE) 166 | public String sendPeppolSbdhMessage (@RequestHeader (name = HEADER_X_TOKEN, required = true) final String xtoken, 167 | @RequestBody final byte [] aPayloadBytes) 168 | { 169 | if (!APConfig.isSendingEnabled ()) 170 | { 171 | LOGGER.info ("Peppol AP sending is disabled"); 172 | throw new HttpNotFoundException (); 173 | } 174 | 175 | if (StringHelper.isEmpty (xtoken)) 176 | { 177 | LOGGER.error ("The specific token header is missing"); 178 | throw new HttpForbiddenException (); 179 | } 180 | if (!xtoken.equals (APConfig.getPhase4ApiRequiredToken ())) 181 | { 182 | LOGGER.error ("The specified token value does not match the configured required token"); 183 | throw new HttpForbiddenException (); 184 | } 185 | 186 | final EPeppolNetwork eStage = APConfig.getPeppolStage (); 187 | final ESML eSML = eStage.isProduction () ? ESML.DIGIT_PRODUCTION : ESML.DIGIT_TEST; 188 | final TrustedCAChecker aAPCA = eStage.isProduction () ? PeppolTrustedCA.peppolProductionAP () 189 | : PeppolTrustedCA.peppolTestAP (); 190 | final Phase4PeppolSendingReport aSendingReport = new Phase4PeppolSendingReport (eSML); 191 | 192 | final PeppolSBDHData aData; 193 | try 194 | { 195 | aData = new PeppolSBDHDataReader (PeppolIdentifierFactory.INSTANCE).extractData (new NonBlockingByteArrayInputStream (aPayloadBytes)); 196 | } 197 | catch (final PeppolSBDHDataReadException ex) 198 | { 199 | // TODO This error handling might be improved to return a status error 200 | // instead 201 | aSendingReport.setSBDHParseException (ex); 202 | aSendingReport.setSendingSuccess (false); 203 | aSendingReport.setOverallSuccess (false); 204 | return aSendingReport.getAsJsonString (); 205 | } 206 | 207 | aSendingReport.setSenderID (aData.getSenderAsIdentifier ()); 208 | aSendingReport.setReceiverID (aData.getReceiverAsIdentifier ()); 209 | aSendingReport.setDocTypeID (aData.getDocumentTypeAsIdentifier ()); 210 | aSendingReport.setProcessID (aData.getProcessAsIdentifier ()); 211 | aSendingReport.setCountryC1 (aData.getCountryC1 ()); 212 | aSendingReport.setSBDHInstanceIdentifier (aData.getInstanceIdentifier ()); 213 | 214 | final String sSenderID = aData.getSenderAsIdentifier ().getURIEncoded (); 215 | final String sReceiverID = aData.getReceiverAsIdentifier ().getURIEncoded (); 216 | final String sDocTypeID = aData.getDocumentTypeAsIdentifier ().getURIEncoded (); 217 | final String sProcessID = aData.getProcessAsIdentifier ().getURIEncoded (); 218 | final String sCountryCodeC1 = aData.getCountryC1 (); 219 | LOGGER.info ("Trying to send Peppol " + 220 | eStage.name () + 221 | " SBDH message from '" + 222 | sSenderID + 223 | "' to '" + 224 | sReceiverID + 225 | "' using '" + 226 | sDocTypeID + 227 | "' and '" + 228 | sProcessID + 229 | "' for '" + 230 | sCountryCodeC1 + 231 | "'"); 232 | 233 | PeppolSender.sendPeppolMessagePredefinedSbdh (aData, eSML, aAPCA, aSendingReport); 234 | 235 | // Return result JSON 236 | return aSendingReport.getAsJsonString (); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/resources/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/reporting/AppReportingHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.reporting; 18 | 19 | import java.io.File; 20 | import java.nio.charset.StandardCharsets; 21 | import java.time.LocalDate; 22 | import java.time.YearMonth; 23 | 24 | import org.jspecify.annotations.NonNull; 25 | import org.jspecify.annotations.Nullable; 26 | import org.slf4j.Logger; 27 | 28 | import com.helger.base.enforce.ValueEnforcer; 29 | import com.helger.base.string.StringHelper; 30 | import com.helger.base.timing.StopWatch; 31 | import com.helger.base.wrapper.Wrapper; 32 | import com.helger.collection.commons.CommonsArrayList; 33 | import com.helger.collection.commons.ICommonsList; 34 | import com.helger.datetime.helper.PDTFactory; 35 | import com.helger.peppol.reporting.api.CPeppolReporting; 36 | import com.helger.peppol.reporting.api.PeppolReportingHelper; 37 | import com.helger.peppol.reporting.api.PeppolReportingItem; 38 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackend; 39 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackendException; 40 | import com.helger.peppol.reporting.eusr.EndUserStatisticsReport; 41 | import com.helger.peppol.reporting.jaxb.eusr.v110.EndUserStatisticsReportType; 42 | import com.helger.peppol.reporting.jaxb.tsr.v101.TransactionStatisticsReportType; 43 | import com.helger.peppol.reporting.tsr.TransactionStatisticsReport; 44 | import com.helger.peppol.reportingsupport.EPeppolReportType; 45 | import com.helger.peppol.reportingsupport.IPeppolReportSenderCallback; 46 | import com.helger.peppol.reportingsupport.IPeppolReportStorage; 47 | import com.helger.peppol.reportingsupport.PeppolReportingSupport; 48 | import com.helger.peppol.reportingsupport.file.IPeppolReportStorageFilenameProvider; 49 | import com.helger.peppol.reportingsupport.file.PeppolReportStorageFileXML; 50 | import com.helger.peppol.security.PeppolTrustedCA; 51 | import com.helger.peppol.servicedomain.EPeppolNetwork; 52 | import com.helger.peppol.sml.ESML; 53 | import com.helger.phase4.config.AS4Configuration; 54 | import com.helger.phase4.logging.Phase4LoggerFactory; 55 | import com.helger.phase4.peppol.Phase4PeppolSendingReport; 56 | import com.helger.phase4.peppolstandalone.APConfig; 57 | import com.helger.phase4.peppolstandalone.controller.HttpForbiddenException; 58 | import com.helger.phase4.peppolstandalone.controller.PeppolSender; 59 | import com.helger.security.certificate.TrustedCAChecker; 60 | 61 | /** 62 | * Helper class for report generation 63 | * 64 | * @author Philip Helger 65 | */ 66 | public final class AppReportingHelper 67 | { 68 | private static final Logger LOGGER = Phase4LoggerFactory.getLogger (AppReportingHelper.class); 69 | 70 | @NonNull 71 | public static YearMonth getValidYearMonthInAPI (final int nYear, final int nMonth) 72 | { 73 | if (nYear < 2024) 74 | throw new HttpForbiddenException ("The year value " + nYear + " is too low"); 75 | if (nMonth < 1 || nMonth > 12) 76 | throw new HttpForbiddenException ("The month value " + nMonth + " is invalid"); 77 | 78 | final LocalDate aNow = PDTFactory.getCurrentLocalDate (); 79 | if (nYear > aNow.getYear ()) 80 | throw new HttpForbiddenException ("The year value " + nYear + " is in the future"); 81 | if (nYear == aNow.getYear () && nMonth > aNow.getMonthValue ()) 82 | throw new HttpForbiddenException ("The month value " + nMonth + " is in the future"); 83 | 84 | return YearMonth.of (nYear, nMonth); 85 | } 86 | 87 | @Nullable 88 | public static TransactionStatisticsReportType createTSR (@NonNull final YearMonth aYearMonth) throws PeppolReportingBackendException 89 | { 90 | LOGGER.info ("Trying to create Peppol Reporting TSR for " + aYearMonth); 91 | 92 | // Now get all items from data storage and store them in a list (we start 93 | // with an initial size of 1K to avoid too many copy operations) 94 | final ICommonsList aReportingItems = new CommonsArrayList <> (1024); 95 | if (PeppolReportingBackend.withBackendDo (APConfig.getConfig (), 96 | aBackend -> aBackend.forEachReportingItem (aYearMonth, 97 | aReportingItems::add)) 98 | .isSuccess ()) 99 | { 100 | // Create report with the read transactions 101 | return TransactionStatisticsReport.builder () 102 | .monthOf (aYearMonth) 103 | .reportingServiceProviderID (APConfig.getMyPeppolSeatID ()) 104 | .reportingItemList (aReportingItems) 105 | .build (); 106 | } 107 | return null; 108 | } 109 | 110 | @Nullable 111 | public static EndUserStatisticsReportType createEUSR (@NonNull final YearMonth aYearMonth) throws PeppolReportingBackendException 112 | { 113 | LOGGER.info ("Trying to create Peppol Reporting EUSR for " + aYearMonth); 114 | 115 | // Now get all items from data storage and store them in a list (we start 116 | // with an initial size of 1K to avoid too many copy operations) 117 | final ICommonsList aReportingItems = new CommonsArrayList <> (1024); 118 | if (PeppolReportingBackend.withBackendDo (APConfig.getConfig (), 119 | aBackend -> aBackend.forEachReportingItem (aYearMonth, 120 | aReportingItems::add)) 121 | .isSuccess ()) 122 | { 123 | // Create report with the read transactions 124 | return EndUserStatisticsReport.builder () 125 | .monthOf (aYearMonth) 126 | .reportingServiceProviderID (APConfig.getMyPeppolSeatID ()) 127 | .reportingItemList (aReportingItems) 128 | .build (); 129 | } 130 | return null; 131 | } 132 | 133 | /** 134 | * Create, validate, store, send and store sending reports for Peppol TSR and EUSR for one period. 135 | * 136 | * @param aYearMonth 137 | * The reporting period to use. May not be null. 138 | */ 139 | public static void createAndSendPeppolReports (@NonNull final YearMonth aYearMonth) 140 | { 141 | ValueEnforcer.notNull (aYearMonth, "YearMonth"); 142 | 143 | final StopWatch aSW = StopWatch.createdStarted (); 144 | LOGGER.info ("Trying to create and send Peppol Reports for " + aYearMonth); 145 | 146 | // How to do AS4 sending 147 | final IPeppolReportSenderCallback aPeppolSender = (aDocTypeID, aProcessID, sMessagePayload) -> { 148 | // Make Network decisions 149 | final EPeppolNetwork eStage = APConfig.getPeppolStage (); 150 | final ESML eSML = eStage.isProduction () ? ESML.DIGIT_PRODUCTION : ESML.DIGIT_TEST; 151 | final TrustedCAChecker aAPCA = eStage.isProduction () ? PeppolTrustedCA.peppolProductionAP () 152 | : PeppolTrustedCA.peppolTestAP (); 153 | // Sender: your company participant ID 154 | final String sSenderID = APConfig.getMyPeppolReportingSenderID (); 155 | if (StringHelper.isEmpty (sSenderID)) 156 | throw new IllegalStateException ("No Peppol Reporting Sender ID is configured"); 157 | 158 | // Receiver: production OpenPeppol; test Helger 159 | // OpenPeppol doesn't offer this participant ID on test :-/ 160 | final String sReceiverID = eStage.isProduction () ? CPeppolReporting.OPENPEPPOL_PARTICIPANT_ID : "9915:helger"; 161 | 162 | final String sCountryC1 = APConfig.getMyPeppolCountryCode (); 163 | if (!PeppolReportingHelper.isValidCountryCode (sCountryC1)) 164 | throw new IllegalStateException ("Invalid country code of Peppol owner is defined: '" + sCountryC1 + "'"); 165 | 166 | // Returns the sending report 167 | final Phase4PeppolSendingReport aSendingReport = PeppolSender.sendPeppolMessageCreatingSbdh (eSML, 168 | aAPCA, 169 | sMessagePayload.getBytes (StandardCharsets.UTF_8), 170 | sSenderID, 171 | sReceiverID, 172 | aDocTypeID.getURIEncoded (), 173 | aProcessID.getURIEncoded (), 174 | sCountryC1); 175 | return aSendingReport.getAsXMLString (); 176 | }; 177 | 178 | { 179 | // TODO eventually change to a different storage form 180 | final IPeppolReportStorage aReportingStorage = new PeppolReportStorageFileXML (new File (AS4Configuration.getDataPath (), 181 | "peppol-reports"), 182 | IPeppolReportStorageFilenameProvider.DEFAULT); 183 | final PeppolReportingSupport aPRS = new PeppolReportingSupport (aReportingStorage); 184 | 185 | // Handle TSR 186 | try 187 | { 188 | // Create 189 | final TransactionStatisticsReportType aTSR = createTSR (aYearMonth); 190 | if (aTSR != null) 191 | { 192 | // Validate and store 193 | final Wrapper aTSRString = new Wrapper <> (); 194 | if (aPRS.validateAndStorePeppolTSR10 (aTSR, aTSRString::set).isSuccess ()) 195 | { 196 | // Send to OpenPeppol 197 | if (aPRS.sendPeppolReport (aYearMonth, EPeppolReportType.TSR_V10, aTSRString.get (), aPeppolSender) 198 | .isSuccess ()) 199 | { 200 | LOGGER.info ("Successfully sent TSR for " + aYearMonth + " to OpenPeppol"); 201 | } 202 | else 203 | LOGGER.error ("Failed to send TSR for " + aYearMonth + " to OpenPeppol"); 204 | } 205 | else 206 | LOGGER.error ("Failed to validate and store TSR for " + aYearMonth); 207 | } 208 | else 209 | LOGGER.error ("Failed to create TSR for " + aYearMonth); 210 | } 211 | catch (final Exception ex) 212 | { 213 | LOGGER.error ("Failed to create TSR for " + aYearMonth, ex); 214 | } 215 | 216 | // Handle EUSR 217 | try 218 | { 219 | // Create 220 | final EndUserStatisticsReportType aEUSR = createEUSR (aYearMonth); 221 | if (aEUSR != null) 222 | { 223 | // Validate and store 224 | final Wrapper aEUSRString = new Wrapper <> (); 225 | if (aPRS.validateAndStorePeppolEUSR11 (aEUSR, aEUSRString::set).isSuccess ()) 226 | { 227 | // Send to OpenPeppol 228 | if (aPRS.sendPeppolReport (aYearMonth, EPeppolReportType.EUSR_V11, aEUSRString.get (), aPeppolSender) 229 | .isSuccess ()) 230 | { 231 | LOGGER.info ("Successfully sent EUSR for " + aYearMonth + " to OpenPeppol"); 232 | } 233 | else 234 | LOGGER.error ("Failed to send EUSR for " + aYearMonth + " to OpenPeppol"); 235 | } 236 | else 237 | LOGGER.error ("Failed to validate and store EUSR for " + aYearMonth); 238 | } 239 | else 240 | LOGGER.error ("Failed to create EUSR for " + aYearMonth); 241 | } 242 | catch (final Exception ex) 243 | { 244 | LOGGER.error ("Failed to create EUSR for " + aYearMonth, ex); 245 | } 246 | } 247 | 248 | aSW.stop (); 249 | LOGGER.info ("Finished processing Peppol Reports after " + aSW.getDuration ()); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/servlet/ServletConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.servlet; 18 | 19 | import java.io.File; 20 | import java.security.KeyStore; 21 | import java.security.cert.X509Certificate; 22 | import java.time.YearMonth; 23 | 24 | import org.jspecify.annotations.NonNull; 25 | import org.slf4j.Logger; 26 | import org.slf4j.bridge.SLF4JBridgeHandler; 27 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Configuration; 30 | import org.springframework.scheduling.annotation.Scheduled; 31 | 32 | import com.helger.base.debug.GlobalDebug; 33 | import com.helger.base.exception.InitializationException; 34 | import com.helger.base.state.ETriState; 35 | import com.helger.base.string.StringHelper; 36 | import com.helger.base.url.URLHelper; 37 | import com.helger.httpclient.HttpDebugger; 38 | import com.helger.mime.CMimeType; 39 | import com.helger.peppol.reporting.api.backend.IPeppolReportingBackendSPI; 40 | import com.helger.peppol.reporting.api.backend.PeppolReportingBackend; 41 | import com.helger.peppol.security.PeppolTrustedCA; 42 | import com.helger.peppol.servicedomain.EPeppolNetwork; 43 | import com.helger.phase4.config.AS4Configuration; 44 | import com.helger.phase4.crypto.AS4CryptoFactoryConfiguration; 45 | import com.helger.phase4.crypto.AS4CryptoFactoryInMemoryKeyStore; 46 | import com.helger.phase4.crypto.IAS4CryptoFactory; 47 | import com.helger.phase4.dump.AS4DumpManager; 48 | import com.helger.phase4.dump.AS4IncomingDumperFileBased; 49 | import com.helger.phase4.dump.AS4OutgoingDumperFileBased; 50 | import com.helger.phase4.incoming.AS4ServerInitializer; 51 | import com.helger.phase4.incoming.mgr.AS4ProfileSelector; 52 | import com.helger.phase4.logging.Phase4LoggerFactory; 53 | import com.helger.phase4.mgr.MetaAS4Manager; 54 | import com.helger.phase4.peppol.servlet.Phase4PeppolDefaultReceiverConfiguration; 55 | import com.helger.phase4.peppolstandalone.APConfig; 56 | import com.helger.phase4.peppolstandalone.reporting.AppReportingHelper; 57 | import com.helger.phase4.profile.peppol.AS4PeppolProfileRegistarSPI; 58 | import com.helger.phase4.profile.peppol.PeppolCRLDownloader; 59 | import com.helger.phase4.profile.peppol.Phase4PeppolHttpClientSettings; 60 | import com.helger.photon.io.WebFileIO; 61 | import com.helger.security.certificate.ECertificateCheckResult; 62 | import com.helger.security.certificate.TrustedCAChecker; 63 | import com.helger.servlet.ServletHelper; 64 | import com.helger.smpclient.peppol.SMPClientReadOnly; 65 | import com.helger.web.scope.mgr.WebScopeManager; 66 | import com.helger.xservlet.requesttrack.RequestTrackerSettings; 67 | 68 | import jakarta.activation.CommandMap; 69 | import jakarta.annotation.PreDestroy; 70 | import jakarta.servlet.ServletContext; 71 | 72 | @Configuration 73 | public class ServletConfig 74 | { 75 | private static final Logger LOGGER = Phase4LoggerFactory.getLogger (ServletConfig.class); 76 | 77 | /** 78 | * This method is a placeholder for retrieving a custom {@link IAS4CryptoFactory}. 79 | * 80 | * @return the {@link IAS4CryptoFactory} to use. May not be null. 81 | */ 82 | @NonNull 83 | public static AS4CryptoFactoryInMemoryKeyStore getCryptoFactoryToUse () 84 | { 85 | final AS4CryptoFactoryConfiguration ret = AS4CryptoFactoryConfiguration.getDefaultInstance (); 86 | // TODO If you have a custom crypto factory, build/return it here 87 | return ret; 88 | } 89 | 90 | @Bean 91 | public ServletRegistrationBean servletRegistrationBean (final ServletContext ctx) 92 | { 93 | // Must be called BEFORE the servlet is instantiated 94 | _init (ctx); 95 | 96 | // Instantiate and register Servlet 97 | final ServletRegistrationBean bean = new ServletRegistrationBean <> (new SpringBootAS4Servlet (), 98 | true, 99 | "/as4"); 100 | bean.setLoadOnStartup (1); 101 | return bean; 102 | } 103 | 104 | private void _init (@NonNull final ServletContext aSC) 105 | { 106 | // Do it only once 107 | if (!WebScopeManager.isGlobalScopePresent ()) 108 | { 109 | WebScopeManager.onGlobalBegin (aSC); 110 | _initGlobalSettings (aSC); 111 | _initAS4 (); 112 | _initPeppolAS4 (); 113 | } 114 | } 115 | 116 | private static void _initGlobalSettings (@NonNull final ServletContext aSC) 117 | { 118 | // Logging: JUL to SLF4J 119 | SLF4JBridgeHandler.removeHandlersForRootLogger (); 120 | SLF4JBridgeHandler.install (); 121 | 122 | // Order matters 123 | GlobalDebug.setProductionModeDirect (AS4Configuration.isGlobalProduction ()); 124 | GlobalDebug.setDebugModeDirect (AS4Configuration.isGlobalDebug ()); 125 | 126 | if (GlobalDebug.isDebugMode ()) 127 | { 128 | RequestTrackerSettings.setLongRunningRequestsCheckEnabled (false); 129 | RequestTrackerSettings.setParallelRunningRequestsCheckEnabled (false); 130 | } 131 | 132 | HttpDebugger.setEnabled (false); 133 | 134 | // Sanity check 135 | if (CommandMap.getDefaultCommandMap () 136 | .createDataContentHandler (CMimeType.MULTIPART_RELATED.getAsString ()) == null) 137 | { 138 | throw new IllegalStateException ("No DataContentHandler for MIME Type '" + 139 | CMimeType.MULTIPART_RELATED.getAsString () + 140 | "' is available. There seems to be a problem with the dependencies/packaging"); 141 | } 142 | 143 | // Init the data path 144 | { 145 | // Get the ServletContext base path 146 | final String sServletContextPath = ServletHelper.getServletContextBasePath (aSC); 147 | // Get the data path 148 | final String sDataPath = AS4Configuration.getDataPath (); 149 | if (StringHelper.isEmpty (sDataPath)) 150 | throw new InitializationException ("No data path was provided!"); 151 | final boolean bFileAccessCheck = false; 152 | // Init the IO layer 153 | WebFileIO.initPaths (new File (sDataPath).getAbsoluteFile (), sServletContextPath, bFileAccessCheck); 154 | } 155 | } 156 | 157 | private static void _initAS4 () 158 | { 159 | // Enforce Peppol profile usage 160 | // This is the programmatic way to enforce exactly this one profile 161 | // In a multi-profile environment, that will not work 162 | AS4ProfileSelector.setCustomDefaultAS4ProfileID (AS4PeppolProfileRegistarSPI.AS4_PROFILE_ID); 163 | 164 | AS4ServerInitializer.initAS4Server (); 165 | 166 | // dump all messages to a file 167 | AS4DumpManager.setIncomingDumper (new AS4IncomingDumperFileBased ()); 168 | AS4DumpManager.setOutgoingDumper (new AS4OutgoingDumperFileBased ()); 169 | } 170 | 171 | private static void _initPeppolAS4 () 172 | { 173 | // Make sure the download of CRL is using Apache HttpClient and that the 174 | // provided settings are used. If e.g. a proxy is needed to access outbound 175 | // resources, it can be configured here 176 | { 177 | final Phase4PeppolHttpClientSettings aHCS = new Phase4PeppolHttpClientSettings (); 178 | // TODO eventually configure an outbound HTTP proxy here as well 179 | PeppolCRLDownloader.setAsDefaultCRLCache (aHCS); 180 | } 181 | 182 | // Throws an exception if configuration parameters are missing 183 | final AS4CryptoFactoryInMemoryKeyStore aCryptoFactory = getCryptoFactoryToUse (); 184 | 185 | // Check if crypto factory configuration is valid 186 | final KeyStore aKS = aCryptoFactory.getKeyStore (); 187 | if (aKS == null) 188 | throw new InitializationException ("Failed to load configured AS4 Key store - fix the configuration"); 189 | LOGGER.info ("Successfully loaded configured AS4 key store from the crypto factory"); 190 | 191 | final KeyStore.PrivateKeyEntry aPKE = aCryptoFactory.getPrivateKeyEntry (); 192 | if (aPKE == null) 193 | throw new InitializationException ("Failed to load configured AS4 private key - fix the configuration"); 194 | LOGGER.info ("Successfully loaded configured AS4 private key from the crypto factory"); 195 | 196 | // Configure the stage correctly 197 | final EPeppolNetwork eStage = APConfig.getPeppolStage (); 198 | 199 | final X509Certificate aAPCert = (X509Certificate) aPKE.getCertificate (); 200 | 201 | final TrustedCAChecker aAPCAChecker = eStage.isProduction () ? PeppolTrustedCA.peppolProductionAP () 202 | : PeppolTrustedCA.peppolTestAP (); 203 | 204 | // Check the configured Peppol AP certificate 205 | // * No caching 206 | // * Use global certificate check mode 207 | final ECertificateCheckResult eCheckResult = aAPCAChecker.checkCertificate (aAPCert, 208 | MetaAS4Manager.getTimestampMgr () 209 | .getCurrentDateTime (), 210 | ETriState.FALSE, 211 | null); 212 | if (eCheckResult.isInvalid ()) 213 | { 214 | // TODO Change from "true" to "false" once you have a Peppol 215 | // certificate so that an exception is thrown 216 | if (false) 217 | { 218 | throw new InitializationException ("The provided certificate is not a Peppol AP certificate. Check result: " + 219 | eCheckResult); 220 | } 221 | LOGGER.error ("The provided certificate is not a valid Peppol AP certificate. Check result: " + eCheckResult); 222 | } 223 | else 224 | LOGGER.info ("Successfully checked that the provided Peppol AP certificate is valid."); 225 | 226 | // Must be set independent on the enabled/disable status 227 | Phase4PeppolDefaultReceiverConfiguration.setAPCAChecker (aAPCAChecker); 228 | 229 | // Eventually enable the receiver check, so that for each incoming request 230 | // the validity is crosscheck against the owning SMP 231 | final String sSMPURL = APConfig.getMySmpUrl (); 232 | final String sAPURL = AS4Configuration.getThisEndpointAddress (); 233 | if (StringHelper.isNotEmpty (sSMPURL) && StringHelper.isNotEmpty (sAPURL)) 234 | { 235 | // To process the message even though the receiver is not registered in 236 | // our AP 237 | Phase4PeppolDefaultReceiverConfiguration.setReceiverCheckEnabled (true); 238 | Phase4PeppolDefaultReceiverConfiguration.setSMPClient (new SMPClientReadOnly (URLHelper.getAsURI (sSMPURL))); 239 | Phase4PeppolDefaultReceiverConfiguration.setAS4EndpointURL (sAPURL); 240 | Phase4PeppolDefaultReceiverConfiguration.setAPCertificate (aAPCert); 241 | LOGGER.info ("phase4 Peppol receiver checks are enabled"); 242 | } 243 | else 244 | { 245 | Phase4PeppolDefaultReceiverConfiguration.setReceiverCheckEnabled (false); 246 | LOGGER.warn ("phase4 Peppol receiver checks are disabled"); 247 | } 248 | 249 | // Initialize the Reporting Backend only once 250 | if (PeppolReportingBackend.getBackendService ().initBackend (APConfig.getConfig ()).isFailure ()) 251 | throw new InitializationException ("Failed to init Peppol Reporting Backend Service"); 252 | } 253 | 254 | // At 05:00 AM, on day 2 of the month 255 | @Scheduled (cron = "0 0 5 2 * *") 256 | public void sendPeppolReportingMessages () 257 | { 258 | if (APConfig.isSchedulePeppolReporting ()) 259 | { 260 | LOGGER.info ("Running scheduled creation and sending of Peppol Reporting messages"); 261 | // Use the previous month 262 | final YearMonth aYearMonth = YearMonth.now ().minusMonths (1); 263 | AppReportingHelper.createAndSendPeppolReports (aYearMonth); 264 | } 265 | else 266 | LOGGER.warn ("Creating and sending Peppol Reports is disabled in the configuration"); 267 | } 268 | 269 | /** 270 | * Special class that is only present to have a graceful shutdown. The the bean method below. 271 | * 272 | * @author Philip Helger 273 | */ 274 | private static final class Destroyer 275 | { 276 | @PreDestroy 277 | public void destroy () 278 | { 279 | if (WebScopeManager.isGlobalScopePresent ()) 280 | { 281 | // Shutdown the Peppol Reporting Backend service, if it was initialized 282 | final IPeppolReportingBackendSPI aPRBS = PeppolReportingBackend.getBackendService (); 283 | if (aPRBS != null && aPRBS.isInitialized ()) 284 | aPRBS.shutdownBackend (); 285 | 286 | AS4ServerInitializer.shutdownAS4Server (); 287 | WebFileIO.resetPaths (); 288 | WebScopeManager.onGlobalEnd (); 289 | } 290 | } 291 | } 292 | 293 | @Bean 294 | public Destroyer destroyer () 295 | { 296 | return new Destroyer (); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/etc/javadoc.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | /* 18 | * based on phloc javadoc CSS. 19 | * (c) 2011-2014 phloc systems. 20 | * Derived from the original javadoc CSS from Sun JDK 21 | */ 22 | 23 | body { 24 | background-color: #FFFFFF; 25 | color: #353833; 26 | font-family: Arial, Helvetica, sans-serif; 27 | font-size: 76%; 28 | margin: 0; 29 | } 30 | 31 | a:link,a:visited { 32 | color: #880000; 33 | text-decoration: none; 34 | } 35 | 36 | a:hover,a:focus { 37 | color: #BB2222; 38 | text-decoration: none; 39 | } 40 | 41 | a:active { 42 | color: #4C6B87; 43 | text-decoration: none; 44 | } 45 | 46 | a[name] { 47 | color: #353833; 48 | } 49 | 50 | a[name]:hover { 51 | color: #353833; 52 | text-decoration: none; 53 | } 54 | 55 | pre { 56 | font-size: 1.3em; 57 | } 58 | 59 | h1 { 60 | font-size: 1.8em; 61 | } 62 | 63 | h2 { 64 | font-size: 1.5em; 65 | } 66 | 67 | h3 { 68 | font-size: 1.4em; 69 | } 70 | 71 | h4 { 72 | font-size: 1.3em; 73 | } 74 | 75 | h5 { 76 | font-size: 1.2em; 77 | } 78 | 79 | h6 { 80 | font-size: 1.1em; 81 | } 82 | 83 | ul { 84 | list-style-type: disc; 85 | } 86 | 87 | code,tt { 88 | font-size: 1.2em; 89 | } 90 | 91 | dt code { 92 | font-size: 1.2em; 93 | } 94 | 95 | table tr td dt code { 96 | font-size: 1.2em; 97 | vertical-align: top; 98 | } 99 | 100 | sup { 101 | font-size: 0.6em; 102 | } 103 | 104 | .clear { 105 | clear: both; 106 | height: 0; 107 | overflow: hidden; 108 | } 109 | 110 | .aboutLanguage { 111 | float: right; 112 | font-size: 0.8em; 113 | margin-top: -7px; 114 | padding: 0 21px; 115 | z-index: 200; 116 | } 117 | 118 | .legalCopy { 119 | margin-left: 0.5em; 120 | } 121 | 122 | .bar a,.bar a:link,.bar a:visited,.bar a:active { 123 | color: #FFFFFF; 124 | text-decoration: none; 125 | } 126 | 127 | .bar a:hover,.bar a:focus { 128 | color: #BB7A2A; 129 | } 130 | 131 | .tab { 132 | background-color: #0066FF; 133 | background-image: url("resources/titlebar.gif"); 134 | background-position: left top; 135 | background-repeat: no-repeat; 136 | color: #FFFFFF; 137 | font-weight: bold; 138 | padding: 8px; 139 | width: 5em; 140 | } 141 | 142 | .bar { 143 | background-image: url("resources/background.gif"); 144 | background-repeat: repeat-x; 145 | color: #FFFFFF; 146 | font-size: 1em; 147 | height: auto; 148 | margin: 0; 149 | padding: 0.8em 0.5em 0.4em 0.8em; 150 | } 151 | 152 | .topNav { 153 | background-image: url("resources/background.gif"); 154 | background-repeat: repeat-x; 155 | clear: right; 156 | color: #FFFFFF; 157 | float: left; 158 | height: 2.8em; 159 | overflow: hidden; 160 | padding: 10px 0 0; 161 | width: 100%; 162 | } 163 | 164 | .bottomNav { 165 | background-image: url("resources/background.gif"); 166 | background-repeat: repeat-x; 167 | clear: right; 168 | color: #FFFFFF; 169 | float: left; 170 | height: 2.8em; 171 | margin-top: 10px; 172 | overflow: hidden; 173 | padding: 10px 0 0; 174 | width: 100%; 175 | } 176 | 177 | .subNav { 178 | background-color: #DEE3E9; 179 | border-bottom: 1px solid #9EADC0; 180 | float: left; 181 | overflow: hidden; 182 | width: 100%; 183 | } 184 | 185 | .subNav div { 186 | clear: left; 187 | float: left; 188 | padding: 0 0 5px 6px; 189 | } 190 | 191 | ul.navList,ul.subNavList { 192 | float: left; 193 | margin: 0 25px 0 0; 194 | padding: 0; 195 | } 196 | 197 | ul.navList li { 198 | float: left; 199 | list-style: none outside none; 200 | padding: 3px 6px; 201 | } 202 | 203 | ul.subNavList li { 204 | float: left; 205 | font-size: 90%; 206 | list-style: none outside none; 207 | } 208 | 209 | .topNav a:link,.topNav a:active,.topNav a:visited,.bottomNav a:link,.bottomNav a:active,.bottomNav a:visited 210 | { 211 | color: #FFFFFF; 212 | text-decoration: none; 213 | } 214 | 215 | .topNav a:hover,.bottomNav a:hover { 216 | color: #BB7A2A; 217 | text-decoration: none; 218 | } 219 | 220 | .navBarCell1Rev { 221 | background-color: #A88834; 222 | background-image: url("resources/tab.gif"); 223 | border: 1px solid #C9AA44; 224 | color: #FFFFFF; 225 | margin: auto 5px; 226 | } 227 | 228 | .header,.footer { 229 | clear: both; 230 | margin: 0 20px; 231 | padding: 5px 0 0; 232 | } 233 | 234 | .indexHeader { 235 | margin: 10px; 236 | position: relative; 237 | } 238 | 239 | .indexHeader h1 { 240 | font-size: 1.3em; 241 | } 242 | 243 | .title { 244 | color: #880000; 245 | margin: 10px 0; 246 | } 247 | 248 | .subTitle { 249 | margin: 5px 0 0; 250 | } 251 | 252 | .header ul { 253 | margin: 0 0 25px; 254 | padding: 0; 255 | } 256 | 257 | .footer ul { 258 | margin: 20px 0 5px; 259 | } 260 | 261 | .header ul li,.footer ul li { 262 | font-size: 1.2em; 263 | list-style: none outside none; 264 | } 265 | 266 | div.details ul.blockList ul.blockList ul.blockList li.blockList h4,div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 267 | { 268 | background-color: #DEE3E9; 269 | border-bottom: 1px solid #9EADC0; 270 | border-top: 1px solid #9EADC0; 271 | margin: 0 0 6px -8px; 272 | padding: 2px 5px; 273 | } 274 | 275 | ul.blockList ul.blockList ul.blockList li.blockList h3 { 276 | background-color: #DEE3E9; 277 | border-bottom: 1px solid #9EADC0; 278 | border-top: 1px solid #9EADC0; 279 | margin: 0 0 6px -8px; 280 | padding: 2px 5px; 281 | } 282 | 283 | ul.blockList ul.blockList li.blockList h3 { 284 | margin: 15px 0; 285 | padding: 0; 286 | } 287 | 288 | ul.blockList li.blockList h2 { 289 | padding: 0 0 20px; 290 | } 291 | 292 | .contentContainer,.sourceContainer,.classUseContainer,.serializedFormContainer,.constantValuesContainer 293 | { 294 | clear: both; 295 | padding: 10px 20px; 296 | position: relative; 297 | } 298 | 299 | .indexContainer { 300 | font-size: 1em; 301 | margin: 10px; 302 | position: relative; 303 | } 304 | 305 | .indexContainer h2 { 306 | font-size: 1.1em; 307 | padding: 0 0 3px; 308 | } 309 | 310 | .indexContainer ul { 311 | margin: 0; 312 | padding: 0; 313 | } 314 | 315 | .indexContainer ul li { 316 | list-style: none outside none; 317 | } 318 | 319 | .contentContainer .description dl dt,.contentContainer .details dl dt,.serializedFormContainer dl dt 320 | { 321 | color: #4E4E4E; 322 | font-size: 1.1em; 323 | font-weight: bold; 324 | margin: 10px 0 0; 325 | } 326 | 327 | .contentContainer .description dl dd,.contentContainer .details dl dd,.serializedFormContainer dl dd 328 | { 329 | margin: 10px 0 10px 20px; 330 | } 331 | 332 | .serializedFormContainer dl.nameValue dt { 333 | display: inline; 334 | font-size: 1.1em; 335 | font-weight: bold; 336 | margin-left: 1px; 337 | } 338 | 339 | .serializedFormContainer dl.nameValue dd { 340 | display: inline; 341 | font-size: 1.1em; 342 | } 343 | 344 | ul.horizontal li { 345 | display: inline; 346 | font-size: 0.9em; 347 | } 348 | 349 | ul.inheritance { 350 | margin: 0; 351 | padding: 0; 352 | } 353 | 354 | ul.inheritance li { 355 | display: inline; 356 | list-style: none outside none; 357 | } 358 | 359 | ul.inheritance li ul.inheritance { 360 | margin-left: 15px; 361 | padding-left: 15px; 362 | padding-top: 1px; 363 | } 364 | 365 | ul.blockList,ul.blockListLast { 366 | margin: 10px 0; 367 | padding: 0; 368 | } 369 | 370 | ul.blockList li.blockList,ul.blockListLast li.blockList { 371 | list-style: none outside none; 372 | margin-bottom: 25px; 373 | } 374 | 375 | ul.blockList ul.blockList li.blockList,ul.blockList ul.blockListLast li.blockList 376 | { 377 | background-color: #F9F9F9; 378 | border: 1px solid #9EADC0; 379 | padding: 0 20px 5px 10px; 380 | } 381 | 382 | ul.blockList ul.blockList ul.blockList li.blockList,ul.blockList ul.blockList ul.blockListLast li.blockList 383 | { 384 | -moz-border-bottom-colors: none; 385 | -moz-border-left-colors: none; 386 | -moz-border-right-colors: none; 387 | -moz-border-top-colors: none; 388 | background-color: #FFFFFF; 389 | border-color: currentColor #9EADC0 #9EADC0; 390 | border-image: none; 391 | border-right: 1px solid #9EADC0; 392 | border-style: none solid solid; 393 | border-width: medium 1px 1px; 394 | padding: 0 0 5px 8px; 395 | } 396 | 397 | ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { 398 | -moz-border-bottom-colors: none; 399 | -moz-border-left-colors: none; 400 | -moz-border-right-colors: none; 401 | -moz-border-top-colors: none; 402 | border-color: currentColor currentColor #9EADC0; 403 | border-image: none; 404 | border-style: none none solid; 405 | border-width: medium medium 1px; 406 | margin-left: 0; 407 | padding-bottom: 15px; 408 | padding-left: 0; 409 | } 410 | 411 | ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { 412 | border-bottom: medium none; 413 | list-style: none outside none; 414 | padding-bottom: 0; 415 | } 416 | 417 | table tr td dl,table tr td dl dt,table tr td dl dd { 418 | margin-bottom: 1px; 419 | margin-top: 0; 420 | } 421 | 422 | .contentContainer table,.classUseContainer table,.constantValuesContainer table 423 | { 424 | border-bottom: 1px solid #9EADC0; 425 | width: 100%; 426 | } 427 | 428 | .contentContainer ul li table,.classUseContainer ul li table,.constantValuesContainer ul li table 429 | { 430 | width: 100%; 431 | } 432 | 433 | .contentContainer .description table,.contentContainer .details table { 434 | border-bottom: medium none; 435 | } 436 | 437 | .contentContainer ul li table th.colOne,.contentContainer ul li table th.colFirst,.contentContainer ul li table th.colLast,.classUseContainer ul li table th,.constantValuesContainer ul li table th,.contentContainer ul li table td.colOne,.contentContainer ul li table td.colFirst,.contentContainer ul li table td.colLast,.classUseContainer ul li table td,.constantValuesContainer ul li table td 438 | { 439 | padding-right: 20px; 440 | vertical-align: top; 441 | } 442 | 443 | .contentContainer ul li table th.colLast,.classUseContainer ul li table th.colLast,.constantValuesContainer ul li table th.colLast,.contentContainer ul li table td.colLast,.classUseContainer ul li table td.colLast,.constantValuesContainer ul li table td.colLast,.contentContainer ul li table th.colOne,.classUseContainer ul li table th.colOne,.contentContainer ul li table td.colOne,.classUseContainer ul li table td.colOne 444 | { 445 | padding-right: 3px; 446 | } 447 | 448 | .overviewSummary caption,.packageSummary caption,.contentContainer ul.blockList li.blockList caption,.summary caption,.classUseContainer caption,.constantValuesContainer caption 449 | { 450 | background-repeat: no-repeat; 451 | clear: none; 452 | color: #FFFFFF; 453 | font-weight: bold; 454 | margin: 0; 455 | overflow: hidden; 456 | padding: 0; 457 | position: relative; 458 | text-align: left; 459 | } 460 | 461 | caption a:link,caption a:hover,caption a:active,caption a:visited { 462 | color: #FFFFFF; 463 | } 464 | 465 | .overviewSummary caption span,.packageSummary caption span,.contentContainer ul.blockList li.blockList caption span,.summary caption span,.classUseContainer caption span,.constantValuesContainer caption span 466 | { 467 | background-image: url("resources/titlebar.gif"); 468 | display: block; 469 | float: left; 470 | height: 18px; 471 | padding-left: 8px; 472 | padding-top: 8px; 473 | white-space: nowrap; 474 | } 475 | 476 | .overviewSummary .tabEnd,.packageSummary .tabEnd,.contentContainer ul.blockList li.blockList .tabEnd,.summary .tabEnd,.classUseContainer .tabEnd,.constantValuesContainer .tabEnd 477 | { 478 | background-image: url("resources/titlebar_end.gif"); 479 | background-position: right top; 480 | background-repeat: no-repeat; 481 | float: left; 482 | position: relative; 483 | width: 10px; 484 | } 485 | 486 | ul.blockList ul.blockList li.blockList table { 487 | margin: 0 0 12px; 488 | width: 100%; 489 | } 490 | 491 | .tableSubHeadingColor { 492 | background-color: #EEEEFF; 493 | } 494 | 495 | .altColor { 496 | background-color: #EEEEEF; 497 | } 498 | 499 | .rowColor { 500 | background-color: #FFFFFF; 501 | } 502 | 503 | .overviewSummary td,.packageSummary td,.contentContainer ul.blockList li.blockList td,.summary td,.classUseContainer td,.constantValuesContainer td 504 | { 505 | padding: 3px 3px 3px 7px; 506 | text-align: left; 507 | } 508 | 509 | th.colFirst,th.colLast,th.colOne,.constantValuesContainer th { 510 | background: none repeat scroll 0 0 #DEE3E9; 511 | border-bottom: 1px solid #9EADC0; 512 | border-top: 1px solid #9EADC0; 513 | padding: 3px 3px 3px 7px; 514 | text-align: left; 515 | } 516 | 517 | td.colOne a:link,td.colOne a:active,td.colOne a:visited,td.colOne a:hover,td.colFirst a:link,td.colFirst a:active,td.colFirst a:visited,td.colFirst a:hover,td.colLast a:link,td.colLast a:active,td.colLast a:visited,td.colLast a:hover,.constantValuesContainer td a:link,.constantValuesContainer td a:active,.constantValuesContainer td a:visited,.constantValuesContainer td a:hover 518 | { 519 | font-weight: bold; 520 | } 521 | 522 | td.colFirst,th.colFirst { 523 | border-left: 1px solid #9EADC0; 524 | white-space: nowrap; 525 | } 526 | 527 | td.colLast,th.colLast { 528 | border-right: 1px solid #9EADC0; 529 | } 530 | 531 | td.colOne,th.colOne { 532 | border-left: 1px solid #9EADC0; 533 | border-right: 1px solid #9EADC0; 534 | } 535 | 536 | table.overviewSummary { 537 | margin-left: 0; 538 | padding: 0; 539 | } 540 | 541 | table.overviewSummary td.colFirst,table.overviewSummary th.colFirst,table.overviewSummary td.colOne,table.overviewSummary th.colOne 542 | { 543 | vertical-align: middle; 544 | width: 25%; 545 | } 546 | 547 | table.packageSummary td.colFirst,table.overviewSummary th.colFirst { 548 | vertical-align: middle; 549 | width: 25%; 550 | } 551 | 552 | .description pre { 553 | margin-top: 0; 554 | } 555 | 556 | .deprecatedContent { 557 | margin: 0; 558 | padding: 10px 0; 559 | } 560 | 561 | .docSummary { 562 | padding: 0; 563 | } 564 | 565 | .sourceLineNo { 566 | color: #008000; 567 | padding: 0 30px 0 0; 568 | } 569 | 570 | h1.hidden { 571 | font-size: 0.9em; 572 | overflow: hidden; 573 | visibility: hidden; 574 | } 575 | 576 | .block { 577 | display: block; 578 | margin: 3px 0 0; 579 | } 580 | 581 | .strong { 582 | font-weight: bold; 583 | } 584 | -------------------------------------------------------------------------------- /src/main/java/com/helger/phase4/peppolstandalone/controller/PeppolSender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-2025 Philip Helger (www.helger.com) 3 | * philip[at]helger[dot]com 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.helger.phase4.peppolstandalone.controller; 18 | 19 | import org.jspecify.annotations.NonNull; 20 | import org.slf4j.Logger; 21 | import org.w3c.dom.Document; 22 | 23 | import com.helger.annotation.Nonempty; 24 | import com.helger.annotation.concurrent.Immutable; 25 | import com.helger.base.system.EJavaVersion; 26 | import com.helger.base.timing.StopWatch; 27 | import com.helger.base.wrapper.Wrapper; 28 | import com.helger.mime.CMimeType; 29 | import com.helger.peppol.sbdh.PeppolSBDHData; 30 | import com.helger.peppol.sml.ISMLInfo; 31 | import com.helger.peppolid.IDocumentTypeIdentifier; 32 | import com.helger.peppolid.IParticipantIdentifier; 33 | import com.helger.peppolid.IProcessIdentifier; 34 | import com.helger.peppolid.factory.IIdentifierFactory; 35 | import com.helger.peppolid.factory.PeppolIdentifierFactory; 36 | import com.helger.peppolid.peppol.doctype.EPredefinedDocumentTypeIdentifier; 37 | import com.helger.peppolid.peppol.process.EPredefinedProcessIdentifier; 38 | import com.helger.phase4.client.IAS4ClientBuildMessageCallback; 39 | import com.helger.phase4.logging.Phase4LoggerFactory; 40 | import com.helger.phase4.model.message.AS4UserMessage; 41 | import com.helger.phase4.model.message.AbstractAS4Message; 42 | import com.helger.phase4.peppol.Phase4PeppolSender; 43 | import com.helger.phase4.peppol.Phase4PeppolSender.PeppolUserMessageBuilder; 44 | import com.helger.phase4.peppol.Phase4PeppolSender.PeppolUserMessageSBDHBuilder; 45 | import com.helger.phase4.peppol.Phase4PeppolSendingReport; 46 | import com.helger.phase4.peppolstandalone.APConfig; 47 | import com.helger.phase4.profile.peppol.Phase4PeppolHttpClientSettings; 48 | import com.helger.phase4.sender.EAS4UserMessageSendResult; 49 | import com.helger.phase4.util.Phase4Exception; 50 | import com.helger.security.certificate.TrustedCAChecker; 51 | import com.helger.smpclient.peppol.SMPClientReadOnly; 52 | import com.helger.xml.serialize.read.DOMReader; 53 | 54 | /** 55 | * This contains the main Peppol sending code. It was extracted from the controller to make it more 56 | * readable 57 | * 58 | * @author Philip Helger 59 | */ 60 | @Immutable 61 | public final class PeppolSender 62 | { 63 | private static final Logger LOGGER = Phase4LoggerFactory.getLogger (PeppolSender.class); 64 | 65 | private PeppolSender () 66 | {} 67 | 68 | /** 69 | * Send a Peppol message where the SBDH is created internally by phase4 70 | * 71 | * @param aSmlInfo 72 | * The SML to be used for receiver lookup 73 | * @param aAPCAChecker 74 | * The Peppol CA checker to be used. 75 | * @param aPayloadBytes 76 | * The main business document to be send 77 | * @param sSenderID 78 | * The Peppol sender Participant ID 79 | * @param sReceiverID 80 | * The Peppol receiver Participant ID 81 | * @param sDocTypeID 82 | * The Peppol document type ID 83 | * @param sProcessID 84 | * The Peppol process ID 85 | * @param sCountryCodeC1 86 | * The Country Code of the sender (C1) 87 | * @return The created sending report and never null. 88 | */ 89 | @NonNull 90 | public static Phase4PeppolSendingReport sendPeppolMessageCreatingSbdh (@NonNull final ISMLInfo aSmlInfo, 91 | @NonNull final TrustedCAChecker aAPCAChecker, 92 | @NonNull final byte [] aPayloadBytes, 93 | @NonNull @Nonempty final String sSenderID, 94 | @NonNull @Nonempty final String sReceiverID, 95 | @NonNull @Nonempty final String sDocTypeID, 96 | @NonNull @Nonempty final String sProcessID, 97 | @NonNull @Nonempty final String sCountryCodeC1) 98 | { 99 | final IIdentifierFactory aIF = PeppolIdentifierFactory.INSTANCE; 100 | final String sMyPeppolSeatID = APConfig.getMyPeppolSeatID (); 101 | 102 | final Phase4PeppolSendingReport aSendingReport = new Phase4PeppolSendingReport (aSmlInfo); 103 | aSendingReport.setCountryC1 (sCountryCodeC1); 104 | aSendingReport.setSenderPartyID (sMyPeppolSeatID); 105 | 106 | EAS4UserMessageSendResult eResult = null; 107 | boolean bExceptionCaught = false; 108 | final StopWatch aSW = StopWatch.createdStarted (); 109 | try 110 | { 111 | // Payload must be XML - even for Text and Binary content 112 | final Document aDoc = DOMReader.readXMLDOM (aPayloadBytes); 113 | if (aDoc == null || aDoc.getDocumentElement () == null) 114 | throw new IllegalStateException ("Failed to read provided payload as XML"); 115 | if (aDoc.getDocumentElement ().getNamespaceURI () == null) 116 | throw new IllegalStateException ("Only XML payloads with a namespace are supported"); 117 | 118 | // Start configuring here 119 | IParticipantIdentifier aSenderID = aIF.parseParticipantIdentifier (sSenderID); 120 | if (aSenderID == null) 121 | { 122 | // Fallback to default scheme 123 | aSenderID = aIF.createParticipantIdentifierWithDefaultScheme (sSenderID); 124 | } 125 | if (aSenderID == null) 126 | throw new IllegalStateException ("Failed to parse the sending participant ID '" + sSenderID + "'"); 127 | aSendingReport.setSenderID (aSenderID); 128 | 129 | IParticipantIdentifier aReceiverID = aIF.parseParticipantIdentifier (sReceiverID); 130 | if (aReceiverID == null) 131 | { 132 | // Fallback to default scheme 133 | aReceiverID = aIF.createParticipantIdentifierWithDefaultScheme (sReceiverID); 134 | } 135 | if (aReceiverID == null) 136 | throw new IllegalStateException ("Failed to parse the receiving participant ID '" + sReceiverID + "'"); 137 | aSendingReport.setReceiverID (aReceiverID); 138 | 139 | IDocumentTypeIdentifier aDocTypeID = aIF.parseDocumentTypeIdentifier (sDocTypeID); 140 | if (aDocTypeID == null) 141 | { 142 | // Fallback to default scheme 143 | aDocTypeID = aIF.createDocumentTypeIdentifierWithDefaultScheme (sDocTypeID); 144 | } 145 | if (aDocTypeID == null) 146 | throw new IllegalStateException ("Failed to parse the document type ID '" + sDocTypeID + "'"); 147 | aSendingReport.setDocTypeID (aDocTypeID); 148 | 149 | IProcessIdentifier aProcessID = aIF.parseProcessIdentifier (sProcessID); 150 | if (aProcessID == null) 151 | { 152 | // Fallback to default scheme 153 | aProcessID = aIF.createProcessIdentifierWithDefaultScheme (sProcessID); 154 | } 155 | if (aProcessID == null) 156 | throw new IllegalStateException ("Failed to parse the process ID '" + sProcessID + "'"); 157 | aSendingReport.setProcessID (aProcessID); 158 | 159 | final SMPClientReadOnly aSMPClient = new SMPClientReadOnly (Phase4PeppolSender.URL_PROVIDER, 160 | aReceiverID, 161 | aSmlInfo); 162 | 163 | aSMPClient.withHttpClientSettings (aHCS -> { 164 | // TODO Add SMP HTTP outbound proxy settings here 165 | // If this block is not used, it may be removed 166 | }); 167 | 168 | // In the meantime each SMP MUST be able to use SHA-256 169 | if (false) 170 | if (EJavaVersion.getCurrentVersion ().isNewerOrEqualsThan (EJavaVersion.JDK_17)) 171 | { 172 | // Work around the disabled SHA-1 in XMLDsig issue 173 | aSMPClient.setSecureValidation (false); 174 | } 175 | 176 | final Phase4PeppolHttpClientSettings aHCS = new Phase4PeppolHttpClientSettings (); 177 | // TODO Add AP HTTP outbound proxy settings here 178 | 179 | final PeppolUserMessageBuilder aBuilder = Phase4PeppolSender.builder () 180 | .httpClientFactory (aHCS) 181 | .documentTypeID (aDocTypeID) 182 | .processID (aProcessID) 183 | .senderParticipantID (aSenderID) 184 | .receiverParticipantID (aReceiverID) 185 | .senderPartyID (sMyPeppolSeatID) 186 | .countryC1 (sCountryCodeC1) 187 | .payload (aDoc.getDocumentElement ()) 188 | .peppolAP_CAChecker (aAPCAChecker) 189 | .smpClient (aSMPClient) 190 | .sbdDocumentConsumer (sbd -> { 191 | // Remember SBDH Instance 192 | // Identifier 193 | aSendingReport.setSBDHInstanceIdentifier (sbd.getStandardBusinessDocumentHeader () 194 | .getDocumentIdentification () 195 | .getInstanceIdentifier ()); 196 | }) 197 | .endpointURLConsumer (aSendingReport::setC3EndpointURL) 198 | .technicalContactConsumer (aSendingReport::setC3TechnicalContact) 199 | .certificateConsumer ( (aAPCertificate, 200 | aCheckDT, 201 | eCertCheckResult) -> { 202 | // Determined by SMP lookup 203 | aSendingReport.setC3Cert (aAPCertificate); 204 | aSendingReport.setC3CertCheckDT (aCheckDT); 205 | aSendingReport.setC3CertCheckResult (eCertCheckResult); 206 | }) 207 | .sendingDateTimeConsumer (aSendingReport::setAS4SendingDT) 208 | .buildMessageCallback (new IAS4ClientBuildMessageCallback () 209 | { 210 | public void onAS4Message (@NonNull final AbstractAS4Message aMsg) 211 | { 212 | // Created AS4 fields 213 | final AS4UserMessage aUserMsg = (AS4UserMessage) aMsg; 214 | aSendingReport.setAS4MessageID (aUserMsg.getEbms3UserMessage () 215 | .getMessageInfo () 216 | .getMessageId ()); 217 | aSendingReport.setAS4ConversationID (aUserMsg.getEbms3UserMessage () 218 | .getCollaborationInfo () 219 | .getConversationId ()); 220 | } 221 | }) 222 | .signalMsgConsumer ( (aSignalMsg, 223 | aMessageMetadata, 224 | aState) -> { 225 | aSendingReport.setAS4ReceivedSignalMsg (aSignalMsg); 226 | }) 227 | .disableValidation (); 228 | final Wrapper aCaughtEx = new Wrapper <> (); 229 | eResult = aBuilder.sendMessageAndCheckForReceipt (aCaughtEx::set); 230 | LOGGER.info ("Peppol client send result: " + eResult); 231 | 232 | if (eResult.isSuccess ()) 233 | { 234 | // TODO determine the enduser ID of the outbound message 235 | // In many simple cases, this might be the sender's participant ID 236 | final String sEndUserID = aSenderID.getURIEncoded (); 237 | 238 | // TODO Enable Peppol Reporting when ready 239 | if (false) 240 | aBuilder.createAndStorePeppolReportingItemAfterSending (sEndUserID); 241 | } 242 | 243 | aSendingReport.setAS4SendingResult (eResult); 244 | 245 | if (aCaughtEx.isSet ()) 246 | { 247 | final Phase4Exception ex = aCaughtEx.get (); 248 | LOGGER.error ("Error sending Peppol message via AS4", ex); 249 | aSendingReport.setAS4SendingException (ex); 250 | bExceptionCaught = true; 251 | } 252 | } 253 | catch (final Exception ex) 254 | { 255 | // Mostly errors on HTTP level 256 | LOGGER.error ("Error sending Peppol message via AS4", ex); 257 | aSendingReport.setAS4SendingException (ex); 258 | bExceptionCaught = true; 259 | } 260 | finally 261 | { 262 | aSW.stop (); 263 | aSendingReport.setOverallDurationMillis (aSW.getMillis ()); 264 | } 265 | 266 | // Result may be null 267 | final boolean bSendingSuccess = eResult != null && eResult.isSuccess (); 268 | aSendingReport.setSendingSuccess (bSendingSuccess); 269 | aSendingReport.setOverallSuccess (bSendingSuccess && !bExceptionCaught); 270 | 271 | return aSendingReport; 272 | } 273 | 274 | /** 275 | * Send a Peppol Factur-X message with PDF payload where the SBDH is created internally by phase4 276 | * 277 | * @param aSmlInfo 278 | * The SML to be used for receiver lookup 279 | * @param aAPCAChecker 280 | * The Peppol CA checker to be used. 281 | * @param aPDFBytes 282 | * The main PDF document to be send 283 | * @param sSenderID 284 | * The Peppol sender Participant ID 285 | * @param sReceiverID 286 | * The Peppol receiver Participant ID 287 | * @param sCountryCodeC1 288 | * The Country Code of the sender (C1) 289 | * @return The created sending report and never null. 290 | */ 291 | @NonNull 292 | public static Phase4PeppolSendingReport sendPeppolFacturXMessageCreatingSbdh (@NonNull final ISMLInfo aSmlInfo, 293 | @NonNull final TrustedCAChecker aAPCAChecker, 294 | @NonNull final byte [] aPDFBytes, 295 | @NonNull @Nonempty final String sSenderID, 296 | @NonNull @Nonempty final String sReceiverID, 297 | @NonNull @Nonempty final String sCountryCodeC1) 298 | { 299 | final IIdentifierFactory aIF = PeppolIdentifierFactory.INSTANCE; 300 | final String sMyPeppolSeatID = APConfig.getMyPeppolSeatID (); 301 | 302 | final Phase4PeppolSendingReport aSendingReport = new Phase4PeppolSendingReport (aSmlInfo); 303 | aSendingReport.setCountryC1 (sCountryCodeC1); 304 | aSendingReport.setSenderPartyID (sMyPeppolSeatID); 305 | 306 | EAS4UserMessageSendResult eResult = null; 307 | boolean bExceptionCaught = false; 308 | final StopWatch aSW = StopWatch.createdStarted (); 309 | try 310 | { 311 | // Start configuring here 312 | IParticipantIdentifier aSenderID = aIF.parseParticipantIdentifier (sSenderID); 313 | if (aSenderID == null) 314 | { 315 | // Fallback to default scheme 316 | aSenderID = aIF.createParticipantIdentifierWithDefaultScheme (sSenderID); 317 | } 318 | if (aSenderID == null) 319 | throw new IllegalStateException ("Failed to parse the sending participant ID '" + sSenderID + "'"); 320 | aSendingReport.setSenderID (aSenderID); 321 | 322 | IParticipantIdentifier aReceiverID = aIF.parseParticipantIdentifier (sReceiverID); 323 | if (aReceiverID == null) 324 | { 325 | // Fallback to default scheme 326 | aReceiverID = aIF.createParticipantIdentifierWithDefaultScheme (sReceiverID); 327 | } 328 | if (aReceiverID == null) 329 | throw new IllegalStateException ("Failed to parse the receiving participant ID '" + sReceiverID + "'"); 330 | aSendingReport.setReceiverID (aReceiverID); 331 | 332 | // Hard coded Factur-X 333 | IDocumentTypeIdentifier aDocTypeID = EPredefinedDocumentTypeIdentifier.urn_peppol_doctype_pdf_xml__urn_cen_eu_en16931_2017_conformant_urn_peppol_france_billing_Factur_X_1_0__D22B; 334 | aSendingReport.setDocTypeID (aDocTypeID); 335 | 336 | // Assume regulated process 337 | IProcessIdentifier aProcessID = EPredefinedProcessIdentifier.urn_peppol_france_billing_regulated; 338 | aSendingReport.setProcessID (aProcessID); 339 | 340 | final SMPClientReadOnly aSMPClient = new SMPClientReadOnly (Phase4PeppolSender.URL_PROVIDER, 341 | aReceiverID, 342 | aSmlInfo); 343 | 344 | aSMPClient.withHttpClientSettings (aHCS -> { 345 | // TODO Add SMP HTTP outbound proxy settings here 346 | // If this block is not used, it may be removed 347 | }); 348 | 349 | final Phase4PeppolHttpClientSettings aHCS = new Phase4PeppolHttpClientSettings (); 350 | // TODO Add AP HTTP outbound proxy settings here 351 | 352 | final PeppolUserMessageBuilder aBuilder = Phase4PeppolSender.builder () 353 | .httpClientFactory (aHCS) 354 | .documentTypeID (aDocTypeID) 355 | .processID (aProcessID) 356 | .senderParticipantID (aSenderID) 357 | .receiverParticipantID (aReceiverID) 358 | .senderPartyID (sMyPeppolSeatID) 359 | .countryC1 (sCountryCodeC1) 360 | .sbdhStandard ("urn:peppol:doctype:pdf+xml") 361 | .sbdhTypeVersion ("0") 362 | .sbdhType ("factur-x") 363 | .payloadBinaryContent (aPDFBytes, 364 | CMimeType.APPLICATION_PDF, 365 | null) 366 | .peppolAP_CAChecker (aAPCAChecker) 367 | .smpClient (aSMPClient) 368 | .sbdDocumentConsumer (sbd -> { 369 | // Remember SBDH Instance 370 | // Identifier 371 | aSendingReport.setSBDHInstanceIdentifier (sbd.getStandardBusinessDocumentHeader () 372 | .getDocumentIdentification () 373 | .getInstanceIdentifier ()); 374 | }) 375 | .endpointURLConsumer (aSendingReport::setC3EndpointURL) 376 | .technicalContactConsumer (aSendingReport::setC3TechnicalContact) 377 | .certificateConsumer ( (aAPCertificate, 378 | aCheckDT, 379 | eCertCheckResult) -> { 380 | // Determined by SMP lookup 381 | aSendingReport.setC3Cert (aAPCertificate); 382 | aSendingReport.setC3CertCheckDT (aCheckDT); 383 | aSendingReport.setC3CertCheckResult (eCertCheckResult); 384 | }) 385 | .sendingDateTimeConsumer (aSendingReport::setAS4SendingDT) 386 | .buildMessageCallback (new IAS4ClientBuildMessageCallback () 387 | { 388 | public void onAS4Message (@NonNull final AbstractAS4Message aMsg) 389 | { 390 | // Created AS4 fields 391 | final AS4UserMessage aUserMsg = (AS4UserMessage) aMsg; 392 | aSendingReport.setAS4MessageID (aUserMsg.getEbms3UserMessage () 393 | .getMessageInfo () 394 | .getMessageId ()); 395 | aSendingReport.setAS4ConversationID (aUserMsg.getEbms3UserMessage () 396 | .getCollaborationInfo () 397 | .getConversationId ()); 398 | } 399 | }) 400 | .signalMsgConsumer ( (aSignalMsg, 401 | aMessageMetadata, 402 | aState) -> { 403 | aSendingReport.setAS4ReceivedSignalMsg (aSignalMsg); 404 | }) 405 | .disableValidation (); 406 | final Wrapper aCaughtEx = new Wrapper <> (); 407 | eResult = aBuilder.sendMessageAndCheckForReceipt (aCaughtEx::set); 408 | LOGGER.info ("Peppol client send result: " + eResult); 409 | 410 | if (eResult.isSuccess ()) 411 | { 412 | // TODO determine the enduser ID of the outbound message 413 | // In many simple cases, this might be the sender's participant ID 414 | final String sEndUserID = aSenderID.getURIEncoded (); 415 | 416 | // TODO Enable Peppol Reporting when ready 417 | if (false) 418 | aBuilder.createAndStorePeppolReportingItemAfterSending (sEndUserID); 419 | } 420 | 421 | aSendingReport.setAS4SendingResult (eResult); 422 | 423 | if (aCaughtEx.isSet ()) 424 | { 425 | final Phase4Exception ex = aCaughtEx.get (); 426 | LOGGER.error ("Error sending Peppol message via AS4", ex); 427 | aSendingReport.setAS4SendingException (ex); 428 | bExceptionCaught = true; 429 | } 430 | } 431 | catch (final Exception ex) 432 | { 433 | // Mostly errors on HTTP level 434 | LOGGER.error ("Error sending Peppol message via AS4", ex); 435 | aSendingReport.setAS4SendingException (ex); 436 | bExceptionCaught = true; 437 | } 438 | finally 439 | { 440 | aSW.stop (); 441 | aSendingReport.setOverallDurationMillis (aSW.getMillis ()); 442 | } 443 | 444 | // Result may be null 445 | final boolean bSendingSuccess = eResult != null && eResult.isSuccess (); 446 | aSendingReport.setSendingSuccess (bSendingSuccess); 447 | aSendingReport.setOverallSuccess (bSendingSuccess && !bExceptionCaught); 448 | 449 | return aSendingReport; 450 | } 451 | 452 | /** 453 | * Send a Peppol message where the SBDH is passed in from the outside 454 | * 455 | * @param aData 456 | * The Peppol SBDH data to be send 457 | * @param aSmlInfo 458 | * The SML to be used for receiver lookup 459 | * @param aAPCAChecker 460 | * The Peppol CA checker to be used. 461 | * @param aSendingReport 462 | * The sending report to be filled. 463 | */ 464 | static void sendPeppolMessagePredefinedSbdh (@NonNull final PeppolSBDHData aData, 465 | @NonNull final ISMLInfo aSmlInfo, 466 | @NonNull final TrustedCAChecker aAPCAChecker, 467 | @NonNull final Phase4PeppolSendingReport aSendingReport) 468 | { 469 | final String sMyPeppolSeatID = APConfig.getMyPeppolSeatID (); 470 | aSendingReport.setSenderPartyID (sMyPeppolSeatID); 471 | 472 | EAS4UserMessageSendResult eResult = null; 473 | boolean bExceptionCaught = false; 474 | final StopWatch aSW = StopWatch.createdStarted (); 475 | try 476 | { 477 | // Start configuring here 478 | final IParticipantIdentifier aReceiverID = aData.getReceiverAsIdentifier (); 479 | 480 | final SMPClientReadOnly aSMPClient = new SMPClientReadOnly (Phase4PeppolSender.URL_PROVIDER, 481 | aReceiverID, 482 | aSmlInfo); 483 | 484 | aSMPClient.withHttpClientSettings (aHCS -> { 485 | // TODO Add SMP HTTP outbound proxy settings here 486 | // If this block is not used, it may be removed 487 | }); 488 | 489 | // In the meantime each SMP MUST be able to use SHA-256 490 | if (false) 491 | if (EJavaVersion.getCurrentVersion ().isNewerOrEqualsThan (EJavaVersion.JDK_17)) 492 | { 493 | // Work around the disabled SHA-1 in XMLDsig issue 494 | aSMPClient.setSecureValidation (false); 495 | } 496 | 497 | final Phase4PeppolHttpClientSettings aHCS = new Phase4PeppolHttpClientSettings (); 498 | // TODO Add AP HTTP outbound proxy settings here 499 | 500 | final PeppolUserMessageSBDHBuilder aBuilder = Phase4PeppolSender.sbdhBuilder () 501 | .httpClientFactory (aHCS) 502 | .payloadAndMetadata (aData) 503 | .senderPartyID (sMyPeppolSeatID) 504 | .peppolAP_CAChecker (aAPCAChecker) 505 | .smpClient (aSMPClient) 506 | .endpointURLConsumer (aSendingReport::setC3EndpointURL) 507 | .technicalContactConsumer (aSendingReport::setC3TechnicalContact) 508 | .certificateConsumer ( (aAPCertificate, 509 | aCheckDT, 510 | eCertCheckResult) -> { 511 | // Determined by SMP lookup 512 | aSendingReport.setC3Cert (aAPCertificate); 513 | aSendingReport.setC3CertCheckDT (aCheckDT); 514 | aSendingReport.setC3CertCheckResult (eCertCheckResult); 515 | }) 516 | .sendingDateTimeConsumer (aSendingReport::setAS4SendingDT) 517 | .buildMessageCallback (new IAS4ClientBuildMessageCallback () 518 | { 519 | public void onAS4Message (@NonNull final AbstractAS4Message aMsg) 520 | { 521 | // Created AS4 fields 522 | final AS4UserMessage aUserMsg = (AS4UserMessage) aMsg; 523 | aSendingReport.setAS4MessageID (aUserMsg.getEbms3UserMessage () 524 | .getMessageInfo () 525 | .getMessageId ()); 526 | aSendingReport.setAS4ConversationID (aUserMsg.getEbms3UserMessage () 527 | .getCollaborationInfo () 528 | .getConversationId ()); 529 | } 530 | }) 531 | .signalMsgConsumer ( (aSignalMsg, 532 | aMessageMetadata, 533 | aState) -> { 534 | aSendingReport.setAS4ReceivedSignalMsg (aSignalMsg); 535 | }); 536 | final Wrapper aCaughtEx = new Wrapper <> (); 537 | eResult = aBuilder.sendMessageAndCheckForReceipt (aCaughtEx::set); 538 | LOGGER.info ("Peppol client send result: " + eResult); 539 | 540 | if (eResult.isSuccess ()) 541 | { 542 | // TODO determine the enduser ID of the outbound message 543 | // In many simple cases, this might be the sender's participant ID 544 | final String sEndUserID = aData.getSenderAsIdentifier ().getURIEncoded (); 545 | 546 | // TODO Enable Peppol Reporting when ready 547 | if (false) 548 | aBuilder.createAndStorePeppolReportingItemAfterSending (sEndUserID); 549 | } 550 | 551 | aSendingReport.setAS4SendingResult (eResult); 552 | 553 | if (aCaughtEx.isSet ()) 554 | { 555 | final Phase4Exception ex = aCaughtEx.get (); 556 | LOGGER.error ("Error sending Peppol message via AS4", ex); 557 | aSendingReport.setAS4SendingException (ex); 558 | bExceptionCaught = true; 559 | } 560 | } 561 | catch (final Exception ex) 562 | { 563 | // Mostly errors on HTTP level 564 | LOGGER.error ("Error sending Peppol message via AS4", ex); 565 | aSendingReport.setAS4SendingException (ex); 566 | bExceptionCaught = true; 567 | } 568 | finally 569 | { 570 | aSW.stop (); 571 | aSendingReport.setOverallDurationMillis (aSW.getMillis ()); 572 | } 573 | 574 | // Result may be null 575 | final boolean bSendingSuccess = eResult != null && eResult.isSuccess (); 576 | aSendingReport.setSendingSuccess (bSendingSuccess); 577 | aSendingReport.setOverallSuccess (bSendingSuccess && !bExceptionCaught); 578 | } 579 | } 580 | --------------------------------------------------------------------------------