diff --git a/.gitignore b/.gitignore
index 6691bb15..fcfc5635 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-# Copyright 2018 Crown Copyright
+# Copyright 2018-2021 Crown Copyright
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -25,33 +25,31 @@ bin/
_book
node_modules/
.extract/
-.project/
.settings/
.terraform/
+*.flattened-pom.xml
terraform.tfvars
terraform.tfstate
terraform.tfstate.backup
dependency-reduced-pom.xml
+*.iws
+*.ipr
##VSCODE default files that dont need to be tracked
eclipse-formatter.xml
.vscode/
-
-
# Created by https://www.toptal.com/developers/gitignore/api/eclipse
# Edit at https://www.toptal.com/developers/gitignore?templates=eclipse
### Eclipse ###
.metadata
-bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
-.settings/
.loadpath
.recommenders
diff --git a/Jenkinsfile b/Jenkinsfile
index c4df9600..58f05827 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 Crown Copyright
+ * Copyright 2018-2021 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,153 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-//node-affinity
-//nodes 1..3 are reserved for Jenkins slave pods.
-//node 0 is used for the Jenkins master
+@Library('jenkinsfile-lib')_
timestamps {
-
- podTemplate(yaml: '''
- apiVersion: v1
- kind: Pod
- metadata:
- name: dind
- spec:
- affinity:
- nodeAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- - weight: 1
- preference:
- matchExpressions:
- - key: palisade-node-name
- operator: In
- values:
- - node1
- - node2
- - node3
- containers:
- - name: jnlp
- image: jenkins/jnlp-slave
- imagePullPolicy: Always
- args:
- - $(JENKINS_SECRET)
- - $(JENKINS_NAME)
- resources:
- requests:
- ephemeral-storage: "4Gi"
- limits:
- ephemeral-storage: "8Gi"
-
- - name: docker-cmds
- image: 779921734503.dkr.ecr.eu-west-1.amazonaws.com/jnlp-did:200608
- imagePullPolicy: IfNotPresent
- command:
- - sleep
- args:
- - 99d
- env:
- - name: DOCKER_HOST
- value: tcp://localhost:2375
- resources:
- requests:
- ephemeral-storage: "4Gi"
- limits:
- ephemeral-storage: "8Gi"
- ''') {
- node(POD_LABEL) {
- def GIT_BRANCH_NAME
-
- stage('Bootstrap') {
- if (env.CHANGE_BRANCH) {
- GIT_BRANCH_NAME=env.CHANGE_BRANCH
- } else {
- GIT_BRANCH_NAME=env.BRANCH_NAME
- }
- echo sh(script: 'env | sort', returnStdout: true)
- }
-
- stage('Prerequisites') {
- dir('Palisade-common') {
- git branch: 'develop', url: 'https://github.com/gchq/Palisade-common.git'
- if (sh(script: "git checkout ${GIT_BRANCH_NAME}", returnStatus: true) == 0) {
- container('docker-cmds') {
- configFileProvider([configFile(fileId: "${env.CONFIG_FILE}", variable: 'MAVEN_SETTINGS')]) {
- sh 'mvn -s $MAVEN_SETTINGS install -P quick'
- }
- }
- }
- }
- dir('Palisade-readers') {
- git branch: 'develop', url: 'https://github.com/gchq/Palisade-readers.git'
- if (sh(script: "git checkout ${GIT_BRANCH_NAME}", returnStatus: true) == 0) {
- container('docker-cmds') {
- configFileProvider([configFile(fileId: "${env.CONFIG_FILE}", variable: 'MAVEN_SETTINGS')]) {
- sh 'mvn -s $MAVEN_SETTINGS install -P quick'
- }
- }
- }
- }
- }
-
- stage('Install, Unit Tests, Checkstyle') {
- dir('Palisade-clients') {
- git branch: GIT_BRANCH_NAME, url: 'https://github.com/gchq/Palisade-clients.git'
- container('docker-cmds') {
- configFileProvider([configFile(fileId: "${env.CONFIG_FILE}", variable: 'MAVEN_SETTINGS')]) {
- sh 'mvn -s $MAVEN_SETTINGS install'
- }
- }
- }
- }
-
- stage('SonarQube analysis') {
- dir('Palisade-clients') {
- container('docker-cmds') {
- withCredentials([string(credentialsId: "${env.SQ_WEB_HOOK}", variable: 'SONARQUBE_WEBHOOK'),
- string(credentialsId: "${env.SQ_KEY_STORE_PASS}", variable: 'KEYSTORE_PASS'),
- file(credentialsId: "${env.SQ_KEY_STORE}", variable: 'KEYSTORE')]) {
- configFileProvider([configFile(fileId: "${env.CONFIG_FILE}", variable: 'MAVEN_SETTINGS')]) {
- withSonarQubeEnv(installationName: 'sonar') {
- if (env.CHANGE_BRANCH) {
- sh 'mvn -s $MAVEN_SETTINGS org.sonarsource.scanner.maven:sonar-maven-plugin:3.7.0.1746:sonar -Dsonar.projectKey="Palisade-Clients-${CHANGE_BRANCH}" -Dsonar.projectName="Palisade-Clients-${CHANGE_BRANCH}" -Dsonar.webhooks.project=$SONARQUBE_WEBHOOK -Djavax.net.ssl.trustStore=$KEYSTORE -Djavax.net.ssl.trustStorePassword=$KEYSTORE_PASS'
- } else {
- sh 'mvn -s $MAVEN_SETTINGS org.sonarsource.scanner.maven:sonar-maven-plugin:3.7.0.1746:sonar -Dsonar.projectKey="Palisade-Clients-${BRANCH_NAME}" -Dsonar.projectName="Palisade-Clients-${BRANCH_NAME}" -Dsonar.webhooks.project=$SONARQUBE_WEBHOOK -Djavax.net.ssl.trustStore=$KEYSTORE -Djavax.net.ssl.trustStorePassword=$KEYSTORE_PASS'
- }
- }
- }
- }
- }
- }
- }
-
- stage("SonarQube Quality Gate") {
- // Wait for SonarQube to prepare the report
- sleep(time: 10, unit: 'SECONDS')
- // Just in case something goes wrong, pipeline will be killed after a timeout
- timeout(time: 5, unit: 'MINUTES') {
- // Reuse taskId previously collected by withSonarQubeEnv
- def qg = waitForQualityGate()
- if (qg.status != 'OK') {
- error "Pipeline aborted due to SonarQube quality gate failure: ${qg.status}"
- }
- }
- }
-
- stage('Maven deploy') {
- dir('Palisade-clients') {
- container('docker-cmds') {
- configFileProvider([configFile(fileId: "${env.CONFIG_FILE}", variable: 'MAVEN_SETTINGS')]) {
- if (("${env.BRANCH_NAME}" == "develop") ||
- ("${env.BRANCH_NAME}" == "master")) {
- sh 'mvn -s $MAVEN_SETTINGS deploy -P quick'
- } else {
- sh "echo - no deploy"
- }
- }
- }
- }
- }
- }
- }
-
-}
\ No newline at end of file
+ clients()
+}
diff --git a/NOTICES.md b/NOTICES.md
index 55319f0c..cfc28df0 100644
--- a/NOTICES.md
+++ b/NOTICES.md
@@ -1,17 +1,46 @@
List of third-party dependencies grouped by their license type
### [Apache Software License 2.0](./licenses/apache_software_license_2.0.txt):
-* Apache Commons Lang ([org.apache.commons:commons-lang3:3.8.1](http://commons.apache.org/proper/commons-lang/))
-* Apache Hadoop Common ([org.apache.hadoop:hadoop-common:3.2.1](no url defined))
-* Apache Hadoop MapReduce Core ([org.apache.hadoop:hadoop-mapreduce-client-core:3.2.1](no url defined))
-* Spring Cloud Starter OpenFeign ([org.springframework.cloud:spring-cloud-starter-openfeign:2.2.0.RELEASE](https://projects.spring.io/spring-cloud))
-* clients-common ([uk.gov.gchq.palisade:clients-common:0.4.0](https://github.com/gchq/Palisade-clients/tree/main/clients-common))
-* common ([uk.gov.gchq.palisade:common:0.4.0](https://github.com/gchq/Palisade-common))
-* readers-common ([uk.gov.gchq.palisade:readers-common:0.4.0](https://github.com/gchq/Palisade-readers/tree/main/readers-common))
+* Jackson-annotations ([com.fasterxml.jackson.core:jackson-annotations:2.11.0](http://github.com/FasterXML/jackson))
+* jackson-databind ([com.fasterxml.jackson.core:jackson-databind:2.11.0](http://github.com/FasterXML/jackson))
+* Jackson-dataformat-XML ([com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.0](https://github.com/FasterXML/jackson-dataformat-xml))
+* Jackson datatype: jdk8 ([com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.0](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8))
+* akka-http-jackson ([com.typesafe.akka:akka-http-jackson_2.13:10.2.1](https://akka.io))
+* akka-http ([com.typesafe.akka:akka-http_2.13:10.2.1](https://akka.io))
+* akka-stream ([com.typesafe.akka:akka-stream_2.13:2.6.10](https://akka.io/))
+* Micronaut ([io.micronaut:micronaut-http-server-netty:2.3.2](http://micronaut.io))
+* Micronaut ([io.micronaut:micronaut-inject:2.3.2](http://micronaut.io))
+* Micronaut ([io.micronaut:micronaut-inject-java:2.3.2](http://micronaut.io))
+* Micronaut ([io.micronaut:micronaut-runtime:2.3.2](http://micronaut.io))
+* Micronaut Test ([io.micronaut.test:micronaut-test-junit5:2.3.2](http://micronaut.io))
+* Reactive Relational Database Connectivity - H2 ([io.r2dbc:r2dbc-h2:0.8.4.RELEASE](https://github.com/r2dbc/r2dbc-h2))
+* RxJava ([io.reactivex.rxjava3:rxjava:3.0.8](https://github.com/ReactiveX/RxJava))
+* AssertJ fluent assertions ([org.assertj:assertj-core:3.19.0](https://assertj.github.io/doc/assertj-core/))
+* org.immutables.value ([org.immutables:value:2.8.2](http://immutables.org/value))
+* spring-boot-autoconfigure ([org.springframework.boot:spring-boot-autoconfigure:2.3.1.RELEASE](https://spring.io/projects/spring-boot))
+* spring-boot-starter-aop ([org.springframework.boot:spring-boot-starter-aop:2.3.1.RELEASE](https://spring.io/projects/spring-boot))
+* spring-boot-starter-data-r2dbc ([org.springframework.boot:spring-boot-starter-data-r2dbc:2.3.1.RELEASE](https://spring.io/projects/spring-boot))
+* spring-boot-starter-test ([org.springframework.boot:spring-boot-starter-test:2.3.1.RELEASE](https://spring.io/projects/spring-boot))
+* Spring Shell Starter ([org.springframework.shell:spring-shell-starter:2.0.0.RELEASE](http://projects.spring.io/spring-boot/spring-shell-parent/spring-shell-starter/))
+* GCHQ Palisade - Akka Client ([uk.gov.gchq.palisade:client-akka:0.5.0-RELEASE](https://github.com/gchq/Palisade-clients/tree/develop/client-akka))
+* GCHQ Palisade - Java Client ([uk.gov.gchq.palisade:client-java:0.5.0-RELEASE](https://github.com/gchq/Palisade-clients/tree/develop/client-java))
+* GCHQ Palisade Common Library ([uk.gov.gchq.palisade:common:0.5.0-RELEASE](https://github.com/gchq/Palisade-common))
### [Eclipse Public License 1.0](./licenses/eclipse_public_license_1.0.html):
-* JUnit ([junit:junit:4.12](http://junit.org))
+* Logback Classic Module ([ch.qos.logback:logback-classic:1.2.3](http://logback.qos.ch/logback-classic))
+
+### [Eclipse Public License 2.0](./licenses/eclipse_public_license_2.0.html):
+* JUnit Jupiter (Aggregator) ([org.junit.jupiter:junit-jupiter:5.7.0](https://junit.org/junit5/))
+* JUnit Jupiter API ([org.junit.jupiter:junit-jupiter-api:5.7.0](https://junit.org/junit5/))
+* JUnit Jupiter Engine ([org.junit.jupiter:junit-jupiter-engine:5.7.0](https://junit.org/junit5/))
+* JUnit Platform Commons ([org.junit.platform:junit-platform-commons:1.7.0](https://junit.org/junit5/))
+* JUnit Platform Engine API ([org.junit.platform:junit-platform-engine:1.7.0](https://junit.org/junit5/))
+
+### [GNU Lesser General Public License 2.1](./licenses/gnu_lgpl_2.1.html):
+* Logback Classic Module ([ch.qos.logback:logback-classic:1.2.3](http://logback.qos.ch/logback-classic))
### [MIT License](./licenses/mit_license.txt):
-* Mockito ([org.mockito:mockito-all:1.10.19](http://www.mockito.org))
-* SLF4J API Module ([org.slf4j:slf4j-api:1.7.25](http://www.slf4j.org))
+* jnr-fuse ([com.github.serceman:jnr-fuse:0.5.5](https://github.com/SerCeMan/jnr-fuse))
+* mockito-core ([org.mockito:mockito-core:3.7.7](https://github.com/mockito/mockito))
+* mockito-junit-jupiter ([org.mockito:mockito-junit-jupiter:3.7.7](https://github.com/mockito/mockito))
+* SLF4J API Module ([org.slf4j:slf4j-api:1.7.26](http://www.slf4j.org))
diff --git a/README.md b/README.md
index e53e704c..65334780 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
-
-
-
- nexus
- *nexusurl*/maven-group/
- central
-
-
-
-
- nexus
-
-
-
- default
-
- true
-
-
-
- *nexusurl*/maven-snapshots/
-
-
-
- nexus
-
-
-
- central
- https://repo.maven.apache.org/maven2/
-
- true
-
-
- true
-
-
-
-
-
-
- central
- https://repo.maven.apache.org/maven2/
-
- true
-
-
- true
-
-
-
-
-
-
-
- org.sonatype.plugins
-
-
-
-
-
- nexus
- *username*
- *password*
-
-
-
+>> ls
+ drwxrwxrwx client-akka
+ drwxrwxrwx client-fuse
+ drwxrwxrwx client-java
+ drwxrwxrwx client-shell
```
-
-
-
-
-
-You are then ready to build with Maven:
+Now you can finally build the repository by running:
```bash
mvn install
```
@@ -146,23 +73,10 @@ mvn install
Palisade-clients is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0) and is covered by [Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/copyright-and-re-use/crown-copyright/).
-
## Contributing
We welcome contributions to the project. Detailed information on our ways of working can be found [here](https://gchq.github.io/Palisade/doc/other/ways_of_working.html).
-
## FAQ
-What versions of Java are supported? We are currently using Java 11.
-
-
-# Client Implementations
-
-The job of the client code is to send the request for data into Palisade and to interpret the result as required for the data processing technology it is written for.
-The responsibility for implementations of the client code is to provide users with a way to request data from Palisade in a way that the user has to make minimal changes to how they would normally use that processing technology.
-Implementations of this component will usually require deep understanding of the data processing technology in order to best hook into that technology, without needing to fork the code for that technology.
-
-
-This directory contains the various client implementations for Palisade that have currently been written. Some are intended to be used as standalone implementations such
-as the [cat client](cat-client/README.md), whilst others are the necessary client library implementations to allow Palisade to be
-used with other frameworks such as the [MapReduce client](mapreduce-client/README.md).
+Q: What versions of Java are supported?
+A: We are currently using Java 11.
diff --git a/client-akka/README.md b/client-akka/README.md
new file mode 100644
index 00000000..97415541
--- /dev/null
+++ b/client-akka/README.md
@@ -0,0 +1,34 @@
+
+#
+
+## A Tool for Complex and Scalable Data Access Policy Enforcement
+
+# Palisade Akka Client
+
+The Akka Palisade Client API provides access to a Palisade cluster and exposes an interface using both Java standard-lib types and Akka types.
+This exists alongside the [Java Client](../client-java) as both a demonstration of a different implementation, and to provide better compatibility with some Palisade internals, most of which make use of Akka.
+
+## API Design
+
+The client follows as simple an API as possible.
+After providing configuration for the location of a cluster, the client is otherwise stateless and presents the flattest data-structure possible.
+The methods and return types are a one-to-one mapping with each service required to interact with (`register` with Palisade Service, `fetch` from Filtered-Resource Service, `read` from Data Service).
+
+## Technologies Used
+
+* [Akka](https://akka.io/) streams and HTTP REST/websockets
+* [Jackson](https://github.com/FasterXML/jackson) JSON parsing
diff --git a/client-akka/mvn_dependency_tree.txt b/client-akka/mvn_dependency_tree.txt
new file mode 100644
index 00000000..70fe7fd9
--- /dev/null
+++ b/client-akka/mvn_dependency_tree.txt
@@ -0,0 +1,25 @@
+uk.gov.gchq.palisade:client-akka:jar:0.5.0-RELEASE
++- uk.gov.gchq.palisade:common:jar:0.5.0-RELEASE:compile
+| +- com.fasterxml.jackson.core:jackson-databind:jar:2.11.0:compile
+| | \- com.fasterxml.jackson.core:jackson-core:jar:2.11.0:compile
+| \- org.apache.avro:avro:jar:1.8.2:compile
+| +- org.codehaus.jackson:jackson-core-asl:jar:1.9.13:compile
+| +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.13:compile
+| +- com.thoughtworks.paranamer:paranamer:jar:2.7:compile
+| +- org.xerial.snappy:snappy-java:jar:1.1.1.3:compile
+| +- org.apache.commons:commons-compress:jar:1.8.1:compile
+| +- org.tukaani:xz:jar:1.5:compile
+| \- org.slf4j:slf4j-api:jar:1.7.30:compile
++- com.typesafe.akka:akka-stream_2.13:jar:2.6.10:compile
+| +- org.scala-lang:scala-library:jar:2.13.3:compile
+| +- com.typesafe.akka:akka-actor_2.13:jar:2.6.10:compile
+| | +- com.typesafe:config:jar:1.4.0:compile
+| | \- org.scala-lang.modules:scala-java8-compat_2.13:jar:0.9.0:compile
+| +- com.typesafe.akka:akka-protobuf-v3_2.13:jar:2.6.10:compile
+| +- org.reactivestreams:reactive-streams:jar:1.0.3:compile
+| \- com.typesafe:ssl-config-core_2.13:jar:0.4.2:compile
+| \- org.scala-lang.modules:scala-parser-combinators_2.13:jar:1.1.2:compile
++- com.typesafe.akka:akka-http_2.13:jar:10.2.1:compile
+| \- com.typesafe.akka:akka-http-core_2.13:jar:10.2.1:compile
+| \- com.typesafe.akka:akka-parsing_2.13:jar:10.2.1:compile
+\- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.0:compile
diff --git a/client-akka/pom.xml b/client-akka/pom.xml
new file mode 100644
index 00000000..953c27d0
--- /dev/null
+++ b/client-akka/pom.xml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+ uk.gov.gchq.palisade
+ clients
+ 0.5.0-${revision}
+ ../pom.xml
+
+ 4.0.0
+
+
+
+ PalisadeDevelopers
+ GCHQ
+ https://github.com/gchq
+
+
+
+
+ client-akka
+ https://github.com/gchq/Palisade-clients/tree/develop/client-akka
+ GCHQ Palisade - Akka Client
+
+ The Akka Palisade Client API provides access to a Palisade cluster and exposes an interface using both Java standard-lib types and Akka types.
+
+
+
+
+ ${scm.url}
+ ${scm.connection}
+ ${scm.developer.connection}
+ HEAD
+
+
+
+
+ 2.13
+ 2.6.10
+ 10.2.1
+ 2.11.0
+
+
+
+
+
+
+ uk.gov.gchq.palisade
+ common
+ 0.5.0-${common.revision}
+
+
+
+
+ com.typesafe.akka
+ akka-stream_${scala.version}
+ ${akka.version}
+
+
+
+ com.typesafe.akka
+ akka-http_${scala.version}
+ ${akka.http.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
+
+
+
+
+
+ src/main/resources
+
+
+
+
+ src/unit-tests/resources
+
+
+ src/component-tests/resources
+
+
+ src/contract-tests/resources
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-test-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+
+ add-test-resources
+ generate-test-resources
+
+ add-test-resource
+
+
+
+
+ true
+ ${basedir}/src/unit-tests/resources
+ ${basedir}/src/component-tests/resources
+ ${basedir}/src/contract-tests/resources
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/AkkaClient.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/AkkaClient.java
new file mode 100644
index 00000000..b9b22b99
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/AkkaClient.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.akka;
+
+import akka.NotUsed;
+import akka.actor.ActorSystem;
+import akka.http.javadsl.Http;
+import akka.http.javadsl.model.ContentTypes;
+import akka.http.javadsl.model.HttpRequest;
+import akka.http.javadsl.model.HttpResponse;
+import akka.http.javadsl.model.ws.Message;
+import akka.http.javadsl.model.ws.WebSocketRequest;
+import akka.http.scaladsl.model.ws.TextMessage.Strict;
+import akka.stream.Materializer;
+import akka.stream.javadsl.AsPublisher;
+import akka.stream.javadsl.BroadcastHub;
+import akka.stream.javadsl.Flow;
+import akka.stream.javadsl.Keep;
+import akka.stream.javadsl.MergeHub;
+import akka.stream.javadsl.Sink;
+import akka.stream.javadsl.Source;
+import akka.stream.javadsl.StreamConverters;
+import akka.util.ByteString;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.reactivestreams.FlowAdapters;
+
+import uk.gov.gchq.palisade.client.akka.model.DataRequest;
+import uk.gov.gchq.palisade.client.akka.model.MessageType;
+import uk.gov.gchq.palisade.client.akka.model.PalisadeRequest;
+import uk.gov.gchq.palisade.client.akka.model.PalisadeResponse;
+import uk.gov.gchq.palisade.client.akka.model.WebSocketMessage;
+import uk.gov.gchq.palisade.resource.LeafResource;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow.Publisher;
+
+/**
+ * Implementation of the client interface that also exposes some akka-specific data-types such as {@link Source}s.
+ */
+public class AkkaClient implements Client {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /**
+ * Specify URL schemes based on whether SSL/TLS encryption should be used over-the-wire.
+ * Ingress to the cluster may use TLS, while cluster-internal comms may use plaintext.
+ */
+ public enum SSLMode {
+ NONE("http", "ws"),
+ SSL_TLS("https", "wss");
+
+ private final String httpScheme;
+ private final String wsScheme;
+
+ SSLMode(final String httpScheme, final String wsScheme) {
+ this.httpScheme = httpScheme;
+ this.wsScheme = wsScheme;
+ }
+
+ public String getHttpScheme() {
+ return httpScheme;
+ }
+
+ public String getWsScheme() {
+ return wsScheme;
+ }
+ }
+
+ private final String palisadeUrl;
+ private final String filteredResourceUrl;
+ private final Map dataUrlMap;
+ private final SSLMode sslMode;
+ private final Materializer materializer;
+ private final Http http;
+
+ /**
+ * Constructor used to create the AkkaClient
+ *
+ * @param palisadeUrl the location of the Palisade Service
+ * @param filteredResourceUrl the location of the Filtered Resource Service
+ * @param dataUrlMap lookup map from the names of Data Services to their locations
+ * @param actorSystem the akka Actor System bean
+ * @param sslMode whether the client should connect using SSL or not
+ */
+ public AkkaClient(final String palisadeUrl, final String filteredResourceUrl, final Map dataUrlMap, final ActorSystem actorSystem, final SSLMode sslMode) {
+ this.palisadeUrl = palisadeUrl;
+ this.filteredResourceUrl = filteredResourceUrl;
+ this.dataUrlMap = dataUrlMap;
+ this.sslMode = sslMode;
+ this.materializer = Materializer.createMaterializer(actorSystem);
+ this.http = Http.get(actorSystem);
+ }
+
+ /**
+ * Registers a request into the Palisade Service, taking a userId, resourceId and Map as a context, it then sends the request to the
+ * Palisade registerDataRequest endpoint via rest and returns the token after the request has ben processed.
+ *
+ * @param userId the userId of the user making the request.
+ * @param resourceId the resourceId requested to read - note this is not necessarily the filename.
+ * @param context the context for this data access.
+ * @return a String uuid token in a CompletionStage object
+ */
+ public CompletionStage register(final String userId, final String resourceId, final Map context) {
+ return http
+ .singleRequest(HttpRequest.POST(String.format("%s://%s/api/registerDataRequest", sslMode.getHttpScheme(), palisadeUrl))
+ .withEntity(ContentTypes.APPLICATION_JSON, serialize(
+ PalisadeRequest.Builder.create()
+ .withUserId(userId)
+ .withResourceId(resourceId)
+ .withContext(context)
+ )))
+ .thenApply(this::readHttpMessage)
+ .thenApply(PalisadeResponse::getToken);
+ }
+
+ /**
+ * By taking the uuid token, this method deserializes the message from the websocket, and if completed, returns the processed LeafResource to the user
+ *
+ * @param token uuid of the request
+ * @return a processed LeafResource that has been processed by the Palisade Service
+ */
+ public Source> fetchSource(final String token) {
+ // Send out CTS messages after each RESOURCE
+ WebSocketMessage cts = WebSocketMessage.Builder.create().withType(MessageType.CTS).noHeaders().noBody();
+
+ // Map inbound messages to outbound CTS until COMPLETE is seen
+ var exposeSinkAndSource = Flow.create()
+ // Merge ws upstream with our decoupled upstream - prefer our source of messages, complete when both ws and ours are complete
+ .mergePreferredMat(MergeHub.of(WebSocketMessage.class), true, false, Keep.right())
+ // Broadcast to both ws downstream and our downstream
+ .alsoToMat(BroadcastHub.of(WebSocketMessage.class), Keep.both());
+
+ // Ser/Des for messages to/from the websocket
+ var clientFlow = Flow.create()
+ .map(msg -> AkkaClient.readWsMessage(msg, materializer))
+ // Expose source and sink to this stage in materialization
+ .viaMat(exposeSinkAndSource, Keep.right())
+ // Take until COMPLETE message is seen
+ .takeWhile(wsMessage -> wsMessage.getType() != MessageType.COMPLETE)
+ // Handle how to 'echo back' a message
+ .map((WebSocketMessage wsMessage) -> {
+ switch (wsMessage.getType()) {
+ case RESOURCE:
+ case ERROR:
+ return cts;
+ default:
+ return wsMessage;
+ }
+ })
+ .map(AkkaClient::writeWsMessage);
+
+ // Make the request using the ser/des flow linked to the oscillator
+ var wsResponse = http.singleWebSocketRequest(
+ WebSocketRequest.create(String.format("%s://%s/resource/%s", sslMode.getWsScheme(), filteredResourceUrl, token)),
+ clientFlow,
+ materializer);
+
+ Sink upstreamSink = wsResponse.second().first();
+ Source downstreamSource = wsResponse.second().second();
+
+ // Once the wsUpgrade request completes
+ return Source.completionStageSource(wsResponse.first()
+ // Initialize connection with a single CTS message
+ .thenRun(() -> Source.single(cts).runWith(upstreamSink, materializer))
+ // Return the connected Source
+ .thenApply(ignored -> downstreamSource))
+ // Take until COMPLETE message is seen
+ .takeWhile(wsMessage -> wsMessage.getType() != MessageType.COMPLETE)
+ // Extract LeafResource from message object
+ .filter(wsMessage -> wsMessage.getType() == MessageType.RESOURCE)
+ .map(msg -> msg.getBodyObject(LeafResource.class));
+ }
+
+ /**
+ * Converts the akka stream to a reactive stream publisher
+ * for use in the {@link #fetchSource(String)} method
+ *
+ * @param token the token returned from the palisade-service by the {@link #register} method.
+ * @return a Reactive streams publisher containing the LeafResource from the Palisade Service
+ */
+ public Publisher fetch(final String token) {
+ // Convert akka source to reactive-streams publisher
+ org.reactivestreams.Publisher rsPub = fetchSource(token)
+ .runWith(Sink.asPublisher(AsPublisher.WITHOUT_FANOUT), materializer);
+
+ // Convert akka reactive-streams to java stdlib flow
+ return FlowAdapters.toFlowPublisher(rsPub);
+ }
+
+ /**
+ * This method connects to the data service to read the leafResource from the original request, linked by the uuid token
+ *
+ * @param token the token returned from the palisade-service by the {@link #register} method.
+ * @param resource that the user wants to read
+ * @return a stream of bytes representing the contents of the resource
+ */
+ public Source> readSource(final String token, final LeafResource resource) {
+ String createConn = resource.getConnectionDetail().createConnection();
+ String dataUrl = dataUrlMap.getOrDefault(createConn, createConn);
+ return Source.completionStageSource(http.singleRequest(
+ HttpRequest.POST(String.format("%s://%s/read/chunked", sslMode.getHttpScheme(), dataUrl))
+ .withEntity(ContentTypes.APPLICATION_JSON, serialize(DataRequest.Builder.create()
+ .withToken(token)
+ .withLeafResourceId(resource.getId()))
+ ))
+ .thenApply(response -> response.entity().getDataBytes()
+ .mapMaterializedValue(ignored -> NotUsed.notUsed())));
+ }
+
+ /**
+ * Converts an akka ByteString source to java a stdlib InputStream
+ *
+ * @param token the token returned from the palisade-service by the {@link #register(String, String, Map)} method.
+ * @param resource a resource returned by the filtered-resource-service that the client wishes to read.
+ * @return a java stdlib InputStream
+ */
+ public InputStream read(final String token, final LeafResource resource) {
+ // Convert akka ByteString source to java stdlib InputStream
+ return readSource(token, resource)
+ .runWith(StreamConverters.asInputStream(), materializer);
+ }
+
+
+ private PalisadeResponse readHttpMessage(final HttpResponse message) {
+ // Akka will sometimes convert a StrictMessage to a StreamedMessage, so we have to handle both cases here
+ StringBuilder builder = message.entity().getDataBytes()
+ .map(ByteString::utf8String)
+ .runFold(new StringBuilder(), StringBuilder::append, materializer)
+ .toCompletableFuture().join();
+ return deserialize(builder.toString(), PalisadeResponse.class);
+ }
+
+ private static WebSocketMessage readWsMessage(final Message message, final Materializer materializer) {
+ // Akka will sometimes convert a StrictMessage to a StreamedMessage, so we have to handle both cases here
+ StringBuilder builder = message.asTextMessage().getStreamedText()
+ .runFold(new StringBuilder(), StringBuilder::append, materializer)
+ .toCompletableFuture().join();
+ return deserialize(builder.toString(), WebSocketMessage.class);
+ }
+
+ private static Message writeWsMessage(final WebSocketMessage message) {
+ return new Strict(serialize(message));
+ }
+
+ private static T deserialize(final String json, final Class clazz) {
+ try {
+ return MAPPER.readValue(json, clazz);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to write message", e);
+ }
+ }
+
+ private static String serialize(final Object o) {
+ try {
+ return MAPPER.writeValueAsString(o);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to write message", e);
+ }
+ }
+}
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/Client.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/Client.java
new file mode 100644
index 00000000..d21431c7
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/Client.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.akka;
+
+import uk.gov.gchq.palisade.resource.LeafResource;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow.Publisher;
+
+/**
+ * Interface for a palisade client that defines the connections to the three outward-facing services.
+ * The expected interaction would be {@link #register} then {@link #fetch} then {@link #read}.
+ */
+public interface Client {
+
+ /**
+ * Register a request with the palisade-service entrypoint.
+ *
+ * @param userId the userId of the user making the request.
+ * @param resourceId the resourceId requested to read - note this is not necessarily the filename.
+ * @param context the context for this data access.
+ * @return the token from the palisade-service which may be used in the following methods.
+ */
+ CompletionStage register(final String userId, final String resourceId, final Map context);
+
+ /**
+ * Fetch the returned {@link LeafResource}s from the filtered-resource-service.
+ * These resources, coupled with the token, are authorised to be read by the data-service.
+ *
+ * @param token the token returned from the palisade-service by the {@link #register} method.
+ * @return reactive streams {@link Publisher} that will request and return {@link LeafResource} results from the filtered-resource-service.
+ */
+ Publisher fetch(final String token);
+
+ /**
+ * Read a single resource from the appropriate data-service specified by the resource's {@link uk.gov.gchq.palisade.resource.ConnectionDetail}.
+ *
+ * @param token the token returned from the palisade-service by the {@link #register(String, String, Map)} method.
+ * @param resource a resource returned by the filtered-resource-service that the client wishes to read.
+ * @return an {@link InputStream} to that resource, with the data-service applying all appropriate rules.
+ */
+ InputStream read(final String token, final LeafResource resource);
+
+}
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/DataRequest.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/DataRequest.java
new file mode 100644
index 00000000..51a22c18
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/DataRequest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.akka.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * The {@link DataRequest} is the input for the data-service where the resource is read.
+ * This message is created by the response from the filtered-resource-service to the client.
+ * It is then routed via the resource's connectionDetail to the appropriate instance of a data-service.
+ */
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+public final class DataRequest {
+
+ private final String token; // Unique identifier for the client's request
+ private final String leafResourceId; // Leaf Resource ID that that is being asked to access
+
+ @JsonCreator
+ private DataRequest(
+ final @JsonProperty("token") String token,
+ final @JsonProperty("leafResourceId") String leafResourceId) {
+
+ this.token = Optional.ofNullable(token)
+ .orElseThrow(() -> new IllegalArgumentException("token cannot be null"));
+ this.leafResourceId = Optional.ofNullable(leafResourceId)
+ .orElseThrow(() -> new IllegalArgumentException("leafResourceId cannot be null"));
+ }
+
+ @Generated
+ public String getToken() {
+ return token;
+ }
+
+ @Generated
+ public String getLeafResourceId() {
+ return leafResourceId;
+ }
+
+ /**
+ * Builder class for the creation of instances of the DataRequest.
+ * This is a variant of the Fluent Builder which will use Java Objects or JsonNodes equivalents for the components in the build.
+ */
+ public static class Builder {
+ /**
+ * Starter method for the Builder class.
+ * This method is called to start the process of creating the DataRequest class.
+ *
+ * @return interface {@link IToken} for the next step in the build.
+ */
+ public static IToken create() {
+ return token -> leafResourceId ->
+ new DataRequest(token, leafResourceId);
+ }
+
+ /**
+ * Adds the token to the message
+ */
+ public interface IToken {
+ /**
+ * Adds the token to the request
+ *
+ * @param token the client's unique token
+ * @return interface {@link ILeafResourceId} for the next step in the build.
+ */
+ ILeafResourceId withToken(String token);
+ }
+
+ /**
+ * Adds the leaf resource id to the message
+ */
+ public interface ILeafResourceId {
+ /**
+ * Adds the leaf resource id to the request
+ *
+ * @param leafResourceId resource ID for the request.
+ * @return the completed DataRequest object
+ */
+ DataRequest withLeafResourceId(String leafResourceId);
+ }
+
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", DataRequest.class.getSimpleName() + "[", "]")
+ .add("token='" + token + "'")
+ .add("leafResourceId='" + leafResourceId + "'")
+ .toString();
+ }
+}
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/MessageType.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/MessageType.java
new file mode 100644
index 00000000..29bdec1d
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/MessageType.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.akka.model;
+
+/**
+ * Type of message (and thus expected headers/body content) sent by either the client or the server.
+ *
+ * The client is expected to only send:
+ *
+ *
{@link MessageType#PING} - is the server alive? reply with a {@link MessageType#PONG}
+ *
{@link MessageType#CTS} - clear to send next {@link MessageType#RESOURCE}, {@link MessageType#ERROR} or {@link MessageType#COMPLETE}
+ *
+ * The server is expected to only send:
+ *
+ *
{@link MessageType#PONG} - the server is alive
+ *
{@link MessageType#RESOURCE} - the next available resource for the client
+ *
{@link MessageType#ERROR} - an error occurred while processing the client's request
+ *
{@link MessageType#COMPLETE} - there is nothing more to return to the client
+ *
+ */
+public enum MessageType {
+
+ // Client
+ PING, // -> PONG
+ CTS, // -> RESOURCE|ERROR|COMPLETE
+
+ // Server
+ PONG,
+ RESOURCE,
+ ERROR,
+ COMPLETE
+
+}
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/PalisadeRequest.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/PalisadeRequest.java
new file mode 100644
index 00000000..671d61eb
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/PalisadeRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.akka.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import uk.gov.gchq.palisade.Context;
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Represents the original data that has been sent from the client to Palisade Service for a request to access data.
+ */
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+public final class PalisadeRequest {
+
+ private final String userId; //Unique identifier for the user.
+ private final String resourceId; //Resource that that is being asked to access.
+
+ // Ignore class type on context object
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, defaultImpl = Context.class)
+ private final Map context;
+
+ @JsonCreator
+ private PalisadeRequest(
+ final @JsonProperty("userId") String userId,
+ final @JsonProperty("resourceId") String resourceId,
+ final @JsonProperty("context") Map context) {
+
+ this.userId = Optional.ofNullable(userId).orElseThrow(() -> new IllegalArgumentException("User ID cannot be null"));
+ this.resourceId = Optional.ofNullable(resourceId).orElseThrow(() -> new IllegalArgumentException("Resource ID cannot be null"));
+ this.context = Optional.ofNullable(context).orElseThrow(() -> new IllegalArgumentException("Context cannot be null"));
+ }
+
+ @Generated
+ public String getUserId() {
+ return userId;
+ }
+
+ @Generated
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ @Generated
+ public Map getContext() {
+ return context;
+ }
+
+ /**
+ * Builder class for the creation of the PalisadeRequest. This is a variant of the Fluent Builder
+ * which will use String or optionally JsonNodes for the components in the build.
+ */
+ public static class Builder {
+
+ /**
+ * Starter method for the Builder class. This method is called to start the process of creating the
+ * PalisadeRequest class.
+ *
+ * @return interface {@link IUserId} for the next step in the build.
+ */
+ public static IUserId create() {
+ return userId -> resourceId -> context ->
+ new PalisadeRequest(userId, resourceId, context);
+ }
+
+ /**
+ * Adds the user ID information to the message.
+ */
+ public interface IUserId {
+ /**
+ * Adds the user's ID.
+ *
+ * @param userId user ID for the request.
+ * @return interface {@link IResourceId} for the next step in the build.
+ */
+ IResourceId withUserId(String userId);
+ }
+
+ /**
+ * Adds the resource ID information to the message.
+ */
+ public interface IResourceId {
+ /**
+ * Adds the resource ID.
+ *
+ * @param resourceId resource ID for the request.
+ * @return interface {@link IContext} for the next step in the build.
+ */
+ IContext withResourceId(String resourceId);
+ }
+
+ /**
+ * Adds the user context information to the message.
+ */
+ public interface IContext {
+ /**
+ * Adds the user context information.
+ *
+ * @param context information about this request.
+ * @return class {@link PalisadeRequest} this builder is set-up to create.
+ */
+ PalisadeRequest withContext(Map context);
+ }
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", PalisadeRequest.class.getSimpleName() + "[", "]")
+ .add("userId='" + userId + "'")
+ .add("resourceId='" + resourceId + "'")
+ .add("context=" + context)
+ .toString();
+ }
+}
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/PalisadeResponse.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/PalisadeResponse.java
new file mode 100644
index 00000000..d8c08ddf
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/PalisadeResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.akka.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Response message that is returned to the client. The message contains information that will identify this request
+ * for access to the data and be used in a subsequent request to see the resources.
+ */
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+public final class PalisadeResponse {
+
+ private final String token; //unique identifier for the request.
+
+ /**
+ * Instantiates a new Response.
+ *
+ * @param token the token
+ */
+ @JsonCreator
+ public PalisadeResponse(final @JsonProperty("token") String token) {
+ this.token = Optional.ofNullable(token).orElseThrow(() -> new IllegalArgumentException("token cannot be null"));
+ }
+
+ /**
+ * Gets token.
+ *
+ * @return the token
+ */
+ @Generated
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", PalisadeResponse.class.getSimpleName() + "[", "]")
+ .add("token='" + token + "'")
+ .toString();
+ }
+}
diff --git a/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/WebSocketMessage.java b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/WebSocketMessage.java
new file mode 100644
index 00000000..12b7e849
--- /dev/null
+++ b/client-akka/src/main/java/uk/gov/gchq/palisade/client/akka/model/WebSocketMessage.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.akka.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Response message that is returned to the client from the filtered-resource-service web-socket.
+ * The message contains information that will identify this request for access to the data and be
+ * used in a subsequent request to the data-service to see the resources.
+ */
+public final class WebSocketMessage {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final MessageType type;
+ private final Map headers;
+ private final String body;
+
+ @JsonCreator
+ private WebSocketMessage(
+ final @JsonProperty("type") MessageType type,
+ final @JsonProperty("headers") Map headers,
+ final @JsonProperty("body") String body) {
+ this.type = type;
+ this.headers = headers;
+ this.body = body;
+ }
+
+ /**
+ * getType returns the type of Websocket message
+ *
+ * @return the type of websocket message
+ */
+ @Generated
+ public MessageType getType() {
+ return type;
+ }
+
+ /**
+ * Gets headers of the websocket message.
+ *
+ * @return the headers of the websocket message
+ */
+ @Generated
+ public Map getHeaders() {
+ return Optional.ofNullable(headers)
+ .orElse(Collections.emptyMap());
+ }
+
+ /**
+ * Gets the body of the websocket message
+ *
+ * @return the body of the websocket message
+ */
+ @Generated
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * Gets the body as an object
+ *
+ * @param the type of Websocket Message
+ * @param clazz the clazz used in deserializing
+ * @return the body object
+ */
+ @JsonIgnore
+ public T getBodyObject(final Class clazz) {
+ try {
+ return MAPPER.readValue(body, clazz);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to deserialize message body as class " + clazz.getName(), e);
+ }
+ }
+
+ /**
+ * Builder class for the creation of instances of the WebSocketMessage.
+ * This is a variant of the Fluent Builder which will use Java Objects or JsonNodes equivalents for the components in the build.
+ */
+ public static class Builder {
+
+ /**
+ * Starter method for the Builder class. This method is called to start the process of creating the
+ * WebSocketMessage class.
+ *
+ * @return public interface {@link IType} for the next step in the build.
+ */
+ public static IType create() {
+ return type -> headers -> body -> new WebSocketMessage(type, headers, body);
+ }
+
+ /**
+ * Adds the type information to the object.
+ */
+ public interface IType {
+ /**
+ * Adds the Type of WebSocketMessage
+ *
+ * @param type of WebSocketMessage
+ * @return interface {@link IHeaders} for the next step in the build.
+ */
+ IHeaders withType(MessageType type);
+ }
+
+ /**
+ * Adds the header information to the object.
+ */
+ public interface IHeaders {
+ /**
+ * Adds the headers to the WebSocketMessage
+ *
+ * @param headers for the WebSocketMessage
+ * @return interface {@link IBody} for the next step in the build.
+ */
+ IBody withHeaders(Map headers);
+
+ /**
+ * Default headers for the Websocket message
+ *
+ * @param key the key, most often the token.HEADER
+ * @param value the value, most often the token
+ * @return interface {@link IBody} for the next step in the build.
+ */
+ default IHeaders withHeader(String key, String value) {
+ return (Map partial) -> {
+ Map headers = new HashMap<>(partial);
+ headers.put(key, value);
+ return withHeaders(headers);
+ };
+ }
+
+ /**
+ * A Default noHeaders interface that adds an emptyMap of headers to the WebSocketMessage
+ *
+ * @return interface {@link IBody} for the next step in the build.
+ */
+ default IBody noHeaders() {
+ return withHeaders(Collections.emptyMap());
+ }
+ }
+
+ /**
+ * Adds the body to the object.
+ */
+ public interface IBody {
+ /**
+ * Adds a serialisedBody to the WebSocketMessage
+ *
+ * @param serialisedBody to add to the WebSocketMessage
+ * @return class {@link WebSocketMessage} for the completed class from the builder.
+ */
+ WebSocketMessage withSerialisedBody(String serialisedBody);
+
+ /**
+ * Adds an object body to the WebSocketMessage which is then seralised before adding to the class
+ *
+ * @param body the body
+ * @return class {@link WebSocketMessage} for the completed class from the builder.
+ */
+ default WebSocketMessage withBody(Object body) {
+ try {
+ return withSerialisedBody(MAPPER.writeValueAsString(body));
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to serialize message body", e);
+ }
+ }
+
+ /**
+ * An interface used to add a null body to the WebSocketMessage
+ *
+ * @return class {@link WebSocketMessage} for the completed class from the builder.
+ */
+ default WebSocketMessage noBody() {
+ return withSerialisedBody(null);
+ }
+ }
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", WebSocketMessage.class.getSimpleName() + "[", "]")
+ .add("type=" + type)
+ .add("headers=" + headers)
+ .add("body='" + body + "'")
+ .toString();
+ }
+}
diff --git a/client-fuse/README.md b/client-fuse/README.md
new file mode 100644
index 00000000..4a481ff8
--- /dev/null
+++ b/client-fuse/README.md
@@ -0,0 +1,60 @@
+
+#
+
+## A Tool for Complex and Scalable Data Access Policy Enforcement
+
+# Palisade FuseFS Client
+
+## Introduction
+
+> ***Filesystem in Userspace (FUSE)*** is a software interface for Unix and Unix-like computer operating systems that lets non-privileged users create their own file systems without editing kernel code.
+This is achieved by running file system code in user space while the FUSE module provides only a "bridge" to the actual kernel interfaces.
+[[1]](https://en.wikipedia.org/wiki/Filesystem_in_Userspace)
+
+Within the context of Palisade, this allows us to make a request with `userId`, `resourceId` and `context`, then create a software-controlled filesystem mount to represent the returned resources and data.
+The Filtered-Resource Service response represents a directory listing (we must convert it into a tree and perform some simple processing).
+The Data Service read represents a file read.
+
+
+## Requirements
+Depending on operating system, you will need an appropriate FUSE implementation.
+
+The following are the most popular libraries for the three major OSes:
+* [libfuse for Linux and BSD](https://github.com/libfuse/libfuse)
+ * [libfuse3-dev for Ubuntu Hirsute](https://packages.ubuntu.com/hirsute/libfuse3-dev)
+ * [libfuse3-dev for Debian Sid](https://packages.debian.org/sid/libfuse3-dev)
+ * [fuse2 for Arch (x86-64)](https://archlinux.org/packages/extra/x86_64/fuse2/)
+* [OSXFUSE for MacOS](https://osxfuse.github.io/)
+* [WinFSP for Windows](http://www.secfs.net/winfsp/)
+
+
+## Usage
+
+Register a request to Palisade and the `` will be populated with resources.
+```shell script
+java -jar client-fuse.jar
+```
+See the [URL configuration](../client-java/README.md#URL) and [client properties](../client-java/README.md#Client%20properties) sections of the [client-java README](../client-java/README.md) for details on the ``.
+
+
+## Notes
+
+In terms of ease-of-use and approachability, FUSE is an easy way to get started with Palisade.
+All existing tools for working with UNIX filesystems and directory trees will work without any changes required.
+
+Despite this, this is not particularly flexible (no option for exposing additional resource metadata), memory-efficient (the full tree of returned resources must be stored), or scalable (not appropriate for a distributed map-reduce scenario).
+A 'power-user' may prefer working directly with the [Java Client](../client-java/README.md).
diff --git a/client-fuse/mvn_dependency_tree.txt b/client-fuse/mvn_dependency_tree.txt
new file mode 100644
index 00000000..ea1f01a4
--- /dev/null
+++ b/client-fuse/mvn_dependency_tree.txt
@@ -0,0 +1,39 @@
+uk.gov.gchq.palisade:client-fuse:jar:0.5.0-RELEASE
++- com.github.serceman:jnr-fuse:jar:0.5.5:compile
+| +- com.github.jnr:jnr-ffi:jar:2.1.12:compile
+| | +- com.github.jnr:jffi:jar:1.2.23:compile
+| | +- com.github.jnr:jffi:jar:native:1.2.23:runtime
+| | +- org.ow2.asm:asm:jar:7.1:compile
+| | +- org.ow2.asm:asm-commons:jar:7.1:compile
+| | +- org.ow2.asm:asm-analysis:jar:7.1:compile
+| | +- org.ow2.asm:asm-tree:jar:7.1:compile
+| | +- org.ow2.asm:asm-util:jar:7.1:compile
+| | +- com.github.jnr:jnr-a64asm:jar:1.0.0:compile
+| | \- com.github.jnr:jnr-x86asm:jar:1.0.2:compile
+| +- com.github.jnr:jnr-posix:jar:3.0.54:compile
+| \- com.github.jnr:jnr-constants:jar:0.9.15:compile
++- uk.gov.gchq.palisade:client-java:jar:0.5.0-RELEASE:compile
+| +- uk.gov.gchq.palisade:common:jar:0.5.0-RELEASE:compile
+| | \- org.apache.avro:avro:jar:1.8.2:compile
+| | +- org.codehaus.jackson:jackson-core-asl:jar:1.9.13:compile
+| | +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.13:compile
+| | +- com.thoughtworks.paranamer:paranamer:jar:2.7:compile
+| | +- org.xerial.snappy:snappy-java:jar:1.1.1.3:compile
+| | +- org.apache.commons:commons-compress:jar:1.8.1:compile
+| | \- org.tukaani:xz:jar:1.5:compile
+| +- com.fasterxml.jackson.core:jackson-databind:jar:2.11.0:compile
+| | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.0:compile
+| | \- com.fasterxml.jackson.core:jackson-core:jar:2.11.0:compile
+| +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.11.0:compile
+| \- io.reactivex.rxjava3:rxjava:jar:3.0.8:compile
+| \- org.reactivestreams:reactive-streams:jar:1.0.3:compile
++- org.slf4j:slf4j-api:jar:1.7.26:compile
++- org.junit.jupiter:junit-jupiter:jar:5.7.0:test
+| +- org.junit.jupiter:junit-jupiter-api:jar:5.6.2:test
+| | +- org.apiguardian:apiguardian-api:jar:1.1.0:test
+| | +- org.opentest4j:opentest4j:jar:1.2.0:test
+| | \- org.junit.platform:junit-platform-commons:jar:1.6.2:test
+| +- org.junit.jupiter:junit-jupiter-params:jar:5.6.2:test
+| \- org.junit.jupiter:junit-jupiter-engine:jar:5.6.2:test
+| \- org.junit.platform:junit-platform-engine:jar:1.6.2:test
+\- org.assertj:assertj-core:jar:3.19.0:test
diff --git a/client-fuse/pom.xml b/client-fuse/pom.xml
new file mode 100644
index 00000000..a81a90a7
--- /dev/null
+++ b/client-fuse/pom.xml
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+ uk.gov.gchq.palisade
+ clients
+ 0.5.0-${revision}
+ ../pom.xml
+
+ 4.0.0
+
+
+
+ PalisadeDevelopers
+ GCHQ
+ https://github.com/gchq
+
+
+
+
+ client-fuse
+ https://github.com/gchq/Palisade-clients/tree/develop/client-fuse
+ GCHQ Palisade - FUSE FS Client
+
+ The Fuse Palisade Client creates a software-controlled filesystem mount to represent the returned resources and data from a query.
+ Returned resources, data and metadata are all presented as a FUSE mounted local directory.
+
+
+
+
+ ${scm.url}
+ ${scm.connection}
+ ${scm.developer.connection}
+ HEAD
+
+
+
+ 11
+ 11
+ 11
+
+ 0.5.5
+ 1.7.26
+
+ 5.7.0
+ 3.19.0
+
+
+
+
+ com.github.serceman
+ jnr-fuse
+ ${jnr-fuse.version}
+
+
+ uk.gov.gchq.palisade
+ client-java
+ ${project.parent.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.jupiter.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+
+
+
+
+ src/main/resources
+
+
+
+
+ src/unit-tests/resources
+
+
+ src/component-tests/resources
+
+
+ src/contract-tests/resources
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-test-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+
+ add-test-resources
+ generate-test-resources
+
+ add-test-resource
+
+
+
+
+ true
+ ${basedir}/src/unit-tests/resources
+ ${basedir}/src/component-tests/resources
+ ${basedir}/src/contract-tests/resources
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/FuseClient.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/FuseClient.java
new file mode 100644
index 00000000..070b801c
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/FuseClient.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.fuse.client.ResourceTreeClient;
+import uk.gov.gchq.palisade.client.fuse.client.ResourceTreeClient.ResourceTreeWithContext;
+import uk.gov.gchq.palisade.client.fuse.fs.ResourceTreeFS;
+import uk.gov.gchq.palisade.client.fuse.tree.impl.LeafResourceNode;
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultClient;
+
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Palisade Client to mount query response as a FUSE filesystem
+ */
+public class FuseClient {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FuseClient.class);
+
+ private static final int MIN_ARGS_LEN = 4;
+ private static final int CLIENT_URI_INDEX = 1;
+ private static final int RESOURCE_ID_INDEX = 2;
+ private static final int MOUNT_DIR_INDEX = 3;
+ private static final int CONTEXT_INDEX = 4;
+
+ private static final String KEY_VALUE_SEP = "=";
+ private static final int KEY_VALUE_LEN = 2;
+ private static final int KEY_INDEX = 0;
+ private static final int VALUE_INDEX = 1;
+
+ private final ResourceTreeClient client;
+
+ /**
+ * Create a new instance of the client, specifying the {@link DefaultClient#connect(String)} uri config string.
+ *
+ * @param clientUri the client uri string, e.g. pal://cluster/?userid=Alice
+ */
+ public FuseClient(final String clientUri) {
+ this.client = new ResourceTreeClient(new DefaultClient().connect(clientUri));
+ }
+
+ /**
+ * Run a simple CLI application for this client, taking the clientUri, resourceId and mountDir from command-line args
+ *
+ * @param args Command-line arguments, expected to contain the application name and three-or-more passed arguments.
+ * The args list should be ordered [this-jar-name, client-uri-config, resource-id, local-mount-point, context-map...]
+ * e.g. [client-fuse.jar, pal://cluster/?userid=Alice, file:/data/local-data-store/, /mnt/palisade, purpose=SALARY]
+ */
+ public static void main(final String... args) {
+ String jarName = args[0];
+ if (args.length >= MIN_ARGS_LEN) {
+ // Parse command-line args
+ Map context = new HashMap<>();
+ for (int i = CONTEXT_INDEX; i < args.length; i++) {
+ String[] keyValue = args[i].split(KEY_VALUE_SEP, KEY_VALUE_LEN);
+ if (keyValue.length == KEY_VALUE_LEN) {
+ context.put(keyValue[KEY_INDEX], keyValue[VALUE_INDEX]);
+ } else {
+ throw new IllegalArgumentException("Expected additional args '=' to be parsed as ['key', 'value'], but was " + Arrays.toString(keyValue));
+ }
+ }
+
+ String clientUri = args[CLIENT_URI_INDEX];
+ String resourceId = args[RESOURCE_ID_INDEX];
+ String mountDir = args[MOUNT_DIR_INDEX];
+
+ // Mount and block for lifetime of the application
+ new FuseClient(clientUri)
+ .mount(resourceId, mountDir, context);
+ } else {
+ LOGGER.error("Usage: {} [= ...]", jarName);
+ }
+ }
+
+ /**
+ * Register a request with Palisade using a configured client.
+ * Mount the fuse directory and block until application exit.
+ * Attempt to gracefully unmount on application exit.
+ *
+ * @param resourceId the requested resourceId
+ * @param mountDir the target mount directory
+ * @param context the context for the Palisade request
+ */
+ public void mount(final String resourceId, final String mountDir, final Map context) {
+ Path mountPath = Paths.get(mountDir).toAbsolutePath();
+
+ ResourceTreeWithContext tree = client.register(resourceId, context);
+ Function reader = node -> client.read(tree.getToken(), node);
+ ResourceTreeFS fuseFs = new ResourceTreeFS(tree, reader);
+
+ try {
+ LOGGER.info("Mounted at {}, press to unmount and exit", mountPath);
+ fuseFs.mount(mountPath, true);
+ } finally {
+ fuseFs.umount();
+ LOGGER.info("Unmounted {}", mountPath);
+ }
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/LeafResourceQueryItem.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/LeafResourceQueryItem.java
new file mode 100644
index 00000000..66da2e42
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/LeafResourceQueryItem.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.client;
+
+import uk.gov.gchq.palisade.client.java.QueryItem;
+import uk.gov.gchq.palisade.resource.LeafResource;
+
+class LeafResourceQueryItem implements QueryItem {
+ private final LeafResource leafResource;
+ private final String token;
+
+ protected LeafResourceQueryItem(final LeafResource leafResource, final String token) {
+ this.leafResource = leafResource;
+ this.token = token;
+ }
+
+ @Override
+ public ItemType getType() {
+ return ItemType.RESOURCE;
+ }
+
+ @Override
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ public String asError() {
+ return null;
+ }
+
+ @Override
+ public LeafResource asResource() {
+ return leafResource;
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/OnNextStubSubscriber.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/OnNextStubSubscriber.java
new file mode 100644
index 00000000..8159023b
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/OnNextStubSubscriber.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.client;
+
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+
+/**
+ * Stub the Subscriber interface such that it only requires an {@link Subscriber#onNext(Object)}
+ * method.
+ *
+ * @param type of elements emitted by the subscriber
+ */
+public interface OnNextStubSubscriber extends Subscriber {
+ default void onSubscribe(final Subscription subscription) {
+ }
+
+ default void onError(final Throwable throwable) {
+ }
+
+ default void onComplete() {
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/ResourceTreeClient.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/ResourceTreeClient.java
new file mode 100644
index 00000000..0c56dd35
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/client/ResourceTreeClient.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.client;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.fuse.tree.ResourceTree;
+import uk.gov.gchq.palisade.client.fuse.tree.impl.LeafResourceNode;
+import uk.gov.gchq.palisade.client.java.QueryItem;
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultQueryResponse;
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultSession;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeResponse;
+import uk.gov.gchq.palisade.resource.ChildResource;
+import uk.gov.gchq.palisade.resource.LeafResource;
+import uk.gov.gchq.palisade.resource.Resource;
+import uk.gov.gchq.palisade.resource.impl.SimpleConnectionDetail;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow.Publisher;
+import java.util.function.UnaryOperator;
+
+/**
+ * Palisade client that stores returned resources into a {@link ResourceTree}.
+ * This allows hierarchical querying of returned resources before requesting to read.
+ * Additionally, the uri scheme is stripped from resourceIds, which allows for using
+ * id's directly as the FUSE mount paths.
+ */
+@SuppressWarnings("unchecked")
+public class ResourceTreeClient {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ResourceTreeClient.class);
+
+ /**
+ * Wraps a resource tree with some additional context, in this case the token of
+ * the Palisade query.
+ */
+ public static class ResourceTreeWithContext extends ResourceTree {
+ private final String token;
+
+ /**
+ * Construct a new ResourceTree and keep track of the given token.
+ *
+ * @param token the response from the Palisade Service used to identify this
+ * request with the Data Service.
+ */
+ public ResourceTreeWithContext(final String token) {
+ this.token = token;
+ }
+
+ public String getToken() {
+ return token;
+ }
+ }
+
+ private final DefaultSession session;
+
+ /**
+ * Construct a new instance of the client using a connected session from e.g.
+ * the {@link uk.gov.gchq.palisade.client.java.internal.dft.DefaultClient}.
+ *
+ * @param session a connected session pointing at an instance of Palisade
+ */
+ public ResourceTreeClient(final DefaultSession session) {
+ this.session = session;
+ }
+
+ /**
+ * Strip the scheme from a URI.
+ *
+ * @param uri the uri to remove the scheme from
+ * @return a scheme-less URI
+ */
+ protected static URI stripScheme(final URI uri) {
+ return URI.create(uri.getSchemeSpecificPart());
+ }
+
+ protected static String stripScheme(final String uri) {
+ return stripScheme(URI.create(uri)).toString();
+ }
+
+ protected Resource stripScheme(final Resource resource) {
+ return resource.id(stripScheme(resource.getId()));
+ }
+
+ /**
+ * Re-apply a 'file:' scheme to a URI.
+ *
+ * @param path the URI-compliant string to add/change the scheme for
+ * @return a URI-compliant string with a 'file:' scheme
+ */
+ protected static String reapplyScheme(final String path) {
+ return "file:" + URI.create(path).getSchemeSpecificPart();
+ }
+
+ protected Resource reapplyScheme(final Resource resource) {
+ return resource.id(reapplyScheme(resource.getId()));
+ }
+
+ /**
+ * Substitute a Data Service address using a env map of connection-detail
+ * service names to their substitutions.
+ * If a serviceName is not found in the map, it is unchanged.
+ *
+ * @param env the map of Data Service names to substitutions
+ * @param serviceName the service-name to lookup in the map
+ * @return the substitution for this service-name
+ */
+ protected String substDataServiceAddress(final Map env, final String serviceName) {
+ return env.getOrDefault(serviceName, serviceName);
+ }
+
+ // While unused, this may be needed depending on the setup of the cluster ingress
+ // It may be alleviated by further development of the client-java module
+ @SuppressWarnings("unused")
+ protected UnaryOperator substDataServiceAddress(final Map env) {
+ return (Resource resource) -> {
+ if (resource instanceof LeafResource) {
+ ((LeafResource) resource).connectionDetail(new SimpleConnectionDetail()
+ .serviceName(substDataServiceAddress(env, ((LeafResource) resource).getConnectionDetail().createConnection())));
+ }
+ return resource;
+ };
+ }
+
+ /**
+ * Apply a formatting function (unary operator) to a resource and each of its parents recursively.
+ *
+ * @param resource the resource to apply the formatter function to (it will also apply to all parents)
+ * @param formatter some function that will edit each of the resources, eg. adding a prefix to the id
+ * @param the type of the resource
+ * @return the resource after applying the formatting function to it and its parents
+ */
+ protected T formatResource(final T resource, final UnaryOperator formatter) {
+ if (resource instanceof ChildResource) {
+ ((ChildResource) resource).parent(formatResource(((ChildResource) resource).getParent(), formatter));
+ }
+ return (T) formatter.apply(resource);
+ }
+
+ /**
+ * Register a request with Palisade.
+ *
+ * @param resourceId the resourceId to query, all returned resources will be this
+ * resource or its children.
+ * @param context the additional context for the request, e.g. purpose.
+ * @return a {@link ResourceTree} paired with the {@link PalisadeResponse#getToken()}
+ * for this query.
+ */
+ public ResourceTreeWithContext register(final String resourceId, final Map context) {
+ LOGGER.debug("Registering request for resource {} with context {}", resourceId, context);
+
+ // Execute the request to Palisade and receive a response
+ CompletableFuture response = session
+ .createQuery(resourceId, context)
+ .execute()
+ .thenApply(DefaultQueryResponse.class::cast);
+
+ // Create a resource tree for the returned resources
+ CompletableFuture resourceTree = response
+ .thenApply(DefaultQueryResponse::getPalisadeResponse)
+ .thenApply((PalisadeResponse palisadeResponse) -> {
+ LOGGER.debug("Registered request and received token {}", palisadeResponse.getToken());
+ return palisadeResponse.getToken();
+ })
+ .thenApply(ResourceTreeWithContext::new);
+
+ // Get the returned (stream of) resources and add them to the tree
+ CompletableFuture populator = response
+ .thenApply(DefaultQueryResponse::stream)
+ .thenCombine(resourceTree,
+ (Publisher stream, ResourceTreeWithContext tree) -> {
+ stream.subscribe((OnNextStubSubscriber) (QueryItem queryItem) -> {
+ UnaryOperator formatter = this::stripScheme;
+ LeafResource leaf = queryItem.asResource();
+ LOGGER.debug("Adding resource {} to tree", leaf.getId());
+ // Strip the URI scheme from the resource
+ formatResource(leaf, formatter);
+ tree.add(leaf);
+ });
+ return tree;
+ });
+
+ // Join on the stream completion (closed websocket)
+ return populator.join();
+ }
+
+ /**
+ * Read a resource from the Data Service.
+ *
+ * @param token the token attached to the {@link ResourceTreeWithContext} that was
+ * returned by the Palisade Service on registering the query.
+ * @param node a leaf node (therefore {@link LeafResource}) from the {@link ResourceTree}
+ * @return the {@link InputStream} to read this resource.
+ */
+ public InputStream read(final String token, final LeafResourceNode node) {
+ UnaryOperator formatter = this::reapplyScheme;
+ QueryItem queryItem = new LeafResourceQueryItem(formatResource(node.get(), formatter), token);
+ LOGGER.debug("Downloading resource {}", queryItem.asResource().getId());
+ return session
+ .fetch(queryItem)
+ .getInputStream();
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/fs/ResourceTreeFS.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/fs/ResourceTreeFS.java
new file mode 100644
index 00000000..14fbe6ee
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/fs/ResourceTreeFS.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.fs;
+
+import jnr.ffi.Pointer;
+import jnr.ffi.types.off_t;
+import jnr.ffi.types.size_t;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.serce.jnrfuse.ErrorCodes;
+import ru.serce.jnrfuse.FuseFillDir;
+import ru.serce.jnrfuse.FuseStubFS;
+import ru.serce.jnrfuse.struct.FileStat;
+import ru.serce.jnrfuse.struct.FuseContext;
+import ru.serce.jnrfuse.struct.FuseFileInfo;
+import ru.serce.jnrfuse.struct.Statvfs;
+
+import uk.gov.gchq.palisade.client.fuse.tree.ParentNode;
+import uk.gov.gchq.palisade.client.fuse.tree.ResourceTree;
+import uk.gov.gchq.palisade.client.fuse.tree.TreeNode;
+import uk.gov.gchq.palisade.client.fuse.tree.impl.LeafResourceNode;
+import uk.gov.gchq.palisade.resource.AbstractLeafResource;
+import uk.gov.gchq.palisade.resource.Resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Implementation of a FuseFS mountable using a {@link ResourceTree} to populate directories.
+ * This uses the stubbed methods from {@link FuseStubFS} where specialisation is not required.
+ * This is where resources (returned from the Filtered-Resource Service) are mounted and presented
+ * to the user.
+ * The API was very much created with C in mind, so there's a lot of setting fields on mutable
+ * objects and returning {@link ErrorCodes} integers from methods.
+ *
+ * @see mount.fuse3
+ */
+// Unchecked casts with instanceof
+// Raw types with instanceof
+// Duplicated code for getting tree node
+@SuppressWarnings({"unchecked", "rawtypes", "DuplicatedCode"})
+public class ResourceTreeFS extends FuseStubFS {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ResourceTreeFS.class);
+
+ private ResourceTree resourceTree;
+ private Function reader;
+
+ /**
+ * Construct an instance of the FuseFS implementation, given a mutable tree which will be
+ * updated with new resources, and a function to read the data from nodes of the tree.
+ *
+ * @param resourceTree a tree collection that will be used for directory listings
+ * @param reader a function for acquiring an {@link InputStream} from a tree node,
+ * used for reading files
+ */
+ // We actively want the resourceTree collection to be mutable
+ // The structure should allow for creation of resources asynchronously to the fs mount
+ @SuppressWarnings("java:S2384")
+ public ResourceTreeFS(final ResourceTree resourceTree, final Function reader) {
+ this.resourceTree = resourceTree;
+ this.reader = reader;
+ }
+
+ private static int getattr(final TreeNode node, final FuseContext ctx, final FileStat stat) {
+ // Mark the file as read-only to the owning user and group, no access to others
+ // Same as "chmod ug=r o=", "chmod 440", or an "ls" permissions line of "r--r-----"
+ final int readOnly = FileStat.S_IRUSR | FileStat.S_IRGRP;
+ if (node instanceof ParentNode) {
+ // Directory, read-only
+ stat.st_mode.set(FileStat.S_IFDIR | readOnly);
+ // Set user-id and group-id from fuse context (whoever mounted)
+ stat.st_uid.set(ctx.uid.get());
+ stat.st_gid.set(ctx.gid.get());
+ return 0;
+ } else if (node instanceof LeafResourceNode) {
+ // Regular file, read-only
+ stat.st_mode.set(FileStat.S_IFREG | readOnly);
+ // unknown file-size so just go for something big
+ stat.st_size.set(Integer.MAX_VALUE);
+ // Set user-id and group-id from fuse context (whoever mounted)
+ stat.st_uid.set(ctx.uid.get());
+ stat.st_gid.set(ctx.gid.get());
+ return 0;
+ } else {
+ return -ErrorCodes.ENOENT();
+ }
+ }
+
+ private static int getxattr(final AbstractLeafResource resource, final String name, final Pointer value) {
+ Object attribute = resource.getAttribute(name);
+ if (attribute != null) {
+ byte[] buf = attribute.toString().getBytes();
+ value.put(0L, buf, 0, buf.length);
+ return 0;
+ } else {
+ // Attribute does not exist on object
+ return -ErrorCodes.ENODATA();
+ }
+ }
+
+ private static int listxattr(final AbstractLeafResource resource, final Pointer list, final long size) {
+ // Fill buffer with null-terminated string from map key-set, if there's space
+ byte[] buf = String.join("\0", resource.getAttributes().keySet()).getBytes();
+ if (buf.length <= size) {
+ list.put(0L, buf, 0, buf.length);
+ return 0;
+ } else {
+ return -ErrorCodes.E2BIG();
+ }
+ }
+
+ private static int read(final InputStream is, final Pointer buffer, final long size, final long offset) {
+ // Fill buffer with bytes from input stream
+ try {
+ byte[] buf = new byte[(int) size];
+ int bytesRead = is.readNBytes(buf, (int) offset, (int) size);
+ buffer.put(0, buf, 0, bytesRead);
+ return bytesRead;
+ } catch (IOException ex) {
+ LOGGER.warn("Failed to read (remote) input-stream", ex);
+ return -ErrorCodes.EREMOTEIO();
+ }
+ }
+
+ private static int readdir(final ParentNode node, final Pointer buf, final FuseFillDir filler, final long offset) {
+ Stream nodeNames = Stream.concat(Stream.of(".", ".."), node.getChildren().stream().map(TreeNode::getId));
+ // Fill buffer with child resource id's using filter function
+ for (String childName : nodeNames.skip(offset).collect(Collectors.toList())) {
+ // Put the childName into the buffer, returns 1 if the buffer is full
+ if (filler.apply(buf, childName, null, 0) == 1) {
+ // Stop if the buffer is full (~32k entities)
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Get the UNIX FS node attributes for a {@link TreeNode}.
+ *
+ * @param path the path to a node in the tree to generate filesystem attributes for
+ * @param stat mutable object to assign attributes into
+ * @return an {@link ErrorCodes} error-code
+ * @see stat
+ */
+ @Override
+ public int getattr(final String path, final FileStat stat) {
+ return resourceTree.getNode(path)
+ .map(node -> getattr(node, this.getContext(), stat))
+ .orElse(-ErrorCodes.ENOENT());
+ }
+
+ /**
+ * Read a filesystem node by copying bytes from the {@link InputStream} into the buffer.
+ *
+ * @param path the path to a (file) node in the tree to read
+ * @param buf the native buffer to write bytes into
+ * @param size the requested number of bytes to write into the buffer
+ * @param offset the offset in the file to start from
+ * @return an {@link ErrorCodes} error-code
+ * @see read
+ */
+ @Override
+ public int read(final String path, final Pointer buf, @size_t final long size, @off_t final long offset, final FuseFileInfo fi) {
+ TreeNode node = resourceTree.getNode(path).orElse(null);
+ if (node == null) {
+ // No entity (tree node) exists for the given path
+ return -ErrorCodes.EBADF();
+ } else if (node instanceof LeafResourceNode) {
+ // Read the file if it's a leaf
+ return read(reader.apply((LeafResourceNode) node), buf, size, offset);
+ } else {
+ // Can't read directories
+ return -ErrorCodes.EISDIR();
+ }
+ }
+
+ /**
+ * Read a filesystem node by copying bytes from the {@link InputStream} into the buffer.
+ *
+ * @param path the path to a (file) node in the tree to read
+ * @param buf the native buffer to write bytes into
+ * @param offset the offset in the file to start from
+ * @return an {@link ErrorCodes} error-code
+ * @see readdir
+ */
+ @Override
+ public int readdir(final String path, final Pointer buf, final FuseFillDir filter, @off_t final long offset, final FuseFileInfo fi) {
+ TreeNode node = resourceTree.getNode(path).orElse(null);
+ if (node == null) {
+ // No entity (tree node) exists for the given path
+ return -ErrorCodes.EBADF();
+ } else if (node instanceof ParentNode) {
+ // Read the directory if it's a parent
+ return readdir((ParentNode) node, buf, filter, offset);
+ } else {
+ // Can't readdir files
+ return -ErrorCodes.ENOTDIR();
+ }
+ }
+
+ /**
+ * Stat the mounted filesystem, report that it is read-only.
+ *
+ * @param path the path to a (file) node in the tree to read
+ * @param stbuf the native object to set flags etc. on
+ * @return an {@link ErrorCodes} error-code
+ * @see statsf
+ */
+ @Override
+ public int statfs(final String path, final Statvfs stbuf) {
+ // Mount read-only filesystem
+ stbuf.f_flag.set(Statvfs.ST_RDONLY);
+ return 0;
+ }
+
+ /**
+ * Get an extended attribute by name on a file.
+ * Only file nodes (leaf resources) have attributes
+ *
+ * @param path the path to a node in the tree to read extended attributes for
+ * @param name the name of a single attribute on the node
+ * @param value the native buffer to fill with the attribute's value
+ * @param size space available in the buffer
+ * @return an {@link ErrorCodes} error-code
+ * @see getxattr
+ */
+ @Override
+ public int getxattr(final String path, final String name, final Pointer value, final long size) {
+ TreeNode node = resourceTree.getNode(path).orElse(null);
+ if (node == null) {
+ // No entity (tree node) exists for the given path
+ return -ErrorCodes.ENOENT();
+ } else if (node.get() instanceof AbstractLeafResource) {
+ // Get an attribute on a file
+ return getxattr((AbstractLeafResource) node.get(), name, value);
+ } else {
+ // At the moment, Palisade only has attributes on leaf resources
+ return 0;
+ }
+ }
+
+ /**
+ * Get all extended attribute names on a file.
+ * Only file nodes (leaf resources) have attributes
+ *
+ * @param path the path to a node in the tree to read extended attributes for
+ * @param list the native buffer to fill with all attributes' values, formatted
+ * as concat'ed null-terminated strings
+ * @param size space available in the buffer
+ * @return an {@link ErrorCodes} error-code
+ * @see listxattr
+ */
+ @Override
+ public int listxattr(final String path, final Pointer list, final long size) {
+ TreeNode node = resourceTree.getNode(path).orElse(null);
+ if (node == null) {
+ // No entity (tree node) exists for the given path
+ return -ErrorCodes.ENOENT();
+ } else if (node.get() instanceof AbstractLeafResource) {
+ return listxattr((AbstractLeafResource) node.get(), list, size);
+ } else {
+ // At the moment, Palisade only has attributes on leaf resources
+ return 0;
+ }
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ChildNode.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ChildNode.java
new file mode 100644
index 00000000..79301eaa
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ChildNode.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree;
+
+/**
+ * Node of a tree which has a parent.
+ * This is analogous to a {@link uk.gov.gchq.palisade.resource.ChildResource}
+ *
+ * @param the type of elements in the tree
+ */
+public interface ChildNode extends TreeNode {
+ ParentNode getParent();
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ParentNode.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ParentNode.java
new file mode 100644
index 00000000..9e0294f0
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ParentNode.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+/**
+ * Node of a tree which is a parent, therefore has children.
+ * At this point, we can implement some defaults for the node as a collection of its children.
+ * This is analogous to a {@link uk.gov.gchq.palisade.resource.ParentResource}
+ *
+ * @param the type of elements in the tree
+ */
+@SuppressWarnings({"NullableProblems", "unchecked", "rawtypes"})
+public interface ParentNode extends TreeNode {
+ Collection> getChildren();
+
+ @Override
+ default Stream traverse() {
+ return Stream.concat(
+ Stream.of(this.get()),
+ this.getChildren().stream().flatMap(TreeNode::traverse)
+ );
+ }
+
+ @Override
+ default int size() {
+ return getChildren().size();
+ }
+
+ @Override
+ default boolean isEmpty() {
+ return getChildren().isEmpty();
+ }
+
+ @Override
+ default boolean contains(final Object o) {
+ return getChildren().contains(o);
+ }
+
+ @Override
+ default Iterator> iterator() {
+ return (Iterator) getChildren().iterator();
+ }
+
+ @Override
+ default Object[] toArray() {
+ return new Object[0];
+ }
+
+ @Override
+ default S[] toArray(final S[] ts) {
+ return (S[]) toArray();
+ }
+
+ @Override
+ default boolean add(final TreeNode resourceTreeNode) {
+ if (resourceTreeNode instanceof ChildNode) {
+ return getChildren().add((ChildNode) resourceTreeNode);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ default boolean remove(final Object o) {
+ return getChildren().remove(o);
+ }
+
+ @Override
+ default boolean containsAll(final Collection> collection) {
+ return getChildren().containsAll(collection);
+ }
+
+ @Override
+ default boolean addAll(final Collection extends TreeNode> collection) {
+ return collection.stream()
+ .map(this::add)
+ .reduce(Boolean::logicalOr)
+ .orElse(false);
+ }
+
+ @Override
+ default boolean removeAll(final Collection> collection) {
+ return collection.stream()
+ .map(this::remove)
+ .reduce(Boolean::logicalOr)
+ .orElse(false);
+ }
+
+ @Override
+ default boolean retainAll(final Collection> collection) {
+ return getChildren().retainAll(collection);
+ }
+
+ @Override
+ default void clear() {
+ getChildren().clear();
+ }
+
+ @Override
+ default void prettyprint(final Consumer printer, final int indent) {
+ printer.accept(String.join("", Collections.nCopies(indent, "\t")) + getId() + "\n");
+ getChildren().forEach(child -> child.prettyprint(printer, indent + 1));
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ResourceTree.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ResourceTree.java
new file mode 100644
index 00000000..f4f22910
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/ResourceTree.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree;
+
+import uk.gov.gchq.palisade.client.fuse.tree.impl.BranchResourceNode;
+import uk.gov.gchq.palisade.client.fuse.tree.impl.LeafResourceNode;
+import uk.gov.gchq.palisade.client.fuse.tree.impl.RootResourceNode;
+import uk.gov.gchq.palisade.resource.ChildResource;
+import uk.gov.gchq.palisade.resource.LeafResource;
+import uk.gov.gchq.palisade.resource.ParentResource;
+import uk.gov.gchq.palisade.resource.Resource;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+/**
+ * A {@link ResourceTree} is a {@link Collection} of {@link Resource}s, structured as a tree.
+ * The tree points to a {@link RootResourceNode}, and traverses the tree to implement the collection
+ * methods.
+ * Each node of the tree is analogous to the type of the resource it holds - eg. {@link LeafResourceNode}s
+ * hold {@link LeafResource}s.
+ */
+// Non-null collections api
+// Unchecked typecasts and raw types associated with .toArray()
+// Override .stream() and use it for implementing other methods
+@SuppressWarnings({"NullableProblems", "unchecked", "rawtypes", "SimplifyStreamApiCallChains"})
+public class ResourceTree implements Collection {
+ private static final String PATH_SEP = "/";
+ private static final Pattern PATH_SEP_PATTERN = Pattern.compile(PATH_SEP);
+ private static final Pattern DOUBLE_SEP_PATTERN = Pattern.compile("^" + PATH_SEP + "+");
+ private static final Pattern TRAILING_SEP_PATTERN = Pattern.compile(PATH_SEP + "+$");
+ protected RootResourceNode root;
+
+ /**
+ * Create a new node of the tree and link it to a (grand-)parent.
+ * This is equivalent to adding a resource to the collection.
+ *
+ * @param parent the (grand-)parent node to attach this resource to
+ * @param resource the resource to add
+ * @return a new {@link TreeNode} for the added resource
+ */
+ private static TreeNode createNode(final ParentNode parent, final Resource resource) {
+ List pathComponents = getPath(resource.getId());
+ String id = "";
+ if (!pathComponents.isEmpty()) {
+ id = pathComponents.get(pathComponents.size() - 1);
+ }
+
+ TreeNode node;
+ if (resource instanceof LeafResource) {
+ node = new LeafResourceNode(id, (ParentNode) parent, (LeafResource) resource);
+ } else if (resource instanceof ParentResource && resource instanceof ChildResource) {
+ node = new BranchResourceNode(id, (ParentNode) parent, (ChildResource) resource);
+ } else if (resource instanceof ParentResource) {
+ node = new RootResourceNode(id, (ParentResource) resource);
+ } else {
+ throw new IllegalArgumentException(resource.getClass().getName() + " is not a valid type");
+ }
+
+ if (parent != null) {
+ parent.add(node);
+ }
+ return node;
+ }
+
+ /**
+ * Format a String-based path into a list of path components.
+ * These are used to select children by name while traversing the tree.
+ *
+ * @param path a path for a tree node, using forward-slash path separators
+ * @return a list of names to select for (grand-)children to reach the node
+ * in the tree
+ */
+ private static List getPath(final String path) {
+ String noDoubleSep = DOUBLE_SEP_PATTERN.matcher(path).replaceAll("");
+ String strippedPath = TRAILING_SEP_PATTERN.matcher(noDoubleSep).replaceAll("");
+ if (strippedPath.isEmpty()) {
+ return List.of();
+ } else {
+ return List.of(PATH_SEP_PATTERN.split(strippedPath));
+ }
+ }
+
+ private static List getPath(final Resource resource) {
+ return getPath(resource.getId());
+ }
+
+ private Optional> getNode(final List idPath) {
+ return getNode(root, idPath);
+ }
+
+ /**
+ * Get a node in the tree by path to the node.
+ *
+ * @param path the path to the node, using forward-slash separators
+ * @return the node if it was found in the tree, {@link Optional#empty()} otherwise
+ */
+ public Optional> getNode(final String path) {
+ return getNode(getPath(path));
+ }
+
+ private Optional> getNode(final Resource resource) {
+ return getNode(getPath(resource));
+ }
+
+ // Remove the first item of a list
+ private static List dropFirst(final List list) {
+ if (list.size() > 1) {
+ return list.subList(1, list.size());
+ } else {
+ return List.of();
+ }
+ }
+
+ // Given a current node and list of child traversals, recursively get the next child in the list
+ private static Optional> getNode(final TreeNode node, final List path) {
+ if (node == null) {
+ // If we've hit a null node, we're not going to find anything
+ return Optional.empty();
+ } else if (path.isEmpty()) {
+ // Non-null node and no more path to traverse
+ return Optional.of(node);
+ } else if (node instanceof ParentNode) {
+ // Not the node we're looking for, but it has children and we have more path values to traverse
+ return ((ParentNode) node).getChildren()
+ .stream()
+ // Get children with the correct name for the next segment of the path
+ .filter(child -> child.getId().equals(path.get(0)))
+ // Drop the now-unnecessary first item off the path list when recursing
+ .map(child -> getNode(child, dropFirst(path)))
+ .flatMap(Optional::stream)
+ // Convert stream to optional
+ .findAny();
+ } else {
+ // We've got to something that has no children, but there's still path values to traverse
+ // Cannot subpath a non-parent (ie. file)
+ return Optional.empty();
+ }
+ }
+
+ /* Implementations for Collection interface */
+
+ @Override
+ public int size() {
+ return (int) root.traverse().count();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return root.isEmpty();
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ return root.traverse().anyMatch(resource -> resource.equals((o)));
+ }
+
+ @Override
+ public Stream stream() {
+ return root.traverse();
+ }
+
+ @Override
+ public Iterator iterator() {
+ return stream().iterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return stream().toArray();
+ }
+
+ @Override
+ public T[] toArray(final T[] ts) {
+ return (T[]) toArray();
+ }
+
+ /**
+ * In order to successfully add a resource, we also have to add any missing (grand-)parents
+ * for that resource too. We also have to traverse the tree to find out where to add the
+ * new (child) resource, or consider the special case where the tree is empty and we're
+ * having to add a new root.
+ *
+ * @param resource the resource to add to the tree
+ * @return true if the tree was modified, false if it was left unchanged
+ */
+ @Override
+ public boolean add(final Resource resource) {
+ if (getNode(resource).isEmpty()) {
+ // If this resource does not exist in the tree, add it
+ if (resource instanceof ChildResource) {
+ // Resource is not a a new root node
+ ParentResource parent = ((ChildResource) resource).getParent();
+ // Add any missing parents (recursively)
+ this.add(parent);
+ // Add this node as a child of its parent
+ return this.getNode(parent)
+ .map(node -> node.add(ResourceTree.createNode((ParentNode) node, resource)))
+ .orElseThrow(() -> new IllegalStateException("Failed to find parent node after adding it to the tree"));
+ } else if (resource instanceof ParentResource && root == null) {
+ // Resource is a new root node
+ root = (RootResourceNode) createNode(null, resource);
+ return true;
+ } else {
+ // Resource requires a new root node, but one already exists
+ throw new IllegalStateException("Adding resource required a new root node, but one already exists");
+ }
+ } else {
+ // If the resource already exists, we're done
+ return false;
+ }
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsAll(final Collection> collection) {
+ return collection.stream().anyMatch(this::contains);
+ }
+
+ @Override
+ public boolean addAll(final Collection extends Resource> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(final Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(final Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/TreeNode.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/TreeNode.java
new file mode 100644
index 00000000..6da7554b
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/TreeNode.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+/**
+ * Node of a {@link ResourceTree}, where each node is a collection of its children.
+ *
+ * @param the type of an object contained within the node
+ */
+public interface TreeNode extends Collection> {
+ /**
+ * Get the identifier for this node. This might be eg. a file name.
+ * This is similar to a Map of Strings to Ts.
+ *
+ * @return the node's identifier
+ */
+ String getId();
+
+ /**
+ * Get the content of this node, an element of the collection
+ *
+ * @return the object at this node
+ */
+ T get();
+
+ /**
+ * Get all objects at this node and below.
+ * Order does not matter.
+ *
+ * @return a stream of objects as the tree is traversed
+ */
+ Stream traverse();
+
+ /**
+ * Try to print this node and its children in a human-readable manner.
+ *
+ * @param printer the logger for the string values
+ * @param indent the depth of indent, proportional to the depth of the node printed in the tree
+ */
+ default void prettyprint(final Consumer printer, final int indent) {
+ printer.accept(String.join("", Collections.nCopies(indent, "\t")) + getId() + "\n");
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/BranchResourceNode.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/BranchResourceNode.java
new file mode 100644
index 00000000..cf2f6aca
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/BranchResourceNode.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree.impl;
+
+import uk.gov.gchq.palisade.client.fuse.tree.ChildNode;
+import uk.gov.gchq.palisade.client.fuse.tree.ParentNode;
+import uk.gov.gchq.palisade.client.fuse.tree.TreeNode;
+import uk.gov.gchq.palisade.resource.ChildResource;
+import uk.gov.gchq.palisade.resource.ParentResource;
+import uk.gov.gchq.palisade.resource.Resource;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A node in a tree which is both a parent and a child.
+ * Therefore it is neither a 'root' or a 'leaf', but a 'branch'.
+ * It is represented by the union of {@link ParentResource} and {@link ChildResource}.
+ * See the {@link uk.gov.gchq.palisade.resource.impl.DirectoryResource} implementation
+ * for an analogous {@link Resource} class.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class BranchResourceNode implements ParentNode, ChildNode {
+ private final String id;
+ private final ParentNode parent;
+ private final Set> children;
+ private final ChildResource resource;
+
+ /**
+ * Create a new branch node, given its id, parent and the resource it represents
+ *
+ * @param id the {@link TreeNode#getId()} identifier for this node
+ * @param parent the node representing the resource's {@link ChildResource#getParent()}
+ * @param resource the {@link TreeNode#get()} collection item stored at this point, which
+ * should implement both {@link ChildResource} and {@link ParentResource}
+ */
+ // We actively want the parent to be a mutable ref, not a copy
+ @SuppressWarnings("java:S2384")
+ public BranchResourceNode(final String id, final ParentNode parent, final ChildResource resource) {
+ if (!(resource instanceof ParentResource)) {
+ throw new IllegalArgumentException("Resource must be both parent and child");
+ }
+ this.id = id;
+ this.parent = parent;
+ this.resource = resource;
+ this.children = new HashSet<>();
+ }
+
+ @Override
+ public ParentNode getParent() {
+ return (ParentNode) parent;
+ }
+
+ @Override
+ public Collection> getChildren() {
+ return (Collection) children;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public Resource get() {
+ return resource;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ this.prettyprint(sb::append, 0);
+ return sb.toString();
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/LeafResourceNode.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/LeafResourceNode.java
new file mode 100644
index 00000000..c1d466be
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/LeafResourceNode.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree.impl;
+
+import uk.gov.gchq.palisade.client.fuse.tree.ChildNode;
+import uk.gov.gchq.palisade.client.fuse.tree.ParentNode;
+import uk.gov.gchq.palisade.client.fuse.tree.TreeNode;
+import uk.gov.gchq.palisade.resource.ChildResource;
+import uk.gov.gchq.palisade.resource.LeafResource;
+import uk.gov.gchq.palisade.resource.ParentResource;
+import uk.gov.gchq.palisade.resource.Resource;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.stream.Stream;
+
+/**
+ * A node in a tree which is only a child.
+ * Therefore it is a 'leaf' of the tree, represented by a {@link LeafResource}.
+ * See the {@link uk.gov.gchq.palisade.resource.impl.FileResource} implementation
+ * for an analogous {@link Resource} class.
+ */
+@SuppressWarnings({"NullableProblems", "unchecked", "rawtypes"})
+public class LeafResourceNode implements ChildNode {
+ private final String id;
+ private final ParentNode parent;
+ private final LeafResource resource;
+
+ /**
+ * Create a new branch node, given its id, parent and the resource it represents
+ *
+ * @param id the {@link TreeNode#getId()} identifier for this node
+ * @param parent the node representing the resource's {@link ChildResource#getParent()}
+ * @param resource the {@link TreeNode#get()} collection item stored at this point, which
+ * should implement {@link LeafResource}
+ */
+ // We actively want the parent to be a mutable ref, not a copy
+ @SuppressWarnings("java:S2384")
+ public LeafResourceNode(final String id, final ParentNode parent, final LeafResource resource) {
+ this.id = id;
+ this.parent = parent;
+ this.resource = resource;
+ }
+
+ @Override
+ public ParentNode getParent() {
+ return (ParentNode) parent;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public LeafResource get() {
+ return resource;
+ }
+
+ @Override
+ public Stream traverse() {
+ return Stream.of(this.get());
+ }
+
+ /* Implementations for Collection interface */
+
+ @Override
+ public int size() {
+ return 0;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ return false;
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return Collections.emptyIterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return new Object[0];
+ }
+
+ @Override
+ public T[] toArray(final T[] ts) {
+ return (T[]) new Object[0];
+ }
+
+ @Override
+ public boolean add(final TreeNode resourceTreeNode) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsAll(final Collection> collection) {
+ return false;
+ }
+
+ @Override
+ public boolean addAll(final Collection extends TreeNode> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(final Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(final Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ // Nothing to do, leaf has no children in its collection
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ this.prettyprint(sb::append, 0);
+ return sb.toString();
+ }
+}
diff --git a/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/RootResourceNode.java b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/RootResourceNode.java
new file mode 100644
index 00000000..ab0a6b69
--- /dev/null
+++ b/client-fuse/src/main/java/uk/gov/gchq/palisade/client/fuse/tree/impl/RootResourceNode.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree.impl;
+
+import uk.gov.gchq.palisade.client.fuse.tree.ChildNode;
+import uk.gov.gchq.palisade.client.fuse.tree.ParentNode;
+import uk.gov.gchq.palisade.client.fuse.tree.TreeNode;
+import uk.gov.gchq.palisade.resource.ChildResource;
+import uk.gov.gchq.palisade.resource.ParentResource;
+import uk.gov.gchq.palisade.resource.Resource;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * A node in a tree which is only a parent.
+ * Therefore it is the 'root' of the tree, represented by a {@link ParentResource}.
+ * See the {@link uk.gov.gchq.palisade.resource.impl.SystemResource} implementation
+ * for an analogous {@link Resource} class.
+ */
+@SuppressWarnings({"NullableProblems", "unchecked", "rawtypes"})
+public class RootResourceNode implements ParentNode {
+ private final String id;
+ private final Set> children;
+ private final ParentResource resource;
+
+ /**
+ * Create a new branch node, given its id, parent and the resource it represents
+ *
+ * @param id the {@link TreeNode#getId()} identifier for this node
+ * @param resource the {@link TreeNode#get()} collection item stored at this point, which
+ * should implement {@link ParentResource}
+ */
+ public RootResourceNode(final String id, final ParentResource resource) {
+ this.id = id;
+ this.resource = resource;
+ this.children = new HashSet<>();
+ }
+
+ @Override
+ public Collection> getChildren() {
+ return (Collection) children;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public Resource get() {
+ return resource;
+ }
+
+ /* Implementations for Collection interface */
+
+ @Override
+ public int size() {
+ return children.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return children.isEmpty();
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ return children.contains(o);
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return (Iterator) children.iterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return new Object[0];
+ }
+
+ @Override
+ public T[] toArray(final T[] ts) {
+ return (T[]) toArray();
+ }
+
+ @Override
+ public boolean add(final TreeNode resourceTreeNode) {
+ if (resourceTreeNode instanceof ChildNode && resourceTreeNode.get() instanceof ChildResource) {
+ return children.add((ChildNode) resourceTreeNode);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ return children.remove(o);
+ }
+
+ @Override
+ public boolean containsAll(final Collection> collection) {
+ return children.containsAll(collection);
+ }
+
+ @Override
+ public boolean addAll(final Collection extends TreeNode> collection) {
+ return collection.stream()
+ .map(this::add)
+ .reduce(Boolean::logicalOr)
+ .orElse(false);
+ }
+
+ @Override
+ public boolean removeAll(final Collection> collection) {
+ return collection.stream()
+ .map(this::remove)
+ .reduce(Boolean::logicalOr)
+ .orElse(false);
+ }
+
+ @Override
+ public boolean retainAll(final Collection> collection) {
+ return children.retainAll(collection);
+ }
+
+ @Override
+ public void clear() {
+ children.clear();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ this.prettyprint(sb::append, 0);
+ return sb.toString();
+ }
+}
diff --git a/client-fuse/src/unit-tests/java/uk/gov/gchq/palisade/client/fuse/tree/ResourceTreeTest.java b/client-fuse/src/unit-tests/java/uk/gov/gchq/palisade/client/fuse/tree/ResourceTreeTest.java
new file mode 100644
index 00000000..55b0f6a7
--- /dev/null
+++ b/client-fuse/src/unit-tests/java/uk/gov/gchq/palisade/client/fuse/tree/ResourceTreeTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.fuse.tree;
+
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.palisade.resource.LeafResource;
+import uk.gov.gchq.palisade.resource.ParentResource;
+import uk.gov.gchq.palisade.resource.impl.DirectoryResource;
+import uk.gov.gchq.palisade.resource.impl.FileResource;
+import uk.gov.gchq.palisade.resource.impl.SimpleConnectionDetail;
+import uk.gov.gchq.palisade.resource.impl.SystemResource;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ResourceTreeTest {
+ @SuppressWarnings({"SimplifyStreamApiCallChains"})
+ @Test
+ void testTreeBuildsCorrectly() {
+ ResourceTree tree = new ResourceTree();
+
+ ParentResource root = new SystemResource()
+ .id("/");
+ ParentResource some = new DirectoryResource()
+ .id("/some")
+ .parent(root);
+ LeafResource file1 = new FileResource()
+ .id("/some/file1")
+ .type("type")
+ .serialisedFormat("format")
+ .connectionDetail(new SimpleConnectionDetail().serviceName("data-service"))
+ .parent(some);
+ LeafResource file2 = new FileResource()
+ .id("/some/file2")
+ .type("type")
+ .serialisedFormat("format")
+ .connectionDetail(new SimpleConnectionDetail().serviceName("data-service"))
+ .parent(some);
+
+ tree.add(file1);
+
+ assertThat(tree.stream().collect(Collectors.toSet()))
+ .as("Tree stream should contain all (three) elements added to the tree")
+ .isEqualTo(Set.of(root, some, file1));
+
+ tree.add(file2);
+
+ assertThat(tree.stream().collect(Collectors.toSet()))
+ .as("Tree stream should contain all (four) elements added to the tree")
+ .isEqualTo(Set.of(root, some, file1, file2));
+ }
+}
diff --git a/client-java/.gitignore b/client-java/.gitignore
new file mode 100644
index 00000000..da7560e0
--- /dev/null
+++ b/client-java/.gitignore
@@ -0,0 +1 @@
+/.apt_generated_tests/
diff --git a/client-java/README.md b/client-java/README.md
new file mode 100644
index 00000000..1d84268a
--- /dev/null
+++ b/client-java/README.md
@@ -0,0 +1,171 @@
+
+#
+
+## A Tool for Complex and Scalable Data Access Policy Enforcement
+
+# Palisade Client (Java)
+
+The Java Palisade Client API provides universal resource access to a Palisade cluster.
+
+### API Design
+
+The design of the API loosely follows that of other well known API's. This design decision was made to provide a familiar feel to accessing Palisade.
+
+Of course there are differences. One of the big differences is that Palisade clients deal with returning resource files and not data in Columnar format.
+
+#### URL
+
+The URL follows the JDBC specification of not exposing the underlying communication protocol. To this end the scheme is set as `pal:[subname]`.
+
+If you are familiar with the JDBC url, then the Palisade URL should be familiar.
+See the examples below:
+```
+Cluster is 'my.cluster', userId is 'alice'
+pal://my.cluster?userid=alice
+
+Cluster is 'localhost:8080', requires authentication, userId is 'alice'
+pal://eve:password@localhost:8080/cluster?userid=alice
+
+Cluster is 'localhost:8080/cluster', userId is 'alice', use an ssl connection, use an http2 connection, maximum 2hrs between server responses
+pal://localhost:8080/cluster?userid=alice&ssl=true&http2=true&poll=7200
+```
+
+
+Note that any user passed as part of the authority portion of the URL (e.g. "eve" in the above example) will simply be copied to the create Palisade Service and Filtered Resource Service URIs. This use is not the `user_id` that is passed as part of the
+REST call to the Palisade Service. The user id is passed via a property (`service.userid`) or as a query parameter (userid).
+
+#### Interfaces
+
+The API is split into many different interfaces, which again, are loosely based upon those of JDBC. Some of these interfaces include:
+
+| Interface | Description |
+| --- | --- |
+| Client | This is analogous to JDBCs driver. This class provides access to actually open and retrieve a session to the Palisade cluster. Clients are not instantiated directly, but by the `ClientManager `asking the client whether it supports a given URL. This way the user of the API does not need to know about its implementation. |
+| Session | This is roughly the same as JDBCs Connection class. A `Session` provides access to create queries and fetch downloads. At this point there is no security for a session as Palisade does not require it. If this changes in the future, the client API will be unaffected. |
+| Query | This is the instance that sends the request to the Palisade Service. This is where the client deviates from JDBC as the design for this is (very) loosely based upon Hibernate's Query. |
+| QueryResponse | Once the query is executes and the `Query` has returned this via a `Future`, a stream of `Message`'s can be retrieved. This class abstracts the underlying mechanisms of how the Filtered Resource Service is accessed. This has no analogue to JDBC or Hibernate as those libraries do not support streams yet. |
+| Message | Two types of messages can be returned from the Filtered Resource Service and these are abstracted into two subclasses of `Message`. The design choice was to either have two sub types, or have a single type (resource) that can contain an Error. Either way is not wrong. This could change quite easily if needed. Currently two subclasses exist for Message. These are `Error` and `Resource`. |
+| Download | A `Download` is retrieved by passing a `Resource` object to the `Session`. The Download abstracts the call to the data service and provides access to an `InputStream` to consume its contents. |
+
+### Example Usage
+
+A unit test to assert that resources are returned may look like the following. Note that here we are using Micronaut to create end-points for the following with Palisade:
+
+* Palisade Service
+* Filtered Resource Service
+* Data Service
+
+Micronaut will find these services and create and then inject an EmbeddedServer to expose them. The embedded server can be used to get the port which it is listening on. The port will be dynamically created enabling parallel tests.
+
+```java
+
+@MicronautTest
+class FullTest {
+
+ @Inject
+ EmbeddedServer embeddedServer;
+
+ @Test
+ void testWithDownloadOutsideStream() throws Exception {
+
+ var port = "" + embeddedServer.getPort();
+ (1) var properties = Map.of(
+ "service.userid", "alice",
+ "service.palisade.port", port,
+ "service.filteredResource.port", port);
+
+ (2) var session = ClientManager.openSession("pal://eve@localhost/cluster", properties);
+ (3) var query = session.createQuery("good_morning", Map.of("purpose", "Alice's purpose"));
+ (4) var publisher = query
+ .execute()
+ .thenApply(QueryResponse::stream)
+ .get();
+
+ (5) var resources = Flowable.fromPublisher(FlowAdapters.toPublisher(publisher))
+ .filter(m -> m.getType() == MessageType.RESOURCE)
+ .map(Resource.class::cast)
+ .collect(Collectors.toList())
+ .blockingGet();
+
+ assertThat(resources).hasSizeGreaterThan(0);
+
+ (6) var resource = resources.get(0);
+ assertThat(resource.getLeafResourceId()).isEqualTo("resources/test-data-0.txt");
+
+ (7) var download = session.fetch(resource);
+ assertThat(download).isNotNull();
+
+ var actual = download.getInputStream();
+ var expected = Thread.currentThread()
+ .getContextClassLoader()
+ .getResourceAsStream("resources/test-data-0.txt");
+
+ (8) assertThat(actual).hasSameContentAs(expected);
+
+
+ }
+}
+
+```
+
+1. Creates a map of properties to be passed to the client. Here we are overriding the port for the Palisade Service and Filtered Resource Service.
+2. Uses the `ClientManager` to create a `Session` from a Palisade URL. Here we are passing the user via the provided property map. If a user is also passed via a query parameter, this user in the query takes precedence.
+3. A new Query is created by passing a query string and an optional map of properties.
+4. The query is executed. The request is submitted to Palisade at this point and a `CompleteableFuture` is returned asynchronously. Once Palisade has processed the request, the future will emit a `Publisher` of `Messages` instances.
+5. Convert the `java.util.current.Flow.Publisher` to an RxJava `Flowable` in order to apply filtering and retrieval into a collection of `Resource` instances.
+6. Use the first resource as a test and make sure it's not null
+7. Using the session we fetch the resource. A `Download` instance is returned. At this point the request has been sent and received from the Data Service. The download object provides access to an `InputStream`. The data is not returned from the server
+ until the input stream is first accessed.
+8. Using AssertJ the two input streams are checked for equality.
+
+### Client properties
+
+Properties can be provided via 2 routes, the url and properties. The DefaultClient specifies that the attributes on the url (query) take precedence over those in the provided property map.
+
+| Name | Property | Query Parameter | Required | Description |
+| --- | --- | --- | --- | --- |
+| User ID | service.userid | userid | YES | The user ID is is used as part of a query to the server |
+| Palisade Service Port | service.palisade.port | psport | NO | If provided will override any port provided on the Palisade URL provided to the session. |
+| Filtered Resource Service Port | service.filteredResource.port | wsport | NO | If provided will override any port provided on the Palisade URL provided to the session. |
+
+Some properties can be overriden, but for testing.
+
+__Note__: These properties are not available as a querystring parameter.
+
+| Name | Property | Default | Description |
+| --- | --- | --- | --- |
+| Palisade Service Path | service.palisade.path | palisade/api/registerDataRequest | the path which is appended to the Palisade service URL |
+| Filtered Resource Service Path | service.filteredResource.path | resource/%t | the path which is appended to the Filtered Resource Service URL |
+| Data Service Path | service.data.path | read/chunked | the path which is appended to the Data Service URL |
+
+## Technologies Used
+
+### Runtime
+
+* [Immutables](https://immutables.github.io/) - Java annotation processors to generate simple, safe and consistent value objects.
+* [Jackson]() - JSON for Java. Handles all the (de)serialisation of objects to/from the Palisade servers.
+
+### Test Only
+
+* [Junit5](https://junit.org/junit5/) - Needs no introduction :)
+* [AssertJ](https://assertj.github.io/doc/) - Excellent testing library
+* [Logback](http://logback.qos.ch/) - Great logging library. Used for testing.
+* [Micronaut HTTP Server](https://micronaut.io/) - Used for testing
+
+## Issues
+
+There is currently a problem with Palisade regarding HTTP/2. For this reason, the client is currently limited to HTTP/1.1 and will not request a protocol upgrade.
diff --git a/client-java/mvn_dependency_tree.txt b/client-java/mvn_dependency_tree.txt
new file mode 100644
index 00000000..6993d24d
--- /dev/null
+++ b/client-java/mvn_dependency_tree.txt
@@ -0,0 +1,63 @@
+uk.gov.gchq.palisade:client-java:jar:0.5.0-RELEASE
++- uk.gov.gchq.palisade:common:jar:0.5.0-RELEASE:compile
+| \- org.apache.avro:avro:jar:1.8.2:compile
+| +- org.codehaus.jackson:jackson-core-asl:jar:1.9.13:compile
+| +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.13:compile
+| +- com.thoughtworks.paranamer:paranamer:jar:2.7:compile
+| +- org.xerial.snappy:snappy-java:jar:1.1.1.3:compile
+| +- org.apache.commons:commons-compress:jar:1.8.1:compile
+| \- org.tukaani:xz:jar:1.5:compile
++- org.slf4j:slf4j-api:jar:1.7.26:compile
++- com.fasterxml.jackson.core:jackson-databind:jar:2.11.0:compile
+| +- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.0:compile
+| \- com.fasterxml.jackson.core:jackson-core:jar:2.11.0:compile
++- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.11.0:compile
++- io.reactivex.rxjava3:rxjava:jar:3.0.8:compile
+| \- org.reactivestreams:reactive-streams:jar:1.0.3:compile
++- org.immutables:value:jar:2.8.2:provided
++- org.junit.jupiter:junit-jupiter-api:jar:5.7.0:test
+| +- org.apiguardian:apiguardian-api:jar:1.1.0:test
+| \- org.opentest4j:opentest4j:jar:1.2.0:test
++- org.junit.jupiter:junit-jupiter-engine:jar:5.7.0:test
++- org.junit.platform:junit-platform-engine:jar:1.7.0:test
++- org.junit.platform:junit-platform-commons:jar:1.7.0:test
++- io.micronaut.test:micronaut-test-junit5:jar:2.3.2:test
+| \- io.micronaut.test:micronaut-test-core:jar:2.3.2:test
++- io.micronaut:micronaut-runtime:jar:2.3.2:test
+| +- io.micronaut:micronaut-http:jar:2.3.2:test
+| +- io.micronaut:micronaut-aop:jar:2.3.2:test
+| +- javax.validation:validation-api:jar:2.0.1.Final:test
+| +- io.reactivex.rxjava2:rxjava:jar:2.2.19:test
+| \- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.11.0:test
++- io.micronaut:micronaut-inject:jar:2.3.2:provided
+| +- javax.annotation:javax.annotation-api:jar:1.3.2:provided
+| +- javax.inject:javax.inject:jar:1:provided
+| +- io.micronaut:micronaut-core:jar:2.3.2:provided
+| | \- com.github.spotbugs:spotbugs-annotations:jar:4.0.3:provided
+| | \- com.google.code.findbugs:jsr305:jar:3.0.2:provided
+| \- org.yaml:snakeyaml:jar:1.26:provided
++- io.micronaut:micronaut-inject-java:jar:2.3.2:test
++- io.micronaut:micronaut-http-server-netty:jar:2.3.2:test
+| +- io.micronaut:micronaut-http-server:jar:2.3.2:test
+| | +- io.micronaut:micronaut-websocket:jar:2.3.2:test
+| | \- io.micronaut:micronaut-router:jar:2.3.2:test
+| +- io.micronaut:micronaut-http-netty:jar:2.3.2:test
+| | +- io.micronaut:micronaut-buffer-netty:jar:2.3.2:test
+| | +- io.netty:netty-handler:jar:4.1.50.Final:test
+| | | \- io.netty:netty-resolver:jar:4.1.50.Final:test
+| | \- io.netty:netty-codec-http2:jar:4.1.50.Final:test
+| \- io.netty:netty-codec-http:jar:4.1.50.Final:test
+| +- io.netty:netty-common:jar:4.1.50.Final:test
+| +- io.netty:netty-buffer:jar:4.1.50.Final:test
+| +- io.netty:netty-transport:jar:4.1.50.Final:test
+| \- io.netty:netty-codec:jar:4.1.50.Final:test
++- org.junit.jupiter:junit-jupiter:jar:5.7.0:test
+| \- org.junit.jupiter:junit-jupiter-params:jar:5.6.2:test
++- org.assertj:assertj-core:jar:3.19.0:test
++- org.mockito:mockito-core:jar:3.7.7:test
+| +- net.bytebuddy:byte-buddy:jar:1.10.11:test
+| +- net.bytebuddy:byte-buddy-agent:jar:1.10.11:test
+| \- org.objenesis:objenesis:jar:3.1:test
++- org.mockito:mockito-junit-jupiter:jar:3.7.7:test
+\- ch.qos.logback:logback-classic:jar:1.2.3:test
+ \- ch.qos.logback:logback-core:jar:1.2.3:test
diff --git a/client-java/pom.xml b/client-java/pom.xml
new file mode 100644
index 00000000..046f2f57
--- /dev/null
+++ b/client-java/pom.xml
@@ -0,0 +1,292 @@
+
+
+
+
+ 4.0.0
+
+
+ uk.gov.gchq.palisade
+ clients
+ 0.5.0-${revision}
+ ../pom.xml
+
+
+
+
+ PalisadeDevelopers
+ GCHQ
+ https://github.com/gchq
+
+
+
+
+ client-java
+ https://github.com/gchq/Palisade-clients/tree/develop/client-java
+ GCHQ Palisade - Java Client
+
+ The Java Palisade Client API provides access to a Palisade deployment, allowing querying and reading of resources.
+
+
+
+
+ ${scm.url}
+ ${scm.connection}
+ ${scm.developer.connection}
+ HEAD
+
+
+
+ 11
+ 11
+
+
+ 2.11.0
+ 3.0.8
+ 1.7.26
+
+
+ 2.8.2
+
+
+ 2.3.2
+ 5.7.0
+ 1.7.0
+ 3.19.0
+ 1.2.3
+ 3.7.7
+
+
+
+
+
+
+ uk.gov.gchq.palisade
+ common
+ 0.5.0-${common.revision}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jdk8
+ ${jackson.version}
+
+
+ io.reactivex.rxjava3
+ rxjava
+ ${rxjava.version}
+
+
+
+
+ org.immutables
+ value
+ ${immutables.version}
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.jupiter.version}
+ test
+
+
+ org.junit.platform
+ junit-platform-engine
+ ${junit.platform.version}
+ test
+
+
+ org.junit.platform
+ junit-platform-commons
+ ${junit.platform.version}
+ test
+
+
+
+
+ io.micronaut.test
+ micronaut-test-junit5
+ ${micronaut.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+
+
+ io.micronaut
+ micronaut-runtime
+ ${micronaut.version}
+ test
+
+
+ io.micronaut
+ micronaut-inject
+ ${micronaut.version}
+ provided
+
+
+ io.micronaut
+ micronaut-inject-java
+ ${micronaut.version}
+ test
+
+
+ io.micronaut
+ micronaut-http-server-netty
+ ${micronaut.version}
+ test
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.jupiter.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ test
+
+
+
+
+
+
+ src/main/resources
+
+
+
+
+ src/unit-tests/resources
+
+
+ src/component-tests/resources
+
+
+ src/contract-tests/resources
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.1.0
+
+ true
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+ flatten.clean
+ clean
+
+ clean
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-test-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+
+ add-test-resources
+ generate-test-resources
+
+ add-test-resource
+
+
+
+
+ true
+ ${basedir}/src/unit-tests/resources
+ ${basedir}/src/component-tests/resources
+ ${basedir}/src/contract-tests/resources
+
+
+
+
+
+
+
+
+
+
diff --git a/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/DownloaderTest.java b/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/DownloaderTest.java
new file mode 100644
index 00000000..da1a9731
--- /dev/null
+++ b/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/DownloaderTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.component.java;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.palisade.client.java.internal.download.Downloader;
+import uk.gov.gchq.palisade.client.java.internal.download.DownloaderException;
+import uk.gov.gchq.palisade.resource.impl.FileResource;
+import uk.gov.gchq.palisade.resource.impl.SimpleConnectionDetail;
+
+import javax.inject.Inject;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.FILE_NAME_0;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.TOKEN;
+
+@MicronautTest
+class DownloaderTest {
+
+ private static final String BASE_URL = "http://localhost:%d/cluster/data/"; // needs port added before use
+ private static final String ENDPOINT = "read/chunked";
+
+ private static ObjectMapper objectMapper;
+
+ private Downloader downloader;
+
+ @Inject
+ EmbeddedServer embeddedServer;
+
+ private URI uri;
+
+ @BeforeAll
+ static void setupAll() {
+ objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
+ }
+
+ @BeforeEach
+ void setup() {
+ this.uri = URI.create(String.format(BASE_URL, embeddedServer.getPort()));
+ this.downloader = Downloader.createDownloader(b -> b
+ .httpClient(HttpClient.newHttpClient())
+ .objectMapper(objectMapper)
+ .path(ENDPOINT)
+ .putServiceNameMap("data-service", uri));
+ }
+
+ @Test
+ void testSuccessfulDownload() throws Exception {
+ var resource = new FileResource()
+ .id(FILE_NAME_0.asString())
+ .connectionDetail(new SimpleConnectionDetail().serviceName("data-service"));
+
+ var download = downloader.fetch(TOKEN, resource);
+
+ // now load both the original file from the classpath (in resources folder) and
+ // the one in /tmp. Both these files are compared byte by byte for equality.
+
+ try (var actual = download.getInputStream();
+ var expected = FILE_NAME_0.createStream();
+ ) {
+ assertThat(actual)
+ .as("check downloaded input stream")
+ .hasSameContentAs(expected);
+ }
+ }
+
+ @Test
+ void testFileNotFound() {
+ var filename = "doesnotexist";
+
+ var resource = new FileResource()
+ .id(filename)
+ .connectionDetail(new SimpleConnectionDetail().serviceName("data-service"));
+
+ var expectedClass = DownloaderException.class;
+ var expectedStatus = 500;
+
+ assertThatExceptionOfType(expectedClass)
+ .as("check correct exception when resource is not found")
+ .isThrownBy(() -> downloader.fetch(TOKEN, resource))
+ .withMessage("[" + expectedStatus + "] Request to DataService '" + uri + ENDPOINT + "' failed")
+ .matches(ex -> ex.getStatusCode() == expectedStatus, "statuscode " + expectedStatus);
+ }
+
+}
diff --git a/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/PalisadeServiceTest.java b/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/PalisadeServiceTest.java
new file mode 100644
index 00000000..62346f8a
--- /dev/null
+++ b/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/PalisadeServiceTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.component.java;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.palisade.client.java.ClientException;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeRequest;
+import uk.gov.gchq.palisade.client.java.internal.request.PalisadeService;
+
+import javax.inject.Inject;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpResponse;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.TOKEN;
+
+@MicronautTest
+class PalisadeServiceTest {
+
+ @Inject
+ ObjectMapper objectMapper;
+ @Inject
+ EmbeddedServer embeddedServer;
+
+ @Test
+ void testSubmit() throws Exception {
+
+ var port = embeddedServer.getPort();
+ var uri = new URI("http://localhost:" + port + "/cluster/palisade/api/registerDataRequest");
+ var palisadeRequest = PalisadeRequest.Builder.create()
+ .withUserId("user_id")
+ .withResourceId("resource_id")
+ .withContext(Map.of("key", "value"));
+
+ var service = PalisadeService.createPalisadeService(b -> b
+ .httpClient(HttpClient.newHttpClient())
+ .objectMapper(objectMapper)
+ .uri(uri));
+
+ var palisadeResponse = service.submit(palisadeRequest);
+
+ assertThat(palisadeResponse)
+ .as("check valid response")
+ .isNotNull()
+ .extracting("token")
+ .isEqualTo(TOKEN);
+
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void testCheckStatusOK202() {
+
+ var expectedResponse = mock(HttpResponse.class);
+
+ when(expectedResponse.statusCode()).thenReturn(202);
+
+ assertThat(PalisadeService.checkStatusOK(expectedResponse))
+ .as("check response is valid")
+ .isEqualTo(expectedResponse);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void testCheckStatusOK404() {
+
+ var response = mock(HttpResponse.class);
+ var expectedException = ClientException.class;
+
+ when(response.statusCode()).thenReturn(404);
+
+ assertThatExceptionOfType(expectedException)
+ .as("check valid exception thrown for response with no body")
+ .isThrownBy(() -> PalisadeService.checkStatusOK(response))
+ .withMessage("Request to Palisade Service failed (404) with no body");
+
+ when(response.body()).thenReturn("body");
+
+ assertThatExceptionOfType(expectedException)
+ .as("check valid exception thrown response with a body")
+ .isThrownBy(() -> PalisadeService.checkStatusOK(response))
+ .withMessage("Request to Palisade Service failed (404) with body:\nbody");
+
+ }
+
+}
diff --git a/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/ResourceClientTest.java b/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/ResourceClientTest.java
new file mode 100644
index 00000000..c3f92212
--- /dev/null
+++ b/client-java/src/component-tests/java/uk/gov/gchq/palisade/component/java/ResourceClientTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.component.java;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.WebSocketMessage;
+import uk.gov.gchq.palisade.client.java.internal.resource.WebSocketClient;
+import uk.gov.gchq.palisade.client.java.testing.ClientTestData;
+import uk.gov.gchq.palisade.resource.ConnectionDetail;
+import uk.gov.gchq.palisade.resource.LeafResource;
+import uk.gov.gchq.palisade.resource.impl.SimpleConnectionDetail;
+
+import javax.inject.Inject;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.FILE_NAME_0;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.FILE_NAME_1;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.TOKEN;
+
+/**
+ * Note that this class must be public for the subscriptions on the event bus to
+ * work. SonarQube complains about this though.
+ *
+ * @since 0.5.0
+ */
+@MicronautTest
+class ResourceClientTest {
+
+ @Inject
+ EmbeddedServer embeddedServer;
+
+ private ObjectMapper objectMapper;
+ private WebSocketClient resourceClient;
+
+ private int port;
+
+ @BeforeEach
+ void setup() {
+ this.port = embeddedServer.getPort();
+ this.objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
+
+ this.resourceClient = WebSocketClient
+ .createResourceClient(b -> b
+ .httpClient(HttpClient.newHttpClient())
+ .token(TOKEN)
+ .uri(URI.create("ws://localhost:" + port + "/cluster/filteredResource/resource/%25t"))
+ .objectMapper(objectMapper))
+ .connect();
+ }
+
+ @Test
+ void testMessageFlow() {
+
+ // There are two resources so we should have 3 events (2 resource and 1
+ // complete)
+
+ var messages = new ArrayList();
+ var message = (WebSocketMessage) null;
+ do {
+ message = resourceClient.poll(5, TimeUnit.SECONDS);
+ if (message != null) {
+ messages.add(message);
+ }
+ } while (message != null && !message.getType().equals(MessageType.COMPLETE));
+
+ assertThat(messages).hasSize(ClientTestData.FILE_NAMES.size() + 2); // (n*resources) + (1*error) + (1*complete)
+
+ assertThat(messages.get(0))
+ .as("check resource event0")
+ .satisfies(msg -> assertThat(msg.getType()).isEqualTo(MessageType.RESOURCE))
+ .extracting(msg -> msg.getBodyObject(LeafResource.class))
+ .extracting("id", "connectionDetail")
+ .containsExactly(FILE_NAME_0.asString(), connDet("http://localhost:" + embeddedServer.getPort() + "/cluster/data"));
+
+ assertThat(messages.get(1))
+ .as("check resource event1")
+ .satisfies(msg -> assertThat(msg.getType()).isEqualTo(MessageType.RESOURCE))
+ .extracting(msg -> msg.getBodyObject(LeafResource.class))
+ .extracting("id", "connectionDetail")
+ .containsExactly(FILE_NAME_1.asString(), connDet("http://localhost:" + embeddedServer.getPort() + "/cluster/data"));
+
+ assertThat(messages.get(2))
+ .as("check event2 (error)")
+ .satisfies(msg -> assertThat(msg.getType()).isEqualTo(MessageType.ERROR))
+ .extracting(msg -> msg.getBodyObject(String.class))
+ .isEqualTo("test error");
+
+ assertThat(messages.get(3))
+ .as("check event3 (complete)")
+ .satisfies(msg -> assertThat(msg.getType()).isEqualTo(MessageType.COMPLETE))
+ .extracting(WebSocketMessage::getBody)
+ .isNull();
+
+ }
+
+ private static ConnectionDetail connDet(final String uri) {
+ return new SimpleConnectionDetail().serviceName("data-service");
+ }
+
+}
diff --git a/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/FullTest.java b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/FullTest.java
new file mode 100644
index 00000000..798f6ec5
--- /dev/null
+++ b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/FullTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.contract.java;
+
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.internal.schedulers.IoScheduler;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.FlowAdapters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.ClientManager;
+import uk.gov.gchq.palisade.client.java.QueryItem.ItemType;
+import uk.gov.gchq.palisade.client.java.QueryResponse;
+
+import javax.inject.Inject;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.FILE_NAME_0;
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.FILE_NAME_1;
+
+/**
+ * @since 0.5.0
+ */
+@MicronautTest
+class FullTest {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FullTest.class);
+
+ @Inject
+ EmbeddedServer embeddedServer;
+
+ /**
+ * Register a request with the Palisade Service, fetch resources from the Filtered-Resource Service, and download from the Data Service.
+ * This test runs in a flatter, non-streaming, non-async manner, where the download is performed on the main thread.
+ *
+ * @throws Exception if no resources are returned, or the download fails
+ */
+ @Test
+ void testWithDownloadOutsideStream() throws Exception {
+
+ var port = embeddedServer.getPort();
+
+ var session = ClientManager.openSession(String.format("pal://localhost:%d/cluster?userid=alice", port));
+ var query = session.createQuery("resource_id");
+ var publisher = query
+ .execute()
+ .thenApply(QueryResponse::stream)
+ .get();
+
+ var resources = Flowable.fromPublisher(FlowAdapters.toPublisher(publisher))
+ .filter(m -> m.getType().equals(ItemType.RESOURCE))
+ .collect(Collectors.toList())
+ .timeout(10, TimeUnit.SECONDS)
+ .blockingGet();
+
+ assertThat(resources).as("check resource count").hasSizeGreaterThan(0);
+
+ var expectedCollection = Map.of(
+ FILE_NAME_0.asString(), FILE_NAME_0.createStream(),
+ FILE_NAME_1.asString(), FILE_NAME_1.createStream()
+ );
+
+ for (var resource : resources) {
+ assertThat(resource.asResource().getId())
+ .as("check leaf resource id")
+ .isIn(expectedCollection.keySet());
+
+ var download = session.fetch(resource);
+ assertThat(download).as("check download exists").isNotNull();
+
+ try (var actual = download.getInputStream();
+ var expected = expectedCollection.get(resource.asResource().getId());
+ ) {
+ assertThat(actual).as("check stream download").hasSameContentAs(expected);
+ }
+ }
+
+ }
+
+ /**
+ * Register a request with the Palisade Service, fetch resources from the Filtered-Resource Service, and download from the Data Service.
+ * This test runs in a nested, streaming, async manner, where the download is performed asynchronously on some reactor thread.
+ *
+ * @throws Exception if no resources are returned, or the download fails
+ */
+ @Test
+ void testWithDownloadInsideStream() throws Exception {
+
+ var session = ClientManager.openSession(String.format("pal://localhost:%d/cluster?userid=alice", embeddedServer.getPort()));
+ var query = session.createQuery("resource_id");
+ var publisher = query
+ .execute()
+ .thenApply(QueryResponse::stream)
+ .get();
+
+ var expectedCollection = Map.of(
+ FILE_NAME_0.asString(), FILE_NAME_0.createStream(),
+ FILE_NAME_1.asString(), FILE_NAME_1.createStream()
+ );
+
+ var disposable = Flowable.fromPublisher(FlowAdapters.toPublisher(publisher))
+ .filter(m -> m.getType().equals(ItemType.RESOURCE))
+ .timeout(10, TimeUnit.SECONDS)
+ .subscribeOn(new IoScheduler())
+ .subscribe((var resource) -> {
+ assertThat(resource.asResource().getId())
+ .as("check leaf resource id")
+ .isIn(expectedCollection.keySet());
+
+ var download = session.fetch(resource);
+ assertThat(download).as("check download exists").isNotNull();
+
+ try (var actual = download.getInputStream();
+ var expected = expectedCollection.get(resource.asResource().getId());
+ ) {
+ assertThat(actual).as("check stream download").hasSameContentAs(expected);
+ }
+ });
+
+ disposable.dispose();
+ }
+}
diff --git a/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/ManualTest.java b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/ManualTest.java
new file mode 100644
index 00000000..f60e2a6e
--- /dev/null
+++ b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/ManualTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.contract.java;
+
+import io.reactivex.rxjava3.core.Flowable;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.FlowAdapters;
+
+import uk.gov.gchq.palisade.client.java.QueryResponse;
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultSession;
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @since 0.5.0
+ */
+
+class ManualTest {
+ static class TestConfiguration extends Configuration {
+ // Expose protected constructor
+ TestConfiguration(final Map properties) {
+ super(properties);
+ }
+ }
+
+ @Test
+ @Disabled
+ void testWithDownloadOutsideStream() throws Exception {
+ // Manual configuration for a local 'helm install' with 'kubectl expose deployment xxx's and 'kubectl expose pod xxx's
+ // Minikube users need to also use 'minikube service xxx'
+ var session = new DefaultSession(new TestConfiguration(Map.of(
+ Configuration.USER_ID, "Alice",
+ Configuration.PALISADE_URI, URI.create("http://192.168.49.2:32586/api/registerDataRequest"),
+ Configuration.FILTERED_RESOURCE_URI, URI.create("ws://192.168.49.2:30598/resource/%25t"),
+ Configuration.DATA_SERVICE_MAP, Map.of("data-service", URI.create("http://192.168.49.2:32405"))
+ )));
+
+ var query = session.createQuery("file:/data/local-data-store/", Map.of("purpose", "SALARY"));
+ var publisher = query
+ .execute()
+ .thenApply(QueryResponse::stream)
+ .get();
+
+ var resources = Flowable.fromPublisher(FlowAdapters.toPublisher(publisher))
+ .filter(m -> m.getType().equals(MessageType.RESOURCE))
+ .collect(Collectors.toList())
+ .blockingGet();
+
+ assertThat(resources).as("check resource count").hasSizeGreaterThan(0);
+
+ assertThat(List.of(resources.get(0), resources.get(1)))
+ .extracting(item -> item.asResource().getId())
+ .as("check leaf resource id")
+ .containsExactly("file:/data/local-data-store/employee_file0.avro", "file:/data/local-data-store/employee_file1.avro");
+
+ var download = session.fetch(resources.get(0));
+ assertThat(download).as("check download exists").isNotNull();
+
+ try (var inputStream = download.getInputStream()) {
+ System.out.println(new BufferedReader(new InputStreamReader(inputStream))
+ .lines().collect(Collectors.joining("\n")));
+ }
+ }
+}
diff --git a/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/DataHttpEndpoint.java b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/DataHttpEndpoint.java
new file mode 100644
index 00000000..163997b7
--- /dev/null
+++ b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/DataHttpEndpoint.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.contract.java.servers;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.HttpStatus;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Body;
+import io.micronaut.http.annotation.Consumes;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Post;
+import io.micronaut.http.annotation.Produces;
+import io.micronaut.http.server.types.files.StreamedFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import uk.gov.gchq.palisade.client.java.internal.model.DataRequest;
+import uk.gov.gchq.palisade.client.java.testing.ClientTestData.Name;
+
+/**
+ * A controller containing our test endpoints
+ */
+@Controller("/cluster/data")
+public class DataHttpEndpoint {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DataHttpEndpoint.class);
+
+ /**
+ * Returns an http response containing an inputstream
+ *
+ * @param request The request
+ * @return an http response containing an inputstream
+ */
+ @SuppressWarnings("resource")
+ @Post("/read/chunked")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ public HttpResponse getTest(@Body final DataRequest request) {
+
+ try {
+
+ // set up MDC
+ MDC.put("server", "DT-SVC");
+
+ LOG.debug("RCVD: body: {}", request);
+
+ var octetStream = MediaType.APPLICATION_OCTET_STREAM_TYPE;
+
+ Name nameTuple;
+ try {
+ var leafResourceId = request.getLeafResourceId();
+ nameTuple = Name.from(leafResourceId);
+ } catch (IllegalArgumentException e) {
+ return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ var seed = nameTuple.getSeed();
+ var bytes = nameTuple.getBytes();
+ var name = nameTuple.getName();
+
+ LOG.debug("LOAD: Created stream of {} bytes for {} from seed value {}", bytes, name, seed);
+
+ var is = nameTuple.createStream();
+ var sf = new StreamedFile(is, octetStream);
+
+ LOG.debug("RETN: Stream");
+
+ return HttpResponse
+ .ok(sf)
+ .contentType(octetStream);
+
+ } finally {
+ MDC.remove("server");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/FilteredResourceWsEndpoint.java b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/FilteredResourceWsEndpoint.java
new file mode 100644
index 00000000..742192a4
--- /dev/null
+++ b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/FilteredResourceWsEndpoint.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.contract.java.servers;
+
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.websocket.WebSocketBroadcaster;
+import io.micronaut.websocket.WebSocketSession;
+import io.micronaut.websocket.annotation.OnClose;
+import io.micronaut.websocket.annotation.OnMessage;
+import io.micronaut.websocket.annotation.OnOpen;
+import io.micronaut.websocket.annotation.ServerWebSocket;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.Token;
+import uk.gov.gchq.palisade.client.java.internal.model.WebSocketMessage;
+import uk.gov.gchq.palisade.resource.impl.FileResource;
+import uk.gov.gchq.palisade.resource.impl.SimpleConnectionDetail;
+import uk.gov.gchq.palisade.resource.impl.SystemResource;
+
+import javax.inject.Inject;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static uk.gov.gchq.palisade.client.java.testing.ClientTestData.FILE_NAMES;
+
+/**
+ * Test websocket endpoint
+ *
+ * @since 0.5.0
+ */
+@ServerWebSocket("/cluster/filteredResource/resource/{token}")
+public class FilteredResourceWsEndpoint {
+
+ private static final String TOKEN_KEY = "token";
+
+ /**
+ * Generates test resources
+ *
+ * @since 0.5.0
+ */
+ public static class ResourceGenerator implements Iterable {
+
+ private final List messages;
+ private final String token;
+
+ /**
+ * Creates a new {@code ResourceGenerator} with the provided {@code token} and
+ * {@code port}
+ *
+ * @param token the token
+ * @param port the port
+ */
+ public ResourceGenerator(final String token, final int port) {
+ this.token = token;
+ this.messages = Stream.of(FILE_NAMES.stream()
+ .map(filename -> WebSocketMessage.Builder.create().withType(MessageType.RESOURCE)
+ .withHeader(Token.HEADER, token).noHeaders()
+ .withBody(new FileResource()
+ .id(filename)
+ .serialisedFormat("format")
+ .type("type")
+ .connectionDetail(new SimpleConnectionDetail().serviceName("data-service"))
+ .parent(new SystemResource().id("parent")))),
+ Stream.of(WebSocketMessage.Builder.create()
+ .withType(MessageType.ERROR)
+ .withHeader(Token.HEADER, token).noHeaders()
+ .withBody("test error")))
+ .flatMap(Function.identity())
+ .collect(Collectors.toList());
+
+ }
+
+ @Override
+ public Iterator iterator() {
+ return messages.iterator();
+ }
+
+ }
+
+ @Inject
+ EmbeddedServer embeddedServer;
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FilteredResourceWsEndpoint.class);
+ private Iterator messages;
+
+ /**
+ * Create a new {@code FilteredResourceWsEndpoint} with the provided
+ * {@code broadcaster}
+ *
+ * @param broadcaster the web socket broadcaster
+ */
+ public FilteredResourceWsEndpoint(final WebSocketBroadcaster broadcaster) {
+ // noop
+ }
+
+ /**
+ * Called when the websocket is opened
+ *
+ * @param token The token which is passed in as a query parameter on the HTTP
+ * request
+ * @param session The web socket session
+ */
+ @OnOpen
+ public void onOpen(final String token, final WebSocketSession session) {
+ try {
+ MDC.put("server", "FR-SVC");
+ assert token != null : "Should have the token as part of the path variable";
+ LOGGER.debug("OPEN: Opening websocket for token {}", token);
+ session.put(TOKEN_KEY, token);
+ this.messages = new ResourceGenerator(token, embeddedServer.getPort()).iterator();
+ LOGGER.debug("OPEN: WebSocket opened");
+ } finally {
+ MDC.remove("server");
+ }
+ }
+
+ /**
+ * Called when a new message arrives
+ *
+ * @param inmsg The incoming message
+ * @param session The web socket session
+ */
+ @OnMessage
+ public void onMessage(final WebSocketMessage inmsg, final WebSocketSession session) {
+ try {
+ MDC.put("server", "FR-SVC");
+ LOGGER.debug("RCVD: {}", inmsg);
+ var type = inmsg.getType();
+ if (type.equals(MessageType.CTS)) {
+ if (messages.hasNext()) {
+ send(session, messages.next());
+ } else {
+ sendComplete(session);
+ }
+ } else {
+ LOGGER.warn("Unknown message type: {}", inmsg.getType());
+ }
+ } finally {
+ MDC.remove("server");
+ }
+ }
+
+ /**
+ * Called when the websocket closes
+ *
+ * @param session The websocket session that is to close
+ */
+ @OnClose
+ public void onClose(final WebSocketSession session) {
+ try {
+ MDC.put("server", "FR-SVC");
+ LOGGER.debug("RCVD: Close Request: {}", session.getId());
+ } finally {
+ MDC.remove("server");
+ }
+ }
+
+ private static void sendComplete(final WebSocketSession session) {
+ send(session, WebSocketMessage.Builder.create()
+ .withType(MessageType.COMPLETE)
+ .noHeaders()
+ .noBody());
+ }
+
+ private static void send(final WebSocketSession session, final WebSocketMessage message) {
+ try {
+ MDC.put("server", "FR-SVC");
+ session.sendSync(message);
+ LOGGER.debug("SEND: {}", message);
+ } finally {
+ MDC.remove("server");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/PalisadeHttpEndpoint.java b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/PalisadeHttpEndpoint.java
new file mode 100644
index 00000000..eb6e6fef
--- /dev/null
+++ b/client-java/src/contract-tests/java/uk/gov/gchq/palisade/contract/java/servers/PalisadeHttpEndpoint.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.contract.java.servers;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Body;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Post;
+import io.micronaut.http.annotation.Produces;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeRequest;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeResponse;
+import uk.gov.gchq.palisade.client.java.testing.ClientTestData;
+
+/**
+ * A controller containing our test endpoints
+ */
+@Controller("/cluster/palisade/api")
+public class PalisadeHttpEndpoint {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PalisadeHttpEndpoint.class);
+
+ /**
+ * Returns a test response from the provide test request
+ *
+ * @param request The test request
+ * @return a test response from the provide test request
+ */
+ @Post("/registerDataRequest")
+ @Produces(MediaType.APPLICATION_JSON)
+ public HttpResponse registerDataRequest(@Body final PalisadeRequest request) {
+ try {
+ MDC.put("server", "PL-SVC");
+ LOG.debug("RCVD: {}", request);
+ var palisadeResponse = new PalisadeResponse(ClientTestData.TOKEN);
+ LOG.debug("RETN: {}", request);
+ return HttpResponse
+ .ok(palisadeResponse)
+ .contentType(MediaType.APPLICATION_JSON_TYPE);
+ } finally {
+ MDC.remove("server");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Client.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Client.java
new file mode 100644
index 00000000..08176f6e
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Client.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+/**
+ * The interface that a client class must implement
+ *
+ * @since 0.5.0
+ */
+public interface Client {
+
+ /**
+ * Retrieves whether the client thinks that it can open a session to the given
+ * URL. Typically clients will return true if they understand the
+ * sub-protocol specified in the URL and false if they do not.
+ *
+ * @param url the URL of the server cluster
+ * @return true if this client understands the given URL;
+ * false otherwise
+ * @throws ClientException if a client error occurs or the url is
+ * {@code null}
+ */
+ boolean acceptsURL(String url);
+
+ /**
+ * Attempts to make a palisade connection to the given URL. The client should
+ * return "null" if it realizes it is the wrong kind of client to connect to the
+ * given URL. This will be common, as when the Palisade client manager is asked
+ * to connect to a given URL it passes the URL to each loaded client in turn.
+ *
+ * The client should throw an ClientException if it is the right
+ * client to connect to the given URL but has trouble connecting to the cluster.
+ *
+ * The {@code Properties} argument can be used to pass arbitrary string
+ * tag/value pairs as connection arguments. Normally at least "user" property
+ * should be included in the {@code Map} object.
+ *
+ * Note: If a property is specified as part of the {@code url} and is
+ * also specified in the {@code Properties} object, it is implementation-defined
+ * as to which value will take precedence. For maximum portability, an
+ * application should only specify a property once.
+ *
+ * @param url the URL of the palisade cluster to which to connect
+ * @return a Session object that represents a connection to the URL
+ * @throws ClientException if a cluster error occurs or the url is
+ * {@code null}
+ */
+ Session connect(String url);
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/ClientException.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/ClientException.java
new file mode 100644
index 00000000..bafb73e8
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/ClientException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+/**
+ * The base {@code Throwable} type for Palisade Clients.
+ *
+ * @since 0.5.0
+ */
+public class ClientException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new instance with the provided {@code message}
+ *
+ * @param message a description of the exception
+ * @see RuntimeException#RuntimeException(String)
+ */
+ public ClientException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new instance with the provided {@code message} and {@code cause}
+ *
+ * @param message a description of the exception
+ * @param cause the underlying reason for this {@code ClientException} (which
+ * is saved for later retrieval by the getCause() method); may be
+ * null indicating the cause is non-existent or unknown.
+ * @see RuntimeException#RuntimeException(String, Throwable)
+ */
+ public ClientException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/ClientManager.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/ClientManager.java
new file mode 100644
index 00000000..b86f0477
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/ClientManager.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultClient;
+import uk.gov.gchq.palisade.client.java.util.Checks;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Stream;
+
+/**
+ * The basic service for managing Palisade clients
+ *
+ *
pal://host:port - standard client (which using http for PS and FRS
+ *
pal:dft://host:port - standard client (which using http for PS and
+ * FRS
+ *
pal:alt://host:port - alternative client
+ *
+ *
+ * @since 0.5.0
+ */
+@SuppressWarnings("java:S1774")
+public final class ClientManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClientManager.class);
+
+ /**
+ * The registered clients. A CopyOnWriteArrayList is used here as it implements
+ * the semantics that we are after. We need to guarantee writes, but do
+ * not need to lock on reads. As the clients are usually registered at startup
+ * and read many times this fits the use case and avoids having to synchronise
+ * on reads.
+ */
+ private static final CopyOnWriteArrayList REGISTERED_CLIENTS = new CopyOnWriteArrayList<>();
+ private static final String PALISADE_CLIENTS_PROPERTY = "palisade.clients";
+ private static final Object LOCK_FOR_INIT_CLIENTS = new Object();
+ private static volatile boolean clientsInitialized;
+
+ static {
+ // add the default client to the palisade.clients system property, appending it
+ // if it is already set
+ Optional
+ .ofNullable(System.getProperty(PALISADE_CLIENTS_PROPERTY, ""))
+ .map(prop -> prop.isEmpty() ? "" : (prop + ":"))
+ .map(prop -> prop + DefaultClient.class.getName())
+ .ifPresent(prop -> System.setProperty(PALISADE_CLIENTS_PROPERTY, prop));
+ }
+
+ private ClientManager() { // prevent instantiation
+ }
+
+ /**
+ * Returns a client for the given palisade URL. The ClientManager attempts to
+ * select an appropriate client from the set of registered Clients.
+ *
+ * @param url a palisade URL of the form pal:subname://host:port/context
+ * @return a client for the provided URL
+ */
+ public static Client getClient(final String url) {
+ LOGGER.debug("ClientManager.getClient(\"{}\"", url);
+ ensureClientsInitialized();
+ return REGISTERED_CLIENTS.stream()
+ .filter(c -> c.acceptsURL(url))
+ .findFirst()
+ .orElseThrow(() -> new ClientException("No suitable client found accepting url: " + url));
+ }
+
+ /**
+ * Attempts to establish a session to the given Palisade cluster {@code url}.
+ *
+ * @param uri a palisade URL of the form pal://clusteraddr?additional=params
+ * @return a session for the provided {@code url}
+ * @throws ClientException if a Palisade access error occurs or the URL is
+ * invalid
+ */
+ public static Session openSession(final String uri) {
+ Checks.checkNotNull(uri, "The url cannot be null");
+
+ LOGGER.debug("ClientManager.openSession(\"{}\"", uri);
+
+ ensureClientsInitialized();
+
+ var client = getClient(uri);
+ return client.connect(uri);
+ }
+
+ /**
+ * Registers the given client with the {@code ClientManager}. A newly-loaded
+ * client class should call the method {@code registerClient} to make itself
+ * known to the {@code ClientManager}. If the client is currently registered, no
+ * action is taken.
+ *
+ * @param client the new Palisade Client that is to be registered with the
+ * {@code ClientManager}
+ * @throws NullPointerException if {@code client} is null
+ * @since 0.5.0
+ */
+ public static void registerClient(final Client client) {
+ // Register the client if it has not already been added to our list */
+ if (client != null) {
+ REGISTERED_CLIENTS.addIfAbsent(client);
+ LOGGER.debug("ClientManager.registerClient: client registered: {}", client.getClass().getName());
+ } else {
+ throw new IllegalArgumentException("Cannot register a null client");
+ }
+ }
+
+ /**
+ * Returns a Stream with all of the currently loaded Palisade clients
+ *
+ * @return the stream of Palisade clients
+ */
+ public static Stream getClients() {
+ ensureClientsInitialized();
+ return REGISTERED_CLIENTS.stream();
+ }
+
+ /*
+ * Load the initial Palisade clients by checking the System property
+ * palisade.clients
+ */
+ @SuppressWarnings({"java:S2221", "java:S2658"})
+ private static void ensureClientsInitialized() {
+ if (clientsInitialized) {
+ return;
+ }
+ synchronized (LOCK_FOR_INIT_CLIENTS) {
+ if (!clientsInitialized) { // again, in case something squeezed in.
+ Optional
+ .ofNullable(System.getProperty(PALISADE_CLIENTS_PROPERTY))
+ .filter(clients -> !clients.isEmpty())
+ .ifPresent(clients -> Arrays
+ .stream(clients.split(":"))
+ .filter(client -> !client.isEmpty())
+ .forEach(ClientManager::loadClient));
+ clientsInitialized = true;
+ LOGGER.debug("Palisade ClientManager initialized");
+ }
+ }
+
+ }
+
+ @SuppressWarnings({"java:S2221", "java:S2658"})
+ private static void loadClient(final String client) {
+ try {
+ LOGGER.debug("ClientManager.initialize: loading {}", client);
+ Class.forName(client, true, ClassLoader.getSystemClassLoader());
+ } catch (Exception ex) {
+ LOGGER.debug("ClientManager.initialize: load failed", ex);
+ }
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Download.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Download.java
new file mode 100644
index 00000000..8ee31498
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Download.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+import java.io.InputStream;
+
+/**
+ * Represents a download that has been fetched. No data is transferred until
+ * {@code #getInputStream()} is called.
+ *
+ * @since 0.5.0
+ */
+public interface Download {
+
+ /**
+ * Returns an {@code InputStream} which contains the stream of bytes for the
+ * file to be downloaded
+ *
+ * @return the input stream of bytes
+ */
+ InputStream getInputStream();
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Query.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Query.java
new file mode 100644
index 00000000..68df9e3b
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Query.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Object used for executing a request to the Palisade cluster and returning the
+ * resources that it produces
+ *
+ * @since 0.5.0
+ */
+public interface Query {
+
+ /**
+ * Executes the query and immediately returns a future which contains the object
+ * which will provide access to returned resources
+ *
+ * @return a {@code CompletableFuture} containing the result of the query
+ * execution
+ */
+ CompletableFuture execute();
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/QueryItem.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/QueryItem.java
new file mode 100644
index 00000000..bcc4cd8e
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/QueryItem.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.java;
+
+import uk.gov.gchq.palisade.resource.LeafResource;
+
+/**
+ * A QueryItem represents a single available resource as reported by the Filtered-Resource-Service, or an error.
+ * This may be then downloaded using the {@link Session#fetch(QueryItem)}.
+ *
+ * @since 0.5.0
+ */
+public interface QueryItem {
+ /**
+ * The ItemType is the type of the QueryItem received.
+ * This is a narrowing of the full range of responses from the Filtered-Resource-Service
+ * to only the output types the {@link QueryResponse} flow will emit.
+ */
+ enum ItemType {
+ RESOURCE,
+ ERROR
+ }
+
+ /**
+ * Get the type of this QueryItem, either a leaf resource or an error message
+ *
+ * @return the QueryItem's type
+ */
+ ItemType getType();
+
+ /**
+ * Get the token to pass to the Data-Service if fetching this item
+ *
+ * @return the token from the palisade-service that will be sent to the data-service
+ */
+ String getToken();
+
+ /**
+ * Get this item's content as an error message
+ *
+ * @return the error message if this item's {@link #getType()} was a {@link ItemType#ERROR}, null otherwise
+ */
+ String asError();
+
+ /**
+ * Get this item's content as a leaf resource
+ *
+ * @return the resource if this item's {@link #getType()} was a {@link ItemType#RESOURCE}, null otherwise
+ */
+ LeafResource asResource();
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/QueryResponse.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/QueryResponse.java
new file mode 100644
index 00000000..461e5494
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/QueryResponse.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+import java.util.concurrent.Flow.Publisher;
+
+/**
+ * A QueryResponse represents the response after executing a query to Palisade
+ *
+ * @since 0.5.0
+ */
+public interface QueryResponse {
+
+ /**
+ * Returns a publisher that, once subscribed to, will emit messages from
+ * palisade. The stream will emit messages of either
+ * {@code MessageType#RESOURCE} or {@code MessageType#ERROR}.
+ *
+ * @return a publisher that, once subscribed to, will emit messages from
+ * palisade
+ */
+ Publisher stream();
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Session.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Session.java
new file mode 100644
index 00000000..f3c4aab8
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/Session.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+import java.util.Map;
+
+/**
+ * A session represents a connection to palisade
+ *
+ * @since 0.5.0
+ */
+public interface Session {
+
+ /**
+ * Returns a new query
+ *
+ * @param queryString The query string
+ * @return a new query
+ */
+ default Query createQuery(final String queryString) {
+ return createQuery(queryString, Map.of());
+ }
+
+ /**
+ * Returns a new query
+ *
+ * @param queryString The query string
+ * @param properties The properties for this query
+ * @return a new query
+ */
+ Query createQuery(String queryString, Map properties);
+
+ /**
+ * Returns a new download of the provided resource
+ *
+ * @param queryItem A {@link QueryItem} with type {@link QueryItem.ItemType#RESOURCE}, representing a resource to download
+ * @return a new download of the provided resource
+ */
+ Download fetch(QueryItem queryItem);
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultClient.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultClient.java
new file mode 100644
index 00000000..892d79fc
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultClient.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.Client;
+import uk.gov.gchq.palisade.client.java.ClientManager;
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkNotNull;
+
+/**
+ * This client is the default implementation and responds to the subname of
+ * "dft".
+ *
+ * @since 0.5.0
+ */
+public class DefaultClient implements Client {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class);
+
+ /**
+ * There is only ever a single instance of the client. This is the instance that
+ * is passed to the {@code ClientManager}.
+ */
+ private static final DefaultClient INSTANCE = new DefaultClient();
+
+ private static boolean registered;
+
+ /*
+ * When this class is loaded by a class loader we need to register it with the
+ * {@code Clientmanager}.
+ */
+ static {
+ load();
+ }
+
+ /**
+ * Returns a new instance of {@code DefaultClient}
+ */
+ public DefaultClient() { // noop
+ }
+
+ @Override
+ public boolean acceptsURL(final String url) {
+ checkNotNull(url, "url is null");
+ boolean accepts = url.startsWith("pal://");
+ if (!accepts) {
+ LOGGER.debug("Client {} does not accept url {}", this.getClass().getName(), url);
+ }
+ return accepts;
+ }
+
+ @Override
+ public DefaultSession connect(final String url) {
+ if (!acceptsURL(url)) {
+ return null;
+ }
+
+ // load the default configuration and merge in overrides
+ var configuration = Configuration.create(url);
+
+ return new DefaultSession(configuration);
+ }
+
+ /**
+ * Loads the single instance of this client in to th {@code ClientManager}. This
+ * method is synchronised as there could be multiple class loaders trying to
+ * load the class.
+ *
+ * @return The single instance
+ */
+ @SuppressWarnings("java:S2221")
+ private static synchronized Client load() {
+ try {
+ if (!registered) {
+ registered = true;
+ ClientManager.registerClient(INSTANCE);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Failed to register {}", DefaultClient.class.getName(), e);
+ }
+ return INSTANCE;
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQuery.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQuery.java
new file mode 100644
index 00000000..cf03095f
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQuery.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.Query;
+import uk.gov.gchq.palisade.client.java.QueryResponse;
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static uk.gov.gchq.palisade.client.java.internal.request.PalisadeService.createPalisadeService;
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkNotNull;
+
+/**
+ * The default {@code Query} implementation for subname {@code dft}
+ *
+ * @since 0.5.0
+ */
+public class DefaultQuery implements Query {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultQuery.class);
+
+ private final DefaultSession session;
+ private final String queryString;
+ private final Map context;
+
+ /**
+ * Creates a new query with the provided {@code session}, and query {@code info}
+ *
+ * @param session The open session to the cluster
+ * @param queryString The resource query string
+ * @param context Properties to be forwarded with the query
+ */
+ public DefaultQuery(final DefaultSession session, final String queryString, final Map context) {
+ this.session = checkNotNull(session);
+ this.queryString = checkNotNull(queryString);
+ this.context = new HashMap<>(checkNotNull(context));
+ }
+
+ @Override
+ public CompletableFuture execute() {
+ var palisadeService = createPalisadeService(b -> b
+ .httpClient(session.getHttpClient())
+ .objectMapper(session.getObjectMapper())
+ .uri(session.getConfiguration().get(Configuration.PALISADE_URI)));
+
+ var palisadeRequest = PalisadeRequest.Builder.create()
+ .withUserId(session.getConfiguration().get(Configuration.USER_ID))
+ .withResourceId(queryString)
+ .withContext(context);
+
+ LOGGER.debug("Executing query: {}", palisadeRequest);
+ return palisadeService
+ .submitAsync(palisadeRequest)
+ .thenApply(response -> new DefaultQueryResponse(session, response));
+
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQueryItem.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQueryItem.java
new file mode 100644
index 00000000..07adee2d
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQueryItem.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import uk.gov.gchq.palisade.client.java.QueryItem;
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.Token;
+import uk.gov.gchq.palisade.client.java.internal.model.WebSocketMessage;
+import uk.gov.gchq.palisade.resource.LeafResource;
+
+import java.util.Optional;
+
+/**
+ * Use a {@link WebSocketMessage} as a {@link QueryItem}
+ * (as long as it has the appropriate type).
+ * This matches up {@link MessageType#RESOURCE} with {@link QueryItem.ItemType#RESOURCE}
+ * and {@link MessageType#ERROR} with {@link QueryItem.ItemType#ERROR}.
+ */
+public class DefaultQueryItem implements QueryItem {
+
+ private final WebSocketMessage message;
+
+ /**
+ * Use a {@link WebSocketMessage} as a {@link QueryItem} (as long as it has the appropriate type)
+ *
+ * @param message a WebSocketMessage of type {@link MessageType#RESOURCE} or {@link MessageType#ERROR}
+ * @throws IllegalArgumentException if the WebSocketMessage is of the wrong {@link MessageType}
+ */
+ public DefaultQueryItem(final WebSocketMessage message) {
+ if (message.getType() != MessageType.RESOURCE && message.getType() != MessageType.ERROR) {
+ throw new IllegalArgumentException("Message must have type " + MessageType.RESOURCE + " or " + MessageType.ERROR + ", not " + message.getType());
+ }
+ this.message = message;
+ }
+
+ @Override
+ public ItemType getType() {
+ switch (message.getType()) {
+ case RESOURCE:
+ return ItemType.RESOURCE;
+ case ERROR:
+ return ItemType.ERROR;
+ default:
+ throw new IllegalArgumentException("Message must have type " + MessageType.RESOURCE + " or " + MessageType.ERROR + ", not " + message.getType());
+ }
+ }
+
+ @Override
+ public String getToken() {
+ return message.getHeaders().get(Token.HEADER);
+ }
+
+ @Override
+ public String asError() {
+ return Optional.of(getType())
+ .filter(ItemType.ERROR::equals)
+ .map(isError -> message.getBodyObject(String.class))
+ .orElse(null);
+ }
+
+ @Override
+ public LeafResource asResource() {
+ return Optional.of(getType())
+ .filter(ItemType.RESOURCE::equals)
+ .map(isResource -> message.getBodyObject(LeafResource.class))
+ .orElse(null);
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQueryResponse.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQueryResponse.java
new file mode 100644
index 00000000..b6013a74
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultQueryResponse.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import io.reactivex.rxjava3.core.BackpressureStrategy;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.FlowableEmitter;
+import org.reactivestreams.FlowAdapters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.QueryItem;
+import uk.gov.gchq.palisade.client.java.QueryResponse;
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeResponse;
+import uk.gov.gchq.palisade.client.java.internal.resource.WebSocketClient;
+
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Default implementation for the "dft" subname
+ *
+ * @since 0.5.0
+ */
+public class DefaultQueryResponse implements QueryResponse {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultQueryResponse.class);
+
+ private final DefaultSession session;
+ private final PalisadeResponse palisadeResponse;
+
+ /**
+ * Returns a newly created {@code DefaultQueryResponse} with the provided
+ * {@code session} and {@code palisadeResponse}
+ *
+ * @param session The session providing connection to the cluster
+ * @param palisadeResponse The response from the cluster
+ */
+ public DefaultQueryResponse(final DefaultSession session, final PalisadeResponse palisadeResponse) {
+ this.session = session;
+ this.palisadeResponse = palisadeResponse;
+ }
+
+ public PalisadeResponse getPalisadeResponse() {
+ return palisadeResponse;
+ }
+
+ @Override
+ public Publisher stream() {
+
+ // our flowable must wrap the websocket client
+
+ var flowable = Flowable.create((final FlowableEmitter emitter) -> {
+
+ LOGGER.debug("Creating stream...");
+
+ /*
+ * Must use a new client here for the websocket connection. If we use the one
+ * from the session, the websocket listener hangs.
+ */
+
+ var httpClientBuilder = HttpClient.newBuilder();
+ if (Boolean.FALSE.equals(session.getConfiguration().get(Configuration.HTTP2_ENABLED))) {
+ httpClientBuilder.version(Version.HTTP_1_1);
+ }
+ var httpClient = httpClientBuilder.build();
+
+ var configuration = session.getConfiguration();
+
+ var webSocketClient = WebSocketClient.createResourceClient(b -> b
+ .httpClient(httpClient)
+ .objectMapper(session.getObjectMapper())
+ .token(palisadeResponse.getToken())
+ .uri(configuration.get(Configuration.FILTERED_RESOURCE_URI)));
+
+ webSocketClient.connect();
+
+ LOGGER.debug("Connected to websocket");
+
+ var timeout = session.getConfiguration().get(Configuration.POLL_SECONDS);
+ var loop = true;
+
+ do {
+ var wsm = webSocketClient.poll(timeout, TimeUnit.SECONDS);
+ if (wsm != null) {
+ if (wsm.getType() == MessageType.COMPLETE) {
+ // we're done, so signal complete and set flag to get out
+ emitter.onComplete();
+ loop = false;
+ LOGGER.debug("emitter.complete");
+ } else {
+ emitter.onNext(new DefaultQueryItem(wsm));
+ }
+ }
+ if (emitter.isCancelled()) {
+ // we're cancelled, so set flag to get out
+ loop = false;
+ LOGGER.debug("emitter.cancelled");
+ }
+ } while (loop);
+
+ }, BackpressureStrategy.BUFFER);
+
+ return FlowAdapters.toFlowPublisher(flowable); // return a Java Flow Publisher.
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultSession.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultSession.java
new file mode 100644
index 00000000..d7f88bb0
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultSession.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import uk.gov.gchq.palisade.client.java.Download;
+import uk.gov.gchq.palisade.client.java.QueryItem;
+import uk.gov.gchq.palisade.client.java.Session;
+import uk.gov.gchq.palisade.client.java.internal.download.Downloader;
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
+import java.util.Map;
+
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkNotNull;
+
+/**
+ * A session for the "dft" subname
+ *
+ * @since 0.5.0
+ */
+public class DefaultSession implements Session {
+
+ private final Configuration configuration;
+
+ /*
+ * Once created, an HttpClient instance is immutable, thus automatically
+ * thread-safe, and multiple requests can be sent with it
+ */
+ private final HttpClient httpClient;
+
+ /*
+ * Shared object mapper passed to downstream services
+ */
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Returns a new instance of {@code DefaultSession} with the provided
+ * {@code configuration}
+ *
+ * @param configuration The client configuration
+ */
+ public DefaultSession(final Configuration configuration) {
+
+ this.configuration = configuration;
+
+ var httpClientBuilder = HttpClient.newBuilder();
+ if (Boolean.FALSE.equals(configuration.get(Configuration.HTTP2_ENABLED))) {
+ httpClientBuilder.version(Version.HTTP_1_1);
+ }
+ this.httpClient = httpClientBuilder.build();
+
+ this.objectMapper = new ObjectMapper()
+ .registerModule(new Jdk8Module())
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+ }
+
+ @SuppressWarnings("java:S1774")
+ @Override
+ public DefaultQuery createQuery(final String queryString, final Map properties) {
+ checkNotNull(queryString, "Missing query");
+ return new DefaultQuery(this, queryString, properties != null ? properties : Map.of());
+ }
+
+ @Override
+ public Download fetch(final QueryItem queryItem) {
+ var token = checkNotNull(queryItem.getToken(), "Missing token");
+ var resource = checkNotNull(queryItem.asResource(), "Missing resource");
+ var downloader = Downloader.createDownloader(b -> b
+ .httpClient(getHttpClient())
+ .objectMapper(getObjectMapper())
+ .path(configuration.get(Configuration.DATA_PATH))
+ .serviceNameMap(configuration.get(Configuration.DATA_SERVICE_MAP)));
+ return downloader.fetch(token, resource);
+ }
+
+ /**
+ * Returns the shared {@code HttpClient} for this session
+ *
+ * @return the shared {@code HttpClient} for this session
+ */
+ public HttpClient getHttpClient() {
+ return this.httpClient;
+ }
+
+ /**
+ * Returns the configuration for this session
+ *
+ * @return the configuration for this session
+ */
+ public Configuration getConfiguration() {
+ return this.configuration;
+ }
+
+ /**
+ * Returns the shared object mapper
+ *
+ * @return the shared object mapper
+ */
+ public ObjectMapper getObjectMapper() {
+ return this.objectMapper;
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/DownloadImpl.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/DownloadImpl.java
new file mode 100644
index 00000000..186829ff
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/DownloadImpl.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.download;
+
+import uk.gov.gchq.palisade.client.java.Download;
+
+import java.io.InputStream;
+import java.net.http.HttpResponse;
+
+/**
+ * A download is returned after a request is received from Data Service. This
+ * object contains access to the input stream and extra information such as size
+ * in bytes and the name.
+ *
+ * @since 0.5.0
+ */
+public class DownloadImpl implements Download {
+
+ private final HttpResponse response;
+
+ /**
+ * Create and returns a new {@code DownloadImpl} with the provided
+ * {@code HttpResponse} returned from the data service.
+ *
+ * @param response from the Data Service
+ */
+ public DownloadImpl(final HttpResponse response) {
+ this.response = response;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return response.body();
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/Downloader.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/Downloader.java
new file mode 100644
index 00000000..7477159f
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/Downloader.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.download;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.immutables.value.Value;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.QueryResponse;
+import uk.gov.gchq.palisade.client.java.internal.impl.ConfigurationException;
+import uk.gov.gchq.palisade.client.java.internal.model.DataRequest;
+import uk.gov.gchq.palisade.client.java.util.ImmutableStyle;
+import uk.gov.gchq.palisade.client.java.util.Util;
+import uk.gov.gchq.palisade.resource.LeafResource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkNotNull;
+
+/**
+ * A {@code Downloader} is responsible for initiating requests to a palisade
+ * Data Service and processing its response.
+ *
+ * @since 0.5.0
+ */
+public final class Downloader {
+
+ /**
+ * Provides the setup for the downloader
+ *
+ * @since 0.5.0
+ */
+ @Value.Immutable
+ @ImmutableStyle
+ public interface DownloaderSetup {
+
+ /**
+ * Exposes the generated builder outside this package
+ *
+ * While the generated implementation (and consequently its builder) is not
+ * visible outside of this package. This builder inherits and exposes all public
+ * methods defined on the generated implementation's Builder class.
+ */
+ class Builder extends ImmutableDownloaderSetup.Builder { // empty
+ }
+
+ /**
+ * Returns the {@code HttpClient}
+ *
+ * @return the {@code HttpClient}
+ */
+ HttpClient getHttpClient();
+
+ /**
+ * Returns the object mapper used for (de)serialisation of websocket messages
+ *
+ * @return the object mapper used for (de)serialisation of websocket messages
+ */
+ ObjectMapper getObjectMapper();
+
+ Map getServiceNameMap();
+
+ /**
+ * Returns the path portion of the URL
+ *
+ * @return the path portion of the URL that should be use when making calls to
+ * the Data Service
+ */
+ String getPath();
+
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Downloader.class);
+
+ private static final int HTTP_STATUS_OK = 200;
+ private static final int HTTP_STATUS_NOT_FOUND = 404;
+
+ private final DownloaderSetup setup;
+
+ /**
+ * Returns a new {@code Downloader}
+ *
+ * @param setup used to configure this instance
+ */
+ private Downloader(final DownloaderSetup setup) {
+ this.setup = checkNotNull(setup, "missing setup");
+ }
+
+ /**
+ * Helper method to create a {@link Downloader} builder
+ *
+ * @param func The builder function
+ * @return a newly created {@code RequestId}
+ */
+ @SuppressWarnings("java:S3242") // Unary Operator vs Function
+ public static Downloader createDownloader(final UnaryOperator func) {
+ return new Downloader(func.apply(new DownloaderSetup.Builder()).build());
+ }
+
+ /**
+ * Start the download process
+ *
+ * @param token The token from the {@link QueryResponse}
+ * @param resource The resource to fetch
+ * @return a download result after successful completion
+ * @throws DownloaderException if any error occurs
+ */
+ @SuppressWarnings("java:S2221")
+ public DownloadImpl fetch(final String token, final LeafResource resource) {
+
+ LOGGER.debug("Downloader Started");
+
+ // using the create method here as the url is assumed correct as it is provided
+ // by palisade
+
+ // create the url which is made up of the base url which is provided as part of
+ // the resource returned from the Filtered Resource Service and the endpoint
+ var serviceName = resource.getConnectionDetail().createConnection();
+ var baseUri = setup.getServiceNameMap().getOrDefault(serviceName, URI.create(serviceName));
+ var uri = Util.createUri(baseUri.toString(), getPath());
+
+ try {
+ var requestBody = getObjectMapper()
+ .writeValueAsString(DataRequest.Builder.create()
+ .withToken(token)
+ .withLeafResourceId(resource.getId()));
+
+ HttpResponse httpResponse;
+ httpResponse = sendRequest(requestBody, uri);
+ var statusCode = httpResponse.statusCode();
+
+ if (statusCode != HTTP_STATUS_OK) {
+ String msg;
+ if (statusCode == HTTP_STATUS_NOT_FOUND) {
+ msg = String.format("DataService '%s' not found", uri);
+ } else {
+ msg = String.format("Request to DataService '%s' failed", uri);
+ }
+ throw new DownloaderException(msg, statusCode);
+ }
+
+ return new DownloadImpl(httpResponse);
+
+ } catch (DownloaderException e) {
+ throw e;
+ } catch (IllegalArgumentException e) {
+ throw new ConfigurationException(String.format("DataService connectionDetail '%s' was invalid, it may not be resolved in config", baseUri), e);
+ } catch (Exception e) {
+ throw new DownloaderException("Caught unknown exception: " + e.getMessage(), e);
+ } finally {
+ LOGGER.debug("Downloader Ended");
+ }
+
+ }
+
+ private HttpResponse sendRequest(final String requestBody, final URI uri) {
+
+ LOGGER.debug("Preparing to send request to {}", uri);
+
+ var httpRequest = HttpRequest.newBuilder(uri)
+ .setHeader("User-Agent", "Palisade Java Client")
+ .header("Content-Type", "application/json")
+ .POST(BodyPublishers.ofString(requestBody))
+ .build();
+
+ try {
+ LOGGER.debug("Sending...");
+ var httpResponse = getHttpClient().send(httpRequest, BodyHandlers.ofInputStream());
+ LOGGER.debug("Got http status: {}", httpResponse.statusCode());
+ return httpResponse;
+ } catch (IOException | InterruptedException e1) {
+ Thread.currentThread().interrupt();
+ throw new DownloaderException("Error occurred making request to data service", e1);
+ }
+
+ }
+
+ private HttpClient getHttpClient() {
+ return setup.getHttpClient();
+ }
+
+ private ObjectMapper getObjectMapper() {
+ return setup.getObjectMapper();
+ }
+
+ private String getPath() {
+ return setup.getPath();
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/DownloaderException.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/DownloaderException.java
new file mode 100644
index 00000000..7ecae8c3
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/download/DownloaderException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.download;
+
+import uk.gov.gchq.palisade.client.java.ClientException;
+
+/**
+ * Root class of client exceptions
+ *
+ * @since 0.5.0
+ */
+public class DownloaderException extends ClientException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final int statusCode;
+
+ /**
+ * Creates a new instance with the provided {@code message} and {@code cause}
+ *
+ * @param message The message
+ * @param cause The cause
+ * @see RuntimeException#RuntimeException(String, Throwable)
+ */
+ public DownloaderException(final String message, final Throwable cause) {
+ super(message, cause);
+ this.statusCode = -1;
+ }
+
+ /**
+ * Creates a new instance with the provided {@code message}
+ *
+ * @param message The message
+ * @param statusCode the HTTP status code or -1 if none
+ * @see RuntimeException#RuntimeException(String)
+ */
+ public DownloaderException(final String message, final int statusCode) {
+ super(String.format("[%d] %s", statusCode, message));
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/impl/Configuration.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/impl/Configuration.java
new file mode 100644
index 00000000..7c98e0aa
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/impl/Configuration.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.Generated;
+import uk.gov.gchq.palisade.client.java.util.Util;
+
+import java.net.URI;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * This is the main configuration object for a session. Create a new
+ * configuration by call {@code Configuration#create(URI)}, passing in the
+ * configuration spec.
+ *
+ * The format of the spec is a URI of the form {@code pal://%cluster-addr%?configkey=%value%}, with the
+ * only required key being {@link Configuration#USER_ID} for the userId.
+ *
+ * @since 0.5.0
+ */
+public class Configuration {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class);
+
+ private interface StringProperty {
+ T valueOf(String string);
+ }
+
+ private static final String QUERY_STRING_SEP = "&";
+ private static final String QUERY_STRING_ASSIGN = "=";
+ private static final String TOKEN_PARAM = "%25t"; // % is percent-encoded to %25
+ private static final int KV_PAIR_LENGTH = 2;
+
+ // Keys for configurable properties
+ /**
+ * The userId for connecting to the palisade services, will be used for all requests.
+ *
+ * Required
+ */
+ public static final String USER_ID = "userid";
+
+ /**
+ * Whether to use SSL (https/wss) connection, if supported by the services.
+ *
+ * Optional, default 'false'
+ */
+ public static final String SSL_ENABLED = "ssl";
+
+ /**
+ * Whether to use HTTP/2 connection (instead of HTTP/1.1), if supported by the services.
+ *
+ * Optional, default 'false'
+ */
+ public static final String HTTP2_ENABLED = "http2";
+
+ /**
+ * Polling timeout in seconds waiting for a websocket response.
+ *
+ * Optional, default '3600'
+ */
+ public static final String POLL_SECONDS = "poll";
+
+ // Allowed user-configurable properties and readers for them (from String to T)
+ protected static final Map> WHITELIST_PROPERTIES = Map.of(
+ USER_ID, String::new,
+ SSL_ENABLED, Boolean::valueOf,
+ HTTP2_ENABLED, Boolean::valueOf,
+ POLL_SECONDS, Long::valueOf
+ );
+
+ // Static keys which are not permitted to be configurable
+ /**
+ * Relative path from cluster root to palisade-service request endpoint.
+ *
+ * Static, default '/palisade/api/registerDataRequest'
+ */
+ protected static final String PALISADE_PATH = "palisade.path";
+ /**
+ * Relative path from cluster root to filtered-resource-service response endpoint, including parameterised token {@link Configuration#TOKEN_PARAM}.
+ *
+ * Static, default '/filteredResource/resource/%t'
+ */
+ protected static final String FILTERED_RESOURCE_PATH = "filtered-resource.path";
+ /**
+ * Relative path from data-service URI as returned by a resource's connection-detail to data-service read endpoint.
+ *
+ * Static, default '/read/chunked'
+ */
+ public static final String DATA_PATH = "data.path";
+ /**
+ * Map from service-names to URIs
+ *
+ * Static, default empty map
+ */
+ public static final String DATA_SERVICE_MAP = "data.service-map";
+
+ // Defaults for above static and configurable keys
+ protected static final Map DEFAULT_PROPERTIES = Map.of(
+ // Static
+ PALISADE_PATH, "/palisade/api/registerDataRequest",
+ FILTERED_RESOURCE_PATH, "/filteredResource/resource/" + TOKEN_PARAM,
+ DATA_PATH, "/read/chunked",
+ // Configurable defaults
+ SSL_ENABLED, Boolean.FALSE,
+ HTTP2_ENABLED, Boolean.FALSE,
+ POLL_SECONDS, 3600L
+ );
+
+ // Required and derived keys for connection properties
+ /**
+ * The original URI Spec string passed to the configuration class.
+ *
+ * Required
+ */
+ public static final String SPEC_URI = "spec.uri";
+ /**
+ * Full path to palisade-service request endpoint.
+ *
+ * Derived, example 'http://my.cluster:1234/ingress/palisade/api/registerDataRequest'
+ */
+ public static final String PALISADE_URI = "palisade.uri";
+ /**
+ * Full path to filtered-resource-service response endpoint, before substituting {@link Configuration#TOKEN_PARAM} for the token.
+ *
+ * Derived, example 'ws://my.cluster:1234/ingress/filteredResource/resource/%t'
+ */
+ public static final String FILTERED_RESOURCE_URI = "filtered-resource.uri";
+
+
+ private final Map properties = new TreeMap<>();
+
+ protected Configuration(final Map properties) {
+ this.properties.putAll(DEFAULT_PROPERTIES);
+ // Override defaults with supplied properties
+ this.properties.putAll(properties);
+ }
+
+ /**
+ * Get a single value from the config map using the given key.
+ * Available keys are declared as public static Strings by the {@link Configuration} class.
+ *
+ * @param key the key for a configmap object
+ * @param the expected type of the object
+ * @return the value from the configmap
+ */
+ @SuppressWarnings("unchecked")
+ public T get(final String key) {
+ return (T) Optional.ofNullable(this.properties.get(key))
+ .orElseThrow(() -> new ConfigurationException(String.format("Missing value for key '%s'", key)));
+ }
+
+ /**
+ * Create a config map from a String spec. This must be a URI-compliant string.
+ * It is parsed as {@code pal://cluster.addr:port?additionalKey=value&otherKey=otherValue},
+ * where cluster.addr:port points to the Palisade cluster's Traefik ingress and any other
+ * configuration key/value pairs are passed in as query parameters.
+ *
+ * @param spec a URI-compliant string of the configuration spec
+ * @return a populated config map
+ */
+ public static Configuration create(final String spec) {
+ return create(URI.create(spec));
+ }
+
+ /**
+ * Create a config map from a URI spec.
+ * It is parsed as {@code pal://cluster.addr:port?additionalKey=value&otherKey=otherValue},
+ * where cluster.addr:port points to the Palisade cluster's Traefik ingress and any other
+ * configuration key/value pairs are passed in as query parameters.
+ *
+ * @param spec a URI of the configuration spec
+ * @return a populated config map
+ */
+ public static Configuration create(final URI spec) {
+ // Parse spec
+ var clusterUri = spec.getAuthority() + spec.getPath();
+ var queryParams = Optional.ofNullable(spec.getQuery())
+ .map(Configuration::parseQueryParams)
+ .orElse(Map.of());
+ var config = new Configuration(queryParams);
+
+ // Default scheme config
+ var palisadeScheme = "http";
+ var filteredResourceScheme = "ws";
+ var dataScheme = "http";
+ // Determine SSL config for URI schemes
+ if (config.get(SSL_ENABLED).equals(Boolean.TRUE)) {
+ palisadeScheme = "https";
+ filteredResourceScheme = "wss";
+ dataScheme = "https";
+ }
+
+ // Build service URLs
+ var palisadeUri = Util.createUri(palisadeScheme + "://" + clusterUri, config.get(PALISADE_PATH));
+ var filteredResourceUri = Util.createUri(filteredResourceScheme + "://" + clusterUri, config.get(FILTERED_RESOURCE_PATH));
+ URI defaultDataUri = Util.createUri(dataScheme + "://" + clusterUri, "/data");
+ // Update config
+ config.properties.putAll(Map.of(
+ SPEC_URI, spec,
+ PALISADE_URI, palisadeUri,
+ FILTERED_RESOURCE_URI, filteredResourceUri,
+ DATA_SERVICE_MAP, Map.of("data-service", defaultDataUri)
+ ));
+
+ // Log properties map for debugging
+ LOGGER.debug("Using spec {} built config:", spec);
+ config.properties.forEach((key, value) -> LOGGER.debug("{} = {}", key, value));
+
+ // Return config
+ return config;
+ }
+
+ /**
+ * Parse a URI QueryParam string into a Map.
+ * e.g. {@code ?key=value&otherKey=otherValue -> (key: value, otherKey: otherValue)}
+ *
+ * @param queryParamString a {@link URI#getQuery()} string
+ * @return a map built from the query string
+ */
+ private static Map parseQueryParams(final String queryParamString) {
+ // Split ?queryParam string over separator - i.e. uri?propA&propB -> [propA, propB]
+ return Arrays.stream(queryParamString.split(QUERY_STRING_SEP))
+ // Assert each property pair is of the format propKey=propVal
+ .map(kvPair -> Optional.of(kvPair.split(QUERY_STRING_ASSIGN))
+ .filter(kvArray -> kvArray.length == KV_PAIR_LENGTH)
+ .map(kvArray -> new SimpleEntry<>(kvArray[0], kvArray[1]))
+ .orElseThrow(() -> new ConfigurationException(String.format("Key-value pair '%s' was not of the format 'key%svalue'", kvPair, QUERY_STRING_ASSIGN))))
+ // Assert the key is in the whitelist and the value can be converted
+ .map(kvEntry -> Optional.ofNullable(WHITELIST_PROPERTIES.get(kvEntry.getKey()))
+ // Can the value be converted?
+ .map((StringProperty> valueClass) -> {
+ try {
+ return (Object) valueClass.valueOf(kvEntry.getValue());
+ } catch (RuntimeException ex) {
+ throw new ConfigurationException(
+ String.format("Failed to convert value '%s' using converter '%s' from key '%s'", kvEntry.getValue(), valueClass, kvEntry.getKey()),
+ ex);
+ }
+ })
+ .map(value -> new SimpleEntry<>(kvEntry.getKey(), value))
+ // Did we fail to find a value converter (illegal key)
+ .orElseThrow(() -> new ConfigurationException(String.format("Illegal configuration key '%s'", kvEntry.getKey()))))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
+ @Override
+ @Generated
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Configuration)) {
+ return false;
+ }
+ final Configuration that = (Configuration) o;
+ return Objects.equals(properties, that.properties);
+ }
+
+ @Override
+ @Generated
+ public int hashCode() {
+ return Objects.hash(properties);
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return properties.toString();
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/impl/ConfigurationException.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/impl/ConfigurationException.java
new file mode 100644
index 00000000..fd027938
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/impl/ConfigurationException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.impl;
+
+import uk.gov.gchq.palisade.client.java.ClientException;
+
+/**
+ * An exception which is thrown when a configuration error occurs
+ *
+ * @since 0.5.0
+ */
+public class ConfigurationException extends ClientException {
+
+ private static final long serialVersionUID = -1663859509675049796L;
+
+ /**
+ * Returns a newly created instance with the provided message
+ *
+ * @param message The message text explaining this exception
+ */
+ public ConfigurationException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Returns a newly created instance with the provided message and cause
+ *
+ * @param message The message text explaining this exception
+ * @param cause The underlying cause
+ */
+ public ConfigurationException(final String message, final Throwable cause) {
+ super(message, cause);
+
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/DataRequest.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/DataRequest.java
new file mode 100644
index 00000000..c3ba7ec6
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/DataRequest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * The {@link DataRequest} is the input for the data-service where the resource is read.
+ * This message is created by the response from the filtered-resource-service to the client.
+ * It is then routed via the resource's connectionDetail to the appropriate instance of a data-service.
+ */
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+public final class DataRequest {
+
+ private final String token; // Unique identifier for the client's request
+ private final String leafResourceId; // Leaf Resource ID that that is being asked to access
+
+ @JsonCreator
+ private DataRequest(
+ final @JsonProperty("token") String token,
+ final @JsonProperty("leafResourceId") String leafResourceId) {
+
+ this.token = Optional.ofNullable(token)
+ .orElseThrow(() -> new IllegalArgumentException("token cannot be null"));
+ this.leafResourceId = Optional.ofNullable(leafResourceId)
+ .orElseThrow(() -> new IllegalArgumentException("leafResourceId cannot be null"));
+ }
+
+ @Generated
+ public String getToken() {
+ return token;
+ }
+
+ @Generated
+ public String getLeafResourceId() {
+ return leafResourceId;
+ }
+
+ /**
+ * Builder class for the creation of instances of the DataRequest.
+ * This is a variant of the Fluent Builder which will use Java Objects or JsonNodes equivalents for the components in the build.
+ */
+ public static class Builder {
+ /**
+ * Starter method for the Builder class.
+ * This method is called to start the process of creating the DataRequest class.
+ *
+ * @return interface {@link IToken} for the next step in the build.
+ */
+ public static IToken create() {
+ return token -> leafResourceId ->
+ new DataRequest(token, leafResourceId);
+ }
+
+ /**
+ * Adds the token to the message
+ */
+ public interface IToken {
+ /**
+ * Adds the token to the request
+ *
+ * @param token the client's unique token
+ * @return interface {@link ILeafResourceId} for the next step in the build.
+ */
+ ILeafResourceId withToken(String token);
+ }
+
+ /**
+ * Adds the leaf resource id to the message
+ */
+ public interface ILeafResourceId {
+ /**
+ * Adds the leaf resource id to the request
+ *
+ * @param leafResourceId resource ID for the request.
+ * @return the completed DataRequest object
+ */
+ DataRequest withLeafResourceId(String leafResourceId);
+ }
+
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", DataRequest.class.getSimpleName() + "[", "]")
+ .add("token='" + token + "'")
+ .add("leafResourceId='" + leafResourceId + "'")
+ .toString();
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/MessageType.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/MessageType.java
new file mode 100644
index 00000000..c7a3d414
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/MessageType.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.model;
+
+/**
+ * Type of message (and thus expected headers/body content) sent by either the client or the server.
+ *
+ * The client is expected to only send:
+ *
+ *
{@link MessageType#PING} - is the server alive? reply with a {@link MessageType#PONG}
+ *
{@link MessageType#CTS} - clear to send next {@link MessageType#RESOURCE}, {@link MessageType#ERROR} or {@link MessageType#COMPLETE}
+ *
+ * The server is expected to only send:
+ *
+ *
{@link MessageType#PONG} - the server is alive
+ *
{@link MessageType#RESOURCE} - the next available resource for the client
+ *
{@link MessageType#ERROR} - an error occurred while processing the client's request
+ *
{@link MessageType#COMPLETE} - there is nothing more to return to the client
+ *
+ */
+public enum MessageType {
+
+ // Client
+ PING, // -> PONG
+ CTS, // -> RESOURCE|ERROR|COMPLETE
+
+ // Server
+ PONG,
+ RESOURCE,
+ ERROR,
+ COMPLETE
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/PalisadeRequest.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/PalisadeRequest.java
new file mode 100644
index 00000000..9a88b138
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/PalisadeRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import uk.gov.gchq.palisade.Context;
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Represents the original data that has been sent from the client to Palisade Service for a request to access data.
+ */
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+public final class PalisadeRequest {
+
+ private final String userId; //Unique identifier for the user.
+ private final String resourceId; //Resource that that is being asked to access.
+
+ // Ignore class type on context object
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, defaultImpl = Context.class)
+ private final Map context;
+
+ @JsonCreator
+ private PalisadeRequest(
+ final @JsonProperty("userId") String userId,
+ final @JsonProperty("resourceId") String resourceId,
+ final @JsonProperty("context") Map context) {
+
+ this.userId = Optional.ofNullable(userId).orElseThrow(() -> new IllegalArgumentException("User ID cannot be null"));
+ this.resourceId = Optional.ofNullable(resourceId).orElseThrow(() -> new IllegalArgumentException("Resource ID cannot be null"));
+ this.context = Optional.ofNullable(context).orElseThrow(() -> new IllegalArgumentException("Context cannot be null"));
+ }
+
+ @Generated
+ public String getUserId() {
+ return userId;
+ }
+
+ @Generated
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ @Generated
+ public Map getContext() {
+ return context;
+ }
+
+ /**
+ * Builder class for the creation of the PalisadeRequest. This is a variant of the Fluent Builder
+ * which will use String or optionally JsonNodes for the components in the build.
+ */
+ public static class Builder {
+
+ /**
+ * Starter method for the Builder class. This method is called to start the process of creating the
+ * PalisadeRequest class.
+ *
+ * @return interface {@link IUserId} for the next step in the build.
+ */
+ public static IUserId create() {
+ return userId -> resourceId -> context ->
+ new PalisadeRequest(userId, resourceId, context);
+ }
+
+ /**
+ * Adds the user ID information to the message.
+ */
+ public interface IUserId {
+ /**
+ * Adds the user's ID.
+ *
+ * @param userId user ID for the request.
+ * @return interface {@link IResourceId} for the next step in the build.
+ */
+ IResourceId withUserId(String userId);
+ }
+
+ /**
+ * Adds the resource ID information to the message.
+ */
+ public interface IResourceId {
+ /**
+ * Adds the resource ID.
+ *
+ * @param resourceId resource ID for the request.
+ * @return interface {@link IContext} for the next step in the build.
+ */
+ IContext withResourceId(String resourceId);
+ }
+
+ /**
+ * Adds the user context information to the message.
+ */
+ public interface IContext {
+ /**
+ * Adds the user context information.
+ *
+ * @param context information about this request.
+ * @return class {@link PalisadeRequest} this builder is set-up to create.
+ */
+ PalisadeRequest withContext(Map context);
+ }
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", PalisadeRequest.class.getSimpleName() + "[", "]")
+ .add("userId='" + userId + "'")
+ .add("resourceId='" + resourceId + "'")
+ .add("context=" + context)
+ .toString();
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/PalisadeResponse.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/PalisadeResponse.java
new file mode 100644
index 00000000..af549fdf
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/PalisadeResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Response message that is returned to the client. The message contains information that will identify this request
+ * for access to the data and be used in a subsequent request to see the resources.
+ */
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+public final class PalisadeResponse {
+
+ private final String token; //unique identifier for the request.
+
+ /**
+ * Instantiates a new Response.
+ *
+ * @param token the token
+ */
+ @JsonCreator
+ public PalisadeResponse(final @JsonProperty("token") String token) {
+ this.token = Optional.ofNullable(token).orElseThrow(() -> new IllegalArgumentException("token cannot be null"));
+ }
+
+ /**
+ * Gets token.
+ *
+ * @return the token
+ */
+ @Generated
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", PalisadeResponse.class.getSimpleName() + "[", "]")
+ .add("token='" + token + "'")
+ .toString();
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/Token.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/Token.java
new file mode 100644
index 00000000..d8390001
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/Token.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.java.internal.model;
+
+/**
+ * Simply stores the expected header key for Tokens
+ * Since the content of tokens are strings, there is no need for further implementation
+ * If desired, this could extend eg. UUID if more meaningful Token processing was desired
+ */
+public final class Token {
+ public static final String HEADER = "x-request-token";
+
+ private Token() {
+ // Tokens are just strings, no need to actually have a class for them
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/WebSocketMessage.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/WebSocketMessage.java
new file mode 100644
index 00000000..fac4d711
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/model/WebSocketMessage.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.java.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Response message that is returned to the client from the filtered-resource-service web-socket.
+ * The message contains information that will identify this request for access to the data and be
+ * used in a subsequent request to the data-service to see the resources.
+ */
+public final class WebSocketMessage {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final MessageType type;
+ private final Map headers;
+ private final String body;
+
+ @JsonCreator
+ private WebSocketMessage(
+ final @JsonProperty("type") MessageType type,
+ final @JsonProperty("headers") Map headers,
+ final @JsonProperty("body") String body) {
+ this.type = type;
+ this.headers = headers;
+ this.body = body;
+ }
+
+ /**
+ * getType returns the type of Websocket message
+ *
+ * @return the type of websocket message
+ */
+ @Generated
+ public MessageType getType() {
+ return type;
+ }
+
+ /**
+ * Gets headers of the websocket message.
+ *
+ * @return the headers of the websocket message
+ */
+ @Generated
+ public Map getHeaders() {
+ return Optional.ofNullable(headers)
+ .orElse(Collections.emptyMap());
+ }
+
+ /**
+ * Gets the body of the websocket message
+ *
+ * @return the body of the websocket message
+ */
+ @Generated
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * Gets the body as an object
+ *
+ * @param the type of Websocket Message
+ * @param clazz the clazz used in deserializing
+ * @return the body object
+ */
+ @JsonIgnore
+ public T getBodyObject(final Class clazz) {
+ try {
+ return MAPPER.readValue(body, clazz);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to deserialize message body as class " + clazz.getName(), e);
+ }
+ }
+
+ /**
+ * Builder class for the creation of instances of the WebSocketMessage.
+ * This is a variant of the Fluent Builder which will use Java Objects or JsonNodes equivalents for the components in the build.
+ */
+ public static class Builder {
+
+ /**
+ * Starter method for the Builder class. This method is called to start the process of creating the
+ * WebSocketMessage class.
+ *
+ * @return public interface {@link IType} for the next step in the build.
+ */
+ public static IType create() {
+ return type -> headers -> body -> new WebSocketMessage(type, headers, body);
+ }
+
+ /**
+ * Adds the type information to the object.
+ */
+ public interface IType {
+ /**
+ * Adds the Type of WebSocketMessage
+ *
+ * @param type of WebSocketMessage
+ * @return interface {@link IHeaders} for the next step in the build.
+ */
+ IHeaders withType(MessageType type);
+ }
+
+ /**
+ * Adds the header information to the object.
+ */
+ public interface IHeaders {
+ /**
+ * Adds the headers to the WebSocketMessage
+ *
+ * @param headers for the WebSocketMessage
+ * @return interface {@link IBody} for the next step in the build.
+ */
+ IBody withHeaders(Map headers);
+
+ /**
+ * Default headers for the Websocket message
+ *
+ * @param key the key, most often the token.HEADER
+ * @param value the value, most often the token
+ * @return interface {@link IBody} for the next step in the build.
+ */
+ default IHeaders withHeader(String key, String value) {
+ return (Map partial) -> {
+ Map headers = new HashMap<>(partial);
+ headers.put(key, value);
+ return withHeaders(headers);
+ };
+ }
+
+ /**
+ * A Default noHeaders interface that adds an emptyMap of headers to the WebSocketMessage
+ *
+ * @return interface {@link IBody} for the next step in the build.
+ */
+ default IBody noHeaders() {
+ return withHeaders(Collections.emptyMap());
+ }
+ }
+
+ /**
+ * Adds the body to the object.
+ */
+ public interface IBody {
+ /**
+ * Adds a serialisedBody to the WebSocketMessage
+ *
+ * @param serialisedBody to add to the WebSocketMessage
+ * @return class {@link WebSocketMessage} for the completed class from the builder.
+ */
+ WebSocketMessage withSerialisedBody(String serialisedBody);
+
+ /**
+ * Adds an object body to the WebSocketMessage which is then seralised before adding to the class
+ *
+ * @param body the body
+ * @return class {@link WebSocketMessage} for the completed class from the builder.
+ */
+ default WebSocketMessage withBody(Object body) {
+ try {
+ return withSerialisedBody(MAPPER.writeValueAsString(body));
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to serialize message body", e);
+ }
+ }
+
+ /**
+ * An interface used to add a null body to the WebSocketMessage
+ *
+ * @return class {@link WebSocketMessage} for the completed class from the builder.
+ */
+ default WebSocketMessage noBody() {
+ return withSerialisedBody(null);
+ }
+ }
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", WebSocketMessage.class.getSimpleName() + "[", "]")
+ .add("type=" + type)
+ .add("headers=" + headers)
+ .add("body='" + body + "'")
+ .toString();
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/request/PalisadeService.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/request/PalisadeService.java
new file mode 100644
index 00000000..2727d782
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/request/PalisadeService.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.request;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.immutables.value.Value;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.ClientException;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeRequest;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeResponse;
+import uk.gov.gchq.palisade.client.java.util.ImmutableStyle;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.IntPredicate;
+import java.util.function.UnaryOperator;
+
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkNotNull;
+
+/**
+ * This class represents the Palisade Service and handles the communication
+ * between the client and the server. A single instance of this class can be
+ * used as it is thread safe.
+ *
+ * @since 0.5.0
+ */
+public final class PalisadeService {
+
+ /**
+ * Provides the setup for the downloader
+ *
+ * @since 0.5.0
+ */
+ @Value.Immutable
+ @ImmutableStyle
+ public interface PalisadeServiceSetup {
+
+ /**
+ * Exposes the generated builder outside this package
+ *
+ * While the generated implementation (and consequently its builder) is not
+ * visible outside of this package. This builder inherits and exposes all public
+ * methods defined on the generated implementation's Builder class.
+ */
+ class Builder extends ImmutablePalisadeServiceSetup.Builder { // empty
+ }
+
+ /**
+ * Returns the {@code HttpClient}
+ *
+ * @return the {@code HttpClient}
+ */
+ HttpClient getHttpClient();
+
+ /**
+ * Returns the object mapper used for (de)serialisation of websocket messages
+ *
+ * @return the object mapper used for (de)serialisation of websocket messages
+ */
+ ObjectMapper getObjectMapper();
+
+ /**
+ * Returns the full URI of the palisade service endpoint to call
+ *
+ * @return the full URI of the palisade service endpoint to call
+ */
+ URI getUri();
+
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PalisadeService.class);
+ private static final IntPredicate IS_HTTP_OK = sts -> sts == 200 || sts == 202;
+
+ private final PalisadeServiceSetup setup;
+
+ /**
+ * Creates a new Palisade service
+ *
+ * @param setup The setup
+ */
+ private PalisadeService(final PalisadeServiceSetup setup) {
+ this.setup = checkNotNull(setup);
+ }
+
+ /**
+ * Helper method to create a {@link PalisadeService} using a builder function
+ *
+ * @param func The builder function
+ * @return a newly created {@code RequestId}
+ */
+ @SuppressWarnings("java:S3242") // Unary Operator vs Function
+ public static PalisadeService createPalisadeService(final UnaryOperator func) {
+ return new PalisadeService(func.apply(new PalisadeServiceSetup.Builder()).build());
+ }
+
+ /**
+ * Returns a response from Palisade for the given request
+ *
+ * @param palisadeRequest which will be passed to Palisade
+ * @return the response from Palisade
+ */
+ public PalisadeResponse submit(final PalisadeRequest palisadeRequest) {
+ return submitAsync(palisadeRequest).join();
+ }
+
+ /**
+ * Returns a completable future which will provide the response from palisade
+ * for the given request
+ *
+ * @param palisadeRequest which will be passed to Palisade
+ * @return a completable future which will provide the response from Palisade
+ */
+ public CompletableFuture submitAsync(final PalisadeRequest palisadeRequest) {
+
+ checkNotNull(palisadeRequest);
+
+ var uri = getUri();
+ var jsonBody = toJson(palisadeRequest);
+ var bodyPublisher = BodyPublishers.ofString(jsonBody);
+
+ LOGGER.debug("SEND: To: [{}], Body: [{}]", uri, palisadeRequest);
+
+ var httpClient = getHttpClient();
+
+ var httpRequest = HttpRequest.newBuilder(uri)
+ .setHeader("Content-Type", "application/json")
+ .POST(bodyPublisher)
+ .build();
+
+ return httpClient
+ .sendAsync(httpRequest, BodyHandlers.ofString())
+ .thenApply(PalisadeService::checkStatusOK)
+ .thenApply(HttpResponse::body)
+ .thenApply(this::toResponse)
+ .thenApply((final PalisadeResponse pr) -> {
+ LOGGER.debug("RCVD: {}", pr);
+ return pr;
+ });
+
+ }
+
+ /**
+ * Check status of provided {@code HttpResponse} is OK.
+ *
+ * @param The type of response body
+ * @param response the HTTP response
+ * @return the provided response
+ */
+ public static HttpResponse checkStatusOK(final HttpResponse response) {
+ int status = response.statusCode();
+ if (!IS_HTTP_OK.test(status)) {
+ var body = response.body();
+ String msg;
+ if (body != null) {
+ msg = String.format("Request to Palisade Service failed (%s) with body:%n%s", status, body);
+ } else {
+ msg = String.format("Request to Palisade Service failed (%s) with no body", status);
+ }
+ throw new ClientException(msg);
+ }
+ return response;
+ }
+
+ // placed in a method to be use fluently as a method reference
+ private String toJson(final Object object) {
+ try {
+ return objectMapper().writeValueAsString(object);
+ } catch (JsonProcessingException cause) {
+ throw new ClientException("Failed to serialise request: " + object.toString(), cause);
+ }
+ }
+
+ // placed in a method to be use fluently as a method reference
+ private PalisadeResponse toResponse(final String string) {
+ try {
+ return objectMapper().readValue(string, PalisadeResponse.class);
+ } catch (JsonProcessingException cause) {
+ throw new ClientException("Failed to deserialise request: " + string, cause);
+ }
+ }
+
+ private PalisadeServiceSetup getSetup() {
+ return this.setup;
+ }
+
+ private ObjectMapper objectMapper() {
+ return getSetup().getObjectMapper();
+ }
+
+ private HttpClient getHttpClient() {
+ return getSetup().getHttpClient();
+ }
+
+ private URI getUri() {
+ return getSetup().getUri();
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/resource/WebSocketClient.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/resource/WebSocketClient.java
new file mode 100644
index 00000000..1bbc701d
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/resource/WebSocketClient.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.resource;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.immutables.value.Value;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.WebSocketMessage;
+import uk.gov.gchq.palisade.client.java.util.Checks;
+import uk.gov.gchq.palisade.client.java.util.ImmutableStyle;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.WebSocket;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.UnaryOperator;
+
+import static uk.gov.gchq.palisade.client.java.internal.resource.WebSocketListener.createResourceClientListener;
+
+/**
+ * An instance of this class manages the communications to the Filtered Resource
+ * Service via WebSockets
+ *
+ * @since 0.5.0
+ */
+public class WebSocketClient {
+
+ /**
+ * Provides service and configuration for a resource client
+ *
+ * @since 0.5.0
+ */
+ @Value.Immutable
+ @ImmutableStyle
+ public interface ResourceClientSetup {
+
+ /**
+ * Exposes the generated builder outside this package
+ *
+ * While the generated implementation (and consequently its builder) is not
+ * visible outside of this package. This builder inherits and exposes all public
+ * methods defined on the generated implementation's Builder class.
+ */
+ class Builder extends ImmutableResourceClientSetup.Builder { // empty
+ }
+
+ /**
+ * Returns the token
+ *
+ * @return the token
+ */
+ String getToken();
+
+ /**
+ * Returns the base web socket uri
+ *
+ * @return the base web socket uri
+ */
+ URI getUri();
+
+ /**
+ * Returns the object mapper used for (de)serialisation of websocket messages
+ *
+ * @return the object mapper used for (de)serialisation of websocket messages
+ */
+ ObjectMapper getObjectMapper();
+
+ /**
+ * Returns the HTTP client that should be used by the {@code WebSocketClient}
+ *
+ * @return the HTTP client that should be used by the {@code WebSocketClient}
+ */
+ HttpClient getHttpClient();
+
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketClient.class);
+
+ private final ResourceClientSetup setup;
+ private final BlockingQueue next = new LinkedBlockingQueue<>(1);
+
+ private WebSocket webSocket;
+
+
+ /**
+ * A {@code ResourceClient} manages the passing of messages to/from a websocket
+ * server
+ *
+ * @param setup The setup instance
+ */
+ public WebSocketClient(final ResourceClientSetup setup) {
+ this.setup = Checks.checkNotNull(setup);
+ }
+
+ /**
+ * Helper method to create a {@link WebSocketClient} using a builder function
+ *
+ * @param func The builder function
+ * @return a newly created {@code RequestId}
+ */
+ @SuppressWarnings("java:S3242") // Unary Operator vs Function
+ public static WebSocketClient createResourceClient(final UnaryOperator func) {
+ return new WebSocketClient(func.apply(new ResourceClientSetup.Builder()).build());
+ }
+
+ /**
+ * Retrieves and removes the next message, waiting up to the specified wait time
+ * if necessary for a message to become available.
+ *
+ * @param timeout how long to wait before giving up, in units of {@code unit}
+ * @param unit a {@code TimeUnit} determining how to interpret the
+ * {@code timeout} parameter
+ * @return the the next message, or {@code null} if the specified waiting time
+ * elapses before a message is available
+ */
+ public WebSocketMessage poll(final long timeout, final TimeUnit unit) {
+ try {
+ return next.poll(timeout, unit);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Thread interrupted while taking next message from queue");
+ }
+ }
+
+ /**
+ * Connect to the server and start communications
+ *
+ * @return this for fluent usage
+ */
+ public WebSocketClient connect() {
+
+ // as soon as a client subscribes, the connection to the filtered resource
+ // service should be instantiated and messages should start to be emitted
+
+ // note that %t has been encoded as %25t in the URI
+
+ var replacedUri = URI.create(getUri().toString().replace("%25t", getToken()));
+
+ LOGGER.debug("Connecting to websocket at: {}", getUri());
+
+ this.webSocket = getHttpClient()
+ .newWebSocketBuilder()
+ .buildAsync(replacedUri, createResourceClientListener(b -> b
+ .eventsHandler(this::put)
+ .objectMapper(getObjectMapper())
+ .token(getToken())))
+ .join();
+
+ LOGGER.debug("WebSocket created to handle token: {}", getToken());
+
+ return this;
+
+ }
+
+ private void put(final WebSocketMessage msg) {
+ LOGGER.trace("Emitted : {}", msg);
+ try {
+ next.put(msg); // block if the last message has not been taken
+ if (msg.getType() == MessageType.COMPLETE) {
+ close();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Thread interrupted while putting next message onto queue");
+ }
+ }
+
+ private void close() {
+ if (webSocket != null) {
+ this.webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "").join();
+ LOGGER.debug("--> CLOSE request sent");
+ this.webSocket = null;
+ }
+ }
+
+ private ObjectMapper getObjectMapper() {
+ return setup.getObjectMapper();
+ }
+
+ private String getToken() {
+ return setup.getToken();
+ }
+
+ private URI getUri() {
+ return setup.getUri();
+ }
+
+ private HttpClient getHttpClient() {
+ return setup.getHttpClient();
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/resource/WebSocketListener.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/resource/WebSocketListener.java
new file mode 100644
index 00000000..ad8a20ab
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/internal/resource/WebSocketListener.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.resource;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.immutables.value.Value;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.Token;
+import uk.gov.gchq.palisade.client.java.internal.model.WebSocketMessage;
+import uk.gov.gchq.palisade.client.java.util.Checks;
+import uk.gov.gchq.palisade.client.java.util.ImmutableStyle;
+
+import java.io.IOException;
+import java.net.http.WebSocket;
+import java.net.http.WebSocket.Listener;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
+import java.util.function.UnaryOperator;
+
+/**
+ * Listens on the in-bound socket connection
+ *
+ * @since 0.5.0
+ */
+public class WebSocketListener implements Listener {
+
+ /**
+ * An object that provides the setup for a {@code ResourceClientListener}
+ *
+ * @since 0.5.0
+ */
+ @Value.Immutable
+ @ImmutableStyle
+ public interface ResourceClientListenerSetup {
+
+ /**
+ * Exposes the generated builder outside this package
+ *
+ * While the generated implementation (and consequently its builder) is not
+ * visible outside of this package. This builder inherits and exposes all public
+ * methods defined on the generated implementation's Builder class.
+ */
+ class Builder extends ImmutableResourceClientListenerSetup.Builder { // empty
+ }
+
+ /**
+ * Returns the token
+ *
+ * @return the token
+ */
+ String getToken();
+
+ /**
+ * Returns the object mapper
+ *
+ * @return the event bus
+ */
+ ObjectMapper getObjectMapper();
+
+ /**
+ * Returns the consumer that will handle web socket events emitted from this
+ * listener
+ *
+ * @return the consumer that will handle web socket events emitted from this
+ * listener
+ */
+ Consumer getEventsHandler();
+
+ }
+
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketListener.class);
+
+ private final ObjectMapper objectMapper;
+ private final Consumer handler;
+ private final Map buffer = new HashMap<>();
+ private final String token;
+
+ /**
+ * A {@code ResourceClient} manages the passing of messages to/from a websocket
+ * server
+ *
+ * @param setup The setup for this listener
+ */
+ public WebSocketListener(final ResourceClientListenerSetup setup) {
+ Checks.checkNotNull(setup);
+ this.token = setup.getToken();
+ this.handler = setup.getEventsHandler();
+ this.objectMapper = setup.getObjectMapper();
+ }
+
+ /**
+ * Helper method to create a {@link WebSocketListener} using a builder function
+ *
+ * @param func The builder function
+ * @return a newly created {@code RequestId}
+ */
+ @SuppressWarnings("java:S3242") // Unary Operator vs Function
+ public static WebSocketListener createResourceClientListener(final UnaryOperator func) {
+ return new WebSocketListener(func.apply(new ResourceClientListenerSetup.Builder()).build());
+ }
+
+ @Override
+ public void onError(final WebSocket ws, final Throwable error) {
+ LOGGER.error("An error occurred while processing the websocket stream:", error);
+ }
+
+ @Override
+ public void onOpen(final WebSocket ws) {
+ Listener.super.onOpen(ws);
+ LOGGER.debug("OPEN: WebSocket Listener has been opened for requests.");
+ send(ws, WebSocketMessage.Builder.create().withType(MessageType.CTS));
+ }
+
+ @Override
+ public CompletionStage> onText(final WebSocket ws, final CharSequence data, final boolean last) {
+ ws.request(1);
+
+ // Buffer CharSequence data in case last==false and the message was split across multiple TCP frames
+ buffer.putIfAbsent(ws, new StringBuilder());
+ buffer.get(ws).append(data);
+
+ if (last) {
+ // Remove buffer once we find the last CharSequence for a message
+ String text = buffer.get(ws).toString();
+ buffer.remove(ws);
+
+ WebSocketMessage wsMsg;
+ try {
+ wsMsg = objectMapper.readValue(text, WebSocketMessage.class);
+ } catch (JsonProcessingException e) {
+ onError(ws, e);
+ return null;
+ }
+
+ LOGGER.debug("RCVD: {}", wsMsg);
+
+ switch (wsMsg.getType()) {
+ case RESOURCE:
+ case ERROR:
+ LOGGER.debug("EMIT: {}", wsMsg);
+ handler.accept(wsMsg);
+ send(ws, WebSocketMessage.Builder.create().withType(MessageType.CTS));
+ break;
+ case COMPLETE:
+ LOGGER.debug("COMPLETE: {}", wsMsg);
+ handler.accept(wsMsg);
+ break;
+ default:
+ LOGGER.warn("Ignoring unsupported '{}' message type", wsMsg.getType());
+ break;
+ }
+ } else {
+ LOGGER.debug("PART: {}", data.length());
+ }
+
+ return CompletableFuture.completedFuture(null);
+
+ }
+
+ @SuppressWarnings("java:S4276")
+ private void send(final WebSocket ws, final WebSocketMessage.Builder.IHeaders messageBuilder) {
+ var message = messageBuilder
+ .withHeader(Token.HEADER, token).noHeaders()
+ .noBody();
+ try {
+ var text = objectMapper.writeValueAsString(message);
+ ws.sendText(text, true);
+ LOGGER.debug("SEND: {}", message);
+ } catch (IOException e) {
+ // we should add this fail to a result object
+ LOGGER.warn("Failed to send message: {}", message, e);
+ }
+ }
+
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/Checks.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/Checks.java
new file mode 100644
index 00000000..9d8f9c9d
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/Checks.java
@@ -0,0 +1,547 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.util;
+
+import java.util.IllegalFormatException;
+
+/**
+ * Checks just like Guava preconditions
+ *
+ * @since 0.5.0
+ */
+public interface Checks {
+
+ /**
+ * Ensures that the provided parameter argument is not null.
+ *
+ * @param The type of argument
+ * @param argument the instance test
+ * @return the argument
+ * @throws IllegalArgumentException if {@code argument} is null
+ */
+ static T checkNotNull(final T argument) {
+ if (argument == null) {
+ throw new IllegalArgumentException("Null argument");
+ }
+ return argument;
+ }
+
+ /**
+ * Ensures that the provided parameter argument is not null.
+ *
+ * @param The type of argument
+ * @param argument the instance test
+ * @param errorMessage the exception message to use if the check fails; will be
+ * converted to a string using
+ * {@link String#valueOf(Object)}
+ * @return the argument
+ * @throws IllegalArgumentException if {@code argument} is null
+ */
+ static T checkNotNull(final T argument, final Object errorMessage) {
+ if (argument == null) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ return argument;
+ }
+
+ /**
+ * Ensures that the provided parameter argument is not null.
+ *
+ * @param The type of argument
+ * @param argument the instance test
+ * @param template a template for the exception message should the check fail.
+ * The message is formed by passing the template and argument to
+ * {@code String.format}. The difference is that if an
+ * formatting error occurs, then the unmodified template will be
+ * returned.
+ * @param args the arguments to be substituted into the message template.
+ * Arguments are converted to strings using
+ * {@link String#valueOf(Object)}.
+ * @return the argument
+ * @throws IllegalArgumentException if {@code argument} is null
+ */
+ static T checkNotNull(final T argument, final String template, final Object... args) {
+ if (argument == null) {
+ throw new IllegalArgumentException(format(template, args));
+ }
+ return argument;
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ static void checkArgument(final boolean expression) {
+ if (!expression) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ * @param expression a boolean expression
+ * @param errorMessage the exception message to use if the check fails; will be
+ * converted to a string using
+ * {@link String#valueOf(Object)}
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ static void checkArgument(final boolean expression, final Object errorMessage) {
+ if (!expression) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ * @param expression a boolean expression
+ * @param template a template for the exception message should the check fail.
+ * The message is formed by passing the template and argument
+ * to {@code String.format}. The difference is that if an
+ * formatting error occurs, then the unmodified template will
+ * be returned.
+ * @param args the arguments to be substituted into the message template.
+ * Arguments are converted to strings using
+ * {@link String#valueOf(Object)}.
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ static void checkArgument(
+ final boolean expression,
+ final String template,
+ final Object... args) {
+ if (!expression) {
+ throw new IllegalArgumentException(format(template, args));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(final boolean b, final String template, final char p1) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(final boolean b, final String template, final int p1) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(final boolean b, final String template, final long p1) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final Object p1) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final char p1, final char p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final char p1, final int p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final char p1, final long p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final char p1, final Object p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final int p1, final char p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final int p1, final int p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final int p1, final long p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final int p1, final Object p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final long p1, final char p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final long p1, final int p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final long p1, final long p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final long p1, final Object p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final Object p1, final char p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final Object p1, final int p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final Object p1, final long p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b, final String template, final Object p1, final Object p2) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @param p3 the error template parameter 3
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b,
+ final String template,
+ final Object p1,
+ final Object p2,
+ final Object p3) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2, p3));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the
+ * calling method.
+ *
+ *
+ * @param b the expression
+ * @param template the error template
+ * @param p1 the error template parameter 1
+ * @param p2 the error template parameter 2
+ * @param p3 the error template parameter 3
+ * @param p4 the error template parameter 4
+ * @see #checkArgument(boolean, String, Object...) for details.
+ */
+ static void checkArgument(
+ final boolean b,
+ final String template,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4) {
+ if (!b) {
+ throw new IllegalArgumentException(format(template, p1, p2, p3, p4));
+ }
+ }
+
+ @SuppressWarnings({"java:S1176", "java:S1166"})
+ private static String format(final String template, final Object... objects) {
+ try {
+ return String.format(template, objects);
+ } catch (IllegalFormatException ife) {
+ // we catch this here as we do not want to throw an error, we'll just return the
+ // template
+ return template;
+ }
+ }
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/ImmutableStyle.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/ImmutableStyle.java
new file mode 100644
index 00000000..1c209bca
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/ImmutableStyle.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.util;
+
+import org.immutables.value.Value;
+import org.immutables.value.Value.Style.ImplementationVisibility;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A style annotation that can be used on interfaces annotated via the Immutable
+ * library to alter how the creation process is performed.
+ *
+ * The overshadowImplementation = true style attribute makes sure that build()
+ * will be declared to return abstract value type Person, not the implementation
+ * ImmutablePerson, following metaphor: implementation type will be
+ * "overshadowed" by abstract value type.
+ *
+ * Essentially, the generated class becomes implementation detail without much
+ * boilerplate which is needed to fully hide implementation behind user-written
+ * code.
+ *
+ * @since 0.5.0
+ */
+@Target({ElementType.PACKAGE, ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS) // Make it class retention for incremental compilation
+@Value.Style(
+ visibility = ImplementationVisibility.PACKAGE,
+ overshadowImplementation = true,
+ depluralize = true,
+ defaults = @Value.Immutable(copy = false)
+)
+public @interface ImmutableStyle { // empty
+}
diff --git a/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/Util.java b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/Util.java
new file mode 100644
index 00000000..a5492e80
--- /dev/null
+++ b/client-java/src/main/java/uk/gov/gchq/palisade/client/java/util/Util.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.util;
+
+import java.net.URI;
+
+/**
+ * Utility functions
+ *
+ * @since 0.5.0
+ */
+public final class Util {
+
+ /**
+ * URI separator
+ */
+ public static final String URI_SEP = "/";
+
+ private Util() { // should not be instantiated
+ }
+
+ /**
+ * Returns a new string with leading and trailing slashes removed
+ *
+ * @param path The path
+ * @return a new string with leading and trailing slashes removed
+ */
+ public static String trimSlashes(final String path) {
+ String result = path.trim();
+ if (result.startsWith("/")) {
+ result = result.substring(1);
+ }
+ if (result.endsWith("/")) {
+ result = result.substring(0, result.length() - 1);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a uri from the provide base path and endpoint(s)
+ *
+ * @param baseUri the base uri
+ * @param endpoint the endpoint
+ * @param endpoints further endpoints
+ * @return a uri from the provide base path and endpoint(s)
+ */
+ public static URI createUri(final String baseUri, final String endpoint, final String... endpoints) {
+ return URI.create(createUrl(baseUri, endpoint, endpoints));
+ }
+
+ /**
+ * Returns a uri from the provide base path and endpoint(s)
+ *
+ * @param baseUri the base uri
+ * @param endpoint the endpoint
+ * @param endpoints further endpoints
+ * @return a uri from the provide base path and endpoint(s)
+ */
+ public static String createUrl(final String baseUri, final String endpoint, final String... endpoints) {
+
+ var uri = new StringBuilder(trimSlashes(baseUri))
+ .append(URI_SEP)
+ .append(trimSlashes(endpoint));
+
+ for (String string : endpoints) {
+ uri.append(URI_SEP).append(trimSlashes(string));
+ }
+
+ return uri.toString();
+ }
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/ClientManagerTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/ClientManagerTest.java
new file mode 100644
index 00000000..bd96e2ad
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/ClientManagerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java;
+
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultClient;
+import uk.gov.gchq.palisade.client.java.internal.dft.DefaultSession;
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+
+import java.net.URI;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static uk.gov.gchq.palisade.client.java.ClientManager.getClient;
+import static uk.gov.gchq.palisade.client.java.ClientManager.openSession;
+
+class ClientManagerTest {
+
+ @Test
+ void testGetClient() {
+ assertThat(getClient("pal://alice@localhost:1234/cluster"))
+ .as("Client is of correct type")
+ .isInstanceOf(DefaultClient.class);
+ }
+
+ @Test
+ void testOpenSessionUrlNoProperties() {
+ var serviceUrl = "pal://localhost:1234/cluster?userid=alice";
+ var dftSession = (DefaultSession) openSession(serviceUrl);
+ var configuration = dftSession.getConfiguration();
+
+ assertThat(configuration.get(Configuration.PALISADE_URI))
+ .as("check generated Palisade URI")
+ .isEqualTo(URI.create("http://localhost:1234/cluster/palisade/api/registerDataRequest"));
+ assertThat(configuration.get(Configuration.FILTERED_RESOURCE_URI))
+ .as("check generated FilteredResource URI")
+ .isEqualTo(URI.create("ws://localhost:1234/cluster/filteredResource/resource/%25t"));
+
+ }
+
+ @Test
+ void testOpenSessionWithUrlAndProperties() {
+ var serviceSpec = "pal://localhost/cluster?userid=alice";
+
+ var dftSession = (DefaultSession) openSession(serviceSpec);
+ var configuration = dftSession.getConfiguration();
+
+ assertThat(configuration.get(Configuration.PALISADE_URI))
+ .as("Generated Palisade URI")
+ .isEqualTo(URI.create("http://localhost/cluster/palisade/api/registerDataRequest"));
+ assertThat(configuration.get(Configuration.FILTERED_RESOURCE_URI))
+ .as("Generated FilteredResource URI")
+ .isEqualTo(URI.create("ws://localhost/cluster/filteredResource/resource/%25t"));
+
+ }
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultClientTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultClientTest.java
new file mode 100644
index 00000000..a0572541
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultClientTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DefaultClientTest {
+
+ @Test
+ void testAcceptsURL() {
+
+ var client = new DefaultClient();
+
+ var url = "pal://localhost";
+ assertThat(client.acceptsURL(url))
+ .as("check %s accepts URL \"%s\"", DefaultClient.class.getSimpleName(), url)
+ .isTrue();
+
+ url = "jdbc://localhost";
+ assertThat(client.acceptsURL(url))
+ .as("check %s does not accept URL \"%s\"", DefaultClient.class.getSimpleName(), url)
+ .isFalse();
+ }
+
+ @Test
+ void testConnect() {
+ var client = new DefaultClient();
+ var session = client.connect("pal://localhost?userid=alice");
+ var expectedClass = DefaultSession.class;
+ assertThat(session)
+ .as("check session type")
+ .isInstanceOf(expectedClass);
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultSessionTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultSessionTest.java
new file mode 100644
index 00000000..60208e40
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/dft/DefaultSessionTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.dft;
+
+import org.junit.jupiter.api.Test;
+
+import uk.gov.gchq.palisade.client.java.internal.impl.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DefaultSessionTest {
+
+ @Test
+ void testCreateQuery() {
+
+ var conf = Configuration.create("pal://localhost/cluster?userid=alice");
+ var session = new DefaultSession(conf);
+ var query = session.createQuery("resource_id");
+ var expectedClass = DefaultQuery.class;
+
+ assertThat(query)
+ .as("check actual type")
+ .isInstanceOf(expectedClass);
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DataRequestSerialisationTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DataRequestSerialisationTest.java
new file mode 100644
index 00000000..687cc9b0
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DataRequestSerialisationTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.download;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import uk.gov.gchq.palisade.client.java.internal.model.DataRequest;
+import uk.gov.gchq.palisade.client.java.testing.AbstractSerialisationTest;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class DataRequestSerialisationTest extends AbstractSerialisationTest {
+
+ /**
+ * Returns a stream of arguments, each having two elements. The first element is
+ * the actual object to be tested, with the second element representing it's
+ * serialised form.
+ *
+ * @return a stream of arguments, each having two elements (test object and JSON
+ * string)
+ */
+ static Stream instances() {
+ return Stream.of(arguments(DataRequest.Builder.create()
+ .withToken("test-request-token")
+ .withLeafResourceId("leaf-resource-id"),
+ "{\"token\":\"test-request-token\",\"leafResourceId\":\"leaf-resource-id\"}"));
+ }
+
+ /**
+ * Test the provided instance
+ *
+ * @param expectedInstance The expected instance
+ * @param expectedJson The expected JSON of the provided instance
+ * @throws Exception if an error occurs
+ * @see AbstractSerialisationTest#testInstance(Object, String)
+ */
+ @ParameterizedTest
+ @MethodSource("instances")
+ void testFullSerialisation(final Object expectedInstance, final String expectedJson) throws Exception {
+ testInstance(expectedInstance, expectedJson);
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DownloadImplTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DownloadImplTest.java
new file mode 100644
index 00000000..e2783302
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DownloadImplTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.download;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpResponse;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.lenient;
+
+@ExtendWith(MockitoExtension.class)
+class DownloadImplTest {
+
+ private static final String FILENAME = "cool.html";
+ private static final String CONTENT_DISPOSITION = String.format("attachment; filename=\"%s\"", FILENAME);
+ private static final InputStream BODY = new ByteArrayInputStream(new byte[]{'a', 'b', 'c'});
+
+ private DownloadImpl download;
+
+ @SuppressWarnings("resource") // suppress potential resource leak warning
+ @BeforeEach
+ void setUp(
+ @Mock final HttpResponse response,
+ @Mock final HttpHeaders headers) throws Exception {
+
+ lenient().when(response.headers()).thenReturn(headers);
+ lenient().when(response.body()).thenReturn(BODY);
+ lenient().when(headers.firstValue("Content-Disposition")).thenReturn(Optional.of(CONTENT_DISPOSITION));
+
+ this.download = new DownloadImpl(response);
+
+ }
+
+ @Test
+ void testGetInputStream() throws Exception {
+ try (var is = download.getInputStream()) {
+ assertThat(is)
+ .as("check input stream")
+ .isEqualTo(BODY);
+ }
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DownloaderExceptionTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DownloaderExceptionTest.java
new file mode 100644
index 00000000..54a60529
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/download/DownloaderExceptionTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.download;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DownloaderExceptionTest {
+
+ /**
+ * Test method for {@link DownloaderException#getStatusCode()}.
+ */
+ @Test
+ void testGetStatusCode() {
+ var expectedCode = 400;
+ var exception = new DownloaderException("oops", expectedCode);
+ assertThat(exception.getStatusCode())
+ .as("check %s's status code", DownloaderException.class.getSimpleName())
+ .isEqualTo(400);
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/impl/ConfigurationTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/impl/ConfigurationTest.java
new file mode 100644
index 00000000..d1a7665c
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/impl/ConfigurationTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.impl;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+class ConfigurationTest {
+
+ private static Configuration configuration;
+
+ @BeforeAll
+ static void setupAll() {
+ configuration = Configuration.create("pal://eve@localhost:8081/cluster?userid=alice");
+ }
+
+ @Test
+ void testGetPalisadeUri() {
+ assertThat(configuration.get(Configuration.PALISADE_URI))
+ .as("check generated Palisade Service URI")
+ .isEqualTo(URI.create("http://eve@localhost:8081/cluster/palisade/api/registerDataRequest"));
+ }
+
+ @Test
+ void testFilteredResourceUri() {
+ assertThat(configuration.get(Configuration.FILTERED_RESOURCE_URI))
+ .as("check generated Filtered Resource Service URI")
+ .isEqualTo(URI.create("ws://eve@localhost:8081/cluster/filteredResource/resource/%25t"));
+ }
+
+ @Test
+ void testDataPath() {
+ assertThat(configuration.get(Configuration.DATA_PATH))
+ .as("check Data Service path")
+ .isEqualTo("/read/chunked");
+ }
+
+ @Test
+ void testUserNone() {
+ var incompleteConfig = Configuration.create("pal://localhost:8081/cluster");
+ assertThatExceptionOfType(ConfigurationException.class)
+ .as("check no user is configured")
+ .isThrownBy(() -> incompleteConfig.get(Configuration.USER_ID));
+ }
+
+ @Test
+ void testUserId() {
+ assertThat(configuration.get(Configuration.USER_ID))
+ .as("check user from query param")
+ .isEqualTo("alice");
+ }
+
+ @Test
+ void testInvalidServiceUrl() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .as("check Configuration validates service URL")
+ .isThrownBy(() -> Configuration.create("\\"));
+ }
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/request/RequestSerialisationTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/request/RequestSerialisationTest.java
new file mode 100644
index 00000000..23e6084b
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/request/RequestSerialisationTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.request;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeRequest;
+import uk.gov.gchq.palisade.client.java.internal.model.PalisadeResponse;
+import uk.gov.gchq.palisade.client.java.testing.AbstractSerialisationTest;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class RequestSerialisationTest extends AbstractSerialisationTest {
+
+ /**
+ * Returns a stream of arguments, each having two elements. The first element is
+ * the actual object to be tested, with the second element representing it's
+ * serialised form.
+ *
+ * @return a stream of arguments, each having two elements (test object and JSON
+ * string)
+ */
+ static Stream instances() {
+ return Stream.of(
+ arguments(
+ new PalisadeResponse("blah"),
+ "{\"token\":\"blah\"}"),
+ arguments(
+ PalisadeRequest.Builder.create()
+ .withUserId("userId")
+ .withResourceId("resourceId")
+ .withContext(Map.of("key", "value")),
+ "{\"resourceId\":\"resourceId\",\"userId\":\"userId\",\"context\":{\"key\":\"value\"}}"));
+ }
+
+ /**
+ * Test the provided instance
+ *
+ * @param expectedInstance The expected instance
+ * @param expectedJson The expected JSON of the provided instance
+ * @throws Exception if an error occurs
+ * @see AbstractSerialisationTest#testInstance(Object, String)
+ */
+ @ParameterizedTest
+ @MethodSource("instances")
+ void testSerialisation(final Object expectedInstance, final String expectedJson) throws Exception {
+ testInstance(expectedInstance, expectedJson);
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/resource/ResourceSerialisationTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/resource/ResourceSerialisationTest.java
new file mode 100644
index 00000000..0f248dfd
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/internal/resource/ResourceSerialisationTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.internal.resource;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import uk.gov.gchq.palisade.client.java.internal.model.MessageType;
+import uk.gov.gchq.palisade.client.java.internal.model.WebSocketMessage;
+import uk.gov.gchq.palisade.client.java.testing.AbstractSerialisationTest;
+import uk.gov.gchq.palisade.resource.impl.FileResource;
+import uk.gov.gchq.palisade.resource.impl.SimpleConnectionDetail;
+import uk.gov.gchq.palisade.resource.impl.SystemResource;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class ResourceSerialisationTest extends AbstractSerialisationTest {
+
+ /**
+ * Returns a stream of arguments, each having two elements. The first element is
+ * the actual object to be tested, with the second element representing it's
+ * serialised form.
+ *
+ * @return a stream of arguments, each having two elements (test object and JSON
+ * string)
+ */
+ static Stream instances() {
+ return Stream.of(
+ arguments(
+ WebSocketMessage.Builder.create()
+ .withType(MessageType.RESOURCE)
+ .withHeaders(Map.of("key", "value"))
+ .withSerialisedBody("body"),
+ "{\"type\":\"RESOURCE\",\"headers\":{\"key\":\"value\"},\"body\":\"body\"}"),
+ arguments(
+ new FileResource()
+ .id("leaf-resource-id")
+ .connectionDetail(new SimpleConnectionDetail().serviceName("serviceName"))
+ .type("type")
+ .serialisedFormat("format")
+ .parent(new SystemResource().id("parent")),
+ "{\"id\":\"leaf-resource-id\",\"serialisedFormat\":\"format\",\"type\":\"type\",\"connectionDetail\":{\"serviceName\":\"serviceName\"},\"attributes\":null,\"parent\":{\"id\":\"parent/\"}}"));
+ }
+
+ /**
+ * Test the provided instance
+ *
+ * @param expectedInstance The expected instance
+ * @param expectedJson The expected JSON of the provided instance
+ * @throws Exception if an error occurs
+ * @see AbstractSerialisationTest#testInstance(Object, String)
+ */
+ @ParameterizedTest
+ @MethodSource("instances")
+ void testSerialisation(final Object expectedInstance, final String expectedJson) throws Exception {
+ testInstance(expectedInstance, expectedJson);
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/testing/AbstractSerialisationTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/testing/AbstractSerialisationTest.java
new file mode 100644
index 00000000..ca153db5
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/testing/AbstractSerialisationTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.testing;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Abstract class providing a serialisation test for a class
+ */
+@SuppressWarnings("java:S2187") // no tests in this class by design
+public class AbstractSerialisationTest {
+
+ private static ObjectMapper mapper;
+
+ @BeforeAll
+ static void setupAll() {
+ mapper = new ObjectMapper().registerModules(new Jdk8Module());
+ }
+
+ @AfterAll
+ static void afterAll() {
+ mapper = null;
+ }
+
+ /**
+ * Tests the serialisation and deserialisation of each object provided by the
+ * method source
+ *
+ * The tests asserted by this method:
+ *
+ *
Equals - Test equality using the each instances equals
+ * method
+ *
Recursive Equals - Tests equality by testing field by field in
+ * the instance
+ *
JSON Attributes - uses JSONAssert to test whether the serialise
+ * instance matches the provided JSON string
+ *
+ *
+ * @param expectedInstance The expected instance
+ * @param expectedJson The expected JSON of the provided instance
+ * @throws Exception if an error occurs
+ */
+ protected void testInstance(final Object expectedInstance, final String expectedJson) throws Exception {
+ var objectMapper = getObjectMapper();
+
+ var valueType = expectedInstance.getClass();
+ var actualJson = objectMapper.writeValueAsString(expectedInstance);
+ var actualInstance = objectMapper.readValue(actualJson, valueType);
+
+ assertThat(actualInstance)
+ .as("check equality using recursive equals()",
+ expectedInstance.getClass().getSimpleName())
+ .usingRecursiveComparison()
+ .isEqualTo(expectedInstance);
+ }
+
+ /**
+ * Returns the object mapper. Override if needed.
+ *
+ * @return the object mapper
+ */
+ protected ObjectMapper getObjectMapper() {
+ return mapper;
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/testing/ClientTestData.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/testing/ClientTestData.java
new file mode 100644
index 00000000..cbf3caad
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/testing/ClientTestData.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.testing;
+
+import org.immutables.value.Value;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Base64;
+import java.util.List;
+import java.util.Random;
+
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkArgument;
+import static uk.gov.gchq.palisade.client.java.util.Checks.checkNotNull;
+
+public abstract class ClientTestData {
+
+ /**
+ * This class represents a "name" of a resource. Of course it's not really a
+ * name, but a scheme on how to generate a resource for testing. The name of the
+ * resource follows this format:
+ *
{@code __}
. To create a name
+ * from a string, simply use {@code #from(String)}. Once the object is created
+ * an InputStream can then be retrieved which will provide the correct random
+ * content.
+ */
+ @Value.Immutable
+ @Value.Style(allParameters = true, typeImmutable = "*Tuple", defaults = @Value.Immutable(builder = false))
+ public interface Name {
+
+ String getName();
+
+ long getSeed();
+
+ int getBytes();
+
+ default String asString() {
+ return getName() + "_" + getSeed() + "_" + getBytes();
+ }
+
+ static Name from(final String nameString) {
+ checkNotNull(nameString, "name string cannot be null");
+ var split = nameString.split("_");
+ checkArgument(split.length == 3, "Should conform to __. e.g. alice-eve_0_1024");
+ var name = split[0];
+ long seed = -1;
+ try {
+ seed = Long.parseLong(split[1]);
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Expected seed to be a long value, but was: " + split[1]);
+ }
+ int bytes = -1;
+ try {
+ bytes = Integer.parseInt(split[2]);
+ checkArgument(seed >= 0, "#bytes must be >0");
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Expected bytes to be an int value > 0, but was: " + split[2]);
+ }
+ return NameTuple.of(name, seed, bytes);
+ }
+
+ /**
+ * Returns an input stream containing bytes generated from a {@code Random}
+ * initialised with the provided seed.
+ *
+ * @return an input stream containing {@code bytes} generated from a
+ * {@code Random} initialised with the provided {@code seed}
+ */
+ default InputStream createStream() {
+ var bytea = new byte[getBytes()];
+ new Random(getSeed()).nextBytes(bytea);
+ return new ByteArrayInputStream(Base64.getEncoder().encode(bytea));
+ }
+
+ }
+
+ public static final String FILE_PREFIX = "test-data_";
+ public static final Name FILE_NAME_0 = NameTuple.of("test-data", 0, 1024);
+ public static final Name FILE_NAME_1 = NameTuple.of("test-data", 1, 1024);
+ public static final List FILE_NAMES = List.of(FILE_NAME_0.asString(), FILE_NAME_1.asString());
+ public static final String TOKEN = "abcd-1";
+
+ private ClientTestData() {
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/util/ChecksTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/util/ChecksTest.java
new file mode 100644
index 00000000..49ce1405
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/util/ChecksTest.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+
+class ChecksTest {
+
+ private static final Class CLASS = IllegalArgumentException.class;
+
+ @Test
+ void testCheckArgumentT() {
+ assertThatExceptionOfType(CLASS).isThrownBy(() -> Checks.checkNotNull(null));
+ }
+
+ @Test
+ void testCheckArgumentTObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkNotNull(null, "oops"))
+ .withMessage("oops");
+ assertThat(Checks.checkNotNull("boo", "oops")).isEqualTo("boo");
+ }
+
+ @Test
+ void testCheckArgumentTStringObjectArray() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkNotNull(null, "oops %s %s", "one", "two"))
+ .withMessage("oops one two");
+ assertThat(Checks.checkNotNull("boo", "oops %s %s", "one", "two")).isEqualTo("boo");
+ }
+
+ @Test
+ void testCheckArgumentBoolean() {
+ assertThatExceptionOfType(CLASS).isThrownBy(() -> Checks.checkArgument(false));
+ assertThatNoException().isThrownBy(() -> Checks.checkArgument(true));
+ }
+
+ @Test
+ void testCheckArgumentBooleanObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops"))
+ .withMessage("oops");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops"));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectArray() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", "one", "two"))
+ .withMessage("oops one two");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", "one", "two"));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringChar() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s", 'x'))
+ .withMessage("oops x");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s", 'x'));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringInt() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s", 2))
+ .withMessage("oops 2");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s", 2));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringLong() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s", 2L))
+ .withMessage("oops 2");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s", 2L));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s", new BigDecimal("12")))
+ .withMessage("oops 12");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s", new BigDecimal("12")));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringCharChar() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 'x', 'y'))
+ .withMessage("oops x y");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 'x', 'y'));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringCharInt() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 'x', 2))
+ .withMessage("oops x 2");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 'x', 2));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringCharLong() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 'x', 2L))
+ .withMessage("oops x 2");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 'x', 2L));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringCharObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 'x', new BigDecimal("12")))
+ .withMessage("oops x 12");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 'x', new BigDecimal("12")));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringIntChar() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2, 'x'))
+ .withMessage("oops 2 x");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2, 'x'));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringIntInt() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2, 4))
+ .withMessage("oops 2 4");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2, 4));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringIntLong() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2, 4L))
+ .withMessage("oops 2 4");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2, 4L));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringIntObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2, new BigDecimal("12")))
+ .withMessage("oops 2 12");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2, new BigDecimal("12")));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringLongChar() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2L, 'x'))
+ .withMessage("oops 2 x");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2L, 'x'));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringLongInt() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2L, 4))
+ .withMessage("oops 2 4");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2L, 4));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringLongLong() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2L, 4L))
+ .withMessage("oops 2 4");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2L, 4L));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringLongObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", 2L, new BigDecimal("12")))
+ .withMessage("oops 2 12");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", 2L, new BigDecimal("12")));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectChar() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", new BigDecimal("12"), 'x'))
+ .withMessage("oops 12 x");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", new BigDecimal("12"), 'x'));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectInt() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", new BigDecimal("12"), 2))
+ .withMessage("oops 12 2");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", new BigDecimal("12"), 2));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectLong() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", new BigDecimal("12"), 2L))
+ .withMessage("oops 12 2");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", new BigDecimal("12"), 2L));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s", new BigDecimal("12"), new BigDecimal("24")))
+ .withMessage("oops 12 24");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s", new BigDecimal("12"), new BigDecimal("24")));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectObjectObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(() -> Checks.checkArgument(false, "oops %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36")))
+ .withMessage("oops 12 24 36");
+ assertThatNoException()
+ .isThrownBy(() -> Checks.checkArgument(true, "oops %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36")));
+ }
+
+ @Test
+ void testCheckArgumentBooleanStringObjectObjectObjectObject() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(
+ () -> Checks.checkArgument(false, "oops %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48")))
+ .withMessage("oops 12 24 36 48");
+ assertThatNoException()
+ .isThrownBy(
+ () -> Checks.checkArgument(true, "oops %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48")));
+ }
+
+ @Test
+ void testCheckArgumentExpressionTemplateObjectArray() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(
+ () -> Checks.checkArgument((false), "oops %s %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48"), new BigDecimal("52")))
+ .withMessage("oops 12 24 36 48 52");
+ assertThatNoException()
+ .isThrownBy(
+ () -> Checks.checkArgument((true), "oops %s %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48"), new BigDecimal("52")));
+
+ }
+
+ @Test
+ void testCheckArgumentObjectTemplateObjectArray() {
+ assertThatExceptionOfType(CLASS)
+ .isThrownBy(
+ () -> Checks.checkArgument(false, "oops %s %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48"), new BigDecimal("52")))
+ .withMessage("oops 12 24 36 48 52");
+ assertThatNoException()
+ .isThrownBy(
+ () -> Checks.checkArgument(true, "oops %s %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48"), new BigDecimal("52")));
+
+ var str1 = "boo";
+ var str2 = Checks.checkNotNull(str1, "oops %s %s %s %s %s", new BigDecimal("12"), new BigDecimal("24"),
+ new BigDecimal("36"), new BigDecimal("48"), new BigDecimal("52"));
+
+ assertThat(str2).isSameAs(str1);
+
+ }
+
+}
diff --git a/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/util/UtilTest.java b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/util/UtilTest.java
new file mode 100644
index 00000000..4a7371fc
--- /dev/null
+++ b/client-java/src/unit-tests/java/uk/gov/gchq/palisade/client/java/util/UtilTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package uk.gov.gchq.palisade.client.java.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UtilTest {
+
+ @Test
+ void testCreateUriFromBasePathAndEndpoint() {
+
+ assertThat(Util.createUri("http://me", "endpoint").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint");
+
+ assertThat(Util.createUri("http://me/", "endpoint").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint");
+
+ assertThat(Util.createUri("http://me", "endpoint/").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint");
+
+ assertThat(Util.createUri("http://me", "/endpoint/").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint");
+ }
+
+ @Test
+ void testCreateUriFromBasePathAndEndpoints() {
+
+ assertThat(Util.createUri("http://me", "endpoint1", "endpoint2").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint1/endpoint2");
+
+ assertThat(Util.createUri("http://me/", "endpoint1", "/endpoint2").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint1/endpoint2");
+
+ assertThat(Util.createUri("http://me", "endpoint1/", "endpoint2/").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint1/endpoint2");
+
+ assertThat(Util.createUri("http://me", "/endpoint1/", "/endpoint2/").toString())
+ .as("check URI created successfully")
+ .isEqualTo("http://me/endpoint1/endpoint2");
+
+ }
+
+}
diff --git a/client-java/src/unit-tests/resources/application.yml b/client-java/src/unit-tests/resources/application.yml
new file mode 100644
index 00000000..46ecf957
--- /dev/null
+++ b/client-java/src/unit-tests/resources/application.yml
@@ -0,0 +1,3 @@
+micronaut:
+ server:
+ http-version: 1.1
\ No newline at end of file
diff --git a/client-java/src/unit-tests/resources/logback.xml b/client-java/src/unit-tests/resources/logback.xml
new file mode 100644
index 00000000..e1f6030e
--- /dev/null
+++ b/client-java/src/unit-tests/resources/logback.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level CLIENT %logger{36} - %msg%n
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %X{server} %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-java/src/unit-tests/resources/mockito-extensions/org.mockito.plugins.MockMaker b/client-java/src/unit-tests/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..ca6ee9ce
--- /dev/null
+++ b/client-java/src/unit-tests/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/client-s3/README.md b/client-s3/README.md
new file mode 100644
index 00000000..354c0548
--- /dev/null
+++ b/client-s3/README.md
@@ -0,0 +1,67 @@
+
+
+#
+
+## A Tool for Complex and Scalable Data Access Policy Enforcement
+
+> :information_source: ***Work In Progress***
+> There are some issues between what Spark expects and what the server returns for a `GetObject`.
+> The server returns a valid AVRO object (which can be successfully read if written to a file).
+> The server also returns what look to be all the correct headers.
+> Despite this, a Spark `read` will return a data-frame with zero records.
+
+# Palisade S3-Server Client
+
+Presents an S3-compliant API wrapping a Palisade deployment.
+
+## Configuration
+The client should be configured using the `palisade-service`, `filtered-resource-service` and (with any appropriate change for the actual service-name) `data-service` fields under the `web.client` yaml configuration property.
+Defaults can be found [here](src/main/resources/application.yaml).
+
+## Usage
+Given a Spark job running against AWS S3 as follows:
+```
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.endpoint", "http://s3.eu-west-2.amazonaws.com/")
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.path.style.access", "true")
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.connection.ssl.enabled", "false")
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "com.amazonaws.auth.DefaultAWSCredentialsProviderChain")
+val nonrecursive = scala.io.Source.fromFile("/schema/nonrecursive.json").mkString
+spark.read.format("avro").option("avroSchema", nonrecursive).load("s3a://palisade-application-dev/data/remote-data-store/data/employee_file0.avro").show()
+```
+_Note that we use a modified non-recursive AVRO schema `/schema/nonrecursive.json` (this excludes the managers field) as recursive schema are not compatible with Spark SQL._
+
+Adapt the Spark job to run against the Palisade S3 client (ensure the client is running and correctly configured).
+This short snippet requires `curl`, but otherwise works wholly within `spark-shell` and the `s3` and `avro` libraries as the previous did:
+```scala
+import sys.process._;
+// User 'Alice' wants 'file:/data/local-data-store/' directory for 'SALARY' purposes
+// We get back the token '09d3a677-3d03-42e0-8cdb-f048f3929f8c', to be used as a bucket-name
+val token = (Seq("curl", "-X", "POST", "http://localhost:8092/register?userId=Alice&resourceId=file%3A%2Fdata%2Flocal-data-store%2F&purpose=SALARY")!!).stripSuffix("\n")
+Thread.sleep(5000)
+
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.endpoint", "localhost:8092/request")
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.path.style.access", "true")
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.connection.ssl.enabled", "false")
+// These are not interpreted or validated by Palisade, but Spark requires them to be non-null
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.access.key", "accesskey")
+spark.sparkContext.hadoopConfiguration.set("fs.s3a.secret.key", "secretkey")
+// spark.read.format("avro").load("s3a://" + token + "/with-policy/employee_small.avro").show()
+val nonrecursive = scala.io.Source.fromFile("/schema/nonrecursive.json").mkString
+spark.read.format("avro").option("avroSchema", nonrecursive).load("s3a://" + token + "/data/employee_file0.avro").show()
+```
diff --git a/client-s3/mvn_dependency_tree.txt b/client-s3/mvn_dependency_tree.txt
new file mode 100644
index 00000000..3e366de8
--- /dev/null
+++ b/client-s3/mvn_dependency_tree.txt
@@ -0,0 +1,61 @@
+uk.gov.gchq.palisade:client-s3:jar:0.5.0-RELEASE
++- uk.gov.gchq.palisade:client-akka:jar:0.5.0-RELEASE:compile
+| +- uk.gov.gchq.palisade:common:jar:0.5.0-RELEASE:compile
+| | \- org.apache.avro:avro:jar:1.8.2:compile
+| | +- org.codehaus.jackson:jackson-core-asl:jar:1.9.13:compile
+| | +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.13:compile
+| | +- com.thoughtworks.paranamer:paranamer:jar:2.7:compile
+| | +- org.xerial.snappy:snappy-java:jar:1.1.1.3:compile
+| | +- org.apache.commons:commons-compress:jar:1.8.1:compile
+| | \- org.tukaani:xz:jar:1.5:compile
+| \- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.0:compile
++- com.typesafe.akka:akka-stream_2.13:jar:2.6.10:compile
+| +- org.scala-lang:scala-library:jar:2.13.3:compile
+| +- com.typesafe.akka:akka-actor_2.13:jar:2.6.10:compile
+| | +- com.typesafe:config:jar:1.4.0:compile
+| | \- org.scala-lang.modules:scala-java8-compat_2.13:jar:0.9.0:compile
+| +- com.typesafe.akka:akka-protobuf-v3_2.13:jar:2.6.10:compile
+| +- org.reactivestreams:reactive-streams:jar:1.0.3:compile
+| \- com.typesafe:ssl-config-core_2.13:jar:0.4.2:compile
+| \- org.scala-lang.modules:scala-parser-combinators_2.13:jar:1.1.2:compile
++- com.typesafe.akka:akka-http_2.13:jar:10.2.1:compile
+| \- com.typesafe.akka:akka-http-core_2.13:jar:10.2.1:compile
+| \- com.typesafe.akka:akka-parsing_2.13:jar:10.2.1:compile
++- com.typesafe.akka:akka-http-jackson_2.13:jar:10.2.1:compile
++- com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.11.0:compile
+| +- com.fasterxml.jackson.core:jackson-core:jar:2.11.0:compile
+| +- com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.11.0:compile
+| | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:compile
+| | \- jakarta.activation:jakarta.activation-api:jar:1.2.2:compile
+| +- org.codehaus.woodstox:stax2-api:jar:4.2:compile
+| \- com.fasterxml.woodstox:woodstox-core:jar:6.2.0:compile
++- com.fasterxml.jackson.core:jackson-databind:jar:2.11.0:compile
++- org.springframework.boot:spring-boot-autoconfigure:jar:2.3.1.RELEASE:compile
+| \- org.springframework.boot:spring-boot:jar:2.3.1.RELEASE:compile
+| +- org.springframework:spring-core:jar:5.2.7.RELEASE:compile
+| | \- org.springframework:spring-jcl:jar:5.2.7.RELEASE:compile
+| \- org.springframework:spring-context:jar:5.2.7.RELEASE:compile
+| +- org.springframework:spring-aop:jar:5.2.7.RELEASE:compile
+| \- org.springframework:spring-expression:jar:5.2.7.RELEASE:compile
++- org.springframework.boot:spring-boot-starter-data-r2dbc:jar:2.3.1.RELEASE:compile
+| +- org.springframework.boot:spring-boot-starter:jar:2.3.1.RELEASE:compile
+| | +- org.springframework.boot:spring-boot-starter-logging:jar:2.3.1.RELEASE:compile
+| | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
+| | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
+| | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.13.3:compile
+| | | | \- org.apache.logging.log4j:log4j-api:jar:2.13.3:compile
+| | | \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile
+| | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
+| | \- org.yaml:snakeyaml:jar:1.26:compile
+| +- org.springframework.data:spring-data-r2dbc:jar:1.1.1.RELEASE:compile
+| | +- org.springframework.data:spring-data-commons:jar:2.3.1.RELEASE:compile
+| | +- org.springframework.data:spring-data-relational:jar:2.0.1.RELEASE:compile
+| | +- org.springframework:spring-tx:jar:5.2.7.RELEASE:compile
+| | +- org.springframework:spring-beans:jar:5.2.7.RELEASE:compile
+| | \- org.slf4j:slf4j-api:jar:1.7.30:compile
+| +- io.r2dbc:r2dbc-spi:jar:0.8.2.RELEASE:compile
+| \- io.r2dbc:r2dbc-pool:jar:0.8.3.RELEASE:compile
+| \- io.projectreactor.addons:reactor-pool:jar:0.1.4.RELEASE:compile
+\- io.r2dbc:r2dbc-h2:jar:0.8.4.RELEASE:compile
+ +- com.h2database:h2:jar:1.4.200:compile
+ \- io.projectreactor:reactor-core:jar:3.3.6.RELEASE:compile
diff --git a/client-s3/pom.xml b/client-s3/pom.xml
new file mode 100644
index 00000000..08365561
--- /dev/null
+++ b/client-s3/pom.xml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+ uk.gov.gchq.palisade
+ clients
+ 0.5.0-${revision}
+ ../pom.xml
+
+ 4.0.0
+
+
+
+ PalisadeDevelopers
+ GCHQ
+ https://github.com/gchq
+
+
+
+
+ client-s3
+ https://github.com/gchq/Palisade-clients/tree/develop/client-akka
+ GCHQ Palisade - S3-Server Client
+
+ The S3 Palisade Client starts a web server presenting an S3-compliant API for querying resources and data returned by Palisade.
+ After POSTing a request, a bucket is returned using the Palisade token, which can be queried using an existing S3-compatible tool (such as Apache Spark).
+
+
+
+
+ ${scm.url}
+ ${scm.connection}
+ ${scm.developer.connection}
+ HEAD
+
+
+
+
+ 2.13
+ 2.6.10
+ 10.2.1
+ 2.11.0
+
+
+
+
+
+
+ uk.gov.gchq.palisade
+ client-akka
+ ${project.parent.version}
+
+
+
+
+ com.typesafe.akka
+ akka-stream_${scala.version}
+ ${akka.version}
+
+
+
+ com.typesafe.akka
+ akka-http_${scala.version}
+ ${akka.http.version}
+
+
+
+ com.typesafe.akka
+ akka-http-jackson_${scala.version}
+ ${akka.http.version}
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ ${jackson.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+
+ io.r2dbc
+ r2dbc-h2
+
+
+
+
+
+
+ src/main/resources
+
+
+
+
+ src/unit-tests/resources
+
+
+ src/component-tests/resources
+
+
+ src/contract-tests/resources
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-test-sources
+ generate-test-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+
+ add-test-resources
+ generate-test-resources
+
+ add-test-resource
+
+
+
+
+ true
+ ${basedir}/src/unit-tests/resources
+ ${basedir}/src/component-tests/resources
+ ${basedir}/src/contract-tests/resources
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ ZIP
+
+
+
+ repackage
+
+ exec
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/S3Client.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/S3Client.java
new file mode 100644
index 00000000..21bac6f3
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/S3Client.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3;
+
+import akka.actor.ActorSystem;
+import akka.stream.javadsl.RunnableGraph;
+import akka.stream.javadsl.Source;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+
+import uk.gov.gchq.palisade.client.s3.web.AkkaHttpServer;
+
+/**
+ * Implementation of the client interface that also exposes some akka-specific data-types such as {@link Source}s.
+ */
+@SpringBootApplication
+public class S3Client {
+ private static final Logger LOGGER = LoggerFactory.getLogger(S3Client.class);
+
+ private final AkkaHttpServer server;
+ private final ActorSystem system;
+
+ /**
+ * Autowire Akka objects in constructor for application ready event
+ *
+ * @param system the default akka actor system
+ * @param server the http server to start (in replacement of spring-boot-starter-web)
+ */
+ public S3Client(
+ final AkkaHttpServer server,
+ final ActorSystem system) {
+ this.server = server;
+ this.system = system;
+ }
+
+ /**
+ * Application entrypoint, creates and runs a spring application, passing in the given command-line args
+ *
+ * @param args command-line arguments passed to the application
+ */
+ public static void main(final String[] args) {
+ LOGGER.debug("{} started with: {}", S3Client.class.getSimpleName(), args);
+ new SpringApplicationBuilder(S3Client.class)
+ .web(WebApplicationType.NONE)
+ .run(args);
+ }
+
+ /**
+ * Runs all available Akka {@link RunnableGraph}s until completion.
+ * The 'main' threads of the application during runtime are the completable futures spawned here.
+ */
+ @EventListener(ApplicationReadyEvent.class)
+ public void serveForever() {
+ this.server.serveForever(this.system);
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/ApplicationConfiguration.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/ApplicationConfiguration.java
new file mode 100644
index 00000000..3455ec6a
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/ApplicationConfiguration.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.config;
+
+import akka.actor.ActorSystem;
+import akka.stream.Materializer;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import uk.gov.gchq.palisade.Generated;
+import uk.gov.gchq.palisade.client.akka.AkkaClient;
+import uk.gov.gchq.palisade.client.akka.AkkaClient.SSLMode;
+import uk.gov.gchq.palisade.client.s3.config.ApplicationConfiguration.ClientMap;
+import uk.gov.gchq.palisade.client.s3.repository.ContentLengthRepository;
+import uk.gov.gchq.palisade.client.s3.repository.PersistenceLayer;
+import uk.gov.gchq.palisade.client.s3.repository.ResourceRepository;
+import uk.gov.gchq.palisade.client.s3.web.AkkaHttpServer;
+import uk.gov.gchq.palisade.client.s3.web.DynamicS3ServerApi;
+import uk.gov.gchq.palisade.client.s3.web.RouteSupplier;
+
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Spring bean dependency injection graph
+ */
+@Configuration
+@EnableConfigurationProperties(ClientMap.class)
+public class ApplicationConfiguration {
+
+ @ConfigurationProperties(prefix = "web")
+ static class ClientMap {
+ private Map client;
+
+ @Generated
+ public Map getClient() {
+ return client;
+ }
+
+ /**
+ * Get a single url for a service name.
+ *
+ * @param key the name of a service
+ * @return the URL for that service
+ */
+ @Generated
+ public String getClient(final String key) {
+ return client.get(key);
+ }
+
+ @Generated
+ public void setClient(final Map client) {
+ this.client = Optional.ofNullable(client)
+ .orElseThrow(() -> new IllegalArgumentException("client cannot be null"));
+ }
+ }
+
+ /**
+ * The HTTP server will serve forever on the supplied {@code server.host} and {@code server.port}
+ * config values.
+ *
+ * @param properties spring internal {@code server.xxx} config file object
+ * @param routeSuppliers collection of routes to bind for this server (see below)
+ * @return the http server
+ */
+ @Bean
+ AkkaHttpServer akkaHttpServer(final ServerProperties properties, final Collection routeSuppliers) {
+ String hostname = Optional.ofNullable(properties.getAddress())
+ .map(InetAddress::getHostAddress)
+ .orElse("0.0.0.0");
+ return new AkkaHttpServer(hostname, properties.getPort(), routeSuppliers);
+ }
+
+ @Bean
+ PersistenceLayer persistenceLayer(final ResourceRepository resourceRepository, final ContentLengthRepository contentLengthRepository) {
+ return new PersistenceLayer(resourceRepository, contentLengthRepository);
+ }
+
+ @Bean
+ AkkaClient akkaClient(final ActorSystem actorSystem, final ClientMap clientMap) {
+ return new AkkaClient(clientMap.getClient("palisade-service"), clientMap.getClient("filtered-resource-service"),
+ Map.copyOf(clientMap.getClient()), actorSystem, SSLMode.NONE);
+ }
+
+ @Bean
+ RouteSupplier s3ServerApi(final AkkaClient akkaClient, final Materializer materializer, final PersistenceLayer persistenceLayer) {
+ return new DynamicS3ServerApi(akkaClient, materializer, persistenceLayer);
+ }
+
+ @Bean
+ ActorSystem actorSystem() {
+ return ActorSystem.create("SpringAkkaActorSystem");
+ }
+
+ @Bean
+ Materializer materialiser(final ActorSystem actorSystem) {
+ return Materializer.createMaterializer(actorSystem);
+ }
+
+ @Bean
+ @ConfigurationProperties("server")
+ ServerProperties serverProperties() {
+ return new ServerProperties();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/JacksonXmlSupport.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/JacksonXmlSupport.java
new file mode 100644
index 00000000..afc7dfb7
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/JacksonXmlSupport.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.config;
+
+import akka.http.javadsl.marshalling.Marshaller;
+import akka.http.javadsl.model.HttpEntity;
+import akka.http.javadsl.model.MediaType;
+import akka.http.javadsl.model.MediaTypes;
+import akka.http.javadsl.model.RequestEntity;
+import akka.http.javadsl.unmarshalling.Unmarshaller;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Configure Jackson for XML serialisation and deserialisation.
+ * S3 is built on XML for message request/response types, whereas Jackson defaults to JSON.
+ */
+public final class JacksonXmlSupport {
+ private static final List XML_MEDIA_TYPES = Arrays.asList(MediaTypes.APPLICATION_XML, MediaTypes.TEXT_XML);
+ private static final ObjectMapper DEFAULT_XML_MAPPER = new XmlMapper()
+ .enable(SerializationFeature.WRAP_ROOT_VALUE);
+
+ private JacksonXmlSupport() {
+ // Hide public constructor for static-method-only class
+ }
+
+ /**
+ * Create a Jackson {@link Marshaller} for serialising to the XML media-type.
+ *
+ * @param the domain type for the marshaller
+ * @return a new marshaller for converting objects to XML
+ */
+ public static Marshaller marshaller() {
+ return Marshaller.wrapEntity(
+ JacksonXmlSupport::toXML,
+ Marshaller.stringToEntity(),
+ MediaTypes.APPLICATION_XML
+ );
+ }
+
+ /**
+ * Create a Jackson {@link Unmarshaller} for deserialising from the XML media-type.
+ *
+ * @param expectedType the expected type to deserialise the XML into
+ * @param the domain type for the unmarshaller
+ * @return a new unmarshaller for converting XML to objects
+ */
+ public static Unmarshaller unmarshaller(final Class expectedType) {
+ return Unmarshaller.forMediaTypes(XML_MEDIA_TYPES, Unmarshaller.entityToString())
+ .thenApply(xml -> fromXML(xml, expectedType));
+ }
+
+ /**
+ * Convert a Java object to XML.
+ *
+ * @param object the Java object to convert
+ * @param the type of the Java object
+ * @return a {@link String} of XML data representing the serialised object
+ */
+ private static String toXML(final T object) {
+ try {
+ return JacksonXmlSupport.DEFAULT_XML_MAPPER.writeValueAsString(object);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot marshal to XML: " + object, e);
+ }
+ }
+
+ /**
+ * Convert an XML String to a Java object.
+ *
+ * @param xml the Java object to convert
+ * @param expectedType the expected type to deserialise the XML into
+ * @param the type of the Java object
+ * @return a Java object representing the deserialisation of the XML data
+ */
+ private static T fromXML(final String xml, final Class expectedType) {
+ try {
+ return JacksonXmlSupport.DEFAULT_XML_MAPPER.readerFor(expectedType).readValue(xml);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot unmarshal XML as " + expectedType.getSimpleName(), e);
+ }
+ }
+}
+
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/R2dbcConfiguration.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/R2dbcConfiguration.java
new file mode 100644
index 00000000..f8d22c51
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/config/R2dbcConfiguration.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.config;
+
+import io.r2dbc.spi.ConnectionFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
+import org.springframework.data.r2dbc.connectionfactory.init.ConnectionFactoryInitializer;
+import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator;
+import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
+import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
+import org.springframework.lang.NonNull;
+
+import uk.gov.gchq.palisade.client.s3.repository.ResourceConverter;
+
+import java.util.List;
+
+/**
+ * Configuration for the reactive R2DBC repositories
+ */
+@Configuration
+@EnableR2dbcRepositories(basePackages = {"uk.gov.gchq.palisade.client.s3.repository"})
+public class R2dbcConfiguration extends AbstractR2dbcConfiguration {
+ private static final Logger LOGGER = LoggerFactory.getLogger(R2dbcConfiguration.class);
+ private final ConnectionFactory defaultConnectionFactory;
+
+ /**
+ * Public constructor for the R2DBC configuration
+ *
+ * @param defaultConnectionFactory the connection factory
+ */
+ public R2dbcConfiguration(final ConnectionFactory defaultConnectionFactory) {
+ this.defaultConnectionFactory = defaultConnectionFactory;
+ LOGGER.debug("Initialised R2DBC repositories");
+ }
+
+ @Override
+ @NonNull
+ public ConnectionFactory connectionFactory() {
+ return this.defaultConnectionFactory;
+ }
+
+ @Override
+ @Bean
+ @NonNull
+ public R2dbcCustomConversions r2dbcCustomConversions() {
+ List> converterList = List.of(
+ new ResourceConverter.Reading(), new ResourceConverter.Writing()
+ );
+ return new R2dbcCustomConversions(getStoreConversions(), converterList);
+ }
+
+ /**
+ * Prepopulate the database with initial SQL commands (create the tables used by the entities and repositories)
+ *
+ * @return an initializer for the factory (executes some initial commands on the database once created)
+ */
+ @Bean
+ ConnectionFactoryInitializer initializer() {
+ ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
+ initializer.setConnectionFactory(defaultConnectionFactory);
+ initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql")));
+ return initializer;
+ }
+
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/CanonicalUser.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/CanonicalUser.java
new file mode 100644
index 00000000..767bd58f
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/CanonicalUser.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.StringJoiner;
+
+
+/**
+ * S3 model for the 'CanonicalUser' XML schema.
+ */
+public class CanonicalUser {
+
+ @JsonProperty(value = "ID", required = true)
+ @JacksonXmlProperty(namespace = "http://s3.amazonaws.com/doc/2006-03-01/")
+ protected String id;
+ @JsonProperty(value = "DisplayName")
+ @JacksonXmlProperty(namespace = "http://s3.amazonaws.com/doc/2006-03-01/")
+ protected String displayName;
+
+ @Generated
+ public String getId() {
+ return id;
+ }
+
+ @Generated
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ @Generated
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Generated
+ public void setDisplayName(final String displayName) {
+ this.displayName = displayName;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", CanonicalUser.class.getSimpleName() + "[", "]")
+ .add("id='" + id + "'")
+ .add("displayName='" + displayName + "'")
+ .toString();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/ListBucketResult.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/ListBucketResult.java
new file mode 100644
index 00000000..02f5719b
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/ListBucketResult.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * S3 model for the 'ListBucketResult' XML schema.
+ */
+// Schema specifies field 'isTruncated', not 'truncated'
+@SuppressWarnings("java:S2047")
+@JacksonXmlRootElement(localName = "ListBucketResult", namespace = "http://s3.amazonaws.com/doc/2006-03-01/")
+public class ListBucketResult {
+
+ @JsonProperty(value = "Metadata")
+ @JacksonXmlElementWrapper(useWrapping = false)
+ protected List metadata;
+ @JsonProperty(value = "Name")
+ protected String name;
+ @JsonProperty(value = "Prefix", required = true)
+ protected String prefix;
+ @JsonProperty(value = "ContinuationToken", required = true)
+ protected String continuationToken;
+ @JsonProperty(value = "NextContinuationToken")
+ protected String nextContinuationToken;
+ @JsonProperty(value = "MaxKeys")
+ protected int maxKeys;
+ @JsonProperty(value = "KeyCount")
+ protected int keyCount;
+ @JsonProperty(value = "Delimiter")
+ protected String delimiter;
+ @JsonProperty(value = "IsTruncated")
+ protected boolean isTruncated;
+ @JsonProperty(value = "Contents")
+ @JacksonXmlElementWrapper(useWrapping = false)
+ protected List contents;
+ @JsonProperty(value = "CommonPrefixes")
+ @JacksonXmlElementWrapper(useWrapping = false)
+ protected List commonPrefixes;
+
+ @Generated
+ public List getMetadata() {
+ if (metadata == null) {
+ metadata = new ArrayList<>();
+ }
+ return this.metadata;
+ }
+
+ @Generated
+ public List getContents() {
+ if (contents == null) {
+ contents = new ArrayList<>();
+ }
+ return this.contents;
+ }
+
+ @Generated
+ public List getCommonPrefixes() {
+ if (commonPrefixes == null) {
+ commonPrefixes = new ArrayList<>();
+ }
+ return this.commonPrefixes;
+ }
+
+ @Generated
+ public String getName() {
+ return name;
+ }
+
+ @Generated
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ @Generated
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Generated
+ public void setPrefix(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Generated
+ public String getContinuationToken() {
+ return continuationToken;
+ }
+
+ @Generated
+ public void setContinuationToken(final String continuationToken) {
+ this.continuationToken = continuationToken;
+ }
+
+ @Generated
+ public String getNextContinuationToken() {
+ return nextContinuationToken;
+ }
+
+ @Generated
+ public void setNextContinuationToken(final String nextContinuationToken) {
+ this.nextContinuationToken = nextContinuationToken;
+ }
+
+ @Generated
+ public int getMaxKeys() {
+ return maxKeys;
+ }
+
+ @Generated
+ public void setMaxKeys(final int maxKeys) {
+ this.maxKeys = maxKeys;
+ }
+
+ @Generated
+ public int getKeyCount() {
+ return keyCount;
+ }
+
+ @Generated
+ public void setKeyCount(final int keyCount) {
+ this.keyCount = keyCount;
+ }
+
+ @Generated
+ public String getDelimiter() {
+ return delimiter;
+ }
+
+ @Generated
+ public void setDelimiter(final String delimiter) {
+ this.delimiter = delimiter;
+ }
+
+ @Generated
+ public boolean getIsTruncated() {
+ return isTruncated;
+ }
+
+ @Generated
+ public void setIsTruncated(final boolean truncated) {
+ isTruncated = truncated;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", ListBucketResult.class.getSimpleName() + "[", "]")
+ .add("metadata=" + metadata)
+ .add("name='" + name + "'")
+ .add("prefix='" + prefix + "'")
+ .add("continuationToken='" + continuationToken + "'")
+ .add("nextContinuationToken='" + nextContinuationToken + "'")
+ .add("maxKeys=" + maxKeys)
+ .add("keyCount=" + keyCount)
+ .add("delimiter='" + delimiter + "'")
+ .add("isTruncated=" + isTruncated)
+ .add("contents=" + contents)
+ .add("commonPrefixes=" + commonPrefixes)
+ .toString();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/ListEntry.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/ListEntry.java
new file mode 100644
index 00000000..7df73c37
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/ListEntry.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.Date;
+import java.util.StringJoiner;
+
+/**
+ * S3 model for the 'ListEntry' XML schema.
+ */
+public class ListEntry {
+
+ @JsonProperty(value = "Key", required = true)
+ protected String key;
+ @JsonProperty(value = "LastModified", required = true)
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+ protected Date lastModified;
+ @JsonProperty(value = "ETag", required = true)
+ protected String eTag;
+ @JsonProperty(value = "Size")
+ protected long size;
+ @JsonProperty(value = "Owner")
+ protected CanonicalUser owner;
+ @JsonProperty(value = "StorageClass", required = true)
+ protected StorageClass storageClass;
+
+ @Generated
+ public String getKey() {
+ return key;
+ }
+
+ @Generated
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ @Generated
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ @Generated
+ public void setLastModified(final Date lastModified) {
+ this.lastModified = lastModified;
+ }
+
+ @Generated
+ public String getETag() {
+ return eTag;
+ }
+
+ @Generated
+ public void setETag(final String eTag) {
+ this.eTag = eTag;
+ }
+
+ @Generated
+ public long getSize() {
+ return size;
+ }
+
+ @Generated
+ public void setSize(final long size) {
+ this.size = size;
+ }
+
+ @Generated
+ public CanonicalUser getOwner() {
+ return owner;
+ }
+
+ @Generated
+ public void setOwner(final CanonicalUser owner) {
+ this.owner = owner;
+ }
+
+ @Generated
+ public StorageClass getStorageClass() {
+ return storageClass;
+ }
+
+ @Generated
+ public void setStorageClass(final StorageClass storageClass) {
+ this.storageClass = storageClass;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", ListEntry.class.getSimpleName() + "[", "]")
+ .add("key='" + key + "'")
+ .add("lastModified=" + lastModified)
+ .add("eTag='" + eTag + "'")
+ .add("size=" + size)
+ .add("owner=" + owner)
+ .add("storageClass=" + storageClass)
+ .toString();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/MetadataEntry.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/MetadataEntry.java
new file mode 100644
index 00000000..4d41a692
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/MetadataEntry.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.StringJoiner;
+
+
+/**
+ * S3 model for the 'MetadataEntry' XML schema.
+ */
+public class MetadataEntry {
+
+ @JsonProperty(value = "Name", required = true)
+ protected String name;
+ @JsonProperty(value = "Value", required = true)
+ protected String value;
+
+ @Generated
+ public String getName() {
+ return name;
+ }
+
+ @Generated
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ @Generated
+ public String getValue() {
+ return value;
+ }
+
+ @Generated
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", MetadataEntry.class.getSimpleName() + "[", "]")
+ .add("name='" + name + "'")
+ .add("value='" + value + "'")
+ .toString();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/PrefixEntry.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/PrefixEntry.java
new file mode 100644
index 00000000..78c4fb47
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/PrefixEntry.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import uk.gov.gchq.palisade.Generated;
+
+import java.util.StringJoiner;
+
+/**
+ * S3 model for the 'PrefixEntry' XML schema.
+ */
+public class PrefixEntry {
+
+ @JsonProperty(value = "Prefix", required = true)
+ protected String prefix;
+
+ @Generated
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Generated
+ public void setPrefix(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", PrefixEntry.class.getSimpleName() + "[", "]")
+ .add("prefix='" + prefix + "'")
+ .toString();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/StorageClass.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/StorageClass.java
new file mode 100644
index 00000000..292c5a93
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/domain/StorageClass.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.domain;
+
+/**
+ * S3 model for the 'StorageClass' XML schema.
+ */
+public enum StorageClass {
+
+ STANDARD,
+ REDUCED_REDUNDANCY,
+ GLACIER,
+ UNKNOWN;
+
+ /**
+ * Convert from Enum to String.
+ *
+ * @return a {@link String} representation of the {@link StorageClass}
+ */
+ public String value() {
+ return name();
+ }
+
+ /**
+ * Convert from String to Enum.
+ *
+ * @param v the string to convert to a storageClass enum
+ * @return a {@link StorageClass}
+ */
+ public static StorageClass fromValue(final String v) {
+ return valueOf(v);
+ }
+
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/AbstractOrphanedChildJsonMixin.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/AbstractOrphanedChildJsonMixin.java
new file mode 100644
index 00000000..482fdee6
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/AbstractOrphanedChildJsonMixin.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.repository;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import uk.gov.gchq.palisade.resource.ParentResource;
+
+/**
+ * The ResourceRepository stores indexable fields and a JSON CLOB for all persisted resources.
+ * This includes, for any given persisted resource, its parent id, which will also be persisted.
+ * Erase parent when storing JSON, it will be rebuilt using the repository.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+// Must be abstract class, not interface, to be used as mixin
+@SuppressWarnings({"java:S1610", "java:S1694"})
+public abstract class AbstractOrphanedChildJsonMixin {
+ /**
+ * Ignore the parent field when serialising to JSON.
+ *
+ * @return irrelevant
+ */
+ @JsonIgnore
+ abstract ParentResource getParent();
+
+ /**
+ * Ignore the parent field when deserialising from JSON.
+ *
+ * @param resource irrelevant
+ */
+ @JsonIgnore
+ abstract void setParent(ParentResource resource);
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/ContentLengthEntity.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/ContentLengthEntity.java
new file mode 100644
index 00000000..a35360fd
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/ContentLengthEntity.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.repository;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.Transient;
+import org.springframework.data.domain.Persistable;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import uk.gov.gchq.palisade.Generated;
+import uk.gov.gchq.palisade.resource.Resource;
+
+import java.io.Serializable;
+import java.util.StringJoiner;
+
+/**
+ * The Database uses this as the object that will be stored in the backing store linked by an ID
+ * In this case the ResourceID and ResourceEntity make up the key
+ * This contains all objects that will be go into the database, including how they are serialised and indexed
+ */
+@Table("content_lengths")
+public class ContentLengthEntity implements Serializable, Persistable {
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column("resource_id")
+ private final String resourceId;
+
+ @Column("content_length")
+ private final Long contentLength;
+
+ @Transient
+ private final boolean isNew;
+
+ @PersistenceConstructor
+ @JsonCreator
+ private ContentLengthEntity(final @JsonProperty("resourceId") String resourceId,
+ final @JsonProperty("parentId") Long contentLength) {
+ this(resourceId, contentLength, false);
+ }
+
+ private ContentLengthEntity(final String resourceId, final Long contentLength, final boolean isNew) {
+ this.resourceId = resourceId;
+ this.contentLength = contentLength;
+ this.isNew = isNew;
+ }
+
+ /**
+ * Constructor used for the Database that takes a Resource and extracts the values
+ * Used for inserting objects into the backing store
+ *
+ * @param resource specified to insert into the backing store
+ * @param contentLength predicted length of this remote datafile
+ */
+ public ContentLengthEntity(final Resource resource, final Long contentLength) {
+ this(
+ resource.getId(),
+ contentLength,
+ true
+ );
+ }
+
+ @Override
+ @JsonIgnore
+ public String getId() {
+ return resourceId;
+ }
+
+ @Override
+ @JsonIgnore
+ public boolean isNew() {
+ return isNew;
+ }
+
+ @Generated
+ public String getResourceId() {
+ return getId();
+ }
+
+ public Long getContentLength() {
+ return contentLength;
+ }
+
+ @Override
+ @Generated
+ public String toString() {
+ return new StringJoiner(", ", ContentLengthEntity.class.getSimpleName() + "[", "]")
+ .add("resourceId='" + resourceId + "'")
+ .add("contentLength=" + contentLength)
+ .add("isNew=" + isNew)
+ .toString();
+ }
+}
diff --git a/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/ContentLengthRepository.java b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/ContentLengthRepository.java
new file mode 100644
index 00000000..b7fd56ee
--- /dev/null
+++ b/client-s3/src/main/java/uk/gov/gchq/palisade/client/s3/repository/ContentLengthRepository.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018-2021 Crown Copyright
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.gov.gchq.palisade.client.s3.repository;
+
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import reactor.core.publisher.Mono;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Repository for storing/caching content-lengths of resources after rules are applied.
+ * Used to inform a client of the S3 server on HTTP Content-Length (e.g. Spark is quite picky about it).
+ */
+public interface ContentLengthRepository extends ReactiveCrudRepository