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.Collections.unmodifiableList;
017import static java.util.stream.Collectors.toList;
018
019import java.io.Serializable;
020import java.net.URI;
021import java.net.URISyntaxException;
022import java.net.URL;
023import java.util.List;
024
025import edu.umd.cs.findbugs.annotations.Nullable;
026import org.shredzone.acme4j.exception.AcmeProtocolException;
027import org.shredzone.acme4j.toolbox.JSON;
028import org.shredzone.acme4j.toolbox.JSON.Value;
029
030/**
031 * Represents a JSON Problem.
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. {@code null} if the server did not provide a title.
073     *
074     * @see #toString()
075     */
076    @Nullable
077    public String getTitle() {
078        return problemJson.get("title").map(Value::asString).orElse(null);
079    }
080
081    /**
082     * Returns a detailed and specific human-readable explanation of the problem. The
083     * text may be localized if supported by the server.
084     *
085     * @see #toString()
086     */
087    @Nullable
088    public String getDetail() {
089        return problemJson.get("detail").map(Value::asString).orElse(null);
090    }
091
092    /**
093     * Returns an URI that identifies the specific occurence of the problem. It is always
094     * an absolute URI.
095     */
096    @Nullable
097    public URI getInstance() {
098        return problemJson.get("instance")
099                        .map(Value::asString)
100                        .map(it ->  {
101                            try {
102                                return baseUrl.toURI().resolve(it);
103                            } catch (URISyntaxException ex) {
104                                throw new IllegalArgumentException("Bad base URL", ex);
105                            }
106                        })
107                        .orElse(null);
108    }
109
110    /**
111     * Returns the {@link Identifier} this problem relates to. May be {@code null}.
112     *
113     * @since 2.3
114     */
115    @Nullable
116    public Identifier getIdentifier() {
117        return problemJson.get("identifier")
118                        .optional()
119                        .map(Value::asIdentifier)
120                        .orElse(null);
121    }
122
123    /**
124     * Returns a list of sub-problems. May be empty, but is never {@code null}.
125     */
126    public List<Problem> getSubProblems() {
127        return unmodifiableList(
128                problemJson.get("subproblems")
129                        .asArray()
130                        .stream()
131                        .map(o -> o.asProblem(baseUrl))
132                        .collect(toList())
133        );
134    }
135
136    /**
137     * Returns the problem as {@link JSON} object, to access other fields.
138     *
139     * @return Problem as {@link JSON} object
140     */
141    public JSON asJSON() {
142        return problemJson;
143    }
144
145    /**
146     * Returns a human-readable description of the problem, that is as specific as
147     * possible. The description may be localized if supported by the server.
148     * <p>
149     * If {@link #getSubProblems()} exist, they will be appended.
150     * <p>
151     * Technically, it returns {@link #getDetail()}. If not set, {@link #getTitle()} is
152     * returned instead. As a last resort, {@link #getType()} is returned.
153     */
154    @Override
155    public String toString() {
156        StringBuilder sb = new StringBuilder();
157
158        if (getDetail() != null) {
159            sb.append(getDetail());
160        } else if (getTitle() != null) {
161            sb.append(getTitle());
162        } else {
163            sb.append(getType());
164        }
165
166        List<Problem> subproblems = getSubProblems();
167
168        if (!subproblems.isEmpty()) {
169            sb.append(" (");
170            boolean first = true;
171            for (Problem sub : subproblems) {
172                if (!first) {
173                    sb.append(" ‒ ");
174                }
175                sb.append(sub.toString());
176                first = false;
177            }
178            sb.append(')');
179        }
180
181        return sb.toString();
182    }
183
184}