├── 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 |
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 |
--------------------------------------------------------------------------------