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