no.digipost.api.useragreements.client.response.ResponseUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of digipost-useragreements-api-client-java Show documentation
Show all versions of digipost-useragreements-api-client-java Show documentation
Java library for interacting with the Digipost UserAgreements API
/**
* Copyright (C) Posten Norge AS
*
* 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 no.digipost.api.useragreements.client.response;
import no.digipost.api.useragreements.client.Error;
import no.digipost.api.useragreements.client.ErrorCode;
import no.digipost.api.useragreements.client.Headers;
import no.digipost.api.useragreements.client.RuntimeIOException;
import no.digipost.api.useragreements.client.UnexpectedResponseException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXB;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.Optional;
import java.util.Scanner;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static java.util.Spliterator.IMMUTABLE;
import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream;
import static no.digipost.api.useragreements.client.ErrorCode.MULTIPLE_ENTITIES;
import static no.digipost.api.useragreements.client.ErrorCode.NO_ENTITY;
public final class ResponseUtils {
private static final Logger RESPONSE_PAYLOAD_LOG = LoggerFactory.getLogger("no.digipost.api.useragreements.client.response_payload");
private static final Pattern startOfNewXmlDocument = Pattern.compile("(?<=\\>)\\s*(?=\\<\\?[xX][mM][lL])");
public static T mapOkResponseOrThrowException(HttpResponse response, Function okResponseMapper) {
final StatusLine statusLine = response.getStatusLine();
if (isOkResponse(statusLine.getStatusCode())) {
return okResponseMapper.apply(response);
} else if (statusLine.getStatusCode() == 429) { // Too Many Requests
TooManyRequestsException tooManyRequests;
try {
tooManyRequests = parseDelayDurationOfRetryAfterHeader(response)
.map(TooManyRequestsException::new)
.orElseGet(TooManyRequestsException::new);
} catch (Exception e) {
tooManyRequests = new TooManyRequestsException();
tooManyRequests.addSuppressed(e);
}
throw tooManyRequests;
} else {
throw new UnexpectedResponseException(statusLine, readErrorEntity(response));
}
}
public static T unmarshallEntity(final HttpResponse response, final Class returnType) {
try (Stream entityStream = unmarshallEntities(response, returnType)) {
Iterator entityIterator = entityStream.limit(2).iterator();
if (!entityIterator.hasNext()) {
throw new UnexpectedResponseException(response.getStatusLine(), NO_ENTITY,
"Message body is empty");
}
T theEntity = entityIterator.next();
if (entityIterator.hasNext()) {
throw new UnexpectedResponseException(response.getStatusLine(), MULTIPLE_ENTITIES,
"Message body contained more than one entity. First: " + theEntity + ", first excess one: " + entityIterator.next());
}
return theEntity;
}
}
public static Stream unmarshallEntities(final HttpResponse response, final Class returnType) {
return streamXmlDocumentsOf(getResponseEntityContent(response))
.peek(RESPONSE_PAYLOAD_LOG::trace)
.map(xml -> {
try {
return JAXB.unmarshal(new ByteArrayInputStream(xml.getBytes()), returnType);
} catch (IllegalStateException | DataBindingException e) {
throw new UnexpectedResponseException(response.getStatusLine(), ErrorCode.GENERAL_ERROR, xml, e);
}
});
}
public static Error readErrorEntity(final HttpResponse response) {
return unmarshallEntity(response, Error.class);
}
public static InputStream getResponseEntityContent(HttpResponse response) {
Optional closeableResponse = Optional.of(response).filter(r -> r instanceof CloseableHttpResponse).map(r -> (CloseableHttpResponse) r);
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (entity == null) {
closeableResponse.flatMap(r -> close(r)).map(RuntimeIOException::from).ifPresent(e -> { throw e; });
return null;
}
try {
return entity.getContent();
} catch (UnsupportedOperationException | IOException e) {
UnexpectedResponseException mainException = new UnexpectedResponseException(statusLine, ErrorCode.GENERAL_ERROR, e.getMessage(), e);
closeableResponse.flatMap(r -> close(r)).ifPresent(mainException::addSuppressed);
throw mainException;
}
}
public static Stream streamXmlDocumentsOf(InputStream inputStream) {
return streamDelimitedStringsOf(inputStream, UTF_8, startOfNewXmlDocument).filter(chunk -> !chunk.trim().isEmpty());
}
public static Stream streamDelimitedStringsOf(InputStream inputStream, Charset charset, Pattern delimiter) {
Scanner contentScanner = new Scanner(inputStream, charset.name());
return stream(spliteratorUnknownSize(contentScanner.useDelimiter(delimiter), IMMUTABLE), false)
.onClose(() -> close(contentScanner, inputStream).map(RuntimeIOException::from).ifPresent(exception -> { throw exception; }));
}
public static Optional close(AutoCloseable ... closeables) {
Exception exception = null;
for (AutoCloseable closeable : closeables) {
try (AutoCloseable autoClosed = closeable) {
continue;
} catch (Exception e) {
if (exception == null) {
exception = e;
} else {
exception.addSuppressed(e);
}
}
}
return Optional.ofNullable(exception);
}
public static Optional parseDelayDurationOfRetryAfterHeader(HttpResponse response) {
return parseDelayDurationOfRetryAfterHeader(response, Clock.systemUTC());
}
public static Optional parseDelayDurationOfRetryAfterHeader(HttpResponse response, Clock clock) {
return getValueOfFirstHeader(response, Headers.Retry_After)
.map(retryAfterValue -> {
try {
long parsedSeconds = Long.parseLong(retryAfterValue);
return Duration.ofSeconds(parsedSeconds);
} catch (NumberFormatException secondsNotParseable) {
try {
Instant parsedInstant = RFC_1123_DATE_TIME.parse(retryAfterValue, Instant::from);
return Duration.between(clock.instant(), parsedInstant);
} catch (RuntimeException e) {
e.addSuppressed(secondsNotParseable);
throw e;
}
}
});
}
public static Optional getValueOfFirstHeader(HttpResponse response, String headerName) {
return Optional.ofNullable(response.getFirstHeader(headerName)).flatMap(h -> Optional.ofNullable(h.getValue()));
}
public static boolean isOkResponse(HttpResponse response) {
return isOkResponse(response.getStatusLine());
}
public static boolean isOkResponse(StatusLine status) {
return isOkResponse(status.getStatusCode());
}
public static boolean isOkResponse(int statusCode) {
return statusCode / 100 == 2;
}
private ResponseUtils() {}
}