001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2015 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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.jivesoftware.smack.packet; 018 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.logging.Logger; 025 026import org.jivesoftware.smack.util.Objects; 027import org.jivesoftware.smack.util.StringUtils; 028import org.jivesoftware.smack.util.XmlStringBuilder; 029 030/** 031 * Represents an XMPP error sub-packet. Typically, a server responds to a request that has 032 * problems by sending the stanza(/packet) back and including an error packet. Each error has a type, 033 * error condition as well as as an optional text explanation. Typical errors are:<p> 034 * 035 * <table border=1> 036 * <caption>XMPP Errors</caption> 037 * <hr><td><b>XMPP Error Condition</b></td><td><b>Type</b></td><td><b>RFC 6120 Section</b></td></hr> 038 * <tr><td>bad-request</td><td>MODIFY</td><td>8.3.3.1</td></tr> 039 * <tr><td>conflict</td><td>CANCEL</td><td>8.3.3.2</td></tr> 040 * <tr><td>feature-not-implemented</td><td>CANCEL</td><td>8.3.3.3</td></tr> 041 * <tr><td>forbidden</td><td>AUTH</td><td>8.3.3.4</td></tr> 042 * <tr><td>gone</td><td>MODIFY</td><td>8.3.3.5</td></tr> 043 * <tr><td>internal-server-error</td><td>WAIT</td><td>8.3.3.6</td></tr> 044 * <tr><td>item-not-found</td><td>CANCEL</td><td>8.3.3.7</td></tr> 045 * <tr><td>jid-malformed</td><td>MODIFY</td><td>8.3.3.8</td></tr> 046 * <tr><td>not-acceptable</td><td> MODIFY</td><td>8.3.3.9</td></tr> 047 * <tr><td>not-allowed</td><td>CANCEL</td><td>8.3.3.10</td></tr> 048 * <tr><td>not-authorized</td><td>AUTH</td><td>8.3.3.11</td></tr> 049 * <tr><td>policy-violation</td><td>AUTH</td><td>8.3.3.12</td></tr> 050 * <tr><td>recipient-unavailable</td><td>WAIT</td><td>8.3.3.13</td></tr> 051 * <tr><td>redirect</td><td>MODIFY</td><td>8.3.3.14</td></tr> 052 * <tr><td>registration-required</td><td>AUTH</td><td>8.3.3.15</td></tr> 053 * <tr><td>remote-server-not-found</td><td>CANCEL</td><td>8.3.3.16</td></tr> 054 * <tr><td>remote-server-timeout</td><td>WAIT</td><td>8.3.3.17</td></tr> 055 * <tr><td>resource-constraint</td><td>WAIT</td><td>8.3.3.18</td></tr> 056 * <tr><td>service-unavailable</td><td>CANCEL</td><td>8.3.3.19</td></tr> 057 * <tr><td>subscription-required</td><td>AUTH</td><td>8.3.3.20</td></tr> 058 * <tr><td>undefined-condition</td><td>WAIT</td><td>8.3.3.21</td></tr> 059 * <tr><td>unexpected-request</td><td>WAIT</td><td>8.3.3.22</td></tr> 060 * </table> 061 * 062 * @author Matt Tucker 063 * @see <a href="http://xmpp.org/rfcs/rfc6120.html#stanzas-error-syntax">RFC 6120 - 8.3.2 Syntax: The Syntax of XMPP error stanzas</a> 064 */ 065public class XMPPError extends AbstractError { 066 067 public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas"; 068 public static final String ERROR = "error"; 069 070 private static final Logger LOGGER = Logger.getLogger(XMPPError.class.getName()); 071 private static final Map<Condition, Type> CONDITION_TO_TYPE = new HashMap<Condition, Type>(); 072 073 static { 074 CONDITION_TO_TYPE.put(Condition.bad_request, Type.MODIFY); 075 CONDITION_TO_TYPE.put(Condition.conflict, Type.CANCEL); 076 CONDITION_TO_TYPE.put(Condition.feature_not_implemented, Type.CANCEL); 077 CONDITION_TO_TYPE.put(Condition.forbidden, Type.AUTH); 078 CONDITION_TO_TYPE.put(Condition.gone, Type.CANCEL); 079 CONDITION_TO_TYPE.put(Condition.internal_server_error, Type.CANCEL); 080 CONDITION_TO_TYPE.put(Condition.item_not_found, Type.CANCEL); 081 CONDITION_TO_TYPE.put(Condition.jid_malformed, Type.MODIFY); 082 CONDITION_TO_TYPE.put(Condition.not_acceptable, Type.MODIFY); 083 CONDITION_TO_TYPE.put(Condition.not_allowed, Type.CANCEL); 084 CONDITION_TO_TYPE.put(Condition.not_authorized, Type.AUTH); 085 CONDITION_TO_TYPE.put(Condition.policy_violation, Type.MODIFY); 086 CONDITION_TO_TYPE.put(Condition.recipient_unavailable, Type.WAIT); 087 CONDITION_TO_TYPE.put(Condition.redirect, Type.MODIFY); 088 CONDITION_TO_TYPE.put(Condition.registration_required, Type.AUTH); 089 CONDITION_TO_TYPE.put(Condition.remote_server_not_found, Type.CANCEL); 090 CONDITION_TO_TYPE.put(Condition.remote_server_timeout, Type.WAIT); 091 CONDITION_TO_TYPE.put(Condition.resource_constraint, Type.WAIT); 092 CONDITION_TO_TYPE.put(Condition.service_unavailable, Type.WAIT); 093 CONDITION_TO_TYPE.put(Condition.subscription_required, Type.WAIT); 094 CONDITION_TO_TYPE.put(Condition.unexpected_request, Type.MODIFY); 095 } 096 097 private final Condition condition; 098 private final String conditionText; 099 private final String errorGenerator; 100 private final Type type; 101 private final Stanza stanza; 102 103 // TODO: Deprecated constructors 104 // deprecate in 4.3 105 106 /** 107 * Create a new XMPPError. 108 * 109 * @param condition 110 * @deprecated use {@link Builder} instead. 111 */ 112 @Deprecated 113 public XMPPError(Condition condition) { 114 this(condition, null, null, null, null, null, null); 115 } 116 117 /** 118 * Create a new XMPPError. 119 * 120 * @param condition 121 * @param applicationSpecificCondition 122 * @deprecated use {@link Builder} instead. 123 */ 124 @Deprecated 125 public XMPPError(Condition condition, ExtensionElement applicationSpecificCondition) { 126 this(condition, null, null, null, null, Arrays.asList(applicationSpecificCondition), null); 127 } 128 129 /** 130 * Creates a new error with the specified type, condition and message. 131 * This constructor is used when the condition is not recognized automatically by XMPPError 132 * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 133 * specification. 134 * 135 * @param type the error type. 136 * @param condition the error condition. 137 * @param descriptiveTexts 138 * @param extensions list of stanza(/packet) extensions 139 * @deprecated use {@link Builder} instead. 140 */ 141 @Deprecated 142 public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts, 143 List<ExtensionElement> extensions) { 144 this(condition, conditionText, errorGenerator, type, descriptiveTexts, extensions, null); 145 } 146 147 /** 148 * Creates a new error with the specified type, condition and message. 149 * This constructor is used when the condition is not recognized automatically by XMPPError 150 * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 151 * specification. 152 * 153 * @param type the error type. 154 * @param condition the error condition. 155 * @param descriptiveTexts 156 * @param extensions list of stanza(/packet) extensions 157 * @param stanza the stanza carrying this XMPP error. 158 */ 159 public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts, 160 List<ExtensionElement> extensions, Stanza stanza) { 161 super(descriptiveTexts, NAMESPACE, extensions); 162 this.condition = Objects.requireNonNull(condition, "condition must not be null"); 163 this.stanza = stanza; 164 // Some implementations may send the condition as non-empty element containing the empty string, that is 165 // <condition xmlns='foo'></condition>, in this case the parser may calls this constructor with the empty string 166 // as conditionText, therefore reset it to null if it's the empty string 167 if (StringUtils.isNullOrEmpty(conditionText)) { 168 conditionText = null; 169 } 170 if (conditionText != null) { 171 switch (condition) { 172 case gone: 173 case redirect: 174 break; 175 default: 176 throw new IllegalArgumentException( 177 "Condition text can only be set with condtion types 'gone' and 'redirect', not " 178 + condition); 179 } 180 } 181 this.conditionText = conditionText; 182 this.errorGenerator = errorGenerator; 183 if (type == null) { 184 Type determinedType = CONDITION_TO_TYPE.get(condition); 185 if (determinedType == null) { 186 LOGGER.warning("Could not determine type for condition: " + condition); 187 determinedType = Type.CANCEL; 188 } 189 this.type = determinedType; 190 } else { 191 this.type = type; 192 } 193 } 194 195 /** 196 * Returns the error condition. 197 * 198 * @return the error condition. 199 */ 200 public Condition getCondition() { 201 return condition; 202 } 203 204 /** 205 * Returns the error type. 206 * 207 * @return the error type. 208 */ 209 public Type getType() { 210 return type; 211 } 212 213 public String getErrorGenerator() { 214 return errorGenerator; 215 } 216 217 public String getConditionText() { 218 return conditionText; 219 } 220 221 /** 222 * Get the stanza carrying the XMPP error. 223 * 224 * @return the stanza carrying the XMPP error. 225 * @since 4.2 226 */ 227 public Stanza getStanza() { 228 return stanza; 229 } 230 231 @Override 232 public String toString() { 233 StringBuilder sb = new StringBuilder("XMPPError: "); 234 sb.append(condition.toString()).append(" - ").append(type.toString()); 235 if (errorGenerator != null) { 236 sb.append(". Generated by ").append(errorGenerator); 237 } 238 return sb.toString(); 239 } 240 241 /** 242 * Returns the error as XML. 243 * 244 * @return the error as XML. 245 */ 246 public XmlStringBuilder toXML() { 247 XmlStringBuilder xml = new XmlStringBuilder(); 248 xml.halfOpenElement(ERROR); 249 xml.attribute("type", type.toString()); 250 xml.optAttribute("by", errorGenerator); 251 xml.rightAngleBracket(); 252 253 xml.halfOpenElement(condition.toString()); 254 xml.xmlnsAttribute(NAMESPACE); 255 if (conditionText != null) { 256 xml.rightAngleBracket(); 257 xml.escape(conditionText); 258 xml.closeElement(condition.toString()); 259 } 260 else { 261 xml.closeEmptyElement(); 262 } 263 264 addDescriptiveTextsAndExtensions(xml); 265 266 xml.closeElement(ERROR); 267 return xml; 268 } 269 270 public static XMPPError.Builder from(Condition condition, String descriptiveText) { 271 Map<String, String> descriptiveTexts = new HashMap<String, String>(); 272 descriptiveTexts.put("en", descriptiveText); 273 return getBuilder().setCondition(condition).setDescriptiveTexts(descriptiveTexts); 274 } 275 276 public static Builder getBuilder() { 277 return new Builder(); 278 } 279 280 public static Builder getBuilder(Condition condition) { 281 return getBuilder().setCondition(condition); 282 } 283 284 public static Builder getBuilder(XMPPError xmppError) { 285 return getBuilder().copyFrom(xmppError); 286 } 287 288 public static final class Builder extends AbstractError.Builder<Builder> { 289 private Condition condition; 290 private String conditionText; 291 private String errorGenerator; 292 private Type type; 293 private Stanza stanza; 294 295 private Builder() { 296 } 297 298 public Builder setCondition(Condition condition) { 299 this.condition = condition; 300 return this; 301 } 302 303 public Builder setType(Type type) { 304 this.type = type; 305 return this; 306 } 307 308 public Builder setConditionText(String conditionText) { 309 this.conditionText = conditionText; 310 return this; 311 } 312 313 public Builder setErrorGenerator(String errorGenerator) { 314 this.errorGenerator = errorGenerator; 315 return this; 316 } 317 318 public Builder setStanza(Stanza stanza) { 319 this.stanza = stanza; 320 return this; 321 } 322 323 public Builder copyFrom(XMPPError xmppError) { 324 setCondition(xmppError.getCondition()); 325 setType(xmppError.getType()); 326 setConditionText(xmppError.getConditionText()); 327 setErrorGenerator(xmppError.getErrorGenerator()); 328 setStanza(xmppError.getStanza()); 329 setDescriptiveTexts(xmppError.descriptiveTexts); 330 setTextNamespace(xmppError.textNamespace); 331 setExtensions(xmppError.extensions); 332 return this; 333 } 334 335 public XMPPError build() { 336 return new XMPPError(condition, conditionText, errorGenerator, type, descriptiveTexts, 337 extensions, stanza); 338 } 339 340 @Override 341 protected Builder getThis() { 342 return this; 343 } 344 } 345 /** 346 * A class to represent the type of the Error. The types are: 347 * 348 * <ul> 349 * <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary) 350 * <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable) 351 * <li>XMPPError.Type.MODIFY - retry after changing the data sent 352 * <li>XMPPError.Type.AUTH - retry after providing credentials 353 * <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning) 354 * </ul> 355 */ 356 public static enum Type { 357 WAIT, 358 CANCEL, 359 MODIFY, 360 AUTH, 361 CONTINUE; 362 363 @Override 364 public String toString() { 365 return name().toLowerCase(Locale.US); 366 } 367 368 public static Type fromString(String string) { 369 string = string.toUpperCase(Locale.US); 370 return Type.valueOf(string); 371 } 372 } 373 374 public enum Condition { 375 bad_request, 376 conflict, 377 feature_not_implemented, 378 forbidden, 379 gone, 380 internal_server_error, 381 item_not_found, 382 jid_malformed, 383 not_acceptable, 384 not_allowed, 385 not_authorized, 386 policy_violation, 387 recipient_unavailable, 388 redirect, 389 registration_required, 390 remote_server_not_found, 391 remote_server_timeout, 392 resource_constraint, 393 service_unavailable, 394 subscription_required, 395 undefined_condition, 396 unexpected_request; 397 398 @Override 399 public String toString() { 400 return this.name().replace('_', '-'); 401 } 402 403 public static Condition fromString(String string) { 404 // Backwards compatibility for older implementations still using RFC 3920. RFC 6120 405 // changed 'xml-not-well-formed' to 'not-well-formed'. 406 if ("xml-not-well-formed".equals(string)) { 407 string = "not-well-formed"; 408 } 409 string = string.replace('-', '_'); 410 Condition condition = null; 411 try { 412 condition = Condition.valueOf(string); 413 } catch (Exception e) { 414 throw new IllegalStateException("Could not transform string '" + string + "' to XMPPErrorCondition", e); 415 } 416 return condition; 417 } 418 } 419 420}