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 if (errorGenerator != null) { 202 sb.append(". Generated by ").append(errorGenerator); 203 } 204 return sb.toString(); 205 } 206 207 @Override 208 public String getElementName() { 209 return ERROR; 210 } 211 212 @Override 213 public String getNamespace() { 214 return StreamOpen.CLIENT_NAMESPACE; 215 } 216 217 /** 218 * Returns the error as XML. 219 * 220 * @return the error as XML. 221 */ 222 public XmlStringBuilder toXML() { 223 return toXML(null); 224 } 225 226 @Override 227 public XmlStringBuilder toXML(String enclosingNamespace) { 228 XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); 229 xml.attribute("type", type.toString()); 230 xml.optAttribute("by", errorGenerator); 231 xml.rightAngleBracket(); 232 233 xml.halfOpenElement(condition.toString()); 234 xml.xmlnsAttribute(ERROR_CONDITION_AND_TEXT_NAMESPACE); 235 if (conditionText != null) { 236 xml.rightAngleBracket(); 237 xml.escape(conditionText); 238 xml.closeElement(condition.toString()); 239 } 240 else { 241 xml.closeEmptyElement(); 242 } 243 244 addDescriptiveTextsAndExtensions(xml); 245 246 xml.closeElement(this); 247 return xml; 248 } 249 250 public static StanzaError.Builder from(Condition condition, String descriptiveText) { 251 StanzaError.Builder builder = getBuilder().setCondition(condition); 252 if (descriptiveText != null) { 253 Map<String, String> descriptiveTexts = new HashMap<>(); 254 descriptiveTexts.put("en", descriptiveText); 255 builder.setDescriptiveTexts(descriptiveTexts); 256 } 257 return builder; 258 } 259 260 public static Builder getBuilder() { 261 return new Builder(); 262 } 263 264 public static Builder getBuilder(Condition condition) { 265 return getBuilder().setCondition(condition); 266 } 267 268 public static Builder getBuilder(StanzaError xmppError) { 269 return getBuilder().copyFrom(xmppError); 270 } 271 272 public static final class Builder extends AbstractError.Builder<Builder> { 273 private Condition condition; 274 private String conditionText; 275 private String errorGenerator; 276 private Type type; 277 private Stanza stanza; 278 279 private Builder() { 280 } 281 282 public Builder setCondition(Condition condition) { 283 this.condition = condition; 284 return this; 285 } 286 287 public Builder setType(Type type) { 288 this.type = type; 289 return this; 290 } 291 292 public Builder setConditionText(String conditionText) { 293 this.conditionText = conditionText; 294 return this; 295 } 296 297 public Builder setErrorGenerator(String errorGenerator) { 298 this.errorGenerator = errorGenerator; 299 return this; 300 } 301 302 public Builder setStanza(Stanza stanza) { 303 this.stanza = stanza; 304 return this; 305 } 306 307 public Builder copyFrom(StanzaError xmppError) { 308 setCondition(xmppError.getCondition()); 309 setType(xmppError.getType()); 310 setConditionText(xmppError.getConditionText()); 311 setErrorGenerator(xmppError.getErrorGenerator()); 312 setStanza(xmppError.getStanza()); 313 setDescriptiveTexts(xmppError.descriptiveTexts); 314 setTextNamespace(xmppError.textNamespace); 315 setExtensions(xmppError.extensions); 316 return this; 317 } 318 319 public StanzaError build() { 320 return new StanzaError(condition, conditionText, errorGenerator, type, descriptiveTexts, 321 extensions, stanza); 322 } 323 324 @Override 325 protected Builder getThis() { 326 return this; 327 } 328 } 329 /** 330 * A class to represent the type of the Error. The types are: 331 * 332 * <ul> 333 * <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary) 334 * <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable) 335 * <li>XMPPError.Type.MODIFY - retry after changing the data sent 336 * <li>XMPPError.Type.AUTH - retry after providing credentials 337 * <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning) 338 * </ul> 339 */ 340 public enum Type { 341 WAIT, 342 CANCEL, 343 MODIFY, 344 AUTH, 345 CONTINUE; 346 347 @Override 348 public String toString() { 349 return name().toLowerCase(Locale.US); 350 } 351 352 public static Type fromString(String string) { 353 string = string.toUpperCase(Locale.US); 354 return Type.valueOf(string); 355 } 356 } 357 358 public enum Condition { 359 bad_request, 360 conflict, 361 feature_not_implemented, 362 forbidden, 363 gone, 364 internal_server_error, 365 item_not_found, 366 jid_malformed, 367 not_acceptable, 368 not_allowed, 369 not_authorized, 370 policy_violation, 371 recipient_unavailable, 372 redirect, 373 registration_required, 374 remote_server_not_found, 375 remote_server_timeout, 376 resource_constraint, 377 service_unavailable, 378 subscription_required, 379 undefined_condition, 380 unexpected_request; 381 382 @Override 383 public String toString() { 384 return this.name().replace('_', '-'); 385 } 386 387 public static Condition fromString(String string) { 388 // Backwards compatibility for older implementations still using RFC 3920. RFC 6120 389 // changed 'xml-not-well-formed' to 'not-well-formed'. 390 if ("xml-not-well-formed".equals(string)) { 391 string = "not-well-formed"; 392 } 393 string = string.replace('-', '_'); 394 Condition condition = null; 395 try { 396 condition = Condition.valueOf(string); 397 } catch (Exception e) { 398 throw new IllegalStateException("Could not transform string '" + string + "' to XMPPErrorCondition", e); 399 } 400 return condition; 401 } 402 } 403 404}