001/**
002 *
003 * Copyright 2003-2007 Jive Software.
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.Map;
023import java.util.logging.Logger;
024
025import org.jivesoftware.smack.util.StringUtils;
026import org.jivesoftware.smack.util.XmlStringBuilder;
027
028/**
029 * Represents an XMPP error sub-packet. Typically, a server responds to a request that has
030 * problems by sending the stanza(/packet) back and including an error packet. Each error has a type,
031 * error condition as well as as an optional text explanation. Typical errors are:<p>
032 *
033 * <table border=1>
034 *      <hr><td><b>XMPP Error Condition</b></td><td><b>Type</b></td><td><b>RFC 6120 Section</b></td></hr>
035 *      <tr><td>bad-request</td><td>MODIFY</td><td>8.3.3.1</td></tr>
036 *      <tr><td>conflict</td><td>CANCEL</td><td>8.3.3.2</td></tr>
037 *      <tr><td>feature-not-implemented</td><td>CANCEL</td><td>8.3.3.3</td></tr>
038 *      <tr><td>forbidden</td><td>AUTH</td><td>8.3.3.4</td></tr>
039 *      <tr><td>gone</td><td>MODIFY</td><td>8.3.3.5</td></tr>
040 *      <tr><td>internal-server-error</td><td>WAIT</td><td>8.3.3.6</td></tr>
041 *      <tr><td>item-not-found</td><td>CANCEL</td><td>8.3.3.7</td></tr>
042 *      <tr><td>jid-malformed</td><td>MODIFY</td><td>8.3.3.8</td></tr>
043 *      <tr><td>not-acceptable</td><td> MODIFY</td><td>8.3.3.9</td></tr>
044 *      <tr><td>not-allowed</td><td>CANCEL</td><td>8.3.3.10</td></tr>
045 *      <tr><td>not-authorized</td><td>AUTH</td><td>8.3.3.11</td></tr>
046 *      <tr><td>policy-violation</td><td>AUTH</td><td>8.3.3.12</td></tr>
047 *      <tr><td>recipient-unavailable</td><td>WAIT</td><td>8.3.3.13</td></tr>
048 *      <tr><td>redirect</td><td>MODIFY</td><td>8.3.3.14</td></tr>
049 *      <tr><td>registration-required</td><td>AUTH</td><td>8.3.3.15</td></tr>
050 *      <tr><td>remote-server-not-found</td><td>CANCEL</td><td>8.3.3.16</td></tr>
051 *      <tr><td>remote-server-timeout</td><td>WAIT</td><td>8.3.3.17</td></tr>
052 *      <tr><td>resource-constraint</td><td>WAIT</td><td>8.3.3.18</td></tr>
053 *      <tr><td>service-unavailable</td><td>CANCEL</td><td>8.3.3.19</td></tr>
054 *      <tr><td>subscription-required</td><td>AUTH</td><td>8.3.3.20</td></tr>
055 *      <tr><td>undefined-condition</td><td>WAIT</td><td>8.3.3.21</td></tr>
056 *      <tr><td>unexpected-request</td><td>WAIT</td><td>8.3.3.22</td></tr>
057 * </table>
058 *
059 * @author Matt Tucker
060 * @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>
061 */
062public class XMPPError extends AbstractError {
063
064    public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas";
065    public static final String ERROR = "error";
066
067    private static final Logger LOGGER = Logger.getLogger(XMPPError.class.getName());
068    private static final Map<Condition, Type> CONDITION_TO_TYPE = new HashMap<Condition, Type>();
069
070    static {
071        CONDITION_TO_TYPE.put(Condition.bad_request, Type.MODIFY);
072        CONDITION_TO_TYPE.put(Condition.conflict, Type.CANCEL);
073        CONDITION_TO_TYPE.put(Condition.feature_not_implemented, Type.CANCEL);
074        CONDITION_TO_TYPE.put(Condition.forbidden, Type.AUTH);
075        CONDITION_TO_TYPE.put(Condition.gone, Type.CANCEL);
076        CONDITION_TO_TYPE.put(Condition.internal_server_error, Type.CANCEL);
077        CONDITION_TO_TYPE.put(Condition.item_not_found, Type.CANCEL);
078        CONDITION_TO_TYPE.put(Condition.jid_malformed, Type.MODIFY);
079        CONDITION_TO_TYPE.put(Condition.not_acceptable, Type.MODIFY);
080        CONDITION_TO_TYPE.put(Condition.not_allowed, Type.CANCEL);
081        CONDITION_TO_TYPE.put(Condition.not_authorized, Type.AUTH);
082        CONDITION_TO_TYPE.put(Condition.policy_violation, Type.MODIFY);
083        CONDITION_TO_TYPE.put(Condition.recipient_unavailable, Type.WAIT);
084        CONDITION_TO_TYPE.put(Condition.redirect, Type.MODIFY);
085        CONDITION_TO_TYPE.put(Condition.registration_required, Type.AUTH);
086        CONDITION_TO_TYPE.put(Condition.remote_server_not_found, Type.CANCEL);
087        CONDITION_TO_TYPE.put(Condition.remote_server_timeout, Type.WAIT);
088        CONDITION_TO_TYPE.put(Condition.resource_constraint, Type.WAIT);
089        CONDITION_TO_TYPE.put(Condition.service_unavailable, Type.WAIT);
090        CONDITION_TO_TYPE.put(Condition.subscription_required, Type.WAIT);
091        CONDITION_TO_TYPE.put(Condition.unexpected_request, Type.MODIFY);
092    }
093    private final Condition condition;
094    private final String conditionText;
095    private final String errorGenerator;
096    private final Type type;
097
098    public XMPPError(Condition condition) {
099        this(condition, null, null, null, null, null);
100    }
101
102    public XMPPError(Condition condition, ExtensionElement applicationSpecificCondition) {
103        this(condition, null, null, null, null, Arrays.asList(applicationSpecificCondition));
104    }
105
106    /**
107     * Creates a new error with the specified type, condition and message.
108     * This constructor is used when the condition is not recognized automatically by XMPPError
109     * i.e. there is not a defined instance of ErrorCondition or it does not apply the default 
110     * specification.
111     * 
112     * @param type the error type.
113     * @param condition the error condition.
114     * @param descriptiveTexts 
115     * @param extensions list of stanza(/packet) extensions
116     */
117    public XMPPError(Condition condition, String conditionText, String errorGenerator, Type type, Map<String, String> descriptiveTexts,
118            List<ExtensionElement> extensions) {
119        super(descriptiveTexts, NAMESPACE, extensions);
120        this.condition = condition;
121        // Some implementations may send the condition as non-empty element containing the empty string, that is
122        // <condition xmlns='foo'></condition>, in this case the parser may calls this constructor with the empty string
123        // as conditionText, therefore reset it to null if it's the empty string
124        if (StringUtils.isNullOrEmpty(conditionText)) {
125            conditionText = null;
126        }
127        if (conditionText != null) {
128            switch (condition) {
129            case gone:
130            case redirect:
131                break;
132            default:
133                throw new IllegalArgumentException(
134                                "Condition text can only be set with condtion types 'gone' and 'redirect', not "
135                                                + condition);
136            }
137        }
138        this.conditionText = conditionText;
139        this.errorGenerator = errorGenerator;
140        if (type == null) {
141            Type determinedType = CONDITION_TO_TYPE.get(condition);
142            if (determinedType == null) {
143                LOGGER.warning("Could not determine type for condition: " + condition);
144                determinedType = Type.CANCEL;
145            }
146            this.type = determinedType;
147        } else {
148            this.type = type;
149        }
150    }
151
152    /**
153     * Returns the error condition.
154     *
155     * @return the error condition.
156     */
157    public Condition getCondition() {
158        return condition;
159    }
160
161    /**
162     * Returns the error type.
163     *
164     * @return the error type.
165     */
166    public Type getType() {
167        return type;
168    }
169
170    public String getErrorGenerator() {
171        return errorGenerator;
172    }
173
174    public String getConditionText() {
175        return conditionText;
176    }
177
178    @Override
179    public String toString() {
180        StringBuilder sb = new StringBuilder("XMPPError: ");
181        sb.append(condition.toString()).append(" - ").append(type.toString());
182        if (errorGenerator != null) {
183            sb.append(". Generated by ").append(errorGenerator);
184        }
185        return sb.toString();
186    }
187
188    /**
189     * Returns the error as XML.
190     *
191     * @return the error as XML.
192     */
193    public XmlStringBuilder toXML() {
194        XmlStringBuilder xml = new XmlStringBuilder();
195        xml.halfOpenElement(ERROR);
196        xml.attribute("type", type.toString());
197        xml.optAttribute("by", errorGenerator);
198        xml.rightAngleBracket();
199
200        xml.halfOpenElement(condition.toString());
201        xml.xmlnsAttribute(NAMESPACE);
202        if (conditionText != null) {
203            xml.rightAngleBracket();
204            xml.escape(conditionText);
205            xml.closeElement(condition.toString());
206        }
207        else {
208            xml.closeEmptyElement();
209        }
210
211        addDescriptiveTextsAndExtensions(xml);
212
213        xml.closeElement(ERROR);
214        return xml;
215    }
216
217    public static XMPPError from(Condition condition, String descriptiveText) {
218        Map<String, String> descriptiveTexts = new HashMap<String, String>();
219        descriptiveTexts.put("en", descriptiveText);
220        return new XMPPError(condition, null, null, null, descriptiveTexts, null);
221    }
222
223    /**
224     * A class to represent the type of the Error. The types are:
225     *
226     * <ul>
227     *      <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary)
228     *      <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable)
229     *      <li>XMPPError.Type.MODIFY - retry after changing the data sent
230     *      <li>XMPPError.Type.AUTH - retry after providing credentials
231     *      <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning)
232     * </ul>
233     */
234    public static enum Type {
235        WAIT,
236        CANCEL,
237        MODIFY,
238        AUTH,
239        CONTINUE;
240
241        @Override
242        public String toString() {
243            // Locale.US not required, since Type consists only of ASCII chars
244            return name().toLowerCase();
245        }
246
247        public static Type fromString(String string) {
248            // Locale.US not required, since Type consists only of ASCII chars
249            string = string.toUpperCase();
250            return Type.valueOf(string);
251        }
252    }
253
254    public enum Condition {
255        bad_request,
256        conflict,
257        feature_not_implemented,
258        forbidden,
259        gone,
260        internal_server_error,
261        item_not_found,
262        jid_malformed,
263        not_acceptable,
264        not_allowed,
265        not_authorized,
266        policy_violation,
267        recipient_unavailable,
268        redirect,
269        registration_required,
270        remote_server_not_found,
271        remote_server_timeout,
272        resource_constraint,
273        service_unavailable,
274        subscription_required,
275        undefined_condition,
276        unexpected_request;
277
278        @Override
279        public String toString() {
280            return this.name().replace('_', '-');
281        }
282
283        public static Condition fromString(String string) {
284            // Backwards compatibility for older implementations still using RFC 3920. RFC 6120
285            // changed 'xml-not-well-formed' to 'not-well-formed'.
286            if ("xml-not-well-formed".equals(string)) {
287                string = "not-well-formed";
288            }
289            string = string.replace('-', '_');
290            Condition condition = null;
291            try {
292                condition = Condition.valueOf(string);
293            } catch (Exception e) {
294                throw new IllegalStateException("Could not transform string '" + string + "' to XMPPErrorCondition", e);
295            }
296            return condition;
297        }
298    }
299
300}