001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2015-2019 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: 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 110 /** 111 * Creates a new error with the specified type, condition and message. 112 * This constructor is used when the condition is not recognized automatically by XMPPError 113 * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 114 * specification. 115 * 116 * @param type the error type. 117 * @param condition the error condition. 118 * @param conditionText TODO javadoc me please 119 * @param errorGenerator TODO javadoc me please 120 * @param descriptiveTexts TODO javadoc me please 121 * @param extensions list of stanza extensions 122 */ 123 public StanzaError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts, 124 List<ExtensionElement> extensions) { 125 super(descriptiveTexts, ERROR_CONDITION_AND_TEXT_NAMESPACE, extensions); 126 this.condition = Objects.requireNonNull(condition, "condition must not be null"); 127 // Some implementations may send the condition as non-empty element containing the empty string, that is 128 // <condition xmlns='foo'></condition>, in this case the parser may calls this constructor with the empty string 129 // as conditionText, therefore reset it to null if it's the empty string 130 if (StringUtils.isNullOrEmpty(conditionText)) { 131 conditionText = null; 132 } 133 if (conditionText != null) { 134 switch (condition) { 135 case gone: 136 case redirect: 137 break; 138 default: 139 throw new IllegalArgumentException( 140 "Condition text can only be set with condtion types 'gone' and 'redirect', not " 141 + condition); 142 } 143 } 144 this.conditionText = conditionText; 145 this.errorGenerator = errorGenerator; 146 if (type == null) { 147 Type determinedType = CONDITION_TO_TYPE.get(condition); 148 if (determinedType == null) { 149 LOGGER.warning("Could not determine type for condition: " + condition); 150 determinedType = Type.CANCEL; 151 } 152 this.type = determinedType; 153 } else { 154 this.type = type; 155 } 156 } 157 158 /** 159 * Returns the error condition. 160 * 161 * @return the error condition. 162 */ 163 public Condition getCondition() { 164 return condition; 165 } 166 167 /** 168 * Returns the error type. 169 * 170 * @return the error type. 171 */ 172 public Type getType() { 173 return type; 174 } 175 176 public String getErrorGenerator() { 177 return errorGenerator; 178 } 179 180 public String getConditionText() { 181 return conditionText; 182 } 183 184 @Override 185 public String toString() { 186 StringBuilder sb = new StringBuilder("XMPPError: "); 187 sb.append(condition.toString()).append(" - ").append(type.toString()); 188 189 String descriptiveText = getDescriptiveText(); 190 if (descriptiveText != null) { 191 sb.append(" [").append(descriptiveText).append(']'); 192 } 193 194 if (errorGenerator != null) { 195 sb.append(". Generated by ").append(errorGenerator); 196 } 197 return sb.toString(); 198 } 199 200 @Override 201 public String getElementName() { 202 return ERROR; 203 } 204 205 @Override 206 public String getNamespace() { 207 return StreamOpen.CLIENT_NAMESPACE; 208 } 209 210 @Override 211 public XmlStringBuilder toXML(XmlEnvironment enclosingNamespace) { 212 XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); 213 xml.attribute("type", type.toString()); 214 xml.optAttribute("by", errorGenerator); 215 xml.rightAngleBracket(); 216 217 xml.halfOpenElement(condition.toString()); 218 xml.xmlnsAttribute(ERROR_CONDITION_AND_TEXT_NAMESPACE); 219 if (conditionText != null) { 220 xml.rightAngleBracket(); 221 xml.escape(conditionText); 222 xml.closeElement(condition.toString()); 223 } 224 else { 225 xml.closeEmptyElement(); 226 } 227 228 addDescriptiveTextsAndExtensions(xml); 229 230 xml.closeElement(this); 231 return xml; 232 } 233 234 public static StanzaError.Builder from(Condition condition, String descriptiveText) { 235 StanzaError.Builder builder = getBuilder().setCondition(condition); 236 if (descriptiveText != null) { 237 Map<String, String> descriptiveTexts = new HashMap<>(); 238 descriptiveTexts.put("en", descriptiveText); 239 builder.setDescriptiveTexts(descriptiveTexts); 240 } 241 return builder; 242 } 243 244 public static Builder getBuilder() { 245 return new Builder(); 246 } 247 248 public static Builder getBuilder(Condition condition) { 249 return getBuilder().setCondition(condition); 250 } 251 252 public static Builder getBuilder(StanzaError xmppError) { 253 return getBuilder().copyFrom(xmppError); 254 } 255 256 public static final class Builder extends AbstractError.Builder<Builder> { 257 private Condition condition; 258 private String conditionText; 259 private String errorGenerator; 260 private Type type; 261 262 private Builder() { 263 } 264 265 public Builder setCondition(Condition condition) { 266 this.condition = condition; 267 return this; 268 } 269 270 public Builder setType(Type type) { 271 this.type = type; 272 return this; 273 } 274 275 public Builder setConditionText(String conditionText) { 276 this.conditionText = conditionText; 277 return this; 278 } 279 280 public Builder setErrorGenerator(String errorGenerator) { 281 this.errorGenerator = errorGenerator; 282 return this; 283 } 284 285 public Builder copyFrom(StanzaError xmppError) { 286 setCondition(xmppError.getCondition()); 287 setType(xmppError.getType()); 288 setConditionText(xmppError.getConditionText()); 289 setErrorGenerator(xmppError.getErrorGenerator()); 290 setDescriptiveTexts(xmppError.descriptiveTexts); 291 setTextNamespace(xmppError.textNamespace); 292 setExtensions(xmppError.extensions); 293 return this; 294 } 295 296 public StanzaError build() { 297 return new StanzaError(condition, conditionText, errorGenerator, type, descriptiveTexts, 298 extensions); 299 } 300 301 @Override 302 protected Builder getThis() { 303 return this; 304 } 305 } 306 /** 307 * A class to represent the type of the Error. The types are: 308 * 309 * <ul> 310 * <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary) 311 * <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable) 312 * <li>XMPPError.Type.MODIFY - retry after changing the data sent 313 * <li>XMPPError.Type.AUTH - retry after providing credentials 314 * <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning) 315 * </ul> 316 */ 317 public enum Type { 318 WAIT, 319 CANCEL, 320 MODIFY, 321 AUTH, 322 CONTINUE; 323 324 @Override 325 public String toString() { 326 return name().toLowerCase(Locale.US); 327 } 328 329 public static Type fromString(String string) { 330 string = string.toUpperCase(Locale.US); 331 return Type.valueOf(string); 332 } 333 } 334 335 public enum Condition { 336 bad_request, 337 conflict, 338 feature_not_implemented, 339 forbidden, 340 gone, 341 internal_server_error, 342 item_not_found, 343 jid_malformed, 344 not_acceptable, 345 not_allowed, 346 not_authorized, 347 policy_violation, 348 recipient_unavailable, 349 redirect, 350 registration_required, 351 remote_server_not_found, 352 remote_server_timeout, 353 resource_constraint, 354 service_unavailable, 355 subscription_required, 356 undefined_condition, 357 unexpected_request; 358 359 @Override 360 public String toString() { 361 return this.name().replace('_', '-'); 362 } 363 364 public static Condition fromString(String string) { 365 string = string.replace('-', '_'); 366 Condition condition = null; 367 try { 368 condition = Condition.valueOf(string); 369 } catch (Exception e) { 370 throw new IllegalStateException("Could not transform string '" + string + "' to XMPPErrorCondition", e); 371 } 372 return condition; 373 } 374 } 375 376}