Skip to content
Snippets Groups Projects
Commit b73690f3 authored by Martin Vogel's avatar Martin Vogel
Browse files

Merge branch 'feature/570-anbindung-routing-api' into 'main'

Anbindung Routing API

See merge request !102
parents 068f3eaa 4e1d44e5
No related branches found
No related tags found
1 merge request!102Anbindung Routing API
Showing
with 370 additions and 119 deletions
......@@ -95,7 +95,7 @@ _The following steps show how to get the SDK running_
```sh
./mvnw clean install -DskipTests
```
5. Configure your `config.yml`
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
......@@ -110,6 +110,59 @@ _The following steps show how to get the SDK running_
<p align="right">(<a href="#top">back to top</a>)</p>
## API Usage for Routing
The Routing-Client allows to retrieve data about areas and services as well as their service destination.
A typical workflow using the `RoutingClient` 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 RoutingClient routingClient = ClientFactory.routingClient(config);
final var citySearchCriterion = "Leip*";
final var zipCodeSearchCriterion = "04229";
// get first 5 area results
final List<Area> areas = routingClient.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 by service identifier and region
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:
- ars amtlicher regionalschlüssel
- ags amtlicher gemeindeschlüssel
- areaId identifier of an area that can be retrieved via [finding areas](#finding-areas)
```java
final RoutingClient routingClient = ClientFactory.routingClient(config);
final DestinationSearch search = DestinationSearch.Builder()
.withLeikaKey("99123456760610")
.withArs("064350014014")
.withLimit(5)
.build();
// get first 5 route results
final List<Route> routes = routingClient.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.
......@@ -374,8 +427,7 @@ if(validationResult.hasError()){
```
## Roadmap
- [ ] Add Routing features
- [ ] Add Callback validation
- [ ] Add auto-reject on technical errors
- [ ] Maven central release of 1.0.0
......
## API Module
The API-module contains interfaces and domain model value classes that provide the basic functionality to build an
sdk-client.
The API-module contains interfaces and domain model value classes that provide the basic functionality to build a sdk-client.
### Structure
......@@ -10,7 +9,7 @@ sdk-client.
- **api.exceptions** - all use case specific the services throw
- **api.services** - all services to authenticate, encrypt, validate and perform REST-requests
There are two service facade interfaces that provide a client centric wrapper around the underlying services,
There are two service facade interfaces that provide a client centric wrapper around the underlying services:
- **Sender** - create a submission, announce attachments, encrypt and send the submission including metadata
- **Subscriber** - poll, receive and decrypt submissions and confirm their valid transmission
......
......@@ -30,6 +30,9 @@ public class ApplicationConfig {
@Builder.Default
private URI metadataSchemaWriteVersion = SchemaConfig.METADATA_V_1_0_0.getSchemaUri();
@Builder.Default
private URI destinationSchemaVersion = SchemaConfig.XZUFI_DESTINATION_SCHEMA.getSchemaUri();
private SenderConfig senderConfig;
private SubscriberConfig subscriberConfig;
......
......@@ -6,14 +6,19 @@ final class ResourcePaths {
}
static final String AUTH_TOKEN_PATH = "/token";
static final String DESTINATIONS_PATH = "/v1/destinations/{destinationId}";
static final String DESTINATIONS_KEY_PATH = "/v1/destinations/{destinationId}/keys/{kid}";
static final String EVENTS_PATH = "/v1/cases/{caseId}/events";
static final String SUBMISSION_PATH = "/v1/submissions/{submissionId}";
static final String SUBMISSIONS_PATH = "/v1/submissions";
static final String SUBMISSION_ATTACHMENT_PATH = "/v1/submissions/{submissionId}/attachments/{attachmentId}";
static final String ROUTING_AREA_PATH = "/v1/areas";
static final String ROUTING_ROUTE_PATH = "/v1/routes";
static final String WELL_KNOWN_KEYS_PATH = "/.well-known/jwks.json";
}
......@@ -14,7 +14,9 @@ public enum SchemaConfig {
EVENTS_SCHEMA_PATH(SCHEMA_BASE_URL.schemaUri.resolve("events/")),
SET_V_1_0_1(SCHEMA_BASE_URL.schemaUri.resolve("set-payload/1.0.1/set-payload.schema.json"), "set_schema_1.0.1.json"),
SET_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("set-payload/1.0.0/set-payload.schema.json"), "set_schema_1.0.0.json"),
METADATA_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("metadata/1.0.0/metadata.schema.json"), "metadata_schema_1.0.0.json");
METADATA_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("metadata/1.0.0/metadata.schema.json"), "metadata_schema_1.0.0.json"),
XZUFI_DESTINATION_SCHEMA(SCHEMA_BASE_URL.schemaUri.resolve("xzufi/destination.schema.json"), "destination_schema.json");
private final URI schemaUri;
......@@ -42,6 +44,12 @@ public enum SchemaConfig {
.collect(Collectors.toList());
}
public static List<String> getDestinationSchemaPaths(final String destinationSchemaBaseDir) {
return Stream.of(XZUFI_DESTINATION_SCHEMA.fileName)
.map(fileName -> destinationSchemaBaseDir + "/" + fileName)
.collect(Collectors.toList());
}
@Override
public String toString() {
return schemaUri.toString();
......
package dev.fitko.fitconnect.api.domain.model.destination;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@JsonTypeName("ReplyChannel_eMail")
@NoArgsConstructor
public class ReplyChannelEMail {
class ReplyChannelEMail {
@JsonProperty("usePgp")
private Boolean usePgp;
......
......@@ -3,34 +3,45 @@ package dev.fitko.fitconnect.api.domain.model.metadata.attachment;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public enum Purpose {
FORM("form"),
ATTACHMENT("attachment"),
REPORT("report");
private final String value;
private static final Map<String, Purpose> CONSTANTS = new HashMap<>();
static {
for (final Purpose p : values()) {
CONSTANTS.put(p.value, p);
}
}
Purpose(final String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
return value;
}
@JsonValue
public String value() {
return this.value;
return value;
}
@JsonCreator
public static Purpose fromValue(final String value) {
return Arrays.stream(Purpose.values())
.filter(enumValue -> enumValue.value.equals(value))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unexpected value '" + value + "'"));
final Purpose constant = CONSTANTS.get(value);
if (constant == null) {
throw new IllegalArgumentException("Unexpected value '" + value + "'");
}
return constant;
}
}
package dev.fitko.fitconnect.api.domain.model.route;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Area {
@JsonProperty("id")
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("type")
private String type;
}
package dev.fitko.fitconnect.api.domain.model.route;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AreaResult {
@JsonProperty("count")
private Integer count;
@JsonProperty("offset")
private Integer offset;
@JsonProperty("totalCount")
private Integer totalCount;
@JsonProperty("areas")
private List<Area> areas = new ArrayList<>();
}
package dev.fitko.fitconnect.api.domain.model.route;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.UUID;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Route {
@JsonProperty("destinationId")
private UUID destinationId;
@JsonProperty("destinationSignature")
private String destinationSignature;
@JsonProperty("destinationParameters")
private RouteDestination destinationParameters;
@JsonProperty("destinationParametersSignature")
private String destinationParametersSignature;
@JsonProperty("destinationName")
private String destinationName;
@JsonProperty("destinationLogo")
private String destinationLogo;
}
package dev.fitko.fitconnect.api.domain.model.route;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import dev.fitko.fitconnect.api.domain.model.destination.ReplyChannel;
import dev.fitko.fitconnect.api.domain.model.destination.StatusEnum;
import dev.fitko.fitconnect.api.domain.model.jwk.ApiJwkSet;
import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RouteDestination {
@JsonProperty("encryptionKid")
private String encryptionKid = null;
@JsonProperty("metadataVersions")
private List<String> metadataVersions = new ArrayList<>();
@JsonProperty("publicKeys")
private ApiJwkSet publicKeys = null;
@JsonProperty("replyChannels")
private ReplyChannel replyChannels = null;
@JsonProperty("status")
private StatusEnum status = null;
@JsonProperty("submissionSchemas")
private List<SubmissionSchema> submissionSchemas = new ArrayList<>();
@JsonProperty("submissionUrl")
private String submissionUrl = null;
}
package dev.fitko.fitconnect.api.domain.model.route;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RouteResult {
@JsonProperty("count")
private Integer count;
@JsonProperty("offset")
private Integer offset;
@JsonProperty("totalCount")
private Integer totalCount;
@JsonProperty("routes")
private List<Route> routes = new ArrayList<>();
}
package dev.fitko.fitconnect.api.domain.schema;
import lombok.Value;
import java.util.List;
@Value
public class SchemaResources {
List<String> setSchemaPaths;
List<String> metadataSchemaPaths;
List<String> destinationSchemaPaths;
}
package dev.fitko.fitconnect.api.exceptions;
public class RoutingException extends RuntimeException {
public RoutingException(final String errorMessage) {
super(errorMessage);
}
public RoutingException(final String errorMessage, final Throwable error) {
super(errorMessage, error);
}
}
......@@ -38,7 +38,7 @@ public interface KeyService {
* @param keyId unique identifier of the {@link RSAKey}
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
*/
RSAKey getPortalSignatureKey(String keyId);
RSAKey getPortalPublicKey(String keyId);
/**
* Get a public signature key for a given key-id from the submission service well-known keys.
......@@ -46,6 +46,15 @@ public interface KeyService {
* @param keyId unique identifier of the {@link RSAKey}
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
*/
RSAKey getSubmissionServiceSignatureKey(String keyId);
RSAKey getSubmissionServicePublicKey(String keyId);
/**
* Get a public signature key for a given key-id from a submission service instance url well known keys.
* The <b>'/well-known/jkws.json'</b> is added to the given url.
*
* @param url custom url to load the well known keys from
* @param keyId unique identifier of the {@link RSAKey} the well known keys are filtered by
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
*/
RSAKey getWellKnownKeysForSubmissionUrl(String url, String keyId);
}
package dev.fitko.fitconnect.api.services.routing;
import dev.fitko.fitconnect.api.domain.model.route.Area;
import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
import dev.fitko.fitconnect.api.domain.model.route.Route;
import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import java.util.List;
/**
* Routing API Service that retrieves data form FIT-Connect Routing API REST-Endpoints
*/
public interface RoutingService {
/**
* Finds an {@link AreaResult} based on a list of multiple filter criteria include a zip code or e.g. city as in List.of("04229", "Leip*").
*
* @param searchExpressions list of string filters
* @param offset offset to start from
* @param limit max entries
*
* @return list of {@link Area}
* @throws RestApiException if a technical error occurred
*/
AreaResult getAreas(List<String> searchExpressions, int offset, int limit) throws RestApiException;
/**
* Finds a {@link RouteResult} by a given service identifier and AT LEAST ONE OTHER search criterion (ars | ags | areaId).
*
* @param leikaKey leikaKey
* @param ars amtlicher regionalschlüssel
* @param ags amtlicher gemeindeschlüssel
* @param areaId areaId
* @param offset offset to start from
* @param limit max entries
*
* @return list of found {@link Route}s matching the search criteria
* @throws RestApiException if a technical error occurred
*/
RouteResult getRoutes(String leikaKey, String ars, String ags, String areaId, int offset, int limit) throws RestApiException;
}
package dev.fitko.fitconnect.api.services.routing;
import dev.fitko.fitconnect.api.domain.model.route.Route;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import java.util.List;
/**
* Service to verify the validity of routing-related data.
*/
public interface RoutingVerificationService {
/**
* Verifies host url, self-service portal signature and DVDV signature of the provided {@link Route}.
*
* @param routes list of routes to be verified
* @param requestedServiceIdentifier requested service identifier to be checked if the route supports it
* @param requestedRegion requested service region to be checked if the routes supports it
* @return {@link ValidationResult}, is ok if validation passes, has an error including an exception if the validation failed
*/
ValidationResult validateRouteDestinations(List<Route> routes, String requestedServiceIdentifier, String requestedRegion);
}
......@@ -50,4 +50,14 @@ public interface SchemaProvider {
* @throws SchemaNotFoundException if the schema is not existing
*/
String loadMetadataSchema(URI schemaUri) throws SchemaNotFoundException;
/**
* Load the schema for payload of signed destination.
*
* @param schemaUri uri of the destination schema that should be loaded
* @return the schema as string
*
* @throws SchemaNotFoundException if the schema is not existing
*/
String loadDestinationSchema(URI schemaUri) throws SchemaNotFoundException;
}
\ No newline at end of file
......@@ -4,6 +4,8 @@ import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import java.util.Map;
/**
* Validator for publicKeys and metadata.
*
......@@ -53,6 +55,15 @@ public interface ValidationService {
*/
ValidationResult validateSetEventSchema(String setEventPayload);
/**
* Validates signature payload claims against a given schema.
*
* @param destinationPayload the payload to be validated
*
* @return a {@link ValidationResult} with an optional error
*/
ValidationResult validateDestinationSchema(Map<String, Object> destinationPayload);
/**
* Compares a given byte[] to its original hash value.
*
......
## Client Module
### Commandline Client
The sdk comes with a commandline client to be able to use the sdk without any coding.
The sdk provides a commandline-client as a runnable .jar, to be able to use the sdk without any coding.
#### Setup & Build
1. Build project root wih ``./mvnw clean package``
2. Go to client/target and find a runnable jar ``client-VERSION.jar``
2. Go to client/target and find a runnable jar ``fit-connect-client.jar``
3. Provide [config yaml](../config.yml):
1. set environment variable ``FIT_CONNECT_CONFIG``:
1. Linux/MacOS: ``export FIT_CONNECT_CONFIG=path/to/config.yml``
......@@ -16,13 +16,13 @@ The sdk comes with a commandline client to be able to use the sdk without any co
var senderClient = ClientFactory.senderClient(config);
````
3. put ``config.yml`` in same path as the .jar-file
5. run client with ``java -jar client-VERSION.jar [COMMAND] [OPTIONS]``
5. run client with ``java -jarfit-connect-client.jar [COMMAND] [OPTIONS]``
#### SEND Example
#### SEND Single Submission Example
The send command submits a new submission to a destination. Apart from optional attachments, all options are mandatory.
````sh
java -jar client-1.0-SNAPSHOT.jar send
java -jar fit-connect-client.jar send
--destinationId=1b7d1a24-a6c8-4050-bb71-ae3749ec432f
--serviceName=Test
--leikaKey=urn:de:fim:leika:leistung:99400048079000
......@@ -35,7 +35,7 @@ java -jar client-1.0-SNAPSHOT.jar send
#### LIST Submissions Example
The list command lists all submissionIds for a given destinationID.
````sh
java -jar client-1.0-SNAPSHOT.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f
java -jar fit-connect-client.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f
````
#### GET Single Submission Example
The get command loads a submission by `submissionId` and stores data and attachments in the given target location. If no target is
......@@ -43,13 +43,13 @@ set, the cmd-client saves the data into in a folder named by the `submissionId`
jar.
````sh
java -jar client-1.0-SNAPSHOT.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
java -jar fit-connect-client.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
````
#### Batch Mode
To send multiple submission with a single command, the client can be used in batch mode:
````sh
java -jar client-1.0-SNAPSHOT.jar batch --data=batch_data.csv
java -jar fit-connect-client.jar batch --data=batch_data.csv
````
Currently, the import of CSV is supported. Follow the schema below, setting up your data:
......@@ -112,95 +112,3 @@ Usage: <main class> [command] [command options]
* --data
Path to submission data as csv
````
### API Flow
The ClientFactory provides fluent API clients for both **Sender** and **Subscriber**.
As the flow chart below shows, the fluent client guides through all essential calls in order to hand in a correct
**submission** as well as receive submissions on the subscriber side.
#### Api client flow for sending a submission
For the actual sender client those calls look like this:
<table>
<tr>
<th>Workflow</th>
<th>Java sample calls</th>
</tr>
<tr>
<td>
```mermaid
flowchart TD
A[Create Client] --> B(Add Attachments)
A[Create Client] --> C(Add Data)
B -->|next| C[Add Data]
C -->|next| D[Add Destination]
D -->|next| E[Add ServiceType]
E -->|send| F[SubmissionForPickup]
```
Add Service Type
</td>
<td>
```java
ClientFactory.senderClient()
.withAttachments(attachments)
.withJsonData("{ caseSpecific: 'Data' }")
.withDestination(UUID.randomUUID())
.withServiceType("ServiceName", "leika:key:service")
.submit();
```
</td>
</tr>
</table>
#### Api client flow for subscribing to a submission
<table>
<tr>
<th>Workflow</th>
<th>Java sample calls</th>
</tr>
<tr>
<td>
```mermaid
flowchart TD
A[Create Client] --> B(Poll List ofAvailable Submissions)
A[Create Client] --> C(Request Submission by ID)
C -->|get| D[Attachments]
C -->|get| E[Metadata]
C -->|get| F[Data]
```
</td>
<td>
```java
var client = ClientFactory.subscriberClient();
var submissions = client.getAvailableSubmissions(destinationId);
// filter submission list for requested one ...
var submission = client.requestSubmission(submissionId)
submission.getAttachments();
submission.getMetadata();
submission.getData();
```
</td>
</tr>
</table>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment