From 8d07e07379383a6b6ebf96caad4655aa85feb131 Mon Sep 17 00:00:00 2001 From: Gaston Dombiak Date: Wed, 28 May 2008 18:59:52 +0000 Subject: [PATCH] Initial implementation of ad-hoc commands. SMACK-242 git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@10453 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 1 + build/resources/META-INF/smack.providers | 43 ++ source/org/jivesoftware/smackx/Form.java | 2 +- .../smackx/commands/AdHocCommand.java | 468 +++++++++++ .../smackx/commands/AdHocCommandManager.java | 725 ++++++++++++++++++ .../smackx/commands/AdHocCommandNote.java | 86 +++ .../smackx/commands/LocalCommand.java | 171 +++++ .../smackx/commands/RemoteCommand.java | 172 +++++ .../smackx/packet/AdHocCommandData.java | 275 +++++++ .../provider/AdHocCommandDataProvider.java | 146 ++++ 10 files changed, 2088 insertions(+), 1 deletion(-) create mode 100755 source/org/jivesoftware/smackx/commands/AdHocCommand.java create mode 100755 source/org/jivesoftware/smackx/commands/AdHocCommandManager.java create mode 100755 source/org/jivesoftware/smackx/commands/AdHocCommandNote.java create mode 100755 source/org/jivesoftware/smackx/commands/LocalCommand.java create mode 100755 source/org/jivesoftware/smackx/commands/RemoteCommand.java create mode 100755 source/org/jivesoftware/smackx/packet/AdHocCommandData.java create mode 100755 source/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 9d6654de9..5b11d4add 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -11,6 +11,7 @@ org.jivesoftware.smackx.filetransfer.FileTransferManager org.jivesoftware.smackx.LastActivityManager org.jivesoftware.smack.ReconnectionManager + org.jivesoftware.smackx.commands.AdHocCommandManager diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 65ff84993..1e1483f25 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -218,4 +218,47 @@ org.jivesoftware.smack.provider.PrivacyProvider + + + command + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider + + + + bad-action + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider$BadActionError + + + + malformed-actionn + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider$MalformedActionError + + + + bad-locale + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider$BadLocaleError + + + + bad-payload + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider$BadPayloadError + + + + bad-sessionid + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider$BadSessionIDError + + + + session-expired + http://jabber.org/protocol/commands + org.jivesoftware.smackx.provider.AdHocCommandDataProvider$SessionExpiredError + + \ No newline at end of file diff --git a/source/org/jivesoftware/smackx/Form.java b/source/org/jivesoftware/smackx/Form.java index 3fae7ad4b..37e782f3e 100644 --- a/source/org/jivesoftware/smackx/Form.java +++ b/source/org/jivesoftware/smackx/Form.java @@ -78,7 +78,7 @@ public class Form { * * @param dataForm the data form used for gathering data. */ - private Form(DataForm dataForm) { + public Form(DataForm dataForm) { this.dataForm = dataForm; } diff --git a/source/org/jivesoftware/smackx/commands/AdHocCommand.java b/source/org/jivesoftware/smackx/commands/AdHocCommand.java new file mode 100755 index 000000000..1affd1ef5 --- /dev/null +++ b/source/org/jivesoftware/smackx/commands/AdHocCommand.java @@ -0,0 +1,468 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.commands; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.packet.AdHocCommandData; + +import java.util.List; + +/** + * An ad-hoc command is responsible for executing the provided service and + * storing the result of the execution. Each new request will create a new + * instance of the command, allowing information related to executions to be + * stored in it. For example suppose that a command that retrieve the list of + * users of a server is implemented. When the command is executed it gets that + * list and the result is stored as a form in the command instance, i.e. the + * getForm method retrieves a form with all the users. + *

+ * Each command has a node that should be unique within a given JID. + *

+ * Commands may have zero or more stages. Each stage is usually used for + * gathering information required for the command execution. Users are able to + * move forward or backward across the different stages. Commands may not be + * cancelled while they are being executed. However, users may request the + * "cancel" action when submitting a stage response indicating that the command + * execution should be aborted. Thus, releasing any collected information. + * Commands that require user interaction (i.e. have more than one stage) will + * have to provide the data forms the user must complete in each stage and the + * allowed actions the user might perform during each stage (e.g. go to the + * previous stage or go to the next stage). + *

+ * All the actions may throw an XMPPException if there is a problem executing + * them. The XMPPError of that exception may have some specific + * information about the problem. The possible extensions are: + * + *

  • malformed-action. Extension of a bad-request error.
  • + *
  • bad-action. Extension of a bad-request error.
  • + *
  • bad-locale. Extension of a bad-request error.
  • + *
  • bad-payload. Extension of a bad-request error.
  • + *
  • bad-sessionid. Extension of a bad-request error.
  • + *
  • session-expired. Extension of a not-allowed error.
  • + *

    + * See the SpecificErrorCondition class for detailed description + * of each one. + *

    + * Use the getSpecificErrorConditionFrom to obtain the specific + * information from an XMPPError. + * + * @author Gabriel Guardincerri + * + */ +public abstract class AdHocCommand { + // TODO Analize the redesign of command by having an ExecutionResponse as a + // result to the execution of every action. That result should have all the + // information related to the execution, e.g. the form to fill. Maybe this + // design is more intuitive and simpler than the current one that has all in + // one class. + + private AdHocCommandData data; + + public AdHocCommand() { + super(); + data = new AdHocCommandData(); + } + + /** + * Returns the specific condition of the error or null if the + * error doesn't have any. + * + * @param error + * the error the get the specific condition from + * @return the specific condition of this error, or null if it doesn't have + * any. + */ + public static SpecificErrorCondition getSpecificErrorConditionFrom( + XMPPError error) { + // This method is implemented to provied an easy way of getting a packet + // extension of the XMPPError. + for (SpecificErrorCondition condition : SpecificErrorCondition.values()) { + if (error.getExtension(condition.toString(), + AdHocCommandData.SpecificError.namespace) != null) { + return condition; + } + } + return null; + } + + /** + * Set the the human readable name of the command, usually used for + * displaying in a UI. + * + * @param name + */ + public void setName(String name) { + data.setName(name); + } + + /** + * Returns the human readable name of the command. + * + * @return the human readable name of the command + */ + public String getName() { + return data.getName(); + } + + /** + * Sets the unique identifier of the command. This value must be unique for + * the OwnerJID. + * + * @param node + * the unique identifier of the command + */ + public void setNode(String node) { + data.setNode(node); + } + + /** + * Returns the unique identifier of the command. It is unique for the + * OwnerJID. + * + * @return the unique identifier of the command + */ + public String getNode() { + return data.getNode(); + } + + /** + * Gets the full JID of the owner of this command. This JID is the "to" of a + * execution request. + * + * @return + */ + public abstract String getOwnerJID(); + + /** + * Returns the notes that the command has at the current stage. + * + * @return a list of notes. + */ + public List getNotes() { + return data.getNotes(); + } + + /** + * Adds a note to the current stage. This should be used when setting a + * response to the execution of an action. All the notes added here are + * returned by the getNotes method during the current stage. + * Once the stage changes all the notes are discarded. + * + * @param note + */ + protected void addNote(AdHocCommandNote note) { + data.addNote(note); + } + + /** + * Returns the form of the current stage. Usually it is the form that must + * be answered to execute the next action. If that is the case it should be + * used by the requester to fill all the information that the executor needs + * to continue to the next stage. It can also be the result of the + * execution. + * + * @return the form of the current stage to fill out or the result of the + * execution. + */ + public Form getForm() { + if (data.getForm() == null) { + return null; + } else { + return new Form(data.getForm()); + } + } + + /** + * Sets the form of the current stage. This should be used when setting a + * response. It could be a form to fill out the information needed to go to + * the next stage or the result of an execution. + * + * @param form + * the form of the current stage to fill out or the result of the + * execution. + */ + protected void setForm(Form form) { + data.setForm(form.getDataFormToSend()); + } + + /** + * Executes the command. This is invoked only on the first stage of the + * command. It is invoked on every command. If there is a problem executing + * the command it throws an XMPPException. + * + * @throws XMPPException + * if there is a problem executing the command. + */ + public abstract void execute() throws XMPPException; + + /** + * Executes the next action of the command with the information provided in + * the response. This form must be the answer form of the + * previous stage. This method will be only invoked for commands that have 1 + * or more stages. If there is a problem executing the command it throws an + * XMPPException. + * + * @param response + * the form answer of the previous stage. + * @throws XMPPException + * if there is a problem executing the command. + */ + public abstract void next(Form response) throws XMPPException; + + /** + * Completes the command execution with the information provided in the + * response. This form must be the answer form of the + * previous stage. This method will be only invoked for commands that have 1 + * or more stages. If there is a problem executing the command it throws an + * XMPPException. + * + * @param response + * the form answer of the previous stage. + * @throws XMPPException + * if there is a problem executing the command. + */ + public abstract void complete(Form response) throws XMPPException; + + /** + * Goes to the previous stage. The requester is asking to re-send the + * information of the previous stage. The command must change it state to + * the previous one. If there is a problem executing the command it throws + * an XMPPException. + * + * @throws XMPPException + * if there is a problem executing the command. + */ + public abstract void prev() throws XMPPException; + + /** + * Cancels the execution of the command. This can be invoked on any stage of + * the execution. If there is a problem executing the command it throws an + * XMPPException. + * + * @throws XMPPException + * if there is a problem executing the command. + */ + public abstract void cancel() throws XMPPException; + + /** + * Returns a collection with the allowed actions based on the current stage + * Possible actions are: prev, next and + * complete. This method will be only invoked for commands that + * have 1 or more stages. + * + * @return a collection with the allowed actions based on the current stage + * as defined in the SessionData. + */ + protected List getActions() { + return data.getActions(); + } + + /** + * Add an action to the current stage available actions. This should be used + * when creating a response. + * + * @param action + */ + protected void addActionAvailable(Action action) { + data.addAction(action); + } + + /** + * Returns which of the actions available for the current stage is + * considered the equivalent to "execute". When the requester sends his + * reply, if no action was defined in the command then the action will be + * assumed "execute" thus assuming the action returned by this method. This + * method will never be invoked for commands that have no stages. + * + * @return which of the actions available for the current stage is + * considered the equivalent to "execute". + */ + protected Action getExecuteAction() { + return data.getExecuteAction(); + } + + /** + * Returns which of the actions available for the current stage is + * considered the equivalent to "execute". This should be used when setting + * a response. When the requester sends his reply, if no action was defined + * in the command then the action will be assumed "execute" thus assuming + * the action returned by this method. + * + * @param action + */ + protected void setExecuteAction(Action action) { + data.setExecuteAction(action); + } + + /** + * Returns the status of the current stage. + * + * @return + */ + public Status getStatus() { + return data.getStatus(); + } + + /** + * Sets the data of the current stage. This should not used. + * + * @param data + */ + void setData(AdHocCommandData data) { + this.data = data; + } + + /** + * Gets the data of the current stage. This should not used. + * + * @param data + */ + AdHocCommandData getData() { + return data; + } + + /** + * Returns if the action is available in the current stage. + * The Action.cancel is always allowed. To define the + * available actions use the addActionAvailable method. + * + * @param action + * The action to check if it is available. + * @return True if the action is available for the current stage. + */ + protected boolean isValidAction(Action action) { + return getActions().contains(action) || Action.cancel.equals(action); + } + + public enum Status { + + /** + * The command is being executed. + */ + executing, + + /** + * The command has completed. The command session has ended. + */ + completed, + + /** + * The command has been canceled. The command session has ended. + */ + canceled + } + + public enum Action { + + /** + * The command should be executed or continue to be executed. This is + * the default value. + */ + execute, + + /** + * The command should be canceled. + */ + cancel, + + /** + * The command should be digress to the previous stage of execution. + */ + prev, + + /** + * The command should progress to the next stage of execution. + */ + next, + + /** + * The command should be completed (if possible). + */ + complete, + + /** + * The action is unknow. This is used when a recieved message has an + * unknown action. It must not be used to send an execution request. + */ + unknown; + } + + public enum SpecificErrorCondition { + + /** + * The responding JID cannot accept the specified action. + */ + badAction, + + /** + * The responding JID does not understand the specified action. + */ + malformedAction, + + /** + * The responding JID cannot accept the specified language/locale. + */ + badLocale, + + /** + * The responding JID cannot accept the specified payload (e.g. the data + * form did not provide one or more required fields). + */ + badPayload, + + /** + * The responding JID cannot accept the specified sessionid. + */ + badSessionid, + + /** + * The requesting JID specified a sessionid that is no longer active + * (either because it was completed, canceled, or timed out). + */ + sessionExpired; + + public String toString() { + String result = null; + switch (this) { + case badAction: + result = "bad-action"; + break; + case malformedAction: + result = "malformed-action"; + break; + case badLocale: + result = "bad-locale"; + break; + case badPayload: + result = "bad-payload"; + break; + case badSessionid: + result = "bad-sessionid"; + break; + case sessionExpired: + result = "session-expired"; + break; + } + return result; + } + } +} diff --git a/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java new file mode 100755 index 000000000..51eea6d74 --- /dev/null +++ b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java @@ -0,0 +1,725 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.commands; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.NodeInformationProvider; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.commands.AdHocCommand.Action; +import org.jivesoftware.smackx.commands.AdHocCommand.Status; +import org.jivesoftware.smackx.packet.AdHocCommandData; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * An AdHocCommandManager is responsible for keeping the list of available + * commands offered by a service and for processing commands requests. + * Typically, instances of this class are private to the service offering ad-hoc + * commands. + * + * @author Gabriel Guardincerri + */ + +public class AdHocCommandManager { + + private final static String discoNamespace = "http://jabber.org/protocol/commands"; + + private final static String discoNode = discoNamespace; + + /** + * The session time out in seconds. + */ + public static int SESSION_TIMEOUT = 2 * 60; + + /** + * Map a XMPPConnection with it AdHocCommandManager. This map have a pair + * key-value for every active connection. + */ + private static Map instances = new ConcurrentHashMap(); + + private static Thread sessionsSweeper; + + /** + * The XMPPConnection that this instances of AdHocCommandManager manages + */ + private XMPPConnection connection; + + /** + * Map a command node with its AdHocCommandInfo. Note: Key=command node, + * Value=command. Command node matches the node attribute sent by command + * requesters. + */ + private Map commands = new ConcurrentHashMap(); + + /** + * Map a command session ID with the instance LocalCommand. The LocalCommand + * is the an objects that has all the information of the current state of + * the command execution. Note: Key=session ID, Value=LocalCommand. Session + * ID matches the sessionid attribute sent by command responders. + */ + private Map executingCommands = new ConcurrentHashMap(); + + /** + * Register the listener for all the connection creations. When a new + * connection is created a new AdHocCommandManager is also created and + * related to that connection. + */ + static { + XMPPConnection + .addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(XMPPConnection connection) { + new AdHocCommandManager(connection); + } + }); + } + + private AdHocCommandManager(XMPPConnection connection) { + super(); + this.connection = connection; + init(); + } + + /** + * Returns the AdHocCommandManager related to the + * connection. + * + * @param connection + * @return + */ + public static AdHocCommandManager getAddHocCommandsManager( + XMPPConnection connection) { + return instances.get(connection); + } + + /** + *

  • Adds listeners to the connection
  • + *
  • Registers the ad-hoc command feature to the ServiceDiscoveryManager
  • + *
  • Registers the items of the feature
  • + *
  • Adds packet listeners to handle execution requests
  • + *
  • Creates and start the session sweeper
  • + * + */ + 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); + } + + public void reconnectionSuccessful() { + // Register this instance since the connection has been + // reestablished + instances.put(connection, AdHocCommandManager.this); + } + + public void reconnectingIn(int seconds) { + // Nothing to do + } + + public void reconnectionFailed(Exception e) { + // Nothing to do + } + }); + + // Add the feature to the service discovery manage to show that this + // connection supports the AdHoc-Commands protocol. + // This information will be used when another client tries to + // discover whether this client supports AdHoc-Commands or not. + ServiceDiscoveryManager.getInstanceFor(connection).addFeature( + discoNamespace); + + // Set the NodeInformationProvider that will provide information about + // which AdHoc-Commands are registered, whenever a disco request is + // received + ServiceDiscoveryManager.getInstanceFor(connection) + .setNodeInformationProvider(discoNode, + new NodeInformationProvider() { + public List getNodeItems() { + AdHocCommandManager adHocCommandManager = AdHocCommandManager + .getAddHocCommandsManager(connection); + List answer = new ArrayList(); + + Collection commandsList = adHocCommandManager + .getRegisteredCommands(connection); + + for (AdHocCommandInfo info : commandsList) { + DiscoverItems.Item item = new DiscoverItems.Item( + info.getOwnerJID()); + item.setName(info.getName()); + item.setNode(info.getNode()); + answer.add(item); + } + + return answer; + } + + public List getNodeFeatures() { + return null; + } + + public List getNodeIdentities() { + return null; + } + }); + + // The packet listener and the filter for processing some AdHoc Commands + // Packets + PacketListener listener = new PacketListener() { + public void processPacket(Packet packet) { + AdHocCommandData requestData = (AdHocCommandData) packet; + processAdHocCommand(requestData); + } + }; + + PacketFilter filter = new PacketTypeFilter(AdHocCommandData.class); + connection.addPacketListener(listener, filter); + + sessionsSweeper = new Thread(new Runnable() { + public void run() { + while (true) { + for (String sessionId : executingCommands.keySet()) { + LocalCommand command = executingCommands.get(sessionId); + // Since the command could be removed in the meanwhile + // of getting the key and getting the value - by a + // processed packet. We must check if it still in the + // map. + if (command != null) { + long creationStamp = command.getCreationStamp(); + // Check if the Session data has expired (default is + // 10 minutes) + // To remove it from the session list it waits for + // the double of the of time out time. This is to + // let + // the requester know why his execution request is + // not accepted. If the session is removed just + // after the time out, then whe the user request to + // continue the execution he will recieved an + // invalid session error and not a time out error. + if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000 * 2) { + // Remove the expired session + executingCommands.remove(sessionId); + } + } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + } + } + } + + }); + sessionsSweeper.start(); + } + + /** + * Process the AdHoc-Command packet that request the execution of some + * action of a command. If this is the first request, this method checks, + * before executing the command, if: + *
  • The requested command exists
  • + *
  • The requester has permissions to execute it
  • + *
  • The command has more than one stage, if so, it saves the command and + * session ID for further use
  • + * + *
    + *
    + * If this is not the first request, this method checks, before executing + * the command, if: + *
  • The session ID of the request was stored
  • + *
  • The session life do not exceed the time out
  • + *
  • The action to execute is one of the available actions
  • + * + * @param requestData + * the packet to process. + */ + private void processAdHocCommand(AdHocCommandData requestData) { + // Only process requests of type SET + if (requestData.getType() != IQ.Type.SET) { + return; + } + + // Creates the response with the corresponding data + AdHocCommandData response = new AdHocCommandData(); + response.setTo(requestData.getFrom()); + response.setPacketID(requestData.getPacketID()); + response.setNode(requestData.getNode()); + response.setId(requestData.getTo()); + + String sessionId = requestData.getSessionID(); + String commandNode = requestData.getNode(); + + if (sessionId == null) { + // A new execution request has been received. Check that the + // command exists + if (!commands.containsKey(commandNode)) { + // Requested command does not exist so return + // item_not_found error. + respondError(response, XMPPError.Condition.item_not_found); + return; + } + + // Create new session ID + sessionId = StringUtils.randomString(15); + + try { + // Create a new instance of the command with the + // corresponding sessioid + LocalCommand command = newInstanceOfCmd(commandNode, sessionId); + + response.setType(IQ.Type.RESULT); + command.setData(response); + + // Check that the requester has enough permission. + // Answer forbidden error if requester permissions are not + // enough to execute the requested command + if (!command.hasPermission(requestData.getFrom())) { + respondError(response, XMPPError.Condition.forbidden); + return; + } + + Action action = requestData.getAction(); + + // If the action is unknown then respond an error. + if (action != null && action.equals(Action.unknown)) { + respondError(response, XMPPError.Condition.bad_request, + AdHocCommand.SpecificErrorCondition.malformedAction); + return; + } + + // If the action is not execute, then it is an invalid action. + if (action != null && !action.equals(Action.execute)) { + respondError(response, XMPPError.Condition.bad_request, + AdHocCommand.SpecificErrorCondition.badAction); + return; + } + + // Increase the state number, so the command knows in witch + // stage it is + command.increaseStage(); + // Executes the command + command.execute(); + + if (command.isLastStage()) { + // If there is only one stage then the command is completed + response.setStatus(Status.completed); + } else { + // Else it is still executing, and is registered to be + // available for the next call + response.setStatus(Status.executing); + executingCommands.put(sessionId, command); + } + + // Sends the response packet + connection.sendPacket(response); + + } catch (XMPPException e) { + // If there is an exception caused by the next, complete, + // prev or cancel method, then that error is returned to the + // requester. + XMPPError error = e.getXMPPError(); + + // If the error type is cancel, then the execution is + // canceled therefore the status must show that, and the + // command be removed from the executing list. + if (XMPPError.Type.CANCEL.equals(error.getType())) { + response.setStatus(Status.canceled); + executingCommands.remove(sessionId); + } + respondError(response, error); + e.printStackTrace(); + } + } else { + LocalCommand command = executingCommands.get(sessionId); + + // Check that a command exists for the specified sessionID + // This also handles if the command was removed in the meanwhile + // of getting the key and the value of the map. + if (command == null) { + respondError(response, XMPPError.Condition.bad_request, + AdHocCommand.SpecificErrorCondition.badSessionid); + return; + } + + // Check if the Session data has expired (default is 10 minutes) + long creationStamp = command.getCreationStamp(); + if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000) { + // Remove the expired session + executingCommands.remove(sessionId); + + // Answer a not_allowed error (session-expired) + respondError(response, XMPPError.Condition.not_allowed, + AdHocCommand.SpecificErrorCondition.sessionExpired); + return; + } + + /* + * Since the requester could send two requests for the same + * executing command i.e. the same session id, all the execution of + * the action must be synchronized to avoid inconsistencies. + */ + synchronized (command) { + Action action = requestData.getAction(); + + // If the action is unknown the respond an error + if (action != null && action.equals(Action.unknown)) { + respondError(response, XMPPError.Condition.bad_request, + AdHocCommand.SpecificErrorCondition.malformedAction); + return; + } + + // If the user didn't specify an action or specify the execute + // action then follow the actual default execute action + if (action == null || Action.execute.equals(action)) { + action = command.getExecuteAction(); + } + + // Check that the specified action was previously + // offered + if (!command.isValidAction(action)) { + respondError(response, XMPPError.Condition.bad_request, + AdHocCommand.SpecificErrorCondition.badAction); + return; + } + + try { + // TODO Check that all the requierd fields of the form are + // filled, if not throw an exception. This will simplify the + // construction of new commands + + // Since all errors were passed, the response is now a + // result + response.setType(IQ.Type.RESULT); + + // Set the new data to the command. + command.setData(response); + + if (Action.next.equals(action)) { + command.increaseStage(); + command.next(new Form(requestData.getForm())); + if (command.isLastStage()) { + // If it is the last stage then the command is + // completed + response.setStatus(Status.completed); + } else { + // Otherwise it is still executing + response.setStatus(Status.executing); + } + } else if (Action.complete.equals(action)) { + command.increaseStage(); + command.complete(new Form(requestData.getForm())); + response.setStatus(Status.completed); + // Remove the completed session + executingCommands.remove(sessionId); + } else if (Action.prev.equals(action)) { + command.decreaseStage(); + command.prev(); + } else if (Action.cancel.equals(action)) { + command.cancel(); + response.setStatus(Status.canceled); + // Remove the canceled session + executingCommands.remove(sessionId); + } + + connection.sendPacket(response); + } catch (XMPPException e) { + // If there is an exception caused by the next, complete, + // prev or cancel method, then that error is returned to the + // requester. + XMPPError error = e.getXMPPError(); + + // If the error type is cancel, then the execution is + // canceled therefore the status must show that, and the + // command be removed from the executing list. + if (XMPPError.Type.CANCEL.equals(error.getType())) { + response.setStatus(Status.canceled); + executingCommands.remove(sessionId); + } + respondError(response, error); + + e.printStackTrace(); + } + } + } + } + + /** + * Responds an error with an specific condition. + * + * @param response + * the response to send + * @param condition + * the condition of the error + */ + private void respondError(AdHocCommandData response, + XMPPError.Condition condition) { + respondError(response, new XMPPError(condition)); + } + + /** + * Responds an error with an specific condition. + * + * @param response + * the response to send + * @param condition + * the condition of the error + */ + private void respondError(AdHocCommandData response, + XMPPError.Condition condition, + AdHocCommand.SpecificErrorCondition specificCondition) { + XMPPError error = new XMPPError(condition); + error + .addExtension(new AdHocCommandData.SpecificError( + specificCondition)); + respondError(response, error); + } + + /** + * Responds an error with an specific error. + * + * @param response + * the response to send + * @param condition + * the condition of the error + */ + private void respondError(AdHocCommandData response, XMPPError error) { + response.setType(IQ.Type.ERROR); + response.setError(error); + connection.sendPacket(response); + } + + /** + * Creates a new instance of a command to be used by a new execution request + * + * @param commandNode + * the command node that identifies it. + * @param sessionID + * the session id of this execution. + * @return the command instance to execute. + * @throws XMPPException + * if there is problem creating the new instance + */ + private LocalCommand newInstanceOfCmd(String commandNode, String sessionID) + throws XMPPException { + // TODO Evaluate the possibility of using a factory for creating the new + // instances. + AdHocCommandInfo commandInfo = commands.get(commandNode); + LocalCommand command = null; + try { + command = (LocalCommand) commandInfo.getCmdClass().newInstance(); + command.setSessionID(sessionID); + command.setName(commandInfo.getName()); + command.setNode(commandInfo.getNode()); + } catch (InstantiationException e) { + e.printStackTrace(); + throw new XMPPException(new XMPPError( + XMPPError.Condition.interna_server_error)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + throw new XMPPException(new XMPPError( + XMPPError.Condition.interna_server_error)); + } + return command; + } + + /** + * Returns the registered commands of this command manager, which is related + * to a connection. + * + * @param connection + * @return + */ + private Collection getRegisteredCommands( + XMPPConnection connection) { + return commands.values(); + } + + /** + * Register a new command to this command manager, which is related to a + * connection. The node is an unique identifier of that + * command for the connection related to this command manager. The + * name is the human readable name of the command. + * The class is the class of the command + * @param node the unique identifier of the command. + * @param name the human readable name of the command. + * @param clazz the class of the command. + */ + public void registerCommand(String node, final String name, Class clazz) { + AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, + connection.getUser(), clazz); + + commands.put(node, commandInfo); + // Set the NodeInformationProvider that will provide information about + // the added command + ServiceDiscoveryManager.getInstanceFor(connection) + .setNodeInformationProvider(node, + new NodeInformationProvider() { + public List getNodeItems() { + return null; + } + + public List getNodeFeatures() { + List answer = new ArrayList(); + answer + .add("http://jabber.org/protocol/commands"); + // TODO check if this service is provided by the + // current connection. + answer.add("jabber:x:data"); + return answer; + } + + public List getNodeIdentities() { + List answer = new ArrayList(); + DiscoverInfo.Identity identity = new DiscoverInfo.Identity( + "automation", name); + identity.setType("command-node"); + answer.add(identity); + return answer; + } + + }); + } + + /** + * Discover the commands of an specific JID. The jid is a + * full JID + * + * @param jid + * the full JID to retrieve the commands for. + * @return the discovered items + * @throws XMPPException + * if the operation failed for some reason. + */ + public DiscoverItems discoverCommands(String jid) throws XMPPException { + ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager + .getInstanceFor(connection); + return serviceDiscoveryManager.discoverItems(jid, discoNode); + } + + /** + * Publish the commands to an specific JID. + * + * @param jid + * the full JID to publish the commands to. + * @throws XMPPException + * if the operation failed for some reason. + */ + public void publishCommands(String jid) throws XMPPException { + ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager + .getInstanceFor(connection); + + // Collects the commands to publish as items + AdHocCommandManager adHocCommandManager = AdHocCommandManager + .getAddHocCommandsManager(connection); + DiscoverItems discoverItems = new DiscoverItems(); + Collection xCommandsList = adHocCommandManager + .getRegisteredCommands(connection); + + for (AdHocCommandInfo info : xCommandsList) { + DiscoverItems.Item item = new DiscoverItems.Item(info.getOwnerJID()); + item.setName(info.getName()); + item.setNode(info.getNode()); + discoverItems.addItem(item); + } + + serviceDiscoveryManager.publishItems(jid, discoNode, discoverItems); + } + + /** + * Returns a command that represents an instance of a command in a remote + * host. It is used to execute remote commands. The concept is similar to + * RMI. Every invocation on this command is equivalent to an invocation in + * the remote command. + * + * @param jid + * the full JID of the host of the remote command + * @param node + * the identifier of the command + * @return a local instance equivalent to the remote command. + */ + public RemoteCommand getRemoteCommand(String jid, String node) { + return new RemoteCommand(connection, node, jid); + } + + /** + * The ad-hoc command information. + * + * @author Gabriel + * + */ + private static class AdHocCommandInfo { + + private String node; + + private String name; + + private String ownerJID; + + private Class cmdClass; + + public AdHocCommandInfo(String node, String name, String ownerJID, + Class cmdClass) { + this.node = node; + this.name = name; + this.ownerJID = ownerJID; + this.cmdClass = cmdClass; + } + + public Class getCmdClass() { + return cmdClass; + } + + public String getName() { + return name; + } + + public String getNode() { + return node; + } + + public String getOwnerJID() { + return ownerJID; + } + } +} diff --git a/source/org/jivesoftware/smackx/commands/AdHocCommandNote.java b/source/org/jivesoftware/smackx/commands/AdHocCommandNote.java new file mode 100755 index 000000000..2175a9d00 --- /dev/null +++ b/source/org/jivesoftware/smackx/commands/AdHocCommandNote.java @@ -0,0 +1,86 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.commands; + +/** + * Notes can be added to a command execution response. A note has to attributes, + * one is the value or message an the other is the type of the note. + * + * @author Gabriel Guardincerri + * + */ +public class AdHocCommandNote { + + private Type type; + + private String value; + + public AdHocCommandNote(Type type, String value) { + this.type = type; + this.value = value; + } + + /** + * Returns the value or message of the note. + * + * @return the value or message of the note. + */ + public String getValue() { + return value; + } + + /** + * Return the type of the note. + * + * @return the type of the note. + */ + public Type getType() { + return type; + } + + /** + * The types of the notes. + * + * @author Gabriel Guardincerri + * + */ + public enum Type { + + /** + * The note is informational only. This is not really an exceptional + * condition. + */ + info, + + /** + * The note indicates a warning. Possibly due to illogical (yet valid) + * data. + */ + warm, + + /** + * The note indicates an error. The text should indicate the reason for + * the error. + */ + error + } + +} diff --git a/source/org/jivesoftware/smackx/commands/LocalCommand.java b/source/org/jivesoftware/smackx/commands/LocalCommand.java new file mode 100755 index 000000000..0b374490e --- /dev/null +++ b/source/org/jivesoftware/smackx/commands/LocalCommand.java @@ -0,0 +1,171 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.commands; + +import org.jivesoftware.smackx.packet.AdHocCommandData; + +/** + * Represents a command that can be executed locally from a remote location. This + * class must be extended to implement an specific ad-hoc command. This class + * provides some useful and common useful: + *
  • Node code
  • + *
  • Node name
  • + *
  • Session ID
  • + *
  • Current Stage
  • + *
  • Available actions
  • + *
  • Default action
  • + *

    + * To implement a new command extend this class and implement all the abstract + * methods. When implementing the actions remember that they could be invoked + * several times, and that you must use the current stage number to know what to + * do. + * + * @author Gabriel Guardincerri + * + */ +public abstract class LocalCommand extends AdHocCommand { + + /** + * The time stamp of first invokation of the command. Used to implement the session timeout. + */ + private long creationStamp; + + /** + * The unique ID of the execution of the command. + */ + private String sessionID; + + /** + * The full JID of the host of the command. + */ + private String ownerJID; + + /** + * The number of the current stage. + */ + private int currenStage; + + public LocalCommand() { + super(); + this.creationStamp = System.currentTimeMillis(); + currenStage = -1; + } + + /** + * The sessionID is an unique identifier of an execution request. This is + * automatically handled and should not be called. + * + * @param sessionID + * the unique session id of this execution + */ + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + getData().setSessionID(sessionID); + } + + /** + * Returns the session ID of this execution. + * + * @return the unique session id of this execution + */ + public String getSessionID() { + return sessionID; + } + + /** + * Sets the JID of the command host. This is automatically handled and should + * not be called. + * + * @param ownerJID + */ + public void setOwnerJID(String ownerJID) { + this.ownerJID = ownerJID; + } + + @Override + public String getOwnerJID() { + return ownerJID; + } + + /** + * Returns the time in milliseconds since this command was executed for + * first time. + * + * @return + */ + public long getCreationStamp() { + return creationStamp; + } + + @Override + void setData(AdHocCommandData data) { + data.setSessionID(sessionID); + super.setData(data); + } + + /** + * Returns if the current stage is the last one. If it is, then the + * execution of some action will complete the execution of the command. + * + * @return true if the command is in the last stage. + */ + public abstract boolean isLastStage(); + + /** + * Increase the current stage number. This is automatically handled and should + * not be called. + * + */ + void increaseStage() { + currenStage++; + } + + /** + * Decrease the current stage number. This is automatically handled and should + * not be called. + * + */ + void decreaseStage() { + currenStage--; + } + + /** + * Returns the currently executing stage number. The first stage number is + * 0. So during the execution of the first action this method will answer 0. + * + * @return + */ + public int getCurrentStage() { + return currenStage; + } + + /** + * Returns if the specified requester has permission to execute all the + * stages of this action. This is checked when the first request is received, + * if the permission is grant then the requester will be able to execute + * all the stages of the command. It is not checked again during the + * execution. + * + * @param jid + * @return true if the user has permission to execute this action + */ + public abstract boolean hasPermission(String jid); +} diff --git a/source/org/jivesoftware/smackx/commands/RemoteCommand.java b/source/org/jivesoftware/smackx/commands/RemoteCommand.java new file mode 100755 index 000000000..3f5cb5ddc --- /dev/null +++ b/source/org/jivesoftware/smackx/commands/RemoteCommand.java @@ -0,0 +1,172 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.commands; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.packet.AdHocCommandData; + +/** + * Represents a command that is in a remote location. Invoking one of the + * execute, next, prev, cancel or complete methods results in executing that + * action in the remote location. In response to that action the internal state + * of the this command instance will change. For example, if the command is a + * single stage command, then invoking the execute action will execute this + * action in the remote location. After that the local instance will have a + * state of "completed" and a form or notes that applies. + * + * @author Gabriel Guardincerri + * + */ +public class RemoteCommand extends AdHocCommand { + + /** + * The connection that is used to execute this command + */ + private XMPPConnection connection; + + /** + * The full JID of the command host + */ + private String jid; + + /** + * The session ID of this execution. + */ + private String sessionID; + + /** + * Creates a new RemoteCommand that uses an specific connection to execute a + * command identified by node in the host identified by + * jid + * + * @param connection + * the connection to use for the execution. + * @param node + * the identifier of the command. + * @param jid + * the JID of the host. + */ + protected RemoteCommand(XMPPConnection connection, String node, String jid) { + super(); + this.connection = connection; + this.jid = jid; + this.setNode(node); + } + + @Override + public void cancel() throws XMPPException { + executeAction(Action.cancel); + } + + @Override + public void complete(Form form) throws XMPPException { + executeAction(Action.complete, form); + } + + @Override + public void execute() throws XMPPException { + executeAction(Action.execute); + } + + /** + * Executes the default action of the command with the information provided + * in the Form. This form must be the anwser form of the previous stage. If + * there is a problem executing the command it throws an XMPPException. + * + * @param response + * the form anwser of the previous stage. + * @throws XMPPException + */ + public void execute(Form form) throws XMPPException { + executeAction(Action.execute, form); + } + + @Override + public void next(Form form) throws XMPPException { + executeAction(Action.next, form); + } + + @Override + public void prev() throws XMPPException { + executeAction(Action.prev); + } + + private void executeAction(Action action) throws XMPPException { + executeAction(action, null); + } + + /** + * Executes the action with the form. + * The action could be any of the available actions. The form must + * be the anwser of the previous stage. It can be null if it is the first stage. + * @param action the action to execute + * @param form the form with the information + * @throws XMPPException if there is a problem executing the command. + */ + private void executeAction(Action action, Form form) throws XMPPException { + // TODO Check that all the requiered fields of the form were filled, if + // not throw the corresponding exeption. This will make a faster response, + // since the request is stoped before it's sended. + AdHocCommandData data = new AdHocCommandData(); + data.setType(IQ.Type.SET); + data.setTo(getOwnerJID()); + data.setNode(getNode()); + data.setSessionID(sessionID); + data.setAction(action); + + if (form != null) { + data.setForm(form.getDataFormToSend()); + } + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(data.getPacketID())); + + connection.sendPacket(data); + + Packet response = collector.nextResult(SmackConfiguration + .getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + + AdHocCommandData responseData = (AdHocCommandData) response; + this.sessionID = responseData.getSessionID(); + super.setData(responseData); + } + + @Override + public String getOwnerJID() { + return jid; + } +} diff --git a/source/org/jivesoftware/smackx/packet/AdHocCommandData.java b/source/org/jivesoftware/smackx/packet/AdHocCommandData.java new file mode 100755 index 000000000..d5998a623 --- /dev/null +++ b/source/org/jivesoftware/smackx/packet/AdHocCommandData.java @@ -0,0 +1,275 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.commands.AdHocCommand; +import org.jivesoftware.smackx.commands.AdHocCommand.Action; +import org.jivesoftware.smackx.commands.AdHocCommand.SpecificErrorCondition; +import org.jivesoftware.smackx.commands.AdHocCommandNote; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the state and the request of the execution of a command. + * + * @author Gabriel Guardincerri + * + */ +public class AdHocCommandData extends IQ { + + /* JID of the command host */ + private String id; + + /* Command name */ + private String name; + + /* Command identifier */ + private String node; + + /* Unique ID of the execution */ + private String sessionID; + + private List notes = new ArrayList(); + + private DataForm form; + + /* Action request to be executed */ + private AdHocCommand.Action action; + + /* Current execution status */ + private AdHocCommand.Status status; + + private ArrayList actions = new ArrayList(); + + private AdHocCommand.Action executeAction; + + private String lang; + + public AdHocCommandData() { + } + + @Override + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append(""); + + if (getType() == Type.RESULT) { + buf.append(""); + } else { + buf.append(">"); + + for (AdHocCommand.Action action : actions) { + buf.append("<").append(action).append("/>"); + } + buf.append(""); + } + } + + if (form != null) { + buf.append(form.toXML()); + } + + for (AdHocCommandNote note : notes) { + buf.append(""); + buf.append(note.getValue()); + buf.append(""); + } + + // TODO ERRORES +// if (getError() != null) { +// buf.append(getError().toXML()); +// } + + buf.append(""); + return buf.toString(); + } + + /** + * Returns the JID of the command host. + * @return + */ + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * Returns the human name of the command + * @return + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Returns the identifier of the command + * @return + */ + public String getNode() { + return node; + } + + public void setNode(String node) { + this.node = node; + } + + /** + * Returns the list of notes that the command has. + * @return + */ + public List getNotes() { + return notes; + } + + public void addNote(AdHocCommandNote note) { + this.notes.add(note); + } + + public void remveNote(AdHocCommandNote note) { + this.notes.remove(note); + } + + /** + * Returns the form of the command. + * @return + */ + public DataForm getForm() { + return form; + } + + public void setForm(DataForm form) { + this.form = form; + } + + /** + * Returns the action to execute. The action is set only on a request. + * @return + */ + public AdHocCommand.Action getAction() { + return action; + } + + public void setAction(AdHocCommand.Action action) { + this.action = action; + } + + /** + * Returns the status of the execution. + * @return + */ + public AdHocCommand.Status getStatus() { + return status; + } + + public void setStatus(AdHocCommand.Status status) { + this.status = status; + } + + public List getActions() { + return actions; + } + + public void addAction(Action action) { + actions.add(action); + } + + public void setExecuteAction(Action executeAction) { + this.executeAction = executeAction; + } + + public Action getExecuteAction() { + return executeAction; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + public String getSessionID() { + return sessionID; + } + + public static class SpecificError implements PacketExtension { + + public static final String namespace = "http://jabber.org/protocol/commands"; + + public SpecificErrorCondition condition; + + public SpecificError(SpecificErrorCondition condition) { + this.condition = condition; + } + + public String getElementName() { + return condition.toString(); + } + public String getNamespace() { + return namespace; + } + + public SpecificErrorCondition getCondition() { + return condition; + } + + public String toXML() { + StringBuffer sb = new StringBuffer(); + sb.append("<").append(getElementName()); + sb.append(" xmlns=\"").append(getNamespace()).append("\"/>"); + return sb.toString(); + } + } + + +} diff --git a/source/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java b/source/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java new file mode 100755 index 000000000..3f0f3ecf6 --- /dev/null +++ b/source/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java @@ -0,0 +1,146 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2005-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.commands.AdHocCommand; +import org.jivesoftware.smackx.commands.AdHocCommand.Action; +import org.jivesoftware.smackx.commands.AdHocCommandNote; +import org.jivesoftware.smackx.packet.AdHocCommandData; +import org.jivesoftware.smackx.packet.DataForm; +import org.xmlpull.v1.XmlPullParser; + +/** + * The AdHocCommandDataProvider parses AdHocCommandData packets. + * + * @author Gabriel Guardincerri + */ + +public class AdHocCommandDataProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + boolean done = false; + AdHocCommandData adHocCommandData = new AdHocCommandData(); + DataFormProvider dataFormProvider = new DataFormProvider(); + + int eventType; + String elementName; + String namespace; + adHocCommandData.setSessionID(parser.getAttributeValue("", "sessionid")); + adHocCommandData.setNode(parser.getAttributeValue("", "node")); + + // Status + String status = parser.getAttributeValue("", "status"); + if (AdHocCommand.Status.executing.toString().equalsIgnoreCase(status)) { + adHocCommandData.setStatus(AdHocCommand.Status.executing); + } else if (AdHocCommand.Status.completed.toString().equalsIgnoreCase(status)) { + adHocCommandData.setStatus(AdHocCommand.Status.completed); + } else if (AdHocCommand.Status.canceled.toString().equalsIgnoreCase(status)) { + adHocCommandData.setStatus(AdHocCommand.Status.canceled); + } + + // Action + String action = parser.getAttributeValue("", "action"); + if (action != null) { + Action realAction = AdHocCommand.Action.valueOf(action); + if (realAction == null || realAction.equals(Action.unknown)) { + adHocCommandData.setAction(Action.unknown); + } else { + adHocCommandData.setAction(realAction); + } + } + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + namespace = parser.getNamespace(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actions")) { + String execute = parser.getAttributeValue("", "execute"); + if (execute != null) { + adHocCommandData.setExecuteAction(AdHocCommand.Action.valueOf(execute)); + } + } else if (parser.getName().equals("next")) { + adHocCommandData.addAction(AdHocCommand.Action.next); + } else if (parser.getName().equals("complete")) { + adHocCommandData.addAction(AdHocCommand.Action.complete); + } else if (parser.getName().equals("prev")) { + adHocCommandData.addAction(AdHocCommand.Action.prev); + } else if (elementName.equals("x") && namespace.equals("jabber:x:data")) { + adHocCommandData.setForm((DataForm) dataFormProvider.parseExtension(parser)); + } else if (parser.getName().equals("note")) { + AdHocCommandNote.Type type = AdHocCommandNote.Type.valueOf(parser + .getAttributeValue("", "type")); + String value = parser.nextText(); + adHocCommandData.addNote(new AdHocCommandNote(type, value)); + } else if (parser.getName().equals("error")) { + XMPPError error = PacketParserUtils.parseError(parser); + adHocCommandData.setError(error); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("command")) { + done = true; + } + } + } + return adHocCommandData; + } + + public static class BadActionError implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badAction); + } + } + + public static class MalformedActionError implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.malformedAction); + } + } + + public static class BadLocaleError implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badLocale); + } + } + + public static class BadPayloadError implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badPayload); + } + } + + public static class BadSessionIDError implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badSessionid); + } + } + + public static class SessionExpiredError implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.sessionExpired); + } + } +}