From 47abf627b7aba2a19b1e98466f7f6a6e5eda9994 Mon Sep 17 00:00:00 2001 From: Matt Tucker Date: Wed, 19 Jul 2006 19:24:00 +0000 Subject: [PATCH] Added privacy list support and improved error handling from Francisco (SMACK-121, SMACK-31). git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@4603 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 1 + build/resources/META-INF/smack.providers | 7 + documentation/index.html | 3 +- documentation/privacy.html | 167 +++++++ .../org/jivesoftware/smack/PacketReader.java | 2 +- .../org/jivesoftware/smack/PrivacyList.java | 57 +++ .../smack/PrivacyListListener.java | 30 ++ .../smack/PrivacyListManager.java | 442 +++++++++++++++++ .../jivesoftware/smack/XMPPConnection.java | 23 +- .../jivesoftware/smack/packet/Privacy.java | 315 ++++++++++++ .../smack/packet/PrivacyItem.java | 454 ++++++++++++++++++ .../jivesoftware/smack/packet/XMPPError.java | 434 ++++++++++++++++- .../smack/provider/PrivacyProvider.java | 134 ++++++ .../smack/util/PacketParserUtils.java | 77 +-- .../smackx/ServiceDiscoveryManager.java | 4 +- .../filetransfer/FileTransferManager.java | 2 +- .../filetransfer/FileTransferNegotiator.java | 17 +- .../filetransfer/IBBTransferNegotiator.java | 2 +- .../Socks5TransferNegotiator.java | 5 +- .../org/jivesoftware/smackx/packet/VCard.java | 4 +- 20 files changed, 2099 insertions(+), 81 deletions(-) create mode 100644 documentation/privacy.html create mode 100644 source/org/jivesoftware/smack/PrivacyList.java create mode 100644 source/org/jivesoftware/smack/PrivacyListListener.java create mode 100644 source/org/jivesoftware/smack/PrivacyListManager.java create mode 100644 source/org/jivesoftware/smack/packet/Privacy.java create mode 100644 source/org/jivesoftware/smack/packet/PrivacyItem.java create mode 100644 source/org/jivesoftware/smack/provider/PrivacyProvider.java diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index eb0ea25ce..09068ad76 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -5,6 +5,7 @@ org.jivesoftware.smackx.ServiceDiscoveryManager + org.jivesoftware.smack.PrivacyListManager org.jivesoftware.smackx.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChat org.jivesoftware.smackx.filetransfer.FileTransferManager diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 98867a4fa..3e3a66f00 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -180,4 +180,11 @@ org.jivesoftware.smackx.provider.IBBProviders$Data + + + query + jabber:iq:privacy + org.jivesoftware.smack.provider.PrivacyProvider + + \ No newline at end of file diff --git a/documentation/index.html b/documentation/index.html index 282a3f745..b7e6afb7a 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -25,12 +25,13 @@
  • Provider Architecture
  • Packet Properties
  • Debugging with Smack +
  • Privacy

  • Smack Extensions Manual diff --git a/documentation/privacy.html b/documentation/privacy.html new file mode 100644 index 000000000..0d6d2335e --- /dev/null +++ b/documentation/privacy.html @@ -0,0 +1,167 @@ + + + Smack: Privacy - Jive Software + + + + + +
    +Privacy +
    + + + +

    What is?

    +

    +Privacy is a method for users to block communications from particular other users. In XMPP this is done by managing one's privacy lists.
    +Server-side privacy lists enable successful completion of the following use cases: +

      +
    • Retrieving one's privacy lists. +
    • Adding, removing, and editing one's privacy lists. +
    • Setting, changing, or declining active lists. +
    • Setting, changing, or declining the default list (i.e., the list that is active by default). +
    • Allowing or blocking messages based on JID, group, or subscription type (or globally). +
    • Allowing or blocking inbound presence notifications based on JID, group, or subscription type (or globally). +
    • Allowing or blocking outbound presence notifications based on JID, group, or subscription type (or globally). +
    • Allowing or blocking IQ stanzas based on JID, group, or subscription type (or globally). +
    • Allowing or blocking all communications based on JID, group, or subscription type (or globally). +
    +

    + +

    How can I use it?

    + +

    +The API implementation releases three main public classes: +

      +
    • PrivacyListManager: this is the main API class to retrieve and handle server privacy lists. +
    • PrivacyList: witch represents one privacy list, with a name, a set of privacy items. For example, the list with visible or invisible. +
    • PrivacyItem: block or allow one aspect of privacy. For example, to allow my friend to see my presence. +
    +
      +
    1. Right from the start, a client MAY get his/her privacy list that is stored in the server:
      +
      +
      +	// Create a privacy manager for the current connection.
      +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
      +	// Retrieve server privacy lists
      +	PrivacyList[] lists = privacyManager.getPrivacyLists();
      +
      +
      +Now the client is able to show every PrivacyItem of the server and also for every list if it is active, default or none of them. The client is a listener of privacy changes.
      +
      +
    2. In order to add a new list in the server, the client MAY implement something like: +
      +
      +	// Set the name of the list
      +	String listName = "newList";
      +
      +	// Create the list of PrivacyItem that will allow or deny some privacy aspect
      +	String user = "tybalt@example.com";
      +	String groupName = "enemies";
      +	ArrayList privacyItems = new ArrayList();
      +
      +	PrivacyItem item = new PrivacyItem(PrivacyRule.JID, true, 1);
      +	item.setValue(user);
      +	privacyItems.add(item);
      +
      +	item = new PrivacyItem(PrivacyRule.SUBSCRIPTION, true, 2);
      +	item.setValue(PrivacyRule.SUBSCRIPTION_BOTH);
      +	privacyItems.add(item);
      +     	
      +	item = new PrivacyItem(PrivacyRule.GROUP, false, 3);
      +	item.setValue(groupName);
      +	item.setFilterMessage(true);
      +	privacyItems.add(item);
      +
      +	// Get the privacy manager for the current connection.
      +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
      +	// Create the new list.
      +	privacyManager.createPrivacyList(listName, Arrays.asList(privacyItems));
      +
      +
      + +
    3. To modify an existent list, the client code MAY be like: +
      +
      +	// Set the name of the list
      +	String listName = "existingList";
      +	// Get the privacy manager for the current connection.
      +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
      +	// Sent the new list to the server.
      +	privacyManager.updatePrivacyList(listName, items);
      +
      +
      +Notice items was defined at the example 2 and MUST contain all the elements in the list (not the "delta"). + +
    4. In order to delete an existing list, the client MAY perform something like: +
      +
      +	// Set the name of the list
      +	String listName = "existingList";
      +	// Get the privacy manager for the current connection.
      +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
      +	// Remove the list.
      +	privacyManager.deletePrivacyList(listName);
      +
      +
      + +
    5. In order to decline the use of an active list, the client MAY perform something like: +
      +
      +	// Get the privacy manager for the current connection.
      +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
      +	// Decline the use of the active list.
      +	privacyManager.declineActiveList();
      +
      +
      + +
    6. In order to decline the use of a default list, the client MAY perform something like: +
      +
      +	// Get the privacy manager for the current connection.
      +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
      +	// Decline the use of the default list.
      +	privacyManager.declineDefaultList();
      +
      +
      + +
    + +

    Listening for Privacy Changes

    +

    +In order to handle privacy changes, clients SHOULD listen manager's updates. +When a list is changed the manager notifies every added listener. Listeners MUST implement the PrivacyListListener interface. + + + +Clients may need to react when a privacy list is modified. The PrivacyListManager lets you add listerners that will be notified when a list has been changed. Listeners should implement the PrivacyListListener interface.
    +The most important notification is updatedPrivacyList that is performed when a privacy list changes its privacy items.
    + +The listener becomes notified after performing: +

    +
    +	// Get the privacy manager for the current connection.
    +	PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(myConnection);
    +	// Add the listener (this) to get notified
    +	privacyManager.addListener(this);
    +
    +
    +

    + +

    References

    + +

    + +


    + + + + + diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index 32277ec25..a7a1e1462 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -607,7 +607,7 @@ class PacketReader { iqPacket.setTo(from); iqPacket.setFrom(to); iqPacket.setType(IQ.Type.ERROR); - iqPacket.setError(new XMPPError(501, "feature-not-implemented")); + iqPacket.setError(new XMPPError(XMPPError.Condition.feature_not_implemented)); connection.sendPacket(iqPacket); return null; } diff --git a/source/org/jivesoftware/smack/PrivacyList.java b/source/org/jivesoftware/smack/PrivacyList.java new file mode 100644 index 000000000..f9de4ab73 --- /dev/null +++ b/source/org/jivesoftware/smack/PrivacyList.java @@ -0,0 +1,57 @@ +package org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.PrivacyItem; + +import java.util.List; + +/** + * A Privacy List is a read only class used to represent a set of allowed or blocked communications. + * Basically it can:
      + * + *
    • Handle many {@link org.jivesoftware.smack.packet.PrivacyItem}.
    • + *
    • Answer if it is the default list.
    • + *
    • Answer if it is the active list.
    • + *
    + * + * {@link PrivacyItem Privacy Items} can handle different kind of blocking communications based on JID, group, + * subscription type or globally. + * + * @author Francisco Vives + */ +public class PrivacyList { + + /** Holds if it is an active list or not **/ + private boolean isActiveList; + /** Holds if it is an default list or not **/ + private boolean isDefaultList; + /** Holds the list name used to print **/ + private String listName; + /** Holds the list of {@see PrivacyItem} **/ + private List items; + + protected PrivacyList(boolean isActiveList, boolean isDefaultList, + String listName, List privacyItems) { + super(); + this.isActiveList = isActiveList; + this.isDefaultList = isDefaultList; + this.listName = listName; + this.items = privacyItems; + } + + public boolean isActiveList() { + return isActiveList; + } + + public boolean isDefaultList() { + return isDefaultList; + } + + public List getItems() { + return items; + } + + public String toString() { + return listName; + } + +} diff --git a/source/org/jivesoftware/smack/PrivacyListListener.java b/source/org/jivesoftware/smack/PrivacyListListener.java new file mode 100644 index 000000000..2d01dcf5b --- /dev/null +++ b/source/org/jivesoftware/smack/PrivacyListListener.java @@ -0,0 +1,30 @@ +package org.jivesoftware.smack; + +import java.util.List; + +/** + * Interface to implement classes to listen for server events about privacy communication. + * Listeners are registered with the {@link PrivacyListManager}. + * + * @see {@link PrivacyListManager#addListener} + * + * @author Francisco Vives + */ +public interface PrivacyListListener { + + /** + * Set or update a privacy list with PrivacyItem. + * + * @param listName the name of the new or updated privacy list. + * @param listItem the PrivacyItems that rules the list. + */ + public void setPrivacyList(String listName, List listItem); + + /** + * A privacy list has been modified by another. It gets notified. + * + * @param listName the name of the updated privacy list. + */ + public void updatedPrivacyList(String listName); + +} \ No newline at end of file diff --git a/source/org/jivesoftware/smack/PrivacyListManager.java b/source/org/jivesoftware/smack/PrivacyListManager.java new file mode 100644 index 000000000..4858ba9b7 --- /dev/null +++ b/source/org/jivesoftware/smack/PrivacyListManager.java @@ -0,0 +1,442 @@ +package org.jivesoftware.smack; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Privacy; +import org.jivesoftware.smack.packet.PrivacyItem; + +/** + * A PrivacyListManager is used by XMPP clients to block or allow communications from other + * users. Use the manager to:
      + *
    • Retrieve privacy lists. + *
    • Add, remove, and edit privacy lists. + *
    • Set, change, or decline active lists. + *
    • Set, change, or decline the default list (i.e., the list that is active by default). + *
    + * Privacy Items can handle different kind of permission communications based on JID, group, + * subscription type or globally (@see PrivacyItem). + * + * @author Francisco Vives + */ +public class PrivacyListManager { + + // Keep the list of instances of this class. + private static Map instances = new Hashtable(); + + private XMPPConnection connection; + private List listeners = new ArrayList(); + PacketFilter packetFilter = new AndFilter(new IQTypeFilter(IQ.Type.SET), + new PacketExtensionFilter("query", "jabber:iq:privacy")); + + static { + // Create a new PrivacyListManager on every established connection. In the init() + // method of PrivacyListManager, we'll add a listener that will delete the + // instance when the connection is closed. + XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { + public void connectionEstablished(XMPPConnection connection) { + new PrivacyListManager(connection); + } + }); + } + /** + * Creates a new privacy manager to maintain the communication privacy. Note: no + * information is sent to or received from the server until you attempt to + * get or set the privacy communication.

    + * + * @param connection the XMPP connection. + */ + private PrivacyListManager(XMPPConnection connection) { + this.connection = connection; + this.init(); + } + + /** Answer the connection userJID that owns the privacy. + * @return the userJID that owns the privacy + */ + private String getUser() { + return connection.getUser(); + } + + /** + * Initializes the packet listeners of the connection that will notify for any set privacy + * package. + */ + private void init() { + // Register the new instance and associate it with the connection + instances.put(connection, this); + // Add a listener to the connection that removes the registered instance when + // the connection is closed + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + // Unregister this instance since the connection has been closed + instances.remove(connection); + } + + public void connectionClosedOnError(Exception e) { + // Unregister this instance since the connection has been closed + instances.remove(connection); + } + }); + + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + + if (packet == null || packet.getError() != null) { + return; + } + // The packet is correct. + Privacy privacy = (Privacy) packet; + + // Prepare the information before starting the notification + int listNameSize = privacy.getItemLists().size(); + Object[] listItemsPairs = privacy.getItemLists().entrySet().toArray(); + + // Notifies the event to the listeners. + synchronized (listeners) { + for (Iterator i = listeners.iterator(); i.hasNext();) { + PrivacyListListener listener = (PrivacyListListener) i.next(); + + // Notifies the created or updated privacy lists + for (int j = 0; j < listNameSize; j++) { + Map.Entry entry = (Map.Entry) listItemsPairs[j]; + String listName = (String) entry.getKey(); + List items = (List) entry.getValue(); + if (items.isEmpty()) { + listener.updatedPrivacyList(listName); + } else { + listener.setPrivacyList(listName, items); + } + } + } + } + + // Send a result package acknowledging the reception of a privacy package. + + // Prepare the IQ packet to send + IQ iq = new IQ() { + public String getChildElementXML() { + return ""; + } + }; + iq.setType(IQ.Type.RESULT); + iq.setFrom(packet.getFrom()); + iq.setPacketID(packet.getPacketID()); + + // Send create & join packet. + connection.sendPacket(iq); + } + }, packetFilter); + } + + /** + * Returns the PrivacyListManager instance associated with a given XMPPConnection. + * + * @param connection the connection used to look for the proper PrivacyListManager. + * @return the PrivacyListManager associated with a given XMPPConnection. + */ + public static PrivacyListManager getInstanceFor(XMPPConnection connection) { + return (PrivacyListManager) instances.get(connection); + } + + /** + * Send the {@link Privacy} packet to the server in order to know some privacy content and then + * waits for the answer. + * + * @param requestPrivacy is the {@link Privacy} packet configured properly whose XML + * will be sent to the server. + * @return a new {@link Privacy} with the data received from the server. + * @exception XMPPException if the request or the answer failed, it raises an exception. + */ + private Privacy getRequest(Privacy requestPrivacy) throws XMPPException { + // The request is a get iq type + requestPrivacy.setType(Privacy.Type.GET); + requestPrivacy.setFrom(this.getUser()); + + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(requestPrivacy.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + + // Send create & join packet. + connection.sendPacket(requestPrivacy); + + // Wait up to a certain number of seconds for a reply. + Privacy privacyAnswer = + (Privacy) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Stop queuing results + response.cancel(); + + // Interprete the result and answer the privacy only if it is valid + if (privacyAnswer == null) { + throw new XMPPException("No response from server."); + } + else if (privacyAnswer.getError() != null) { + throw new XMPPException(privacyAnswer.getError()); + } + return privacyAnswer; + } + + /** + * Send the {@link Privacy} packet to the server in order to modify the server privacy and + * waits for the answer. + * + * @param requestPrivacy is the {@link Privacy} packet configured properly whose xml will be sent + * to the server. + * @return a new {@link Privacy} with the data received from the server. + * @exception XMPPException if the request or the answer failed, it raises an exception. + */ + private Packet setRequest(Privacy requestPrivacy) throws XMPPException { + + // The request is a get iq type + requestPrivacy.setType(Privacy.Type.SET); + requestPrivacy.setFrom(this.getUser()); + + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(requestPrivacy.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + + // Send create & join packet. + connection.sendPacket(requestPrivacy); + + // Wait up to a certain number of seconds for a reply. + Packet privacyAnswer = response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Stop queuing results + response.cancel(); + + // Interprete the result and answer the privacy only if it is valid + if (privacyAnswer == null) { + throw new XMPPException("No response from server."); + } else if (privacyAnswer.getError() != null) { + throw new XMPPException(privacyAnswer.getError()); + } + return privacyAnswer; + } + + /** + * Answer a privacy containing the list structre without {@link PrivacyItem}. + * + * @return a Privacy with the list names. + */ + private Privacy getPrivacyWithListNames() throws XMPPException { + + // The request of the list is an empty privacy message + Privacy request = new Privacy(); + + // Send the package to the server and get the answer + return getRequest(request); + } + + /** + * Answer the active privacy list. + * + * @return the {@see PrivacyList} of the active list. + */ + public PrivacyList getActiveList() throws XMPPException { + Privacy privacyAnswer = this.getPrivacyWithListNames(); + String listName = privacyAnswer.getActiveName(); + boolean isDefaultAndActive = privacyAnswer.getActiveName() != null + && privacyAnswer.getDefaultName() != null + && privacyAnswer.getActiveName().equals( + privacyAnswer.getDefaultName()); + return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName)); + } + + /** + * Answer the default privacy list. + * + * @return the {@see PrivacyList} of the default list. + */ + public PrivacyList getDefaultList() throws XMPPException { + Privacy privacyAnswer = this.getPrivacyWithListNames(); + String listName = privacyAnswer.getDefaultName(); + boolean isDefaultAndActive = privacyAnswer.getActiveName() != null + && privacyAnswer.getDefaultName() != null + && privacyAnswer.getActiveName().equals( + privacyAnswer.getDefaultName()); + return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName)); + } + + /** + * Answer the privacy list items under listName with the allowed and blocked permissions. + * + * @param listName the name of the list to get the allowed and blocked permissions. + * @return a list of {@link PrivacyItem} under the list listName. + */ + private List getPrivacyListItems(String listName) throws XMPPException { + + // The request of the list is an privacy message with an empty list + Privacy request = new Privacy(); + request.setPrivacyList(listName, new ArrayList()); + + // Send the package to the server and get the answer + Privacy privacyAnswer = getRequest(request); + + return privacyAnswer.getPrivacyList(listName); + } + + /** + * Answer the privacy list items under listName with the allowed and blocked permissions. + * + * @param listName the name of the list to get the allowed and blocked permissions. + * @return a {@link PrivacyList} under the list listName. + */ + public PrivacyList getPrivacyList(String listName) throws XMPPException { + + PrivacyList list = new PrivacyList(false, false, listName, getPrivacyListItems(listName)); + return list; + } + + /** + * Answer every privacy list with the allowed and blocked permissions. + * + * @return a List of {@link PrivacyList}. + */ + public PrivacyList[] getPrivacyLists() throws XMPPException { + Privacy privacyAnswer = this.getPrivacyWithListNames(); + Set names = privacyAnswer.getPrivacyListNames(); + PrivacyList[] lists = new PrivacyList[names.size()]; + String listName; + boolean isActiveList; + boolean isDefaultList; + int index=0; + for (Iterator iter = names.iterator(); iter.hasNext();) { + listName = (String) iter.next(); + isActiveList = listName.equals(privacyAnswer.getActiveName()); + isDefaultList = listName.equals(privacyAnswer.getDefaultName()); + lists[index] = new PrivacyList(isActiveList, isDefaultList, + listName, getPrivacyListItems(listName)); + index = index + 1; + } + return lists; + } + + + /** + * Set or change the active list to listName. + * + * @param listName the list name to set as the active one. + * @exception XMPPException if the request or the answer failed, it raises an exception. + */ + public void setActiveListName(String listName) throws XMPPException { + + // The request of the list is an privacy message with an empty list + Privacy request = new Privacy(); + request.setActiveName(listName); + + // Send the package to the server + setRequest(request); + } + + /** + * Client declines the use of active lists. + */ + public void declineActiveList() throws XMPPException { + + // The request of the list is an privacy message with an empty list + Privacy request = new Privacy(); + request.setDeclineActiveList(true); + + // Send the package to the server + setRequest(request); + } + + /** + * Set or change the default list to listName. + * + * @param listName the list name to set as the default one. + * @exception XMPPException if the request or the answer failed, it raises an exception. + */ + public void setDefaultListName(String listName) throws XMPPException { + + // The request of the list is an privacy message with an empty list + Privacy request = new Privacy(); + request.setDefaultName(listName); + + // Send the package to the server + setRequest(request); + } + + /** + * Client declines the use of default lists. + */ + public void declineDefaultList() throws XMPPException { + + // The request of the list is an privacy message with an empty list + Privacy request = new Privacy(); + request.setDeclineDefaultList(true); + + // Send the package to the server + setRequest(request); + } + + /** + * The client has created a new list. It send the new one to the server. + * + * @param listName the list that has changed its content. + * @param privacyItems a List with every {@link PrivacyItem} in the list. + */ + public void createPrivacyList(String listName, List privacyItems) throws XMPPException { + + this.updatePrivacyList(listName, privacyItems); + } + + /** + * The client has edited an existing list. It updates the server content with the resulting + * list of {@link PrivacyItem}. The {@link PrivacyItem} list MUST contain all elements in the + * list (not the "delta"). + * + * @param listName the list that has changed its content. + * @param privacyItems a List with every {@link PrivacyItem} in the list. + */ + public void updatePrivacyList(String listName, List privacyItems) throws XMPPException { + + // Build the privacy package to add or update the new list + Privacy request = new Privacy(); + request.setPrivacyList(listName, privacyItems); + + // Send the package to the server + setRequest(request); + } + + /** + * Remove a privacy list. + * + * @param listName the list that has changed its content. + */ + public void deletePrivacyList(String listName) throws XMPPException { + + // The request of the list is an privacy message with an empty list + Privacy request = new Privacy(); + request.setPrivacyList(listName, new ArrayList()); + + // Send the package to the server + setRequest(request); + } + + /** + * Adds a packet listener that will be notified of any new update in the user + * privacy communication. + * + * @param listener a packet listener. + */ + public void addListener(PrivacyListListener listener) { + // Keep track of the listener so that we can manually deliver extra + // messages to it later if needed. + synchronized (listeners) { + listeners.add(listener); + } + } +} diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index 53bb15c98..2a28afd25 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -277,16 +277,16 @@ public class XMPPConnection { } } catch (UnknownHostException uhe) { - throw new XMPPException( - "Could not connect to " + host + ":" + port + ".", - new XMPPError(504), - uhe); + String errorMessage = "Could not connect to " + host + ":" + port + "."; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.remote_server_timeout, errorMessage), + uhe); } catch (IOException ioe) { - throw new XMPPException( - "XMPPError connecting to " + host + ":" + port + ".", - new XMPPError(502), - ioe); + String errorMessage = "XMPPError connecting to " + host + ":" + + port + "."; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.remote_server_error, errorMessage), ioe); } this.serviceName = config.getServiceName(); try { @@ -966,9 +966,10 @@ public class XMPPConnection { } catch (IOException ioe) { throw new XMPPException( - "XMPPError establishing connection with server.", - new XMPPError(502), - ioe); + "XMPPError establishing connection with server.", + new XMPPError(XMPPError.Condition.remote_server_error, + "XMPPError establishing connection with server."), + ioe); } // If debugging is enabled, we open a window and write out all network traffic. diff --git a/source/org/jivesoftware/smack/packet/Privacy.java b/source/org/jivesoftware/smack/packet/Privacy.java new file mode 100644 index 000000000..c21a52dc1 --- /dev/null +++ b/source/org/jivesoftware/smack/packet/Privacy.java @@ -0,0 +1,315 @@ +package org.jivesoftware.smack.packet; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.List; +import java.util.Set; + +/** + * A Privacy IQ Packet, is used by the {@see PrivacyListManager} and {@see PrivacyProvider} to allow + * and block communications from other users. It contains the appropriate structure to suit + * user-defined privacy lists. Different configured Privacy packages are used in the Server – + * Manager communication in order to: + *

      + *
    • Retrieving one's privacy lists. + *
    • Adding, removing, and editing one's privacy lists. + *
    • Setting, changing, or declining active lists. + *
    • Setting, changing, or declining the default list (i.e., the list that is active by default). + *
    + * Privacy Items can handle different kind of blocking communications based on JID, group, + * subscription type or globally {@see PrivacyItem} + * + * @author Francisco Vives + */ + +public class Privacy extends IQ { + /** declineActiveList is true when the user declines the use of the active list **/ + private boolean declineActiveList=false; + /** activeName is the name associated with the active list set for the session **/ + private String activeName; + /** declineDefaultList is true when the user declines the use of the default list **/ + private boolean declineDefaultList=false; + /** defaultName is the name of the default list that applies to the user as a whole **/ + private String defaultName; + /** itemLists holds the set of privacy items classified in lists. It is a map where the + * key is the name of the list and the value a collection with privacy items. **/ + private Map itemLists = new HashMap(); + + /** + * Set or update a privacy list with {@link PrivacyItem}. + * + * @param listName the name of the new privacy list. + * @param listItem the {@link PrivacyItem} that rules the list. + * @return the privacy List. + */ + public List setPrivacyList(String listName, List listItem) { + // Add new list to the itemLists + this.getItemLists().put(listName, listItem); + return listItem; + } + + /** + * Set the active list based on the default list. + * + * @return the active List. + */ + public List setActivePrivacyList() { + this.setActiveName(this.getDefaultName()); + return (List) this.getItemLists().get(this.getActiveName()); + } + + /** + * Deletes an existing privacy list. If the privacy list being deleted was the default list + * then the user will end up with no default list. Therefore, the user will have to set a new + * default list. + * + * @param listName the name of the list being deleted. + */ + public void deletePrivacyList(String listName) { + // Remove the list from the cache + this.getItemLists().remove(listName); + + // Check if deleted list was the default list + if (this.getDefaultName() != null && listName.equals(this.getDefaultName())) { + this.setDefaultName(null); + } + } + + /** + * Returns the active privacy list or null if none was found. + * + * @return list with {@link PrivacyItem} or null if none was found. + */ + public List getActivePrivacyList() { + // Check if we have the default list + if (this.getActiveName() == null) { + return null; + } else { + return (List) this.getItemLists().get(this.getActiveName()); + } + } + + /** + * Returns the default privacy list or null if none was found. + * + * @return list with {@link PrivacyItem} or null if none was found. + */ + public List getDefaultPrivacyList() { + // Check if we have the default list + if (this.getDefaultName() == null) { + return null; + } else { + return (List) this.getItemLists().get(this.getDefaultName()); + } + } + + /** + * Returns a specific privacy list. + * + * @param listName the name of the list to get. + * @return a List with {@link PrivacyItem} + */ + public List getPrivacyList(String listName) { + return (List) this.getItemLists().get(listName); + } + + /** + * Returns the privacy item in the specified order. + * + * @param order the order of the element. + * @return a List with {@link PrivacyItem} + */ + public PrivacyItem getItem(String listName, int order) { + Iterator values = getPrivacyList(listName).iterator(); + PrivacyItem itemFound = null; + while (itemFound == null && values.hasNext()) { + PrivacyItem element = (PrivacyItem) values.next(); + if (element.getOrder() == order) { + itemFound = element; + } + } + return itemFound; + } + + + /** + * Sets a given privacy list as the new user default list. + * + * @param newDefault the new default privacy list. + * @return if the default list was changed. + */ + public boolean changeDefaultList(String newDefault) { + if (this.getItemLists().containsKey(newDefault)) { + this.setDefaultName(newDefault); + return true; + } else { + return false; + } + } + + + /** + * Remove the list. + * + * @param listName name of the list to remove. + */ + public void deleteList(String listName) { + this.getItemLists().remove(listName); + } + + + /** + * Returns the name associated with the active list set for the session. Communications + * will be verified against the active list.

    + * + * @return the name of the active list. + */ + public String getActiveName() { + return activeName; + } + + /** + * Sets the name associated with the active list set for the session. Communications + * will be verified against the active list.

    + * + * @param activeName is the name of the active list. + */ + public void setActiveName(String activeName) { + this.activeName = activeName; + } + + /** + * Returns the name of the default list that applies to the user as a whole. Default list is + * processed if there is no active list set for the target session/resource to which a stanza + * is addressed, or if there are no current sessions for the user. + * + * @return the name of the default list. + */ + public String getDefaultName() { + return defaultName; + } + + /** + * Sets the name of the default list that applies to the user as a whole. Default list is + * processed if there is no active list set for the target session/resource to which a stanza + * is addressed, or if there are no current sessions for the user. + * + * If there is no default list set, then all Privacy Items are processed. + * + * @param defaultName is the name of the default list. + */ + public void setDefaultName(String defaultName) { + this.defaultName = defaultName; + } + + /** + * Returns the collection of privacy list that the user holds. A Privacy List contains a set of + * rules that define if communication with the list owner is allowed or denied. + * Users may have zero, one or more privacy items. + * + * @return a map where the key is the name of the list and the value the + * collection of privacy items. + */ + public Map getItemLists() { + return itemLists; + } + + /** + * Returns whether the receiver allows or declines the use of an active list. + * + * @return the decline status of the list. + */ + public boolean isDeclineActiveList() { + return declineActiveList; + } + + /** + * Sets whether the receiver allows or declines the use of an active list. + * + * @param declineActiveList indicates if the receiver declines the use of an active list. + */ + public void setDeclineActiveList(boolean declineActiveList) { + this.declineActiveList = declineActiveList; + } + + /** + * Returns whether the receiver allows or declines the use of a default list. + * + * @return the decline status of the list. + */ + public boolean isDeclineDefaultList() { + return declineDefaultList; + } + + /** + * Sets whether the receiver allows or declines the use of a default list. + * + * @param declineActiveList indicates if the receiver declines the use of a default list. + */ + public void setDeclineDefaultList(boolean declineDefaultList) { + this.declineDefaultList = declineDefaultList; + } + + /** + * Returns all the list names the user has defined to group restrictions. + * + * @return a Set with Strings containing every list names. + */ + public Set getPrivacyListNames() { + return this.itemLists.keySet(); + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append(""); + + // Add the active tag + if (this.isDeclineActiveList()) { + buf.append(""); + } else { + if (this.getActiveName() != null) { + buf.append(""); + } + } + // Add the default tag + if (this.isDeclineDefaultList()) { + buf.append(""); + } else { + if (this.getDefaultName() != null) { + buf.append(""); + } + } + + // Add the list with their privacy items + int listNameSize = this.getItemLists().size(); + Iterator listItemsPairs = this.getItemLists().entrySet().iterator(); + for (int i = 0; i < listNameSize; i++) { + Map.Entry entry = (Map.Entry) listItemsPairs.next(); + String listName = (String) entry.getKey(); + List items = (List) entry.getValue(); + // Begin the list tag + if (items.isEmpty()) { + buf.append(""); + } else { + buf.append(""); + } + for (Iterator itemIterator = items.iterator(); itemIterator.hasNext();) { + PrivacyItem item = (PrivacyItem) itemIterator.next(); + // Append the item xml representation + buf.append(item.toXML()); + } + // Close the list tag + if (!items.isEmpty()) { + buf.append(""); + } + } + + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append(""); + String generatedXML = buf.toString(); + return generatedXML; + } + +} diff --git a/source/org/jivesoftware/smack/packet/PrivacyItem.java b/source/org/jivesoftware/smack/packet/PrivacyItem.java new file mode 100644 index 000000000..a671b634f --- /dev/null +++ b/source/org/jivesoftware/smack/packet/PrivacyItem.java @@ -0,0 +1,454 @@ +package org.jivesoftware.smack.packet; + +/** + * A privacy item acts a rule that when matched defines if a packet should be blocked or not. + * + * Privacy Items can handle different kind of blocking communications based on JID, group, + * subscription type or globally by:

      + *
    • Allowing or blocking messages. + *
    • Allowing or blocking inbound presence notifications. + *
    • Allowing or blocking outbound presence notifications. + *
    • Allowing or blocking IQ stanzas. + *
    • Allowing or blocking all communications. + *
    + * @author Francisco Vives + */ +public class PrivacyItem { + /** allow is the action associated with the item, it can allow or deny the communication. */ + private boolean allow; + /** order is a non-negative integer that is unique among all items in the list. */ + private int order; + /** rule hold the kind of communication ([jid|group|subscription]) it will allow or block and + * identifier to apply the action. + * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID. + * If the type is "group", then the 'value' attribute SHOULD contain the name of a group + * in the user's roster. + * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to", + * "from", or "none". */ + private PrivacyRule rule; + + /** blocks incoming IQ stanzas. */ + private boolean filterIQ = false; + /** filterMessage blocks incoming message stanzas. */ + private boolean filterMessage = false; + /** blocks incoming presence notifications. */ + private boolean filterPresence_in = false; + /** blocks outgoing presence notifications. */ + private boolean filterPresence_out = false; + + /** + * Creates a new privacy item. + * + * @param type the type. + */ + public PrivacyItem(String type, boolean allow, int order) { + this.setRule(PrivacyRule.fromString(type)); + this.setAllow(allow); + this.setOrder(order); + } + + /** + * Returns the action associated with the item, it MUST be filled and will allow or deny + * the communication. + * + * @return the allow communication status. + */ + public boolean isAllow() { + return allow; + } + + /** + * Sets the action associated with the item, it can allow or deny the communication. + * + * @param allow indicates if the receiver allow or deny the communication. + */ + private void setAllow(boolean allow) { + this.allow = allow; + } + + + /** + * Returns whether the receiver allow or deny incoming IQ stanzas or not. + * + * @return the iq filtering status. + */ + public boolean isFilterIQ() { + return filterIQ; + } + + + /** + * Sets whether the receiver allows or denies incoming IQ stanzas or not. + * + * @param filterIQ indicates if the receiver allows or denies incoming IQ stanzas. + */ + public void setFilterIQ(boolean filterIQ) { + this.filterIQ = filterIQ; + } + + + /** + * Returns whether the receiver allows or denies incoming messages or not. + * + * @return the message filtering status. + */ + public boolean isFilterMessage() { + return filterMessage; + } + + + /** + * Sets wheather the receiver allows or denies incoming messages or not. + * + * @param filterMessage indicates if the receiver allows or denies incoming messages or not. + */ + public void setFilterMessage(boolean filterMessage) { + this.filterMessage = filterMessage; + } + + + /** + * Returns whether the receiver allows or denies incoming presence or not. + * + * @return the iq filtering incoming presence status. + */ + public boolean isFilterPresence_in() { + return filterPresence_in; + } + + + /** + * Sets whether the receiver allows or denies incoming presence or not. + * + * @param filterPresence_in indicates if the receiver allows or denies filtering incoming presence. + */ + public void setFilterPresence_in(boolean filterPresence_in) { + this.filterPresence_in = filterPresence_in; + } + + + /** + * Returns whether the receiver allows or denies incoming presence or not. + * + * @return the iq filtering incoming presence status. + */ + public boolean isFilterPresence_out() { + return filterPresence_out; + } + + + /** + * Sets whether the receiver allows or denies outgoing presence or not. + * + * @param filterPresence_out indicates if the receiver allows or denies filtering outgoing presence + */ + public void setFilterPresence_out(boolean filterPresence_out) { + this.filterPresence_out = filterPresence_out; + } + + + /** + * Returns the order where the receiver is processed. List items are processed in + * ascending order. + * + * The order MUST be filled and its value MUST be a non-negative integer + * that is unique among all items in the list. + * + * @return the order number. + */ + public int getOrder() { + return order; + } + + + /** + * Sets the order where the receiver is processed. + * + * The order MUST be filled and its value MUST be a non-negative integer + * that is unique among all items in the list. + * + * @param order indicates the order in the list. + */ + private void setOrder(int order) { + this.order = order; + } + + /** + * Sets the element identifier to apply the action. + * + * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID. + * If the type is "group", then the 'value' attribute SHOULD contain the name of a group + * in the user's roster. + * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to", + * "from", or "none". + * + * @param value is the identifier to apply the action. + */ + public void setValue(String value) { + if (!(this.getRule() == null && value == null)) { + this.getRule().setValue(value); + } + } + + /** + * Returns the type hold the kind of communication it will allow or block. + * It MUST be filled with one of these values: jid, group or subscription. + * + * @return the type of communication it represent. + */ + public String getType() { + if (this.getRule() == null) { + return null; + } else { + return this.getRule().getType(); + } + } + + /** + * Returns the element identifier to apply the action. + * + * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID. + * If the type is "group", then the 'value' attribute SHOULD contain the name of a group + * in the user's roster. + * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to", + * "from", or "none". + * + * @return the identifier to apply the action. + */ + public String getValue() { + if (this.getRule() == null) { + return null; + } else { + return this.getRule().getValue(); + } + } + + + /** + * Returns whether the receiver allows or denies every kind of communication. + * + * When filterIQ, filterMessage, filterPresence_in and filterPresence_out are not set + * the receiver will block all communications. + * + * @return the all communications status. + */ + public boolean isFilterEverything() { + return !(this.isFilterIQ() || this.isFilterMessage() || this.isFilterPresence_in() + || this.isFilterPresence_out()); + } + + + private PrivacyRule getRule() { + return rule; + } + + private void setRule(PrivacyRule rule) { + this.rule = rule; + } + /** + * Answer an xml representation of the receiver according to the RFC 3921. + * + * @return the text xml representation. + */ + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append(""); + } else { + buf.append(">"); + if (this.isFilterIQ()) { + buf.append(""); + } + if (this.isFilterMessage()) { + buf.append(""); + } + if (this.isFilterPresence_in()) { + buf.append(""); + } + if (this.isFilterPresence_out()) { + buf.append(""); + } + buf.append(""); + } + return buf.toString(); + } + + + /** + * Privacy Rule represents the kind of action to apply. + * It holds the kind of communication ([jid|group|subscription]) it will allow or block and + * identifier to apply the action. + */ + + public static class PrivacyRule { + /** + * Type defines if the rule is based on JIDs, roster groups or presence subscription types. + * Available values are: [jid|group|subscription] + */ + private String type; + /** + * The value hold the element identifier to apply the action. + * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID. + * If the type is "group", then the 'value' attribute SHOULD contain the name of a group + * in the user's roster. + * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to", + * "from", or "none". + */ + private String value; + + /** + * JID being analyzed should have a resource match, domain match or bare JID match. + */ + public static final String GROUP = "group"; + /** + * JID being analyzed should belong to a roster group of the list's owner. + */ + public static final String JID = "jid"; + /** + * JID being analyzed should belong to a contact present in the owner's roster with + * the specified subscription status. + */ + public static final String SUBSCRIPTION = "subscription"; + + /** + * If the type is "subscription", then the 'value' attribute MUST be one of "both", + * "to", "from", or "none" + */ + public static final String SUBSCRIPTION_BOTH = "subscription"; + public static final String SUBSCRIPTION_TO = "to"; + public static final String SUBSCRIPTION_FROM = "from"; + public static final String SUBSCRIPTION_NONE = "none"; + + /** + * Returns the type constant associated with the String value. + */ + protected static PrivacyRule fromString(String value) { + String type = null; + if (value == null) { + return null; + } + if (SUBSCRIPTION.equalsIgnoreCase(value)) { + type = SUBSCRIPTION; + } + else if (GROUP.equalsIgnoreCase(value)) { + type = GROUP; + } + else if (JID.equalsIgnoreCase(value)) { + type = JID; + } + // Default to available. + else { + return null; + } + PrivacyRule rule = new PrivacyRule(); + rule.setType(type); + return rule; + } + + /** + * Returns the type hold the kind of communication it will allow or block. + * It MUST be filled with one of these values: jid, group or subscription. + * + * @return the type of communication it represent. + */ + public String getType() { + return type; + } + + /** + * Sets the action associated with the item, it can allow or deny the communication. + * + * @param allow indicates if the receiver allows or denies the communication. + */ + private void setType(String type) { + this.type = type; + } + + /** + * Returns the element identifier to apply the action. + * + * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID. + * If the type is "group", then the 'value' attribute SHOULD contain the name of a group + * in the user's roster. + * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to", + * "from", or "none". + * + * @return the identifier to apply the action. + */ + public String getValue() { + return value; + } + + /** + * Sets the element identifier to apply the action. + * + * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID. + * If the type is "group", then the 'value' attribute SHOULD contain the name of a group + * in the user's roster. + * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to", + * "from", or "none". + * + * @param value is the identifier to apply the action. + */ + protected void setValue(String value) { + if (this.isSuscription()) { + setSuscriptionValue(value); + } else { + this.value = value; + } + } + + /** + * Sets the element identifier to apply the action. + * + * The 'value' attribute MUST be one of "both", "to", "from", or "none". + * + * @param value is the identifier to apply the action. + */ + private void setSuscriptionValue(String value) { + String setValue = null; + if (value == null) { + setValue = null; + } + if (SUBSCRIPTION_BOTH.equalsIgnoreCase(value)) { + setValue = SUBSCRIPTION_BOTH; + } + else if (SUBSCRIPTION_TO.equalsIgnoreCase(value)) { + setValue = SUBSCRIPTION_TO; + } + else if (SUBSCRIPTION_FROM.equalsIgnoreCase(value)) { + setValue = SUBSCRIPTION_FROM; + } + else if (SUBSCRIPTION_NONE.equalsIgnoreCase(value)) { + setValue = SUBSCRIPTION_NONE; + } + // Default to available. + else { + setValue = null; + } + this.value = setValue; + } + + /** + * Returns if the receiver represents a subscription rule. + * + * @return if the receiver represents a subscription rule. + */ + public boolean isSuscription () { + return this.getValue() == SUBSCRIPTION; + } + } +} diff --git a/source/org/jivesoftware/smack/packet/XMPPError.java b/source/org/jivesoftware/smack/packet/XMPPError.java index 0c03fe27d..618f19e21 100644 --- a/source/org/jivesoftware/smack/packet/XMPPError.java +++ b/source/org/jivesoftware/smack/packet/XMPPError.java @@ -20,29 +20,43 @@ package org.jivesoftware.smack.packet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + /** * Represents a XMPP error sub-packet. Typically, a server responds to a request that has - * problems by sending the packet back and including an error packet. Each error has a code - * as well as as an optional text explanation. Typical error codes are as follows:

    + * problems by sending the packet back and including an error packet. Each error has a code, type, + * error condition as well as as an optional text explanation. Typical errors are:

    * * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * + *
    + * + * + * > + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * *
    CodeDescription
    302 Redirect
    400 Bad Request
    401 Unauthorized
    402 Payment Required
    403 Forbidden
    404 Not Found
    405 Not Allowed
    406 Not Acceptable
    407 Registration Required
    408 Request Timeout
    409 Conflict
    500 Internal Server XMPPError
    501 Not Implemented
    502 Remote Server Error
    503 Service Unavailable
    504 Remote Server Timeout
    CodeXMPP ErrorType
    500interna-server-errorWAIT
    403forbiddenAUTH
    400bad-requestMODIFY
    404item-not-foundCANCEL
    409conflictCANCEL
    501feature-not-implementedCANCEL
    302goneMODIFY
    400jid-malformedMODIFY
    406no-acceptable MODIFY
    405not-allowedCANCEL
    401not-authorizedAUTH
    402payment-requiredAUTH
    404recipient-unavailableWAIT
    302redirectMODIFY
    407registration-requiredAUTH
    404remote-server-not-foundCANCEL
    504remote-server-timeoutWAIT
    502remote-server-errorCANCEL
    500resource-constraintWAIT
    503service-unavailableCANCEL
    407subscription-requiredAUTH
    500undefined-conditionWAIT
    400unexpected-conditionWAIT
    408request-timeoutCANCEL
    * * @author Matt Tucker @@ -50,29 +64,119 @@ package org.jivesoftware.smack.packet; public class XMPPError { private int code; + private Type type; + private String condition; private String message; + private List applicationExtensions = null; + /** - * Creates a new error with the specified code and no message.. + * Creates a new error with the specified condition infering the type and code. + * If the Condition is predefined, client code should be like: + * new XMPPError(XMPPError.Condition.remote_server_timeout); + * If the Condition is not predefined, invocations should be like + * new XMPPError(new XMPPError.Condition("my_own_error")); + * + * @param code the error code. + */ + public XMPPError(Condition condition) { + this.init(condition); + this.message = null; + } + + /** + * Creates a new error with the specified condition and message infering the type and code. + * If the Condition is predefined, client code should be like: + * new XMPPError(XMPPError.Condition.remote_server_timeout, "Error Explanation"); + * If the Condition is not predefined, invocations should be like + * new XMPPError(new XMPPError.Condition("my_own_error"), "Error Explanation"); * * @param code the error code. + * @param message a message describing the error. + */ + public XMPPError(Condition condition, String messageText) { + this.init(condition); + this.message = messageText; + } + + /** + * Creates a new error with the specified code and no message. + * + * @param code the error code. + * @Deprecated new errors should be created using the constructor XMPPError(condition) */ public XMPPError(int code) { this.code = code; this.message = null; } - + /** * Creates a new error with the specified code and message. + * deprecated * * @param code the error code. * @param message a message describing the error. + * @Deprecated new errors should be created using the constructor XMPPError(condition, message) */ public XMPPError(int code, String message) { this.code = code; this.message = message; } + + + /** + * Creates a new error with the specified code, type, condition and message. + * This constructor is used when the condition is not recognized automatically by XMPPError + * i.e. there is not a defined instance of ErrorCondition or it does not applies the default + * specification. + * + * @param code the error code. + * @param type the error type. + * @param condition the error condition. + * @param message a message describing the error. + */ + public XMPPError(int code, Type type, String condition, String message, List extension) { + this.code = code; + this.type = type; + this.condition = condition; + this.message = message; + this.applicationExtensions = extension; + } + /** + * Initialize the error infering the type and code for the received condition. + * + * @param condition the error condition. + */ + private void init(Condition condition) { + // Look for the condition and its default code and type + ErrorSpecification defaultErrorSpecification = ErrorSpecification.specFor(condition); + this.condition = condition.value; + if (defaultErrorSpecification != null) { + // If there is a default error specification for the received condition, + // it get configured with the infered type and code. + this.type = defaultErrorSpecification.getType(); + this.code = defaultErrorSpecification.getCode(); + } + } + /** + * Returns the error condition. + * + * @return the error condition. + */ + public String getCondition() { + return condition; + } + + /** + * Returns the error type. + * + * @return the error type. + */ + public Type getType() { + return type; + } + /** * Returns the error code. * @@ -97,21 +201,299 @@ public class XMPPError { * @return the error as XML. */ public String toXML() { - StringBuilder buf = new StringBuilder(); - buf.append(""); - if (message != null) { - buf.append(message); + StringBuffer buf = new StringBuffer(); + buf.append(""); + if (condition != null) { + buf.append("<").append(condition); + buf.append(" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>"); + } + if (message != null) { + buf.append(""); + buf.append(message); + buf.append(""); + } + for (Iterator extensions = this.getExtensions(); extensions.hasNext();) { + PacketExtension element = (PacketExtension) extensions.next(); + buf.append(element.toXML()); + } buf.append(""); return buf.toString(); } public String toString() { - StringBuilder txt = new StringBuilder(); + StringBuffer txt = new StringBuffer(); txt.append("(").append(code).append(")"); if (message != null) { txt.append(" ").append(message); } return txt.toString(); } + + /** + * Returns an Iterator for the error extensions attached to the xmppError. + * An application MAY provide application-specific error information by including a + * properly-namespaced child in the error element. + * + * @return an Iterator for the error extensions. + */ + public synchronized Iterator getExtensions() { + if (applicationExtensions == null) { + return Collections.EMPTY_LIST.iterator(); + } + return Collections.unmodifiableList(new ArrayList(applicationExtensions)).iterator(); + } + + /** + * Returns the first patcket extension that matches the specified element name and + * namespace, or null if it doesn't exist. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML element namespace of the packet extension. + * @return the extension, or null if it doesn't exist. + */ + public synchronized PacketExtension getExtension(String elementName, String namespace) { + if (applicationExtensions == null || elementName == null || namespace == null) { + return null; + } + for (Iterator i=applicationExtensions.iterator(); i.hasNext(); ) { + PacketExtension ext = (PacketExtension)i.next(); + if (elementName.equals(ext.getElementName()) && namespace.equals(ext.getNamespace())) { + return ext; + } + } + return null; + } + + /** + * Adds a packet extension to the error. + * + * @param extension a packet extension. + */ + public synchronized void addExtension(PacketExtension extension) { + if (applicationExtensions == null) { + applicationExtensions = new ArrayList(); + } + applicationExtensions.add(extension); + } + + /** + * Set the packet extension to the error. + * + * @param extension a packet extension. + */ + public synchronized void setExtension(List extension) { + applicationExtensions = extension; + } + + + /** + * A class to represent the type of the Error. The types are: + * + *

      + *
    • XMPPError.Type.WAIT - retry after waiting (the error is temporary) + *
    • XMPPError.Type.CANCEL - do not retry (the error is unrecoverable) + *
    • XMPPError.Type.MODIFY - retry after changing the data sent + *
    • XMPPError.Type.AUTH - retry after providing credentials + *
    • XMPPError.Type.CONTINUE - proceed (the condition was only a warning) + *
    + */ + public static class Type { + + public static final Type WAIT = new Type("wait"); + public static final Type CANCEL = new Type("cancel"); + public static final Type MODIFY = new Type("modify"); + public static final Type AUTH = new Type("auth"); + public static final Type CONTINUE = new Type("continue"); + + /** + * Converts a String into the corresponding types. Valid String values + * that can be converted to types are: "wait", "cancel", "modify", "auth" or a user defined. + * + * @param type the String value to covert. + * @return the corresponding Type. + */ + public static Type fromString(String type) { + if (type == null) { + return null; + } + type = type.toLowerCase(); + if (CANCEL.toString().equals(type)) { + return CANCEL; + } + else if (CONTINUE.toString().equals(type)) { + return CONTINUE; + } + else if (WAIT.toString().equals(type)) { + return WAIT; + } + else if (MODIFY.toString().equals(type)) { + return MODIFY; + } + else if (AUTH.toString().equals(type)) { + return AUTH; + } + else { + return null; + } + } + + private String value; + + private Type(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + /** + * A class to represent predefined error conditions. + */ + public static class Condition { + + public static final Condition interna_server_error = new Condition("internal-server-error"); + public static final Condition forbidden = new Condition("forbidden"); + public static final Condition bad_request = new Condition("bad-request"); + public static final Condition conflict = new Condition("conflict"); + public static final Condition feature_not_implemented = new Condition("feature-not-implemented"); + public static final Condition gone = new Condition("gone"); + public static final Condition item_not_found = new Condition("item-not-found"); + public static final Condition jid_malformed = new Condition("jid-malformed"); + public static final Condition no_acceptable = new Condition("not-acceptable"); + public static final Condition not_allowed = new Condition("not-allowed"); + public static final Condition not_authorized = new Condition("not-authorized"); + public static final Condition payment_required = new Condition("payment-required"); + public static final Condition recipient_unavailable = new Condition("recipient-unavailable"); + public static final Condition redirect = new Condition("redirect"); + public static final Condition registration_required = new Condition("registration-required"); + public static final Condition remote_server_error = new Condition("remote-server-error"); + public static final Condition remote_server_not_found = new Condition("remote-server-not-found"); + public static final Condition remote_server_timeout = new Condition("remote-server-timeout"); + public static final Condition resource_constraint = new Condition("resource-constraint"); + public static final Condition service_unavailable = new Condition("service-unavailable"); + public static final Condition subscription_required = new Condition("subscription-required"); + public static final Condition undefined_condition = new Condition("undefined-condition"); + public static final Condition unexpected_condition = new Condition("unexpected-condition"); + public static final Condition request_timeout = new Condition("request-timeout"); + + private String value; + + public Condition(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + + /** + * A class to represent the error specification used to infer common usage. + */ + private static class ErrorSpecification { + private int code; + private Type type; + private Condition condition; + private static HashMap instances = errorSpecifications(); + + private ErrorSpecification(Condition condition, Type type, int code) { + this.code = code; + this.type = type; + this.condition = condition; + } + + private static HashMap errorSpecifications() { + HashMap instances = new HashMap(22); + instances.put(Condition.interna_server_error, new ErrorSpecification( + Condition.interna_server_error, Type.WAIT, 500)); + instances.put(Condition.forbidden, new ErrorSpecification(Condition.forbidden, + Type.AUTH, 403)); + instances.put(Condition.bad_request, new XMPPError.ErrorSpecification( + Condition.bad_request, Type.MODIFY, 400)); + instances.put(Condition.item_not_found, new XMPPError.ErrorSpecification( + Condition.item_not_found, Type.CANCEL, 404)); + instances.put(Condition.conflict, new XMPPError.ErrorSpecification( + Condition.conflict, Type.CANCEL, 409)); + instances.put(Condition.feature_not_implemented, new XMPPError.ErrorSpecification( + Condition.feature_not_implemented, Type.CANCEL, 501)); + instances.put(Condition.gone, new XMPPError.ErrorSpecification( + Condition.gone, Type.MODIFY, 302)); + instances.put(Condition.jid_malformed, new XMPPError.ErrorSpecification( + Condition.jid_malformed, Type.MODIFY, 400)); + instances.put(Condition.no_acceptable, new XMPPError.ErrorSpecification( + Condition.no_acceptable, Type.MODIFY, 406)); + instances.put(Condition.not_allowed, new XMPPError.ErrorSpecification( + Condition.not_allowed, Type.CANCEL, 405)); + instances.put(Condition.not_authorized, new XMPPError.ErrorSpecification( + Condition.not_authorized, Type.AUTH, 401)); + instances.put(Condition.payment_required, new XMPPError.ErrorSpecification( + Condition.payment_required, Type.AUTH, 402)); + instances.put(Condition.recipient_unavailable, new XMPPError.ErrorSpecification( + Condition.recipient_unavailable, Type.WAIT, 404)); + instances.put(Condition.redirect, new XMPPError.ErrorSpecification( + Condition.redirect, Type.MODIFY, 302)); + instances.put(Condition.registration_required, new XMPPError.ErrorSpecification( + Condition.registration_required, Type.AUTH, 407)); + instances.put(Condition.remote_server_not_found, new XMPPError.ErrorSpecification( + Condition.remote_server_not_found, Type.CANCEL, 404)); + instances.put(Condition.remote_server_timeout, new XMPPError.ErrorSpecification( + Condition.remote_server_timeout, Type.WAIT, 504)); + instances.put(Condition.remote_server_error, new XMPPError.ErrorSpecification( + Condition.remote_server_error, Type.CANCEL, 502)); + instances.put(Condition.resource_constraint, new XMPPError.ErrorSpecification( + Condition.resource_constraint, Type.WAIT, 500)); + instances.put(Condition.service_unavailable, new XMPPError.ErrorSpecification( + Condition.service_unavailable, Type.CANCEL, 503)); + instances.put(Condition.subscription_required, new XMPPError.ErrorSpecification( + Condition.subscription_required, Type.AUTH, 407)); + instances.put(Condition.undefined_condition, new XMPPError.ErrorSpecification( + Condition.undefined_condition, Type.WAIT, 500)); + instances.put(Condition.unexpected_condition, new XMPPError.ErrorSpecification( + Condition.unexpected_condition, Type.WAIT, 400)); + instances.put(Condition.request_timeout, new XMPPError.ErrorSpecification( + Condition.request_timeout, Type.CANCEL, 408)); + + return instances; + } + + protected static ErrorSpecification specFor(Condition condition) { + return (ErrorSpecification) instances.get(condition); + } + + /** + * Returns the error condition. + * + * @return the error condition. + */ + protected Condition getCondition() { + return condition; + } + + /** + * Returns the error type. + * + * @return the error type. + */ + protected Type getType() { + return type; + } + + /** + * Returns the error code. + * + * @return the error code. + */ + protected int getCode() { + return code; + } + } } diff --git a/source/org/jivesoftware/smack/provider/PrivacyProvider.java b/source/org/jivesoftware/smack/provider/PrivacyProvider.java new file mode 100644 index 000000000..140b1815c --- /dev/null +++ b/source/org/jivesoftware/smack/provider/PrivacyProvider.java @@ -0,0 +1,134 @@ +package org.jivesoftware.smack.provider; + +import java.util.ArrayList; + +import org.jivesoftware.smack.packet.DefaultPacketExtension; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Privacy; +import org.jivesoftware.smack.packet.PrivacyItem; +import org.xmlpull.v1.XmlPullParser; + +/** + * The PrivacyProvider parses {@link Privacy} packets. {@link Privacy} + * Parses the query sub-document and creates an instance of {@link Privacy}. + * For each item in the list element, it creates an instance + * of {@link PrivacyItem} and {@link PrivacyRule}. + * + * @author Francisco Vives + */ +public class PrivacyProvider implements IQProvider { + + public PrivacyProvider() { + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + Privacy privacy = new Privacy(); + /* privacy.addExtension(PacketParserUtils.parsePacketExtension(parser + .getName(), parser.getNamespace(), parser)); */ + privacy.addExtension(new DefaultPacketExtension(parser.getName(), parser.getNamespace())); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("active")) { + String activeName = parser.getAttributeValue("", "name"); + if (activeName == null) { + privacy.setDeclineActiveList(true); + } else { + privacy.setActiveName(activeName); + } + } + else if (parser.getName().equals("default")) { + String defaultName = parser.getAttributeValue("", "name"); + if (defaultName == null) { + privacy.setDeclineDefaultList(true); + } else { + privacy.setDefaultName(defaultName); + } + } + else if (parser.getName().equals("list")) { + parseList(parser, privacy); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return privacy; + } + + // Parse the list complex type + public void parseList(XmlPullParser parser, Privacy privacy) throws Exception { + boolean done = false; + String listName = parser.getAttributeValue("", "name"); + ArrayList items = new ArrayList(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + items.add(parseItem(parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("list")) { + done = true; + } + } + } + + privacy.setPrivacyList(listName, items); + } + + // Parse the list complex type + public PrivacyItem parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + // Retrieves the required attributes + String actionValue = parser.getAttributeValue("", "action"); + String orderValue = parser.getAttributeValue("", "order"); + String type = parser.getAttributeValue("", "type"); + + /* + * According the action value it sets the allow status. The fall-through action is assumed + * to be "allow" + */ + boolean allow = true; + if ("allow".equalsIgnoreCase(actionValue)) { + allow = true; + } else if ("deny".equalsIgnoreCase(actionValue)) { + allow = false; + } + // Set the order number + int order = Integer.parseInt(orderValue); + + // Create the privacy item + PrivacyItem item = new PrivacyItem(type, allow, order); + item.setValue(parser.getAttributeValue("", "value")); + + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("iq")) { + item.setFilterIQ(true); + } + if (parser.getName().equals("message")) { + item.setFilterMessage(true); + } + if (parser.getName().equals("presence-in")) { + item.setFilterPresence_in(true); + } + if (parser.getName().equals("presence-out")) { + item.setFilterPresence_out(true); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } +} diff --git a/source/org/jivesoftware/smack/util/PacketParserUtils.java b/source/org/jivesoftware/smack/util/PacketParserUtils.java index 43556283f..430508dc2 100644 --- a/source/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/source/org/jivesoftware/smack/util/PacketParserUtils.java @@ -21,9 +21,7 @@ package org.jivesoftware.smack.util; import java.beans.PropertyDescriptor; -import java.util.Map; -import java.util.Iterator; -import java.util.HashMap; +import java.util.*; import java.io.ObjectInputStream; import java.io.ByteArrayInputStream; @@ -31,7 +29,6 @@ import org.jivesoftware.smack.packet.*; import org.jivesoftware.smack.provider.PacketExtensionProvider; import org.jivesoftware.smack.provider.ProviderManager; import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; /** * Utility class that helps to parse packets. Any parsing packets method that must be shared @@ -69,7 +66,7 @@ public class PacketParserUtils { String subject = null; String body = null; String thread = null; - Map properties = null; + Map properties = null; while (!done) { int eventType = parser.next(); if (eventType == XmlPullParser.START_TAG) { @@ -115,8 +112,7 @@ public class PacketParserUtils { message.setThread(thread); // Set packet properties. if (properties != null) { - for (Iterator i=properties.keySet().iterator(); i.hasNext(); ) { - String name = (String)i.next(); + for (String name : properties.keySet()) { message.setProperty(name, properties.get(name)); } } @@ -186,10 +182,9 @@ public class PacketParserUtils { else if (elementName.equals("properties") && namespace.equals(PROPERTIES_NAMESPACE)) { - Map properties = parseProperties(parser); + Map properties = parseProperties(parser); // Set packet properties. - for (Iterator i=properties.keySet().iterator(); i.hasNext(); ) { - String name = (String)i.next(); + for (String name : properties.keySet()) { presence.setProperty(name, properties.get(name)); } } @@ -219,8 +214,8 @@ public class PacketParserUtils { * @return a map of the properties. * @throws Exception if an error occurs while parsing the properties. */ - public static Map parseProperties(XmlPullParser parser) throws Exception { - Map properties = new HashMap(); + public static Map parseProperties(XmlPullParser parser) throws Exception { + Map properties = new HashMap(); while (true) { int eventType = parser.next(); if (eventType == XmlPullParser.START_TAG && parser.getName().equals("property")) { @@ -257,7 +252,7 @@ public class PacketParserUtils { value = new Double(valueText); } else if ("boolean".equals(type)) { - value = new Boolean(valueText); + value = Boolean.valueOf(valueText); } else if ("string".equals(type)) { value = valueText; @@ -297,25 +292,52 @@ public class PacketParserUtils { * @throws Exception if an exception occurs while parsing the packet. */ public static XMPPError parseError(XmlPullParser parser) throws Exception { - String errorCode = "-1"; + final String errorNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas"; + String errorCode = "-1"; + String type = null; String message = null; + String condition = null; + List extensions = new ArrayList(); + + // Parse the error header for (int i=0; i error since client doesn't contain // the specified node response.setType(IQ.Type.ERROR); - response.setError(new XMPPError(404, "item-not-found")); + response.setError(new XMPPError(XMPPError.Condition.item_not_found)); } connection.sendPacket(response); } @@ -236,7 +236,7 @@ public class ServiceDiscoveryManager { else { // Return error since specified node was not found response.setType(IQ.Type.ERROR); - response.setError(new XMPPError(404, "item-not-found")); + response.setError(new XMPPError(XMPPError.Condition.item_not_found)); } } connection.sendPacket(response); diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java index e8e91d1f3..8294fbd6f 100644 --- a/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java @@ -172,7 +172,7 @@ public class FileTransferManager { IQ rejection = FileTransferNegotiator.createIQ( initiation.getPacketID(), initiation.getFrom(), initiation .getTo(), IQ.Type.ERROR); - rejection.setError(new XMPPError(403)); + rejection.setError(new XMPPError(XMPPError.Condition.forbidden)); connection.sendPacket(rejection); } } diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java index d46caf0c2..c6febc2de 100644 --- a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java @@ -223,12 +223,13 @@ public class FileTransferNegotiator { .getFeatureNegotiationForm()); if (streamMethodField == null) { - XMPPError error = new XMPPError(400); + String errorMessage = "No stream methods contained in packet."; + XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage); IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), IQ.Type.ERROR); iqPacket.setError(error); connection.sendPacket(iqPacket); - throw new XMPPException("No stream methods contained in packet.", error); + throw new XMPPException(errorMessage, error); } // select the appropriate protocol @@ -278,8 +279,9 @@ public class FileTransferNegotiator { } if (!isByteStream && !isIBB) { - XMPPError error = new XMPPError(400); - throw new XMPPException("No acceptable transfer mechanism", error); + XMPPError error = new XMPPError(XMPPError.Condition.bad_request, + "No acceptable transfer mechanism"); + throw new XMPPException(error.getMessage(), error); } if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) { @@ -299,7 +301,7 @@ public class FileTransferNegotiator { * @param si The Stream Initiation request to reject. */ public void rejectStream(final StreamInitiation si) { - XMPPError error = new XMPPError(403, "Offer Declined"); + XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined"); IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), IQ.Type.ERROR); iqPacket.setError(error); @@ -409,8 +411,9 @@ public class FileTransferNegotiator { } if (!isByteStream && !isIBB) { - XMPPError error = new XMPPError(400); - throw new XMPPException("No acceptable transfer mechanism", error); + XMPPError error = new XMPPError(XMPPError.Condition.bad_request, + "No acceptable transfer mechanism"); + throw new XMPPException(error.getMessage(), error); } if (isByteStream && isIBB) { diff --git a/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java index ef779a239..a3501a50e 100644 --- a/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java +++ b/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java @@ -377,7 +377,7 @@ public class IBBTransferNegotiator extends StreamNegotiator { private void sendCancelMessage(Message message) { IQ error = FileTransferNegotiator.createIQ(message.getPacketID(), message.getFrom(), message.getTo(), IQ.Type.ERROR); - error.setError(new XMPPError(504)); + error.setError(new XMPPError(XMPPError.Condition.remote_server_timeout, "Cancel Message Transfer")); connection.sendPacket(error); } diff --git a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java index fa893c33e..f28e9c203 100644 --- a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java +++ b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java @@ -220,8 +220,9 @@ public class Socks5TransferNegotiator extends StreamNegotiator { } } if (selectedHost == null || socket == null || !socket.isConnected()) { - throw new XMPPException( - "Could not establish socket with any provided host", new XMPPError(406)); + String errorMessage = "Could not establish socket with any provided host"; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.no_acceptable, errorMessage)); } return new SelectedHostInfo(selectedHost, socket); diff --git a/source/org/jivesoftware/smackx/packet/VCard.java b/source/org/jivesoftware/smackx/packet/VCard.java index 7664edfd2..21303fb82 100644 --- a/source/org/jivesoftware/smackx/packet/VCard.java +++ b/source/org/jivesoftware/smackx/packet/VCard.java @@ -522,7 +522,9 @@ public class VCard extends IQ { result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); if (result == null) { - throw new XMPPException(new XMPPError(408, "Timeout getting VCard information")); + String errorMessage = "Timeout getting VCard information"; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.request_timeout, errorMessage)); } if (result.getError() != null) { throw new XMPPException(result.getError());