<br />
<div align="center"><img src="https://www.fitko.de/fileadmin/_processed_/b/9/csm_FIT-Connect_3e8e926015.jpg" alt="Logo" width="50%" height="50%"> </div>

<br/>

![](https://git.fitko.de/fit-connect/sdk-java/badges/main/pipeline.svg)
## About the FIT-Connect Java SDK

The Java SDK for FIT-Connect enables to build clients for senders and subscribers without directly interacting with any REST-Endpoints.
It provides a simple fluent API that guides through the creation and sending of a submission, as well as receiving submissions as a subscriber.

For further information, check out the official docs: [FIT-Connect Documentation](https://docs.fitko.de/fit-connect/docs/) as well as the:

* [Self-Service-Portal (Testing Environment)](https://portal.auth-testing.fit-connect.fitko.dev/)
* [FITKO project website](https://www.fitko.de/projektmanagement/fit-connect)

## **Outline**
[[_TOC_]]

## Getting Started

How to set up the SDK project locally.

### Build Dependencies

This section lists major frameworks/libraries used in the SDK.

* Java 11 (LTS)
* Maven 3.x
* Junit 5

FIT-Connect dependencies:
* [JWKValidator 1.5.0](https://git.fitko.de/fit-connect/jwk-validator)

Further 3rd party dependencies:

* Nimbus-Jose JWT
* Jackson FasterXMl
* JCommander
* Snakeyaml
* OpenCSV 
* OkHttp 

### Prerequisites

* Java Runtime >= 11, check your current setup in your commandline
  ```sh
  java --version 
  ```
<p align="right">(<a href="#top">back to top</a>)</p>

### Add FIT-Connect SDK to your build

To add a dependency on FIT-Connect using Maven, use the following:

```xml
<dependency>
  <groupId>dev.fitko.fitconnect.sdk</groupId>
  <artifactId>client</artifactId>
  <version>[Latest Version]</version>
</dependency>
```
With `[Latest Version]` of the last stable build or snapshot.

If you use a snapshot version, please add the maven snapshot repo to your pom.
```xml
<distributionManagement>
  <snapshotRepository>
  <id>ossrh</id>
  <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
  </snapshotRepository>
</distributionManagement>
```

<p align="right">(<a href="#top">back to top</a>)</p>

## Modules

See the projects' module documentation and the javadoc on the badge for more specific information:

* [API Module ](api/README.md) </br> [![javadoc](https://javadoc.io/badge2/dev.fitko.fitconnect.sdk/api/javadoc.svg)](https://javadoc.io/doc/dev.fitko.fitconnect.sdk/api)
* [Core Module ](core/README.md) </br>[![javadoc](https://javadoc.io/badge2/dev.fitko.fitconnect.sdk/core/javadoc.svg)](https://javadoc.io/doc/dev.fitko.fitconnect.sdk/core)
* [Client Module ](client/README.md) </br> [![javadoc](https://javadoc.io/badge2/dev.fitko.fitconnect.sdk/client/javadoc.svg)](https://javadoc.io/doc/dev.fitko.fitconnect.sdk/client)
* [Integration-Test Module ](integration-tests/README.md)

## Setup

_The following steps show how to get the SDK running_

1. Create and [account](https://docs.fitko.de/fit-connect/docs/getting-started/account) on the self-service portal
2. Get your sender/subscriber API-key credentials
3. Clone the sdk repo
   ```sh
   git clone https://git.fitko.de/fit-connect/sdk-java
   ```
4. Build the project with the included maven wrapper (no separate maven installation needed)
   ```sh
   ./mvnw clean install -DskipTests
   ```
5. Configure your `config.yml` (see [template](/config.yml))
    - add Sender OAuth credentials from the self-service portal to *SENDER* section 
    - add Subscriber OAuth credentials from the self-service portal to *SUBSCRIBER* section 
    - add reference to private decryption key (JWK) to *SUBSCRIBER* section
    - add reference to private signature key (JWK) to *SUBSCRIBER* section


6. Load a configuration via:
    ````java
      final ApplicationConfig config = ApplicationConfigLoader.loadConfigFromPath(Path.of("path/to/config.yml"));
      ````
7. Afterwards the config is used to initialize clients with the `ClientFactory` that offer clients for sender, subscriber and routing:
    ````java
      final SenderClient senderClient = ClientFactory.getSenderClient(config);
        //
      final SubscriberClient subscriberClient = ClientFactory.getSubscriberClient(config);
        //
      final RouterClient routerClient = ClientFactory.getRouterClient(config);
   ````

<p align="right">(<a href="#top">back to top</a>)</p>

## API Usage for Routing
The Router-Client allows to retrieve data about areas and services as well as their service destination.
A typical workflow using the `RouterClient` and `SenderClient` would be:

1) Find the `areaId` for an area via routing
2) Find the destination for a `leikaKey` and an `areaId` via routing
3) Submit a new submission to the destination using the `SenderClient`

### Finding Areas
Areas can be searched with one or more search criteria:

```java
final RouterClient routerClient = ClientFactory.getRouterClient(config);

final var citySearchCriterion = "Leip*";
final var zipCodeSearchCriterion = "04229";

// get first 5 area results
final List<Area> areas = routerClient.findAreas(List.of(citySearchCriterion, zipCodeSearchCriterion), 0, 5);

LOGGER.info("Found {} areas", areas.size());
for (final Area area : areas){
    LOGGER.info("Area {} with id {} found", area.getName(), area.getId());
}
```

### Finding Destinations 

For searching a destination the `DestinationSearch.Builder` is used pass a search request to the routing client.
The `leikaKey` is mandatory, as well as (max.) one other search criterion for the area/region, like one of:
- *areaId* = identifier of an area that can be retrieved via [findAreas(...)](#finding-areas) of the routing client
- ARS = *amtlicher Regionalschlüssel*
- AGS = *amtlicher Gemeindeschlüssel*

__Note:__ Both, the `leikaKey` service-identifier and the region keys `ars/ags`cannot be retrieved via the routing client, but can be found here: 
- [https://fimportal.de/](https://fimportal.de/) catalog for finding leikaKey service identifier                                        
- [https://opengovtech.de/](https://opengovtech.de/ars/) for looking up regional keys                                                  


#### Find destination by service identifier and *areaId*

```java
final RouterClient routerClient = ClientFactory.getRouterClient(config);

final DestinationSearch search = DestinationSearch.Builder()
        .withLeikaKey("99123456760610")
        .withAreaId("48566") // areaId of "Leipzig"
        .withLimit(3)
        .build();

// get first 3 route results
final List<Route> routes = routerClient.findDestinations(search);

LOGGER.info("Found {} routes for service identifier {}", routes.size(), leikaKey);
for (final Route route : routes){
    LOGGER.info("Route {} with destinationId {} found", route.getName(), route.getDestinationId());
}
```

#### Find destination by service identifier and region key (ARS/AGS)

Besides the areaId another search criterion for the area/region can be used as well:

```java
final RouterClient routerClient = ClientFactory.getRouterClient(config);

final DestinationSearch search = DestinationSearch.Builder()
        .withLeikaKey("99123456760610")
        .withArs("147130000000") // example ars for "Leipzig"
        .withLimit(3)
        .build();

// get first 3 route results
final List<Route> routes = routerClient.findDestinations(search);

LOGGER.info("Found {} routes for service identifier {}", routes.size(), leikaKey);
for (final Route route : routes){
    LOGGER.info("Route {} with destinationId {} found", route.getName(), route.getDestinationId());
}
```

## API Usage for Sender

For sending submission and already encrypted submissions builder are provided to construct the necessary payload to be sent. 
Builders allow method chaining and guide through all necessary steps.

The examples below expect a loaded configuration via the ApplicationConfigLoader.

### Hand in already encrypted submission (e.g. from frontend)
If all data, metadata and attachments are encrypted outside the SDK the sender client allows to retrieve the public key for encryption as well as sending of already encrypted payloads.

> For further information on how to implement end-2-end encryption please check the [FIT-Connect documentation](https://docs.fitko.de/fit-connect/docs/getting-started/encryption).

#### 1.  Retrieve public encryption key:

```java
final SenderClient senderClient = ClientFactory.getSenderClient(config);

// destination id that was retrieved via the router client
final var destinationId = UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");

final String publicJwkAsJsonString =  senderClient.getPublicKeyForDestination(destinationId);
```

#### 2. Send encrypted data

To send an encrypted submission the builder `SendableEncryptedSubmission.Builder` is needed to construct an `SendableEncryptedSubmission` object.
The payload can be submitted as shown below via the `ClientFactory`.

If optional attachments are added, the UUID needs to be provided for each attachment to be able to announce them before the submission is actually sent. 
This is necessary because the SDK does not have access to the already encrypted metadata that, which contains that information.

```java
// The constructed client can be reused to send multiple submissions
final SenderClient senderClient = ClientFactory.getSenderClient(config);

final SendableEncryptedSubmission encryptedSubmission = SendableEncryptedSubmission.Builder()
        .setDestination(UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14"))
        .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Führerscheinummeldung")
        .setEncryptedMetadata("$encrpyt€ed metadata")
        .setEncryptedData("{$encrpyt€ed json}")
        .addEncryptedAttachment(UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14"), "$encrpyt€ed @tt@chment") // optional
        .build();

final SentSubmission sentSubmission = senderClient.send(encryptedSubmission);
```

| **Important** |
| ------------- |
| If destination id (`destinationId`) and service type (`leikaKey`) are provided by a frontend component, they MUST NOT be blindly trusted. Instead, the sender's backend MUST check if sending submissions of this service type to the specified destination is allowed. |


### Hand in a new submission with unencrypted data

If all data, metadata and attachments are encrypted in the sender using the SDK, the client automatically handles the encryption.

Before the actual sending, a set of validations is applied to guarantee a compatibility with the subscriber. One of these validations checks, if the submission data meets the requirements of the provided schema URI. Currently, this submission data schema validation is only run, if the data was provided in JSON form. A similar XML validation will be implemented in the future.

Be aware that this example is not end-2-end encrypted, see [FIT-Connect documentation](https://docs.fitko.de/fit-connect/docs/getting-started/encryption) for details.

To send a submission the builder `SendableSubmission.Builder` is needed to construct a `SendableSubmission` object.
The payload can be submitted as shown below via the `ClientFactory`.

```java
// The constructed client can be reused to send multiple submissions
final SenderClient senderClient = ClientFactory.getSenderClient(config);

final SendableSubmission sendableSubmission = SendableSubmission.Builder()
        .setDestination(UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14"))
        .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Führerscheinummeldung")
        .setJsonData("{ \"someString\" : \"foo\"}", URI.create("https://test.fitko.dev/fit-connect/schema/submission-data-schema.json"))
        // optional properties
        .addAttachment(Attachment.fromPath(Path.of("path/to/attachment.txt"), "text/plain"))
        .setReplyChannel(ReplyChannel.fromEmail("test@mail.org"))
        .build();

final SentSubmission sentSubmission = senderClient.send(sendableSubmission);

```

### Read the latest status of a submission

For checking the current status of a submission ``getStatusForSubmission`` gets the latest event from the event-log.

```java
final SentSubmission sentSubmission =  ... // persisted sent submission by sender client
final EventStatus submissionStatus = ClientFactory.getSenderClient(config).getStatusForSubmission(sentSubmission);

LOGGER.info("Current status for submission {} => {}", sentSubmission.getSubmissionId(), submissionStatus.getStatus());
```
The example output shows the current state of the submission after being created, following the transitions in the diagram below:

````plaintext
Current status for submission 43cf7163-5163-4bc8-865e-be96e271ecc3 => incomplete
````

<img src="https://docs.fitko.de/fit-connect/assets/images/status-ebe91122f32321e22f094882a66c1139.svg">

### Validating callbacks

When receiving callbacks from the FIT-Connect system, sender (and subscriber) of submissions have to check if the received data is valid to prevent unauthorized calls from messing with the business processes and user data. The values needed for this validation are transmitted  within the received HTTP requests. FIT-Connect uses the HMAC mechanism to proof the validity of its calls.

More details on how this method works can be found here:
- https://docs.fitko.de/fit-connect/docs/details/callbacks/#callback-validation
- https://www.rfc-editor.org/rfc/rfc2104

The Java SDK provides a convenient method for validating callbacks, its usage could look like this:

```java
final SenderClient senderClient = ClientFactory.getSenderClient(config);

final ValidationResult validationResult = senderClient.validateCallback("hmac", 0L, "body", "secret");

if(validationResult.hasError()){
    LOGGER.error(validationresult.getError().getMessage());
}
```

## API Usage for Subscriber

### Retrieving submissions
Submissions can be fetched by id or as a list of submissions for a specific case.
#### List with pagination
Limit and offset parameters allow to page through the result.
```java
final var subscriberClient = ClientFactory.getSubscriberClient(config);

final int offset = 0;
final int limit = 100;
final var destinationId = UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");

final Set<SubmissionForPickup> firstOneHundredSubmissions = subscriberClient.getAvailableSubmissionsforDestination(destinationId), limit, offset);
```

#### List without pagination
Listing available submissions without pagination pulls the first 500 entries.
```java
final var subscriberClient = ClientFactory.getSubscriberClient(config);

final var destinationId= UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");

final Set<SubmissionForPickup> submissions = subscriberClient.getAvailableSubmissionsForDestination(destinationId);
```
#### Receive single submission 

```java
// by id
final var submissionId = UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");
final ReceivedSubmission receivedSubmission =  ClientFactory.getSubscriberClient(config).requestSubmission(submissionId);
```
```java
// by object
final SubmissionForPickup submissionForPickup = // code for sending submission;
final ReceivedSubmission receivedSubmission =  ClientFactory.getSubscriberClient(config).requestSubmission(submissionForPickup);
```

Now, the received submission allows access to the decrypted data, attachments, metadata and ids
```java
 // access data
final String data = receivedSubmission.getDataAsString();
final URI dataSchemaUri = receivedSubmission.getDataSchemaUri();
final String mimeType = receivedSubmission.getDataMimeType();

// access metadata
final Metadata metadata = receivedSubmission.getSubmissionMetdata();
// access reply channel
final ReplyChannel replyChannel = metadata.getReplyChannel();

// access attachments
for(final Attachment attachment : receivedSubmission.getAttachments()){
    final String originalFilename = attachment.getFilename();
    final String attachmentMimeType = attachment.getMimeType();
    // different formats to retrieve the attachment content
    final byte[] attachmentDataAsBytes = attachment.getDataAsBytes();
    final String attachmentDataAsString = attachment.getDataAString(StandardCharsets.UTF_8);
}

// access further ids of submission
final UUID caseId  = receivedSubmission.getCaseId();
final UUID submissionId  = receivedSubmission.getSubmissionId();
final UUID destinationId  = receivedSubmission.getDestinationId();
```

### Sending events to the event-log

In order to accept or reject a submission, the subscriber client can send events ([security-event-tokens](https://www.rfc-editor.org/rfc/rfc8417.html)) to the event-log.
For more details please see the documentation on [events](https://docs.fitko.de/fit-connect/docs/getting-started/event-log/overview) and the creation of [set-events](https://docs.fitko.de/fit-connect/docs/getting-started/event-log/set-creation).

#### Accepting a submission
If the functional review by the subscriber was positive, the submission can be accepted with an `accept-submission` event. 
For this event a list of optional problems can be sent, the submission is still accepted but with remarks. 
```java
final var submissionId = UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");
final SubscriberClient subscriberClient = ClientFactory.getSubscriberClient(config);

// fetch submission with all data first
final ReceivedSubmission receivedSubmission = subscriberClient.requestSubmission(submissionId);

// accept received submission (without problems)
receivedSubmission.acceptSubmission();
        
// accept with an optional list of problems
receivedSubmission.acceptSubmission(List.of(new MyCustomProblem()));;
 
```
After the accept event was sent the submission transitions into the state `deleted` and is removed.
#### Rejecting a submission
If the functional or technical review by the subscriber was negative, e.g. there is a discrepancy within the semantics of the data, the submission can be rejected with an `reject-submission` event.
The rejection takes a list of `dev.fitko.fitconnect.api.domain.model.event.problems.*` as parameter to specify the problem. 
See the Fit-Connect documentation for more details on [available (technical) problems](https://docs.fitko.de/fit-connect/docs/receiving/verification).
```java
final var submissionId = UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");
final SubscriberClient subscriberClient = ClientFactory.getSubscriberClient(config);

// fetch submission with all data first
final ReceivedSubmission receivedSubmission = subscriberClient.requestSubmission(submissionId);

// reject with a list of problems
receivedSubmission.rejectSubmission(List.of(new DataSchemaViolation()));
```
After the rejection event was sent the submission transitions into the state `deleted` and is removed.

#### Rejecting a submission via subscriber client directly
Instead of retrieving the full submission with all data first as seen in the examples above, a ``SubmissionForPickup`` can also be rejected directly on the subscriber client:
```java
final var destinationId = UUID.fromString("d2d43892-9d9c-4630-980a-5af341179b14");
final SubscriberClient subscriberClient = ClientFactory.getSubscriberClient(config);

final Set<SubmissionForPickup> submissions = subscriberClient.getAvailableSubmissionsForDestination(destinationId);

submissions.forEach(submission -> subscriberClient.rejectSubmission(submission, List.of(new TechnicalProblem())))
```

### Read the latest status of a submission

For checking the current status of a submission ``getStatusForSubmission`` gets the latest event from the event-log. 
> Calling the status as a subscriber, authentication tags are not validated !

```java
final var subscriberClient = ClientFactory.getSubscriberClient(config);

final var destinationId = UUID.fromString("5872e50a-a776-4c3b-9f41-7e48c6ac8491");
final var caseId = UUID.fromString("9600356f-f694-4790-ab8a-5c73387ab7c7");
final var submissionId = UUID.fromString("25efc57a-7197-4c14-9cfe-cfc87a954ef5");

final EventStatus submissionStatus = subscriberClient.getStatusForSubmission(destinationId, caseId, submissionId);

LOGGER.info("Current status for submission {} => {}", sentSubmission.getSubmissionId(), submissionStatus.getStatus());
```
The example output shows the current state of the submission after being created, following the transitions in the diagram below:

````plaintext
Current status for submission 43cf7163-5163-4bc8-865e-be96e271ecc3 => incomplete
````

<img src="https://docs.fitko.de/fit-connect/assets/images/status-ebe91122f32321e22f094882a66c1139.svg">

### Validating callbacks

The validation of callbacks works similar to the sender side (see [Sender Callback Validation](#validating-callbacks)), but instead of the `SenderClient`, we use the `SubscriberClient`:

```java
final SubscriberClient subscriberClient = ClientFactory.getSubscriberClient(config);

final ValidationResult validationResult = subscriberClient.validateCallback("hmac", 0L, "body", "secret");

if(validationResult.hasError()){
    LOGGER.error(validationresult.getError().getMessage());
}
```

## Submission Data Validation
In order to validate the submitted submission data a local schema directory can be provided within in the ``config.yaml``:

````yaml
submissionDataSchemaPath: "/path/to/schema/dir"
````

All schemas in that directory will be loaded and can be referenced in the ``SendableSubmission`` whilst sending. 
If no schema is provided or the key is not set in the config, the URI for submission data validation will be fetched via HTTP.

Use Cases:
- submission data schema is a `URN` that cannot be loaded via HTTP
- submission data schema `URI` is not accessible by the SDK

## Error Handling
### Exceptions
All clients accessible via the ``ClientFactory`` throw a custom `RuntimeException` if a technical error occurred:
- SenderClient => throws ``FitConnectSenderException``
- SubscriberClient => throws ``FitConnectSubscriberException``
- RouterClient => throws ``FitConnectRouterException``

For all other issues regarding the configuration and set-up of the SDK a ``FitConnectInitialisationException`` will be thrown.

### Auto-Reject
The SDK performs validations on all kind of requests e.g. checks for data integrity and JWT-validity. 
If one of the validations fails on subscriber side, whilst retrieving, the submission it will be auto-rejected. 
This means a ``REJECT`` event is sent to the event-log with a subsequent deletion of that invalid submission.

Please see the [documentation on verification](https://docs.fitko.de/fit-connect/docs/receiving/verification) for all tests and checks that are executed. 

## Integration Tests

Integration tests do not run per default with `mvn test`, but they can be executed with the maven via `mvn verify`. 
They expect the following environment variables to be set in the rn configuration of the IDE or on the local terminal:

* SENDER_CLIENT_ID
* SENDER_CLIENT_SECRET
* SUBSCRIBER_CLIENT_ID
* SUBSCRIBER_CLIENT_SECRET
* TEST_DESTINATION_ID

Submit submissions are cleaned on every test run, please be aware that this might affect the present submissions on the destination that is configured.
Currently, the tests are set up for the test-environment with:

```
var authBaseUrl = "https://auth-testing.fit-connect.fitko.dev";
var routingBaseUrl = "https://routing-api-testing.fit-connect.fitko.dev";
var selfServicePortalUrl = "https://portal.auth-testing.fit-connect.fitko.dev";
var submissionBaseUrl = "https://submission-api-testing.fit-connect.fitko.dev";
```

## Roadmap
- [ ] Self-Service-Portal Client
- [ ] Generation of JWK Test Keys 

See the [open issues](https://git.fitko.de/fit-connect/planning/-/boards/44?search=SDK) for a full list of proposed features (and known issues).

<p align="right">(<a href="#top">back to top</a>)</p>

<!-- CONTACT -->

## Contact

[FIT-Connect Contact Page](https://docs.fitko.de/fit-connect/contact/) for further information

<p align="right">(<a href="#top">back to top</a>)</p>


## License
Source code is licensed under the [EUPL](https://joinup.ec.europa.eu/page/eupl-text-11-12).

*Rechtlicher Hinweis: Dieses Software Development Kit (SDK) ist dazu bestimmt, die Anbindung einer Software an die FIT-Connect-Infrastruktur zu ermöglichen.
Hierfür kann das SDK in die anzubindenden Software integriert werden.
Erfolgt die Integration des SDK in unveränderter Form, liegt keine Bearbeitung im Sinne der EUPL bzw. des deutschen Urheberrechts vor.
Die Art und Weise der Verlinkung des SDK führt insbesondere nicht zur Schaffung eines abgeleiteten Werkes.
Die unveränderte Übernahme des SDK in eine anzubindende Software führt damit nicht dazu, dass die anzubindende Software unter den Bedingungen der EUPL zu lizenzieren ist.
Für die Weitergabe des SDK selbst - in unveränderter oder bearbeiteter Form, als Quellcode oder ausführbares Programm - gelten die Lizenzbedingungen der EUPL in unveränderter Weise.*

<p align="right">(<a href="#top">back to top</a>)</p>