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<ExtensionElement> 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}