001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.spi.connector.transport.http.RFC9457;
020
021import java.io.IOException;
022
023/**
024 * A reporter for RFC 9457 messages.
025 * RFC 9457 is a standard for reporting problems in HTTP responses as a JSON object.
026 * There are members specified in the RFC but none of those appear to be required,
027 * see <a href=https://www.rfc-editor.org/rfc/rfc9457#section-3-7>rfc9457 section 3.7</a>
028 * Given the JSON fields are not mandatory, this reporter simply extracts the body of the
029 * response without validation.
030 * A RFC 9457 message is detected by the content type {@value #CONTENT_TYPE_PROBLEM_DETAILS_JSON} in the response header.
031 *
032 * @param <T> The type of the response.
033 * @param <E> The base exception type to throw if the response is not a RFC9457 message.
034 * @param <R> The type of the request or request builder (which allows to modify headers)
035 * @see <a href=https://www.rfc-editor.org/rfc/rfc9457#section-3-7>RFC 9457</a>
036 */
037public abstract class RFC9457Reporter<T, E extends Exception, R> {
038    public static final String CONTENT_TYPE_PROBLEM_DETAILS_JSON = "application/problem+json";
039    public static final String CONTENT_TYPE_PROBLEM_DETAILS_JSON_AND_ANY = "application/problem+json,*/*";
040
041    protected abstract boolean isRFC9457Message(T response);
042
043    protected abstract int getStatusCode(T response);
044
045    protected abstract String getReasonPhrase(T response);
046
047    protected abstract String getBody(T response) throws IOException;
048
049    /**
050     * Prepares the request to accept RFC 9457 responses.
051     * This involves setting/updating the "Accept" header to include "application/problem+json".
052     * @param request The request or request builder to prepare
053     * @see <a href=https://www.rfc-editor.org/rfc/rfc9457#section-3-2>RFC 9457 section 3.2</a>
054     */
055    public abstract void prepareRequest(R request);
056
057    protected boolean hasRFC9457ContentType(String contentType) {
058        if (contentType == null) {
059            return false;
060        }
061        // strip off parameters
062        int idx = contentType.indexOf(';');
063        if (idx > -1) {
064            contentType = contentType.substring(0, idx);
065        }
066        return CONTENT_TYPE_PROBLEM_DETAILS_JSON.equals(contentType);
067    }
068
069    /**
070     * Generates a {@link HttpRFC9457Exception} if the response type is a RFC 9457 message.
071     * Otherwise, it throws the base exception
072     *
073     * @param response The response to check for RFC 9457 messages.
074     * @param baseException The base exception to throw if the response is not a RFC 9457 message.
075     */
076    public void generateException(T response, BiConsumerChecked<Integer, String, E> baseException)
077            throws E, HttpRFC9457Exception {
078        int statusCode = getStatusCode(response);
079        String reasonPhrase = getReasonPhrase(response);
080
081        if (isRFC9457Message(response)) {
082            String body;
083            try {
084                body = getBody(response);
085            } catch (IOException ignore) {
086                // No body found but it is representing a RFC 9457 message due to the content type.
087                throw new HttpRFC9457Exception(statusCode, reasonPhrase, RFC9457Payload.INSTANCE);
088            }
089
090            if (body != null && !body.isEmpty()) {
091                RFC9457Payload rfc9457Payload = RFC9457Parser.parse(body);
092                throw new HttpRFC9457Exception(statusCode, reasonPhrase, rfc9457Payload);
093            }
094            throw new HttpRFC9457Exception(statusCode, reasonPhrase, RFC9457Payload.INSTANCE);
095        }
096        baseException.accept(statusCode, reasonPhrase);
097    }
098}