001: /**
002: * Copyright (C) 2004-2007 Jive Software. All rights reserved.
003: *
004: * This software is published under the terms of the GNU Public License (GPL),
005: * a copy of which is included in this distribution.
006: */package org.xmpp.packet;
007:
008: import org.dom4j.DocumentFactory;
009: import org.dom4j.Element;
010: import org.dom4j.QName;
011: import org.dom4j.io.OutputFormat;
012: import org.dom4j.io.XMLWriter;
013:
014: import java.io.StringWriter;
015: import java.util.Iterator;
016:
017: /**
018: * A packet error. Errors must have a type and condition. Optionally, they
019: * can include explanation text.
020: *
021: * @author Matt Tucker
022: */
023: public class PacketError {
024:
025: private static final String ERROR_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas";
026:
027: private static DocumentFactory docFactory = DocumentFactory
028: .getInstance();
029:
030: private Element element;
031:
032: /**
033: * Construcs a new PacketError with the specified condition. The error
034: * type will be set to the default for the specified condition.
035: *
036: * @param condition the error condition.
037: */
038: public PacketError(Condition condition) {
039: this .element = docFactory.createElement("error");
040: setCondition(condition);
041: setType(condition.getDefaultType());
042: }
043:
044: /**
045: * Constructs a new PacketError with the specified condition and type.
046: *
047: * @param condition the error condition.
048: * @param type the error type.
049: */
050: public PacketError(Condition condition, Type type) {
051: this .element = docFactory.createElement("error");
052: setCondition(condition);
053: setType(type);
054: }
055:
056: /**
057: * Constructs a new PacketError.
058: *
059: * @param type the error type.
060: * @param condition the error condition.
061: * @param text the text description of the error.
062: */
063: public PacketError(Condition condition, Type type, String text) {
064: this .element = docFactory.createElement("error");
065: setType(type);
066: setCondition(condition);
067: setText(text, null);
068: }
069:
070: /**
071: * Constructs a new PacketError.
072: *
073: * @param type the error type.
074: * @param condition the error condition.
075: * @param text the text description of the error.
076: * @param lang the language code of the error description (e.g. "en").
077: */
078: public PacketError(Condition condition, Type type, String text,
079: String lang) {
080: this .element = docFactory.createElement("error");
081: setType(type);
082: setCondition(condition);
083: setText(text, lang);
084: }
085:
086: /**
087: * Constructs a new PacketError using an existing Element. This is useful
088: * for parsing incoming error Elements into PacketError objects.
089: *
090: * @param element the error Element.
091: */
092: public PacketError(Element element) {
093: this .element = element;
094: }
095:
096: /**
097: * Returns the error type.
098: *
099: * @return the error type.
100: * @see Type
101: */
102: public Type getType() {
103: String type = element.attributeValue("type");
104: if (type != null) {
105: return Type.fromXMPP(type);
106: } else {
107: return null;
108: }
109: }
110:
111: /**
112: * Sets the error type.
113: *
114: * @param type the error type.
115: * @see Type
116: */
117: public void setType(Type type) {
118: element.addAttribute("type", type == null ? null : type
119: .toXMPP());
120: }
121:
122: /**
123: * Returns the error condition.
124: *
125: * @return the error condition.
126: * @see Condition
127: */
128: public Condition getCondition() {
129: for (Iterator i = element.elementIterator(); i.hasNext();) {
130: Element el = (Element) i.next();
131: if (el.getNamespaceURI().equals(ERROR_NAMESPACE)
132: && !el.getName().equals("text")) {
133: return Condition.fromXMPP(el.getName());
134: }
135: }
136: // Looking for XMPP condition failed. See if a legacy error code exists,
137: // which can be mapped into an XMPP error condition.
138: String code = element.attributeValue("code");
139: if (code != null) {
140: try {
141: return Condition.fromLegacyCode(Integer.parseInt(code));
142: } catch (Exception e) {
143: // Ignore -- unable to map legacy code into a valid condition
144: // so return null.
145: }
146: }
147: return null;
148: }
149:
150: /**
151: * Sets the error condition.
152: *
153: * @param condition the error condition.
154: * @see Condition
155: */
156: public void setCondition(Condition condition) {
157: if (condition == null) {
158: throw new NullPointerException("Condition cannot be null");
159: }
160: // Set the error code for legacy support.
161: element.addAttribute("code", Integer.toString(condition
162: .getLegacyCode()));
163:
164: Element conditionElement = null;
165: for (Iterator i = element.elementIterator(); i.hasNext();) {
166: Element el = (Element) i.next();
167: if (el.getNamespaceURI().equals(ERROR_NAMESPACE)
168: && !el.getName().equals("text")) {
169: conditionElement = el;
170: }
171: }
172: if (conditionElement != null) {
173: element.remove(conditionElement);
174: }
175:
176: conditionElement = docFactory.createElement(condition.toXMPP(),
177: ERROR_NAMESPACE);
178: element.add(conditionElement);
179: }
180:
181: /**
182: * Returns a text description of the error, or <tt>null</tt> if there
183: * is no text description.
184: *
185: * @return the text description of the error.
186: */
187: public String getText() {
188: return element.elementText("text");
189: }
190:
191: /**
192: * Sets the text description of the error.
193: *
194: * @param text the text description of the error.
195: */
196: public void setText(String text) {
197: setText(text, null);
198: }
199:
200: /**
201: * Sets the text description of the error. Optionally, a language code
202: * can be specified to indicate the language of the description.
203: *
204: * @param text the text description of the error.
205: * @param lang the language code of the description, or <tt>null</tt> to specify
206: * no language code.
207: */
208: public void setText(String text, String lang) {
209: Element textElement = element.element("text");
210: // If text is null, clear the text.
211: if (text == null) {
212: if (textElement != null) {
213: element.remove(textElement);
214: }
215: return;
216: }
217:
218: if (textElement == null) {
219: textElement = docFactory.createElement("text",
220: ERROR_NAMESPACE);
221: if (lang != null) {
222: textElement.addAttribute(QName.get("lang", "xml",
223: "http://www.w3.org/XML/1998/namespace"), lang);
224: }
225: element.add(textElement);
226: }
227: textElement.setText(text);
228: }
229:
230: /**
231: * Returns the text description's language code, or <tt>null</tt> if there
232: * is no language code associated with the description text.
233: *
234: * @return the language code of the text description, if it exists.
235: */
236: public String getTextLang() {
237: Element textElement = element.element("text");
238: if (textElement != null) {
239: return textElement.attributeValue(QName.get("lang", "xml",
240: "http://www.w3.org/XML/1998/namespace"));
241: }
242: return null;
243: }
244:
245: /**
246: * Returns the DOM4J Element that backs the error. The element is the definitive
247: * representation of the error and can be manipulated directly to change
248: * error contents.
249: *
250: * @return the DOM4J Element.
251: */
252: public Element getElement() {
253: return element;
254: }
255:
256: /**
257: * Returns the textual XML representation of this error.
258: *
259: * @return the textual XML representation of this error.
260: */
261: public String toXML() {
262: return element.asXML();
263: }
264:
265: public String toString() {
266: StringWriter out = new StringWriter();
267: XMLWriter writer = new XMLWriter(out, OutputFormat
268: .createPrettyPrint());
269: try {
270: writer.write(element);
271: } catch (Exception e) {
272: // Ignore.
273: }
274: return out.toString();
275: }
276:
277: /**
278: * Type-safe enumeration for the error condition.<p>
279: *
280: * Implementation note: XMPP error conditions use "-" characters in
281: * their names such as "bad-request". Because "-" characters are not valid
282: * identifier parts in Java, they have been converted to "_" characters in
283: * the enumeration names, such as <tt>bad_request</tt>. The {@link #toXMPP()} and
284: * {@link #fromXMPP(String)} methods can be used to convert between the
285: * enumertation values and XMPP error code strings.
286: */
287: public enum Condition {
288:
289: /**
290: * The sender has sent XML that is malformed or that cannot be processed
291: * (e.g., an IQ stanza that includes an unrecognized value of the 'type'
292: * attribute); the associated error type SHOULD be "modify".
293: */
294: bad_request("bad-request", Type.modify, 400),
295:
296: /**
297: * Access cannot be granted because an existing resource or session
298: * exists with the same name or address; the associated error type
299: * SHOULD be "cancel".
300: */
301: conflict("conflict", Type.cancel, 409),
302:
303: /**
304: * The feature requested is not implemented by the recipient or
305: * server and therefore cannot be processed; the associated error
306: * type SHOULD be "cancel".
307: */
308: feature_not_implemented("feature-not-implemented", Type.cancel,
309: 501),
310:
311: /**
312: * The requesting entity does not possess the required permissions to
313: * perform the action; the associated error type SHOULD be "auth".
314: */
315: forbidden("forbidden", Type.auth, 403),
316:
317: /**
318: * The recipient or server can no longer be contacted at this address
319: * (the error stanza MAY contain a new address in the XML character
320: * data of the <gone/> element); the associated error type SHOULD be
321: * "modify".
322: */
323: gone("gone", Type.modify, 302),
324:
325: /**
326: * The server could not process the stanza because of a misconfiguration
327: * or an otherwise-undefined internal server error; the associated error
328: * type SHOULD be "wait".
329: */
330: internal_server_error("internal-server-error", Type.wait, 500),
331:
332: /**
333: * The addressed JID or item requested cannot be found; the associated
334: * error type SHOULD be "cancel".
335: */
336: item_not_found("item-not-found", Type.cancel, 404),
337:
338: /**
339: * The sending entity has provided or communicated an XMPP address
340: * (e.g., a value of the 'to' attribute) or aspect thereof (e.g.,
341: * a resource identifier) that does not adhere to the syntax defined
342: * in Addressing Scheme (Section 3); the associated error type SHOULD
343: * be "modify".
344: */
345: jid_malformed("jid-malformed", Type.modify, 400),
346:
347: /**
348: * The recipient or server understands the request but is refusing
349: * to process it because it does not meet criteria defined by the
350: * recipient or server (e.g., a local policy regarding acceptable
351: * words in messages); the associated error type SHOULD be "modify".
352: */
353: not_acceptable("not-acceptable", Type.modify, 406),
354:
355: /**
356: * The recipient or server does not allow any entity to perform
357: * the action; the associated error type SHOULD be "cancel".
358: */
359: not_allowed("not-allowed", Type.cancel, 405),
360:
361: /**
362: * The sender must provide proper credentials before being allowed
363: * to perform the action, or has provided improper credentials;
364: * the associated error type SHOULD be "auth".
365: */
366: not_authorized("not-authorized", Type.auth, 401),
367:
368: /**
369: * The requesting entity is not authorized to access the requested
370: * service because payment is required; the associated error type
371: * SHOULD be "auth".
372: */
373: payment_required("payment-required", Type.auth, 402),
374:
375: /**
376: * The intended recipient is temporarily unavailable; the associated
377: * error type SHOULD be "wait" (note: an application MUST NOT return
378: * this error if doing so would provide information about the intended
379: * recipient's network availability to an entity that is not authorized
380: * to know such information).
381: */
382: recipient_unavailable("recipient-unavailable", Type.wait, 404),
383:
384: /**
385: * The recipient or server is redirecting requests for this
386: * information to another entity, usually temporarily (the error
387: * stanza SHOULD contain the alternate address, which MUST be a
388: * valid JID, in the XML character data of the <redirect/> element);
389: * the associated error type SHOULD be "modify".
390: */
391: redirect("redirect", Type.modify, 302),
392:
393: /**
394: * The requesting entity is not authorized to access the requested
395: * service because registration is required; the associated error
396: * type SHOULD be "auth".
397: */
398: registration_required("registration-required", Type.auth, 407),
399:
400: /**
401: * A remote server or service specified as part or all of the JID
402: * of the intended recipient does not exist; the associated error
403: * type SHOULD be "cancel".
404: */
405: remote_server_not_found("remote-server-not-found", Type.cancel,
406: 404),
407:
408: /**
409: * A remote server or service specified as part or all of the JID of
410: * the intended recipient (or required to fulfill a request) could not
411: * be contacted within a reasonable amount of time; the associated
412: * error type SHOULD be "wait".
413: */
414: remote_server_timeout("remote-server-timeout", Type.wait, 504),
415:
416: /**
417: * The server or recipient lacks the system resources necessary to
418: * service the request; the associated error type SHOULD be "wait".
419: */
420: resource_constraint("resource-constraint", Type.wait, 500),
421:
422: /**
423: * The server or recipient does not currently provide the requested
424: * service; the associated error type SHOULD be "cancel".
425: */
426: service_unavailable("service-unavailable", Type.cancel, 503),
427:
428: /**
429: * The requesting entity is not authorized to access the requested
430: * service because a subscription is required; the associated error
431: * type SHOULD be "auth".
432: */
433: subscription_required("subscription-required", Type.auth, 407),
434:
435: /**
436: * The error condition is not one of those defined by the other
437: * conditions in this list; any error type may be associated with
438: * this condition, and it SHOULD be used only in conjunction with
439: * an application-specific condition.<p>
440: *
441: * Implementation note: the default type for this condition is
442: * {@link Type#wait}, which is not specified in the XMPP protocol.
443: */
444: undefined_condition("undefined-condition", Type.wait, 500),
445:
446: /**
447: * The recipient or server understood the request but was not
448: * expecting it at this time (e.g., the request was out of order);
449: * the associated error type SHOULD be "wait".
450: */
451: unexpected_request("unexpected-request", Type.wait, 400);
452:
453: /**
454: * Converts a String value into its Condition representation.
455: *
456: * @param condition the String value.
457: * @return the condition corresponding to the String.
458: */
459: public static Condition fromXMPP(String condition) {
460: if (condition == null) {
461: throw new NullPointerException();
462: }
463: condition = condition.toLowerCase();
464: if (bad_request.toXMPP().equals(condition)) {
465: return bad_request;
466: } else if (conflict.toXMPP().equals(condition)) {
467: return conflict;
468: } else if (feature_not_implemented.toXMPP().equals(
469: condition)) {
470: return feature_not_implemented;
471: } else if (forbidden.toXMPP().equals(condition)) {
472: return forbidden;
473: } else if (gone.toXMPP().equals(condition)) {
474: return gone;
475: } else if (internal_server_error.toXMPP().equals(condition)) {
476: return internal_server_error;
477: } else if (item_not_found.toXMPP().equals(condition)) {
478: return item_not_found;
479: } else if (jid_malformed.toXMPP().equals(condition)) {
480: return jid_malformed;
481: } else if (not_acceptable.toXMPP().equals(condition)) {
482: return not_acceptable;
483: } else if (not_allowed.toXMPP().equals(condition)) {
484: return not_allowed;
485: } else if (not_authorized.toXMPP().equals(condition)) {
486: return not_authorized;
487: } else if (payment_required.toXMPP().equals(condition)) {
488: return payment_required;
489: } else if (recipient_unavailable.toXMPP().equals(condition)) {
490: return recipient_unavailable;
491: } else if (redirect.toXMPP().equals(condition)) {
492: return redirect;
493: } else if (registration_required.toXMPP().equals(condition)) {
494: return registration_required;
495: } else if (remote_server_not_found.toXMPP().equals(
496: condition)) {
497: return remote_server_not_found;
498: } else if (remote_server_timeout.toXMPP().equals(condition)) {
499: return remote_server_timeout;
500: } else if (resource_constraint.toXMPP().equals(condition)) {
501: return resource_constraint;
502: } else if (service_unavailable.toXMPP().equals(condition)) {
503: return service_unavailable;
504: } else if (subscription_required.toXMPP().equals(condition)) {
505: return subscription_required;
506: } else if (undefined_condition.toXMPP().equals(condition)) {
507: return undefined_condition;
508: } else if (unexpected_request.toXMPP().equals(condition)) {
509: return unexpected_request;
510: } else {
511: throw new IllegalArgumentException("Condition invalid:"
512: + condition);
513: }
514: }
515:
516: public static Condition fromLegacyCode(int code) {
517: if (bad_request.getLegacyCode() == code) {
518: return bad_request;
519: } else if (conflict.getLegacyCode() == code) {
520: return conflict;
521: } else if (feature_not_implemented.getLegacyCode() == code) {
522: return feature_not_implemented;
523: } else if (forbidden.getLegacyCode() == code) {
524: return forbidden;
525: } else if (gone.getLegacyCode() == code) {
526: return gone;
527: } else if (internal_server_error.getLegacyCode() == code) {
528: return internal_server_error;
529: } else if (item_not_found.getLegacyCode() == code) {
530: return item_not_found;
531: } else if (jid_malformed.getLegacyCode() == code) {
532: return jid_malformed;
533: } else if (not_acceptable.getLegacyCode() == code) {
534: return not_acceptable;
535: } else if (not_allowed.getLegacyCode() == code) {
536: return not_allowed;
537: } else if (not_authorized.getLegacyCode() == code) {
538: return not_authorized;
539: } else if (payment_required.getLegacyCode() == code) {
540: return payment_required;
541: } else if (recipient_unavailable.getLegacyCode() == code) {
542: return recipient_unavailable;
543: } else if (redirect.getLegacyCode() == code) {
544: return redirect;
545: } else if (registration_required.getLegacyCode() == code) {
546: return registration_required;
547: } else if (remote_server_not_found.getLegacyCode() == code) {
548: return remote_server_not_found;
549: } else if (remote_server_timeout.getLegacyCode() == code) {
550: return remote_server_timeout;
551: } else if (resource_constraint.getLegacyCode() == code) {
552: return resource_constraint;
553: } else if (service_unavailable.getLegacyCode() == code) {
554: return service_unavailable;
555: } else if (subscription_required.getLegacyCode() == code) {
556: return subscription_required;
557: } else if (undefined_condition.getLegacyCode() == code) {
558: return undefined_condition;
559: } else if (unexpected_request.getLegacyCode() == code) {
560: return unexpected_request;
561: } else {
562: throw new IllegalArgumentException("Code invalid:"
563: + code);
564: }
565: }
566:
567: private String value;
568: private int code;
569: private Type defaultType;
570:
571: private Condition(String value, Type defaultType, int code) {
572: this .value = value;
573: this .defaultType = defaultType;
574: this .code = code;
575: }
576:
577: /**
578: * Returns the default {@link Type} associated with this condition. Each
579: * error condition has an error type that it is usually associated with.
580: *
581: * @return the default error type.
582: */
583: public Type getDefaultType() {
584: return defaultType;
585: }
586:
587: /**
588: * Returns the legacy error code associated with the error. Error code mappings
589: * are based on <a href="http://www.jabber.org/jeps/jep-0086.html">JEP-0086</a>.
590: * Support for legacy error codes is necessary since many "Jabber" clients
591: * do not understand XMPP error codes. The {@link #fromLegacyCode(int)} method
592: * will convert numeric error codes into Conditions.
593: *
594: * @return the legacy error code.
595: */
596: public int getLegacyCode() {
597: return code;
598: }
599:
600: /**
601: * Returns the error code as a valid XMPP error code string.
602: *
603: * @return the XMPP error code value.
604: */
605: public String toXMPP() {
606: return value;
607: }
608: }
609:
610: /**
611: * Error type. Valid types are:<ul>
612: *
613: * <li>{@link #cancel Error.Type.cancel} -- do not retry (the error is unrecoverable).
614: * <li>{@link #continue_processing Error.Type.continue_processing} -- proceed
615: * (the condition was only a warning). Equivalent to the XMPP error type
616: * "continue".
617: * <li>{@link #modify Error.Type.modify} -- retry after changing the data sent.
618: * <li>{@link #auth Eror.Type.auth} -- retry after providing credentials.
619: * <li>{@link #wait Error.Type.wait} -- retry after waiting (the error is temporary).
620: * </ul>
621: *
622: * Implementation note: one of the XMPP error types is "continue". Because "continue"
623: * is a reserved Java keyword, the enum name is <tt>continue_processing</tt>. The
624: * {@link #toXMPP()} and {@link #fromXMPP(String)} methods can be used to convert
625: * between the enumertation values and XMPP error type strings.
626: */
627: public enum Type {
628:
629: /**
630: * Do not retry (the error is unrecoverable).
631: */
632: cancel("cancel"),
633:
634: /**
635: * Proceed (the condition was only a warning). This represents
636: * the "continue" error code in XMPP; because "continue" is a
637: * reserved keyword in Java the enum name has been changed.
638: */
639: continue_processing("continue"),
640:
641: /**
642: * Retry after changing the data sent.
643: */
644: modify("modify"),
645:
646: /**
647: * Retry after providing credentials.
648: */
649: auth("auth"),
650:
651: /**
652: * Retry after waiting (the error is temporary).
653: */
654: wait("wait");
655:
656: /**
657: * Converts a String value into its Type representation.
658: *
659: * @param type the String value.
660: * @return the condition corresponding to the String.
661: */
662: public static Type fromXMPP(String type) {
663: if (type == null) {
664: throw new NullPointerException();
665: }
666: type = type.toLowerCase();
667: if (cancel.toXMPP().equals(type)) {
668: return cancel;
669: } else if (continue_processing.toXMPP().equals(type)) {
670: return continue_processing;
671: } else if (modify.toXMPP().equals(type)) {
672: return modify;
673: } else if (auth.toXMPP().equals(type)) {
674: return auth;
675: } else if (wait.toXMPP().equals(type)) {
676: return wait;
677: } else {
678: throw new IllegalArgumentException("Type invalid:"
679: + type);
680: }
681: }
682:
683: private String value;
684:
685: private Type(String value) {
686: this .value = value;
687: }
688:
689: /**
690: * Returns the error code as a valid XMPP error code string.
691: *
692: * @return the XMPP error code value.
693: */
694: public String toXMPP() {
695: return value;
696: }
697: }
698: }
|