Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ DesiredResponse response = client.queryForDocument(new QueryForDocumentRequest(y
// Do whatever you like with the response
```

### Setting Headers

You can add headers that will be included in all requests to the OPA server:

```java
// Add a single header
OpaClient client = OpaClient.builder()
.opaConfiguration("http://localhost:8181")
.header("X-API-Key", "your-api-key")
.header("Authorization", "Bearer your-token")
.build();

// Add multiple headers at once
Map<String, String> headers = new HashMap<>();
headers.put("X-API-Key", "your-api-key");
headers.put("X-Tenant-ID", "tenant-123");
headers.put("Authorization", "Bearer your-token");

OpaClient client = OpaClient.builder()
.opaConfiguration("http://localhost:8181")
.headers(headers)
.build();

// You can also combine both approaches
OpaClient client = OpaClient.builder()
.opaConfiguration("http://localhost:8181")
.headers(headers)
.header("X-Request-ID", "req-456")
.build();
```

### Query for a list of documents

This requires [commons-lang3](https://mvnrepository.com/artifact/org.apache.commons/commons-lang3) to be present on your classpath.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,26 @@ public class XmasHappening {
public static final String NAME_QUERY_PATH = "name_rule/access_to_chimney";
public static final String OPA_URL = "http://localhost:8181/";


public static void main(String[] args) {


List<OpaInput> sleighCrew = List.of(
new OpaInput("SantaClaus", 99, SantaPartyMember.SANTA.name()),
new OpaInput("Buddy", 17, SantaPartyMember.ELF.name()),
new OpaInput("Grinch",22, SantaPartyMember.GRINCH.name()),
new OpaInput("Grinch", 22, SantaPartyMember.GRINCH.name()),
new OpaInput("Rudolf", 33, SantaPartyMember.REINDEER.name()),
new OpaInput("Niko", 25, SantaPartyMember.REINDEER.name()));

sleighCrew.forEach(XmasHappening::callOpa);

}

static void callOpa(OpaInput testInput){
static void callOpa(OpaInput testInput) {
log.info("Creating query for input {}", testInput);
OpaQueryApi queryApi = OpaClient.builder().opaConfiguration(OPA_URL).build();
OpaQueryApi queryApi = OpaClient.builder()
.opaConfiguration(OPA_URL)
.header("X-API-Key", "secret-key-12345")
.header("X-Request-Source", "xmas-app")
.build();

QueryForDocumentRequest ageRequest = new QueryForDocumentRequest(testInput, AGE_QUERY_PATH);
OpaOutput ageResponse = queryApi.queryForDocument(ageRequest, OpaOutput.class);
Expand Down
62 changes: 58 additions & 4 deletions src/main/java/com/bisnode/opa/client/OpaClient.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.bisnode.opa.client;


import com.bisnode.opa.client.data.OpaDataApi;
import com.bisnode.opa.client.data.OpaDataClient;
import com.bisnode.opa.client.data.OpaDocument;
Expand All @@ -16,11 +15,14 @@

import java.lang.reflect.ParameterizedType;
import java.net.http.HttpClient;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* Opa client featuring {@link OpaDataApi}, {@link OpaQueryApi} and {@link OpaPolicyApi}
* Opa client featuring {@link OpaDataApi}, {@link OpaQueryApi} and
* {@link OpaPolicyApi}
*/
public class OpaClient implements OpaQueryApi, OpaDataApi, OpaPolicyApi {

Expand Down Expand Up @@ -75,20 +77,71 @@ public void createOrUpdatePolicy(OpaPolicy policy) {
public static class Builder {
private OpaConfiguration opaConfiguration;
private ObjectMapper objectMapper;
private Map<String, String> headers = new HashMap<>();

/**
* @param url URL including protocol and port
*/
public Builder opaConfiguration(String url) {
this.opaConfiguration = new OpaConfiguration(url);
this.opaConfiguration = new OpaConfiguration(url, HttpClient.Version.HTTP_1_1, headers);
return this;
}

/**
* @param objectMapper ObjectMapper to be used for JSON
* serialization/deserialization
*/
public Builder objectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
return this;
}

/**
* Add a header that will be included in all requests to the OPA server
*
* @param name header name
* @param value header value
* @return this builder instance
*/
public Builder header(String name, String value) {
Objects.requireNonNull(name, "Header name cannot be null");
Objects.requireNonNull(value, "Header value cannot be null");
this.headers.put(name, value);
// Recreate configuration if it already exists to include the new header
if (this.opaConfiguration != null) {
this.opaConfiguration = new OpaConfiguration(
this.opaConfiguration.getUrl(),
this.opaConfiguration.getHttpVersion(),
this.headers);
}
return this;
}

/**
* Add multiple headers that will be included in all requests to the OPA
* server
*
* @param headers map of header names to values
* @return this builder instance
*/
public Builder headers(Map<String, String> headers) {
if (headers != null) {
headers.forEach((name, value) -> {
Objects.requireNonNull(name, "Header name cannot be null");
Objects.requireNonNull(value, "Header value cannot be null");
});
this.headers.putAll(headers);
// Recreate configuration if it already exists to include the new headers
if (this.opaConfiguration != null) {
this.opaConfiguration = new OpaConfiguration(
this.opaConfiguration.getUrl(),
this.opaConfiguration.getHttpVersion(),
this.headers);
}
}
return this;
}

public OpaClient build() {
Objects.requireNonNull(opaConfiguration, "build() called without opaConfiguration provided");
HttpClient httpClient = HttpClient.newBuilder()
Expand All @@ -97,7 +150,8 @@ public OpaClient build() {
ObjectMapper objectMapper = Optional.ofNullable(this.objectMapper)
.orElseGet(ObjectMapperFactory.getInstance()::create);
OpaRestClient opaRestClient = new OpaRestClient(opaConfiguration, httpClient, objectMapper);
return new OpaClient(new OpaQueryClient(opaRestClient), new OpaDataClient(opaRestClient), new OpaPolicyClient(opaRestClient));
return new OpaClient(new OpaQueryClient(opaRestClient), new OpaDataClient(opaRestClient),
new OpaPolicyClient(opaRestClient));
}
}
}
62 changes: 50 additions & 12 deletions src/main/java/com/bisnode/opa/client/OpaConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import java.beans.ConstructorProperties;
import java.net.URI;
import java.net.http.HttpClient;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
Expand All @@ -11,26 +14,45 @@
public final class OpaConfiguration {
private final String url;
private final HttpClient.Version httpVersion;
private final Map<String, String> headersMap;

/**
* @param url base URL to OPA server, containing protocol, and port (eg. http://localhost:8181)
* @param url base URL to OPA server, containing protocol, and port (eg.
* http://localhost:8181)
*/
@ConstructorProperties({"url"})
@ConstructorProperties({ "url" })
public OpaConfiguration(String url) {
this.url = url;
this.httpVersion = "https".equals(URI.create(url).getScheme()) ?
HttpClient.Version.HTTP_2 :
HttpClient.Version.HTTP_1_1;
this.httpVersion = "https".equals(URI.create(url).getScheme()) ? HttpClient.Version.HTTP_2
: HttpClient.Version.HTTP_1_1;
this.headersMap = Collections.emptyMap();
}

/**
* @param url base URL to OPA server, containing protocol, and port (eg. http://localhost:8181)
* @param url base URL to OPA server, containing protocol, and port (eg.
* http://localhost:8181)
* @param httpVersion preferred HTTP version to use for the client
*/
@ConstructorProperties({"url", "httpVersion"})
@ConstructorProperties({ "url", "httpVersion" })
public OpaConfiguration(String url, HttpClient.Version httpVersion) {
this.url = url;
this.httpVersion = httpVersion;
this.headersMap = Collections.emptyMap();
}

/**
* @param url base URL to OPA server, containing protocol, and port
* (eg.
* http://localhost:8181)
* @param httpVersion preferred HTTP version to use for the client
* @param headers headers to be added to all requests
*/
@ConstructorProperties({ "url", "httpVersion", "headers" })
public OpaConfiguration(String url, HttpClient.Version httpVersion, Map<String, String> headers) {
this.url = url;
this.httpVersion = httpVersion;
this.headersMap = headers != null ? Collections.unmodifiableMap(new HashMap<>(headers))
: Collections.emptyMap();
}

/**
Expand All @@ -41,7 +63,8 @@ public String getUrl() {
}

/**
* Get HTTP version configured for the client. If not configured will use HTTP2 for "https" scheme
* Get HTTP version configured for the client. If not configured will use HTTP2
* for "https" scheme
* and HTTP1.1 for "http" scheme.
*
* @return httpVersion configured for use by the client
Expand All @@ -50,23 +73,38 @@ public HttpClient.Version getHttpVersion() {
return this.httpVersion;
}

/**
* Get headers that will be added to all requests
*
* @return unmodifiable map of headers
*/
public Map<String, String> getHeaders() {
return this.headersMap;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
OpaConfiguration that = (OpaConfiguration) o;
return Objects.equals(url, that.url);
return Objects.equals(url, that.url) &&
Objects.equals(httpVersion, that.httpVersion) &&
Objects.equals(headersMap, that.headersMap);
}

@Override
public int hashCode() {
return Objects.hash(url);
return Objects.hash(url, httpVersion, headersMap);
}

@Override
public String toString() {
return "OpaConfiguration{" +
"url='" + url + '\'' +
", httpVersion=" + httpVersion +
", headers=" + headersMap +
'}';
}
}
24 changes: 17 additions & 7 deletions src/main/java/com/bisnode/opa/client/rest/OpaRestClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,25 @@ public OpaRestClient(OpaConfiguration opaConfiguration, HttpClient httpClient, O
}

/**
* Create {@link java.net.http.HttpRequest.Builder} with configured url using provided endpoint
* Create {@link java.net.http.HttpRequest.Builder} with configured url using
* provided endpoint
*
* @param endpoint desired opa endpoint
* @throws OpaClientException if URL or endpoint is invalid
*/
public HttpRequest.Builder getBasicRequestBuilder(String endpoint) {
OpaUrl url = OpaUrl.of(opaConfiguration.getUrl(), endpoint).normalized();
return HttpRequest.newBuilder(url.toUri());
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(url.toUri());

// Add headers from configuration
opaConfiguration.getHeaders().forEach(requestBuilder::header);

return requestBuilder;
}

/**
* Gets {@link java.net.http.HttpRequest.BodyPublisher} that is capable of serializing to JSON
* Gets {@link java.net.http.HttpRequest.BodyPublisher} that is capable of
* serializing to JSON
*
* @param body object to be serialized
*/
Expand All @@ -51,15 +58,16 @@ public HttpRequest.BodyPublisher getJsonBodyPublisher(Object body) throws JsonPr
/**
* Gets {@link JsonBodyHandler} that will deserialize JSON to desired class type
*
* @param responseType desired response type
* @param responseType desired response type
* @param <T> desired response type
*/
public <T> JsonBodyHandler<T> getJsonBodyHandler(JavaType responseType) {
return new JsonBodyHandler<>(responseType, objectMapper);
}

/**
* Sends provided request and returns response mapped using {@link java.net.http.HttpResponse.BodyHandler}
* Sends provided request and returns response mapped using
* {@link java.net.http.HttpResponse.BodyHandler}
*
* @param request request to be sent
* @param bodyHandler handler that indicates how to transform incoming body
Expand All @@ -68,11 +76,13 @@ public <T> JsonBodyHandler<T> getJsonBodyHandler(JavaType responseType) {
* @throws IOException is propagated from {@link HttpClient}
* @throws InterruptedException is propagated from {@link HttpClient}
*/
public <T> HttpResponse<T> sendRequest(HttpRequest request, HttpResponse.BodyHandler<T> bodyHandler) throws IOException, InterruptedException {
public <T> HttpResponse<T> sendRequest(HttpRequest request, HttpResponse.BodyHandler<T> bodyHandler)
throws IOException, InterruptedException {
try {
HttpResponse<T> response = httpClient.send(request, bodyHandler);
if (response.statusCode() >= 300) {
throw new OpaClientException("Error in communication with OPA server, status code: " + response.statusCode());
throw new OpaClientException(
"Error in communication with OPA server, status code: " + response.statusCode());
}
return response;
} catch (SocketException exception) {
Expand Down
26 changes: 26 additions & 0 deletions src/test/groovy/com/bisnode/opa/client/ManagingDocumentSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,30 @@ class ManagingDocumentSpec extends Specification {
thrown(OpaServerConnectionException)

}

def 'should include headers in document requests'() {
given:
def documentPath = 'somePath'
def endpoint = "/v1/data/$documentPath"
def headerName = 'X-API-Key'
def headerValue = 'secret-key-123'
def client = OpaClient.builder()
.opaConfiguration(url)
.header(headerName, headerValue)
.build()
wireMockServer
.stubFor(put(urlEqualTo(endpoint))
.withHeader(ContentType.HEADER_NAME, equalTo(APPLICATION_JSON))
.withHeader(headerName, equalTo(headerValue))
.willReturn(aResponse()
.withStatus(200)
.withHeader(ContentType.HEADER_NAME, APPLICATION_JSON)
.withBody('{}')))
when:
client.createOrOverwriteDocument(new OpaDocument(documentPath, DOCUMENT))
then:
noExceptionThrown()
wireMockServer.verify(putRequestedFor(urlEqualTo(endpoint))
.withHeader(headerName, equalTo(headerValue)))
}
}
Loading