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