001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2017 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j;
015
016import static java.util.stream.Collectors.toUnmodifiableList;
017
018import java.io.Serializable;
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.net.URL;
022import java.util.List;
023import java.util.Optional;
024
025import org.shredzone.acme4j.exception.AcmeProtocolException;
026import org.shredzone.acme4j.toolbox.JSON;
027import org.shredzone.acme4j.toolbox.JSON.Value;
028
029/**
030 * A JSON problem. It contains further, machine- and human-readable details about the
031 * reason of an error or failure.
032 *
033 * @see <a href="https://tools.ietf.org/html/rfc7807">RFC 7807</a>
034 */
035public class Problem implements Serializable {
036    private static final long serialVersionUID = -8418248862966754214L;
037
038    private final URL baseUrl;
039    private final JSON problemJson;
040
041    /**
042     * Creates a new {@link Problem} object.
043     *
044     * @param problem
045     *            Problem as JSON structure
046     * @param baseUrl
047     *            Document's base {@link URL} to resolve relative URIs against
048     */
049    public Problem(JSON problem, URL baseUrl) {
050        this.problemJson = problem;
051        this.baseUrl = baseUrl;
052    }
053
054    /**
055     * Returns the problem type. It is always an absolute URI.
056     */
057    public URI getType() {
058        return problemJson.get("type")
059                    .map(Value::asString)
060                    .map(it -> {
061                        try {
062                            return baseUrl.toURI().resolve(it);
063                        } catch (URISyntaxException ex) {
064                            throw new IllegalArgumentException("Bad base URL", ex);
065                        }
066                    })
067                    .orElseThrow(() -> new AcmeProtocolException("Problem without type"));
068    }
069
070    /**
071     * Returns a short, human-readable summary of the problem. The text may be localized
072     * if supported by the server. Empty if the server did not provide a title.
073     *
074     * @see #toString()
075     */
076    public Optional<String> getTitle() {
077        return problemJson.get("title").map(Value::asString);
078    }
079
080    /**
081     * Returns a detailed and specific human-readable explanation of the problem. The
082     * text may be localized if supported by the server.
083     *
084     * @see #toString()
085     */
086    public Optional<String> getDetail() {
087        return problemJson.get("detail").map(Value::asString);
088    }
089
090    /**
091     * Returns a URI that identifies the specific occurence of the problem. It is always
092     * an absolute URI.
093     */
094    public Optional<URI> getInstance() {
095        return problemJson.get("instance")
096                        .map(Value::asString)
097                        .map(it ->  {
098                            try {
099                                return baseUrl.toURI().resolve(it);
100                            } catch (URISyntaxException ex) {
101                                throw new IllegalArgumentException("Bad base URL", ex);
102                            }
103                        });
104    }
105
106    /**
107     * Returns the {@link Identifier} this problem relates to.
108     *
109     * @since 2.3
110     */
111    public Optional<Identifier> getIdentifier() {
112        return problemJson.get("identifier")
113                        .optional()
114                        .map(Value::asIdentifier);
115    }
116
117    /**
118     * Returns a list of sub-problems.
119     */
120    public List<Problem> getSubProblems() {
121        return problemJson.get("subproblems")
122                        .asArray()
123                        .stream()
124                        .map(o -> o.asProblem(baseUrl))
125                        .collect(toUnmodifiableList());
126    }
127
128    /**
129     * Returns the problem as {@link JSON} object, to access other, non-standard fields.
130     *
131     * @return Problem as {@link JSON} object
132     */
133    public JSON asJSON() {
134        return problemJson;
135    }
136
137    /**
138     * Returns a human-readable description of the problem, that is as specific as
139     * possible. The description may be localized if supported by the server.
140     * <p>
141     * If {@link #getSubProblems()} exist, they will be appended.
142     * <p>
143     * Technically, it returns {@link #getDetail()}. If not set, {@link #getTitle()} is
144     * returned instead. As a last resort, {@link #getType()} is returned.
145     */
146    @Override
147    public String toString() {
148        var sb = new StringBuilder();
149
150        if (getDetail().isPresent()) {
151            sb.append(getDetail().get());
152        } else if (getTitle().isPresent()) {
153            sb.append(getTitle().get());
154        } else {
155            sb.append(getType());
156        }
157
158        var subproblems = getSubProblems();
159
160        if (!subproblems.isEmpty()) {
161            sb.append(" (");
162            var first = true;
163            for (var sub : subproblems) {
164                if (!first) {
165                    sb.append(" ‒ ");
166                }
167                sb.append(sub.toString());
168                first = false;
169            }
170            sb.append(')');
171        }
172
173        return sb.toString();
174    }
175
176}