001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.bean.rfc7807; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.net.*; 022import java.util.*; 023 024import org.apache.juneau.commons.bean.*; 025import org.apache.juneau.marshall.*; 026 027/** 028 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807">RFC 7807 — Problem Details for HTTP APIs</a> 029 * bean. 030 * 031 * <p> 032 * Represents the canonical {@code application/problem+json} document. The five standard members defined by 033 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">§3.1</a> are modelled as typed 034 * properties; arbitrary <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">§3.2</a> 035 * extension members flatten into the top-level JSON object via a {@link BeanProp @BeanProp("*")} triplet. 036 * 037 * <h5 class='section'>Example:</h5> 038 * <p class='bjava'> 039 * Problem <jv>problem</jv> = <jk>new</jk> Problem() 040 * .setType(<jk>new</jk> URI(<js>"https://example.com/probs/out-of-credit"</js>)) 041 * .setTitle(<js>"You do not have enough credit."</js>) 042 * .setStatus(403) 043 * .setDetail(<js>"Your current balance is 30, but that costs 50."</js>) 044 * .setInstance(<jk>new</jk> URI(<js>"/account/12345/msgs/abc"</js>)) 045 * .set(<js>"balance"</js>, 30) 046 * .set(<js>"accounts"</js>, <jsm>list</jsm>(<js>"/account/12345"</js>, <js>"/account/67890"</js>)); 047 * 048 * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.write(<jv>problem</jv>); 049 * </p> 050 * 051 * <h5 class='section'>Notes:</h5> 052 * <ul class='spaced-list'> 053 * <li>The {@code type} field is nullable. {@link #getType()} returns the raw value (possibly {@code null}); 054 * {@link #getTypeOrDefault()} returns the spec default {@code "about:blank"} per 055 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 §3.1</a> when the 056 * field is unset. The field is never eagerly defaulted, so a fresh {@code Problem} round-trips without a 057 * synthetic {@code "type":"about:blank"} on the wire. 058 * <li>RFC 7807 was obsoleted by <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457</a> in 059 * July 2023, but the data model and the {@code application/problem+json} IANA registration are unchanged. 060 * </ul> 061 * 062 * <h5 class='section'>See Also:</h5><ul> 063 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanRfc7807">juneau-bean-rfc7807</a> 064 * </ul> 065 */ 066@Marshalled 067public class Problem { 068 069 /** 070 * The default {@code type} URI defined by 071 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 §3.1</a> for problem 072 * documents that omit the {@code type} member. 073 */ 074 public static final URI DEFAULT_TYPE = URI.create("about:blank"); 075 076 private URI type; 077 private String title; 078 private Integer status; 079 private String detail; 080 private URI instance; 081 private Map<String,Object> extra; 082 083 /** 084 * Convenience factory for the common "build a problem from a status code + title + detail" call site. 085 * 086 * <p> 087 * Populates only the {@code status}, {@code title}, and {@code detail} members. Callers wire from 088 * {@link Throwable}s or HTTP exception types themselves — for example: 089 * <p class='bjava'> 090 * Problem.<jsm>fromStatus</jsm>(<jv>e</jv>.getStatusCode(), <jv>e</jv>.getStatusLine().getReasonPhrase(), <jv>e</jv>.getMessage()); 091 * </p> 092 * 093 * @param status The HTTP status code (e.g. {@code 404}). 094 * @param title A short, human-readable summary of the problem type. Can be <jk>null</jk> to leave the title unset. 095 * @param detail A human-readable explanation specific to this occurrence. Can be <jk>null</jk> to leave the detail unset. 096 * @return A new {@link Problem} populated with the supplied values. 097 */ 098 public static Problem fromStatus(int status, String title, String detail) { 099 return new Problem().setStatus(status).setTitle(title).setDetail(detail); 100 } 101 102 /** 103 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">type</a> URI that identifies 104 * the problem type. 105 * 106 * <p> 107 * Returns the raw field value, which may be <jk>null</jk> when the {@code type} member is absent. Use 108 * {@link #getTypeOrDefault()} to get the spec default {@code "about:blank"} for absent values. 109 * 110 * @return The {@code type} URI, or <jk>null</jk> if not set. 111 */ 112 public URI getType() { 113 return type; 114 } 115 116 /** 117 * Returns the {@code type} URI, defaulting to {@link #DEFAULT_TYPE} ({@code "about:blank"}) per 118 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 §3.1</a> when the 119 * field is <jk>null</jk>. 120 * 121 * <p> 122 * The underlying field is left untouched, so this method never causes a synthetic {@code "type":"about:blank"} 123 * to appear on the wire. 124 * 125 * @return The {@code type} URI, never <jk>null</jk>. 126 */ 127 @BeanIgnore 128 public URI getTypeOrDefault() { 129 return or(type, DEFAULT_TYPE); 130 } 131 132 /** 133 * Sets the {@code type} URI. 134 * 135 * @param value The new value. Can be <jk>null</jk> to unset the property. 136 * @return This object. 137 */ 138 public Problem setType(URI value) { 139 type = value; 140 return this; 141 } 142 143 /** 144 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">title</a> — a short, 145 * human-readable summary of the problem type. 146 * 147 * @return The title, or <jk>null</jk> if not set. 148 */ 149 public String getTitle() { 150 return title; 151 } 152 153 /** 154 * Sets the {@code title}. 155 * 156 * @param value The new value. Can be <jk>null</jk> to unset the property. 157 * @return This object. 158 */ 159 public Problem setTitle(String value) { 160 title = value; 161 return this; 162 } 163 164 /** 165 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">status</a> — the HTTP 166 * status code originally generated by the origin server for this occurrence. 167 * 168 * <p> 169 * Modelled as {@link Integer} (boxed) because RFC 7807 declares {@code status} as OPTIONAL. 170 * 171 * @return The status code, or <jk>null</jk> if not set. 172 */ 173 public Integer getStatus() { 174 return status; 175 } 176 177 /** 178 * Sets the {@code status} HTTP status code. 179 * 180 * @param value The new value. Can be <jk>null</jk> to unset the property. 181 * @return This object. 182 */ 183 public Problem setStatus(Integer value) { 184 status = value; 185 return this; 186 } 187 188 /** 189 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">detail</a> — a 190 * human-readable explanation specific to this occurrence of the problem. 191 * 192 * @return The detail, or <jk>null</jk> if not set. 193 */ 194 public String getDetail() { 195 return detail; 196 } 197 198 /** 199 * Sets the {@code detail}. 200 * 201 * @param value The new value. Can be <jk>null</jk> to unset the property. 202 * @return This object. 203 */ 204 public Problem setDetail(String value) { 205 detail = value; 206 return this; 207 } 208 209 /** 210 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">instance</a> URI that 211 * identifies the specific occurrence of the problem. 212 * 213 * @return The {@code instance} URI, or <jk>null</jk> if not set. 214 */ 215 public URI getInstance() { 216 return instance; 217 } 218 219 /** 220 * Sets the {@code instance} URI. 221 * 222 * @param value The new value. Can be <jk>null</jk> to unset the property. 223 * @return This object. 224 */ 225 public Problem setInstance(URI value) { 226 instance = value; 227 return this; 228 } 229 230 /** 231 * Generic <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">extension-member</a> 232 * keyset. 233 * 234 * @return All extension keys on this problem. Never <jk>null</jk>. 235 */ 236 @BeanProp("*") 237 public Set<String> extraKeys() { 238 return extra == null ? Collections.emptySet() : u(extra.keySet()); 239 } 240 241 /** 242 * Generic extension-member getter. 243 * 244 * <p> 245 * Used by the bean framework to read arbitrary 246 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">§3.2</a> extension members 247 * that flatten into the top-level JSON object. 248 * 249 * @param property The property name to retrieve. Can be <jk>null</jk> (a <jk>null</jk> key simply resolves to no value). 250 * @return The property value, or <jk>null</jk> if the property does not exist. 251 */ 252 @BeanProp("*") 253 public Object get(String property) { 254 return extra == null ? null : extra.get(property); 255 } 256 257 /** 258 * Generic extension-member setter. 259 * 260 * <p> 261 * Used by the bean framework to set arbitrary 262 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">§3.2</a> extension members. 263 * The extension map is lazily initialised on first use. 264 * 265 * @param property The property name to set. Can be <jk>null</jk> ({@link LinkedHashMap} tolerates a <jk>null</jk> key). 266 * @param value The new value for the property. Can be <jk>null</jk> (stored as <jk>null</jk>). 267 * @return This object. 268 */ 269 @BeanProp("*") 270 public Problem set(String property, Object value) { 271 if (extra == null) 272 extra = m(); 273 extra.put(property, value); 274 return this; 275 } 276}