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}