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}