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}