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 *      <hr><td><b>XMPP Error Condition</b></td><td><b>Type</b></td><td><b>RFC 6120 Section</b></td></hr>
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>MODIFY</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>AUTH</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>WAIT</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 */
065public class XMPPError extends AbstractError {
066
067    public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas";
068    public static final String ERROR = "error";
069
070    private static final Logger LOGGER = Logger.getLogger(XMPPError.class.getName());
071    private static final Map<Condition, Type> CONDITION_TO_TYPE = new HashMap<Condition, Type>();
072
073    static {
074        CONDITION_TO_TYPE.put(Condition.bad_request, Type.MODIFY);
075        CONDITION_TO_TYPE.put(Condition.conflict, Type.CANCEL);
076        CONDITION_TO_TYPE.put(Condition.feature_not_implemented, Type.CANCEL);
077        CONDITION_TO_TYPE.put(Condition.forbidden, Type.AUTH);
078        CONDITION_TO_TYPE.put(Condition.gone, Type.CANCEL);
079        CONDITION_TO_TYPE.put(Condition.internal_server_error, Type.CANCEL);
080        CONDITION_TO_TYPE.put(Condition.item_not_found, Type.CANCEL);
081        CONDITION_TO_TYPE.put(Condition.jid_malformed, Type.MODIFY);
082        CONDITION_TO_TYPE.put(Condition.not_acceptable, Type.MODIFY);
083        CONDITION_TO_TYPE.put(Condition.not_allowed, Type.CANCEL);
084        CONDITION_TO_TYPE.put(Condition.not_authorized, Type.AUTH);
085        CONDITION_TO_TYPE.put(Condition.policy_violation, Type.MODIFY);
086        CONDITION_TO_TYPE.put(Condition.recipient_unavailable, Type.WAIT);
087        CONDITION_TO_TYPE.put(Condition.redirect, Type.MODIFY);
088        CONDITION_TO_TYPE.put(Condition.registration_required, Type.AUTH);
089        CONDITION_TO_TYPE.put(Condition.remote_server_not_found, Type.CANCEL);
090        CONDITION_TO_TYPE.put(Condition.remote_server_timeout, Type.WAIT);
091        CONDITION_TO_TYPE.put(Condition.resource_constraint, Type.WAIT);
092        CONDITION_TO_TYPE.put(Condition.service_unavailable, Type.WAIT);
093        CONDITION_TO_TYPE.put(Condition.subscription_required, Type.WAIT);
094        CONDITION_TO_TYPE.put(Condition.unexpected_request, Type.MODIFY);
095    }
096
097    private final Condition condition;
098    private final String conditionText;
099    private final String errorGenerator;
100    private final Type type;
101    private final Stanza stanza;
102
103    // TODO: Deprecated constructors
104    // deprecate in 4.3
105
106    /**
107     * Create a new XMPPError.
108     *
109     * @param condition
110     * @deprecated use {@link Builder} instead.
111     */
112    @Deprecated
113    public XMPPError(Condition condition) {
114        this(condition, null, null, null, null, null, null);
115    }
116
117    /**
118     * Create a new XMPPError.
119     *
120     * @param condition
121     * @param applicationSpecificCondition
122     * @deprecated use {@link Builder} instead.
123     */
124    @Deprecated
125    public XMPPError(Condition condition, ExtensionElement applicationSpecificCondition) {
126        this(condition, null, null, null, null, Arrays.asList(applicationSpecificCondition), null);
127    }
128
129    /**
130     * Creates a new error with the specified type, condition and message.
131     * This constructor is used when the condition is not recognized automatically by XMPPError
132     * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 
133     * specification.
134     * 
135     * @param type the error type.
136     * @param condition the error condition.
137     * @param descriptiveTexts 
138     * @param extensions list of stanza(/packet) extensions
139     * @deprecated use {@link Builder} instead.
140     */
141    @Deprecated
142    public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts,
143            List<ExtensionElement> extensions) {
144        this(condition, conditionText, errorGenerator, type, descriptiveTexts, extensions, null);
145    }
146
147    /**
148     * Creates a new error with the specified type, condition and message.
149     * This constructor is used when the condition is not recognized automatically by XMPPError
150     * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 
151     * specification.
152     * 
153     * @param type the error type.
154     * @param condition the error condition.
155     * @param descriptiveTexts 
156     * @param extensions list of stanza(/packet) extensions
157     * @param stanza the stanza carrying this XMPP error.
158     */
159    public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts,
160            List<ExtensionElement> extensions, Stanza stanza) {
161        super(descriptiveTexts, NAMESPACE, extensions);
162        this.condition = Objects.requireNonNull(condition, "condition must not be null");
163        this.stanza = stanza;
164        // Some implementations may send the condition as non-empty element containing the empty string, that is
165        // <condition xmlns='foo'></condition>, in this case the parser may calls this constructor with the empty string
166        // as conditionText, therefore reset it to null if it's the empty string
167        if (StringUtils.isNullOrEmpty(conditionText)) {
168            conditionText = null;
169        }
170        if (conditionText != null) {
171            switch (condition) {
172            case gone:
173            case redirect:
174                break;
175            default:
176                throw new IllegalArgumentException(
177                                "Condition text can only be set with condtion types 'gone' and 'redirect', not "
178                                                + condition);
179            }
180        }
181        this.conditionText = conditionText;
182        this.errorGenerator = errorGenerator;
183        if (type == null) {
184            Type determinedType = CONDITION_TO_TYPE.get(condition);
185            if (determinedType == null) {
186                LOGGER.warning("Could not determine type for condition: " + condition);
187                determinedType = Type.CANCEL;
188            }
189            this.type = determinedType;
190        } else {
191            this.type = type;
192        }
193    }
194
195    /**
196     * Returns the error condition.
197     *
198     * @return the error condition.
199     */
200    public Condition getCondition() {
201        return condition;
202    }
203
204    /**
205     * Returns the error type.
206     *
207     * @return the error type.
208     */
209    public Type getType() {
210        return type;
211    }
212
213    public String getErrorGenerator() {
214        return errorGenerator;
215    }
216
217    public String getConditionText() {
218        return conditionText;
219    }
220
221    /**
222     * Get the stanza carrying the XMPP error.
223     *
224     * @return the stanza carrying the XMPP error.
225     * @since 4.2
226     */
227    public Stanza getStanza() {
228        return stanza;
229    }
230
231    @Override
232    public String toString() {
233        StringBuilder sb = new StringBuilder("XMPPError: ");
234        sb.append(condition.toString()).append(" - ").append(type.toString());
235        if (errorGenerator != null) {
236            sb.append(". Generated by ").append(errorGenerator);
237        }
238        return sb.toString();
239    }
240
241    /**
242     * Returns the error as XML.
243     *
244     * @return the error as XML.
245     */
246    public XmlStringBuilder toXML() {
247        XmlStringBuilder xml = new XmlStringBuilder();
248        xml.halfOpenElement(ERROR);
249        xml.attribute("type", type.toString());
250        xml.optAttribute("by", errorGenerator);
251        xml.rightAngleBracket();
252
253        xml.halfOpenElement(condition.toString());
254        xml.xmlnsAttribute(NAMESPACE);
255        if (conditionText != null) {
256            xml.rightAngleBracket();
257            xml.escape(conditionText);
258            xml.closeElement(condition.toString());
259        }
260        else {
261            xml.closeEmptyElement();
262        }
263
264        addDescriptiveTextsAndExtensions(xml);
265
266        xml.closeElement(ERROR);
267        return xml;
268    }
269
270    public static XMPPError.Builder from(Condition condition, String descriptiveText) {
271        Map<String, String> descriptiveTexts = new HashMap<String, String>();
272        descriptiveTexts.put("en", descriptiveText);
273        return getBuilder().setCondition(condition).setDescriptiveTexts(descriptiveTexts);
274    }
275
276    public static Builder getBuilder() {
277        return new Builder();
278    }
279
280    public static Builder getBuilder(Condition condition) {
281        return getBuilder().setCondition(condition);
282    }
283
284    public static Builder getBuilder(XMPPError xmppError) {
285        return getBuilder().copyFrom(xmppError);
286    }
287
288    public static final class Builder extends AbstractError.Builder<Builder> {
289        private Condition condition;
290        private String conditionText;
291        private String errorGenerator;
292        private Type type;
293        private Stanza stanza;
294
295        private Builder() {
296        }
297
298        public Builder setCondition(Condition condition) {
299            this.condition = condition;
300            return this;
301        }
302
303        public Builder setType(Type type) {
304            this.type = type;
305            return this;
306        }
307
308        public Builder setConditionText(String conditionText) {
309            this.conditionText = conditionText;
310            return this;
311        }
312
313        public Builder setErrorGenerator(String errorGenerator) {
314            this.errorGenerator = errorGenerator;
315            return this;
316        }
317
318        public Builder setStanza(Stanza stanza) {
319            this.stanza = stanza;
320            return this;
321        }
322
323        public Builder copyFrom(XMPPError xmppError) {
324            setCondition(xmppError.getCondition());
325            setType(xmppError.getType());
326            setConditionText(xmppError.getConditionText());
327            setErrorGenerator(xmppError.getErrorGenerator());
328            setStanza(xmppError.getStanza());
329            setDescriptiveTexts(xmppError.descriptiveTexts);
330            setTextNamespace(xmppError.textNamespace);
331            setExtensions(xmppError.extensions);
332            return this;
333        }
334
335        public XMPPError build() {
336            return new XMPPError(condition, conditionText, errorGenerator, type, descriptiveTexts,
337            extensions, stanza);
338        }
339
340        @Override
341        protected Builder getThis() {
342            return this;
343        }
344    }
345    /**
346     * A class to represent the type of the Error. The types are:
347     *
348     * <ul>
349     *      <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary)
350     *      <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable)
351     *      <li>XMPPError.Type.MODIFY - retry after changing the data sent
352     *      <li>XMPPError.Type.AUTH - retry after providing credentials
353     *      <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning)
354     * </ul>
355     */
356    public static enum Type {
357        WAIT,
358        CANCEL,
359        MODIFY,
360        AUTH,
361        CONTINUE;
362
363        @Override
364        public String toString() {
365            return name().toLowerCase(Locale.US);
366        }
367
368        public static Type fromString(String string) {
369            string = string.toUpperCase(Locale.US);
370            return Type.valueOf(string);
371        }
372    }
373
374    public enum Condition {
375        bad_request,
376        conflict,
377        feature_not_implemented,
378        forbidden,
379        gone,
380        internal_server_error,
381        item_not_found,
382        jid_malformed,
383        not_acceptable,
384        not_allowed,
385        not_authorized,
386        policy_violation,
387        recipient_unavailable,
388        redirect,
389        registration_required,
390        remote_server_not_found,
391        remote_server_timeout,
392        resource_constraint,
393        service_unavailable,
394        subscription_required,
395        undefined_condition,
396        unexpected_request;
397
398        @Override
399        public String toString() {
400            return this.name().replace('_', '-');
401        }
402
403        public static Condition fromString(String string) {
404            // Backwards compatibility for older implementations still using RFC 3920. RFC 6120
405            // changed 'xml-not-well-formed' to 'not-well-formed'.
406            if ("xml-not-well-formed".equals(string)) {
407                string = "not-well-formed";
408            }
409            string = string.replace('-', '_');
410            Condition condition = null;
411            try {
412                condition = Condition.valueOf(string);
413            } catch (Exception e) {
414                throw new IllegalStateException("Could not transform string '" + string + "' to XMPPErrorCondition", e);
415            }
416            return condition;
417        }
418    }
419
420}