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 * <tr><th>XMPP Error Condition</th><th>Type</th><th>RFC 6120 Section</th></tr> 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>CANCEL</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>MODIFY</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>MODIFY</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 */ 065// TODO Rename this class to StanzaError (RFC 6120 ยง 8.3) in Smack 4.3, as this is what this class actually is. SMACK-769 066// TODO Use StanzaErrorTextElement here. 067public class XMPPError extends AbstractError { 068 069 public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas"; 070 public static final String ERROR = "error"; 071 072 private static final Logger LOGGER = Logger.getLogger(XMPPError.class.getName()); 073 static final Map<Condition, Type> CONDITION_TO_TYPE = new HashMap<Condition, Type>(); 074 075 static { 076 CONDITION_TO_TYPE.put(Condition.bad_request, Type.MODIFY); 077 CONDITION_TO_TYPE.put(Condition.conflict, Type.CANCEL); 078 CONDITION_TO_TYPE.put(Condition.feature_not_implemented, Type.CANCEL); 079 CONDITION_TO_TYPE.put(Condition.forbidden, Type.AUTH); 080 CONDITION_TO_TYPE.put(Condition.gone, Type.CANCEL); 081 CONDITION_TO_TYPE.put(Condition.internal_server_error, Type.CANCEL); 082 CONDITION_TO_TYPE.put(Condition.item_not_found, Type.CANCEL); 083 CONDITION_TO_TYPE.put(Condition.jid_malformed, Type.MODIFY); 084 CONDITION_TO_TYPE.put(Condition.not_acceptable, Type.MODIFY); 085 CONDITION_TO_TYPE.put(Condition.not_allowed, Type.CANCEL); 086 CONDITION_TO_TYPE.put(Condition.not_authorized, Type.AUTH); 087 CONDITION_TO_TYPE.put(Condition.policy_violation, Type.MODIFY); 088 CONDITION_TO_TYPE.put(Condition.recipient_unavailable, Type.WAIT); 089 CONDITION_TO_TYPE.put(Condition.redirect, Type.MODIFY); 090 CONDITION_TO_TYPE.put(Condition.registration_required, Type.AUTH); 091 CONDITION_TO_TYPE.put(Condition.remote_server_not_found, Type.CANCEL); 092 CONDITION_TO_TYPE.put(Condition.remote_server_timeout, Type.WAIT); 093 CONDITION_TO_TYPE.put(Condition.resource_constraint, Type.WAIT); 094 CONDITION_TO_TYPE.put(Condition.service_unavailable, Type.CANCEL); 095 CONDITION_TO_TYPE.put(Condition.subscription_required, Type.AUTH); 096 CONDITION_TO_TYPE.put(Condition.undefined_condition, Type.MODIFY); 097 CONDITION_TO_TYPE.put(Condition.unexpected_request, Type.WAIT); 098 } 099 100 private final Condition condition; 101 private final String conditionText; 102 private final String errorGenerator; 103 private final Type type; 104 private final Stanza stanza; 105 106 // TODO: Deprecated constructors 107 // deprecate in 4.3 108 109 /** 110 * Create a new XMPPError. 111 * 112 * @param condition error condition. 113 * @deprecated use {@link Builder} instead. 114 */ 115 @Deprecated 116 public XMPPError(Condition condition) { 117 this(condition, null, null, null, null, null, null); 118 } 119 120 /** 121 * Create a new XMPPError. 122 * 123 * @param condition error condition. 124 * @param applicationSpecificCondition application specific condition. 125 * @deprecated use {@link Builder} instead. 126 */ 127 @Deprecated 128 public XMPPError(Condition condition, ExtensionElement applicationSpecificCondition) { 129 this(condition, null, null, null, null, Arrays.asList(applicationSpecificCondition), null); 130 } 131 132 /** 133 * Creates a new error with the specified type, condition and message. 134 * This constructor is used when the condition is not recognized automatically by XMPPError 135 * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 136 * specification. 137 * 138 * @param type the error type. 139 * @param condition the error condition. 140 * @param conditionText 141 * @param errorGenerator 142 * @param descriptiveTexts 143 * @param extensions list of stanza(/packet) extensions 144 * @deprecated use {@link Builder} instead. 145 */ 146 @Deprecated 147 public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts, 148 List<ExtensionElement> extensions) { 149 this(condition, conditionText, errorGenerator, type, descriptiveTexts, extensions, null); 150 } 151 152 /** 153 * Creates a new error with the specified type, condition and message. 154 * This constructor is used when the condition is not recognized automatically by XMPPError 155 * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 156 * specification. 157 * 158 * @param type the error type. 159 * @param condition the error condition. 160 * @param conditionText 161 * @param errorGenerator 162 * @param descriptiveTexts 163 * @param extensions list of stanza(/packet) extensions 164 * @param stanza the stanza carrying this XMPP error. 165 */ 166 public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts, 167 List<ExtensionElement> extensions, Stanza stanza) { 168 super(descriptiveTexts, NAMESPACE, extensions); 169 this.condition = Objects.requireNonNull(condition, "condition must not be null"); 170 this.stanza = stanza; 171 // Some implementations may send the condition as non-empty element containing the empty string, that is 172 // <condition xmlns='foo'></condition>, in this case the parser may calls this constructor with the empty string 173 // as conditionText, therefore reset it to null if it's the empty string 174 if (StringUtils.isNullOrEmpty(conditionText)) { 175 conditionText = null; 176 } 177 if (conditionText != null) { 178 switch (condition) { 179 case gone: 180 case redirect: 181 break; 182 default: 183 throw new IllegalArgumentException( 184 "Condition text can only be set with condtion types 'gone' and 'redirect', not " 185 + condition); 186 } 187 } 188 this.conditionText = conditionText; 189 this.errorGenerator = errorGenerator; 190 if (type == null) { 191 Type determinedType = CONDITION_TO_TYPE.get(condition); 192 if (determinedType == null) { 193 LOGGER.warning("Could not determine type for condition: " + condition); 194 determinedType = Type.CANCEL; 195 } 196 this.type = determinedType; 197 } else { 198 this.type = type; 199 } 200 } 201 202 /** 203 * Returns the error condition. 204 * 205 * @return the error condition. 206 */ 207 public Condition getCondition() { 208 return condition; 209 } 210 211 /** 212 * Returns the error type. 213 * 214 * @return the error type. 215 */ 216 public Type getType() { 217 return type; 218 } 219 220 public String getErrorGenerator() { 221 return errorGenerator; 222 } 223 224 public String getConditionText() { 225 return conditionText; 226 } 227 228 /** 229 * Get the stanza carrying the XMPP error. 230 * 231 * @return the stanza carrying the XMPP error. 232 * @since 4.2 233 */ 234 public Stanza getStanza() { 235 return stanza; 236 } 237 238 @Override 239 public String toString() { 240 StringBuilder sb = new StringBuilder("XMPPError: "); 241 sb.append(condition.toString()).append(" - ").append(type.toString()); 242 if (errorGenerator != null) { 243 sb.append(". Generated by ").append(errorGenerator); 244 } 245 return sb.toString(); 246 } 247 248 /** 249 * Returns the error as XML. 250 * 251 * @return the error as XML. 252 */ 253 public XmlStringBuilder toXML() { 254 XmlStringBuilder xml = new XmlStringBuilder(); 255 xml.halfOpenElement(ERROR); 256 xml.attribute("type", type.toString()); 257 xml.optAttribute("by", errorGenerator); 258 xml.rightAngleBracket(); 259 260 xml.halfOpenElement(condition.toString()); 261 xml.xmlnsAttribute(NAMESPACE); 262 if (conditionText != null) { 263 xml.rightAngleBracket(); 264 xml.escape(conditionText); 265 xml.closeElement(condition.toString()); 266 } 267 else { 268 xml.closeEmptyElement(); 269 } 270 271 addDescriptiveTextsAndExtensions(xml); 272 273 xml.closeElement(ERROR); 274 return xml; 275 } 276 277 public static XMPPError.Builder from(Condition condition, String descriptiveText) { 278 XMPPError.Builder builder = getBuilder().setCondition(condition); 279 if (descriptiveText != null) { 280 Map<String, String> descriptiveTexts = new HashMap<>(); 281 descriptiveTexts.put("en", descriptiveText); 282 builder.setDescriptiveTexts(descriptiveTexts); 283 } 284 return builder; 285 } 286 287 public static Builder getBuilder() { 288 return new Builder(); 289 } 290 291 public static Builder getBuilder(Condition condition) { 292 return getBuilder().setCondition(condition); 293 } 294 295 public static Builder getBuilder(XMPPError xmppError) { 296 return getBuilder().copyFrom(xmppError); 297 } 298 299 public static final class Builder extends AbstractError.Builder<Builder> { 300 private Condition condition; 301 private String conditionText; 302 private String errorGenerator; 303 private Type type; 304 private Stanza stanza; 305 306 private Builder() { 307 } 308 309 public Builder setCondition(Condition condition) { 310 this.condition = condition; 311 return this; 312 } 313 314 public Builder setType(Type type) { 315 this.type = type; 316 return this; 317 } 318 319 public Builder setConditionText(String conditionText) { 320 this.conditionText = conditionText; 321 return this; 322 } 323 324 public Builder setErrorGenerator(String errorGenerator) { 325 this.errorGenerator = errorGenerator; 326 return this; 327 } 328 329 public Builder setStanza(Stanza stanza) { 330 this.stanza = stanza; 331 return this; 332 } 333 334 public Builder copyFrom(XMPPError xmppError) { 335 setCondition(xmppError.getCondition()); 336 setType(xmppError.getType()); 337 setConditionText(xmppError.getConditionText()); 338 setErrorGenerator(xmppError.getErrorGenerator()); 339 setStanza(xmppError.getStanza()); 340 setDescriptiveTexts(xmppError.descriptiveTexts); 341 setTextNamespace(xmppError.textNamespace); 342 setExtensions(xmppError.extensions); 343 return this; 344 } 345 346 public XMPPError build() { 347 return new XMPPError(condition, conditionText, errorGenerator, type, descriptiveTexts, 348 extensions, stanza); 349 } 350 351 @Override 352 protected Builder getThis() { 353 return this; 354 } 355 } 356 /** 357 * A class to represent the type of the Error. The types are: 358 * 359 * <ul> 360 * <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary) 361 * <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable) 362 * <li>XMPPError.Type.MODIFY - retry after changing the data sent 363 * <li>XMPPError.Type.AUTH - retry after providing credentials 364 * <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning) 365 * </ul> 366 */ 367 public static enum Type { 368 WAIT, 369 CANCEL, 370 MODIFY, 371 AUTH, 372 CONTINUE; 373 374 @Override 375 public String toString() { 376 return name().toLowerCase(Locale.US); 377 } 378 379 public static Type fromString(String string) { 380 string = string.toUpperCase(Locale.US); 381 return Type.valueOf(string); 382 } 383 } 384 385 public enum Condition { 386 bad_request, 387 conflict, 388 feature_not_implemented, 389 forbidden, 390 gone, 391 internal_server_error, 392 item_not_found, 393 jid_malformed, 394 not_acceptable, 395 not_allowed, 396 not_authorized, 397 policy_violation, 398 recipient_unavailable, 399 redirect, 400 registration_required, 401 remote_server_not_found, 402 remote_server_timeout, 403 resource_constraint, 404 service_unavailable, 405 subscription_required, 406 undefined_condition, 407 unexpected_request; 408 409 @Override 410 public String toString() { 411 return this.name().replace('_', '-'); 412 } 413 414 public static Condition fromString(String string) { 415 // Backwards compatibility for older implementations still using RFC 3920. RFC 6120 416 // changed 'xml-not-well-formed' to 'not-well-formed'. 417 if ("xml-not-well-formed".equals(string)) { 418 string = "not-well-formed"; 419 } 420 string = string.replace('-', '_'); 421 Condition condition = null; 422 try { 423 condition = Condition.valueOf(string); 424 } catch (Exception e) { 425 throw new IllegalStateException("Could not transform string '" + string + "' to XMPPErrorCondition", e); 426 } 427 return condition; 428 } 429 } 430 431}