no.digipost.api.useragreements.client.response.ResponseUtils.java Source code

Java tutorial

Introduction

Here is the source code for no.digipost.api.useragreements.client.response.ResponseUtils.java

Source

/**
 * 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> T mapOkResponseOrThrowException(HttpResponse response,
            Function<HttpResponse, T> 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> T unmarshallEntity(final HttpResponse response, final Class<T> returnType) {
        try (Stream<T> entityStream = unmarshallEntities(response, returnType)) {
            Iterator<T> 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 <T> Stream<T> unmarshallEntities(final HttpResponse response, final Class<T> 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<CloseableHttpResponse> 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<String> streamXmlDocumentsOf(InputStream inputStream) {
        return streamDelimitedStringsOf(inputStream, UTF_8, startOfNewXmlDocument)
                .filter(chunk -> !chunk.trim().isEmpty());
    }

    public static Stream<String> 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<Exception> 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<Duration> parseDelayDurationOfRetryAfterHeader(HttpResponse response) {
        return parseDelayDurationOfRetryAfterHeader(response, Clock.systemUTC());
    }

    public static Optional<Duration> 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<String> 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() {
    }

}