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}