blob: 8dac9994701a5d8fcf07d907b398b1885098b18f [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2005-2008 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smackx.commands;
22
23import org.jivesoftware.smack.*;
24import org.jivesoftware.smack.filter.PacketFilter;
25import org.jivesoftware.smack.filter.PacketTypeFilter;
26import org.jivesoftware.smack.packet.IQ;
27import org.jivesoftware.smack.packet.Packet;
28import org.jivesoftware.smack.packet.PacketExtension;
29import org.jivesoftware.smack.packet.XMPPError;
30import org.jivesoftware.smack.util.StringUtils;
31import org.jivesoftware.smackx.Form;
32import org.jivesoftware.smackx.NodeInformationProvider;
33import org.jivesoftware.smackx.ServiceDiscoveryManager;
34import org.jivesoftware.smackx.commands.AdHocCommand.Action;
35import org.jivesoftware.smackx.commands.AdHocCommand.Status;
36import org.jivesoftware.smackx.packet.AdHocCommandData;
37import org.jivesoftware.smackx.packet.DiscoverInfo;
38import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
39import org.jivesoftware.smackx.packet.DiscoverItems;
40
41import java.util.ArrayList;
42import java.util.Collection;
43import java.util.Collections;
44import java.util.List;
45import java.util.Map;
46import java.util.WeakHashMap;
47import java.util.concurrent.ConcurrentHashMap;
48
49/**
50 * An AdHocCommandManager is responsible for keeping the list of available
51 * commands offered by a service and for processing commands requests.
52 *
53 * Pass in a Connection instance to
54 * {@link #getAddHocCommandsManager(org.jivesoftware.smack.Connection)} in order to
55 * get an instance of this class.
56 *
57 * @author Gabriel Guardincerri
58 */
59public class AdHocCommandManager {
60
61 private static final String DISCO_NAMESPACE = "http://jabber.org/protocol/commands";
62
63 private static final String discoNode = DISCO_NAMESPACE;
64
65 /**
66 * The session time out in seconds.
67 */
68 private static final int SESSION_TIMEOUT = 2 * 60;
69
70 /**
71 * Map a Connection with it AdHocCommandManager. This map have a key-value
72 * pair for every active connection.
73 */
74 private static Map<Connection, AdHocCommandManager> instances =
75 new ConcurrentHashMap<Connection, AdHocCommandManager>();
76
77 /**
78 * Register the listener for all the connection creations. When a new
79 * connection is created a new AdHocCommandManager is also created and
80 * related to that connection.
81 */
82 static {
83 Connection.addConnectionCreationListener(new ConnectionCreationListener() {
84 public void connectionCreated(Connection connection) {
85 new AdHocCommandManager(connection);
86 }
87 });
88 }
89
90 /**
91 * Returns the <code>AdHocCommandManager</code> related to the
92 * <code>connection</code>.
93 *
94 * @param connection the XMPP connection.
95 * @return the AdHocCommandManager associated with the connection.
96 */
97 public static AdHocCommandManager getAddHocCommandsManager(Connection connection) {
98 return instances.get(connection);
99 }
100
101 /**
102 * Thread that reaps stale sessions.
103 */
104 private Thread sessionsSweeper;
105
106 /**
107 * The Connection that this instances of AdHocCommandManager manages
108 */
109 private Connection connection;
110
111 /**
112 * Map a command node with its AdHocCommandInfo. Note: Key=command node,
113 * Value=command. Command node matches the node attribute sent by command
114 * requesters.
115 */
116 private Map<String, AdHocCommandInfo> commands = Collections
117 .synchronizedMap(new WeakHashMap<String, AdHocCommandInfo>());
118
119 /**
120 * Map a command session ID with the instance LocalCommand. The LocalCommand
121 * is the an objects that has all the information of the current state of
122 * the command execution. Note: Key=session ID, Value=LocalCommand. Session
123 * ID matches the sessionid attribute sent by command responders.
124 */
125 private Map<String, LocalCommand> executingCommands = new ConcurrentHashMap<String, LocalCommand>();
126
127 private AdHocCommandManager(Connection connection) {
128 super();
129 this.connection = connection;
130 init();
131 }
132
133 /**
134 * Registers a new command with this command manager, which is related to a
135 * connection. The <tt>node</tt> is an unique identifier of that command for
136 * the connection related to this command manager. The <tt>name</tt> is the
137 * human readable name of the command. The <tt>class</tt> is the class of
138 * the command, which must extend {@link LocalCommand} and have a default
139 * constructor.
140 *
141 * @param node the unique identifier of the command.
142 * @param name the human readable name of the command.
143 * @param clazz the class of the command, which must extend {@link LocalCommand}.
144 */
145 public void registerCommand(String node, String name, final Class<? extends LocalCommand> clazz) {
146 registerCommand(node, name, new LocalCommandFactory() {
147 public LocalCommand getInstance() throws InstantiationException, IllegalAccessException {
148 return clazz.newInstance();
149 }
150 });
151 }
152
153 /**
154 * Registers a new command with this command manager, which is related to a
155 * connection. The <tt>node</tt> is an unique identifier of that
156 * command for the connection related to this command manager. The <tt>name</tt>
157 * is the human readeale name of the command. The <tt>factory</tt> generates
158 * new instances of the command.
159 *
160 * @param node the unique identifier of the command.
161 * @param name the human readable name of the command.
162 * @param factory a factory to create new instances of the command.
163 */
164 public void registerCommand(String node, final String name, LocalCommandFactory factory) {
165 AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, connection.getUser(), factory);
166
167 commands.put(node, commandInfo);
168 // Set the NodeInformationProvider that will provide information about
169 // the added command
170 ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(node,
171 new NodeInformationProvider() {
172 public List<DiscoverItems.Item> getNodeItems() {
173 return null;
174 }
175
176 public List<String> getNodeFeatures() {
177 List<String> answer = new ArrayList<String>();
178 answer.add(DISCO_NAMESPACE);
179 // TODO: check if this service is provided by the
180 // TODO: current connection.
181 answer.add("jabber:x:data");
182 return answer;
183 }
184
185 public List<DiscoverInfo.Identity> getNodeIdentities() {
186 List<DiscoverInfo.Identity> answer = new ArrayList<DiscoverInfo.Identity>();
187 DiscoverInfo.Identity identity = new DiscoverInfo.Identity(
188 "automation", name, "command-node");
189 answer.add(identity);
190 return answer;
191 }
192
193 @Override
194 public List<PacketExtension> getNodePacketExtensions() {
195 return null;
196 }
197
198 });
199 }
200
201 /**
202 * Discover the commands of an specific JID. The <code>jid</code> is a
203 * full JID.
204 *
205 * @param jid the full JID to retrieve the commands for.
206 * @return the discovered items.
207 * @throws XMPPException if the operation failed for some reason.
208 */
209 public DiscoverItems discoverCommands(String jid) throws XMPPException {
210 ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
211 .getInstanceFor(connection);
212 return serviceDiscoveryManager.discoverItems(jid, discoNode);
213 }
214
215 /**
216 * Publish the commands to an specific JID.
217 *
218 * @param jid the full JID to publish the commands to.
219 * @throws XMPPException if the operation failed for some reason.
220 */
221 public void publishCommands(String jid) throws XMPPException {
222 ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
223 .getInstanceFor(connection);
224
225 // Collects the commands to publish as items
226 DiscoverItems discoverItems = new DiscoverItems();
227 Collection<AdHocCommandInfo> xCommandsList = getRegisteredCommands();
228
229 for (AdHocCommandInfo info : xCommandsList) {
230 DiscoverItems.Item item = new DiscoverItems.Item(info.getOwnerJID());
231 item.setName(info.getName());
232 item.setNode(info.getNode());
233 discoverItems.addItem(item);
234 }
235
236 serviceDiscoveryManager.publishItems(jid, discoNode, discoverItems);
237 }
238
239 /**
240 * Returns a command that represents an instance of a command in a remote
241 * host. It is used to execute remote commands. The concept is similar to
242 * RMI. Every invocation on this command is equivalent to an invocation in
243 * the remote command.
244 *
245 * @param jid the full JID of the host of the remote command
246 * @param node the identifier of the command
247 * @return a local instance equivalent to the remote command.
248 */
249 public RemoteCommand getRemoteCommand(String jid, String node) {
250 return new RemoteCommand(connection, node, jid);
251 }
252
253 /**
254 * <ul>
255 * <li>Adds listeners to the connection</li>
256 * <li>Registers the ad-hoc command feature to the ServiceDiscoveryManager</li>
257 * <li>Registers the items of the feature</li>
258 * <li>Adds packet listeners to handle execution requests</li>
259 * <li>Creates and start the session sweeper</li>
260 * </ul>
261 */
262 private void init() {
263 // Register the new instance and associate it with the connection
264 instances.put(connection, this);
265
266 // Add a listener to the connection that removes the registered instance
267 // when the connection is closed
268 connection.addConnectionListener(new ConnectionListener() {
269 public void connectionClosed() {
270 // Unregister this instance since the connection has been closed
271 instances.remove(connection);
272 }
273
274 public void connectionClosedOnError(Exception e) {
275 // Unregister this instance since the connection has been closed
276 instances.remove(connection);
277 }
278
279 public void reconnectionSuccessful() {
280 // Register this instance since the connection has been
281 // reestablished
282 instances.put(connection, AdHocCommandManager.this);
283 }
284
285 public void reconnectingIn(int seconds) {
286 // Nothing to do
287 }
288
289 public void reconnectionFailed(Exception e) {
290 // Nothing to do
291 }
292 });
293
294 // Add the feature to the service discovery manage to show that this
295 // connection supports the AdHoc-Commands protocol.
296 // This information will be used when another client tries to
297 // discover whether this client supports AdHoc-Commands or not.
298 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(
299 DISCO_NAMESPACE);
300
301 // Set the NodeInformationProvider that will provide information about
302 // which AdHoc-Commands are registered, whenever a disco request is
303 // received
304 ServiceDiscoveryManager.getInstanceFor(connection)
305 .setNodeInformationProvider(discoNode,
306 new NodeInformationProvider() {
307 public List<DiscoverItems.Item> getNodeItems() {
308
309 List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
310 Collection<AdHocCommandInfo> commandsList = getRegisteredCommands();
311
312 for (AdHocCommandInfo info : commandsList) {
313 DiscoverItems.Item item = new DiscoverItems.Item(
314 info.getOwnerJID());
315 item.setName(info.getName());
316 item.setNode(info.getNode());
317 answer.add(item);
318 }
319
320 return answer;
321 }
322
323 public List<String> getNodeFeatures() {
324 return null;
325 }
326
327 public List<Identity> getNodeIdentities() {
328 return null;
329 }
330
331 @Override
332 public List<PacketExtension> getNodePacketExtensions() {
333 return null;
334 }
335 });
336
337 // The packet listener and the filter for processing some AdHoc Commands
338 // Packets
339 PacketListener listener = new PacketListener() {
340 public void processPacket(Packet packet) {
341 AdHocCommandData requestData = (AdHocCommandData) packet;
342 processAdHocCommand(requestData);
343 }
344 };
345
346 PacketFilter filter = new PacketTypeFilter(AdHocCommandData.class);
347 connection.addPacketListener(listener, filter);
348
349 sessionsSweeper = null;
350 }
351
352 /**
353 * Process the AdHoc-Command packet that request the execution of some
354 * action of a command. If this is the first request, this method checks,
355 * before executing the command, if:
356 * <ul>
357 * <li>The requested command exists</li>
358 * <li>The requester has permissions to execute it</li>
359 * <li>The command has more than one stage, if so, it saves the command and
360 * session ID for further use</li>
361 * </ul>
362 *
363 * <br>
364 * <br>
365 * If this is not the first request, this method checks, before executing
366 * the command, if:
367 * <ul>
368 * <li>The session ID of the request was stored</li>
369 * <li>The session life do not exceed the time out</li>
370 * <li>The action to execute is one of the available actions</li>
371 * </ul>
372 *
373 * @param requestData
374 * the packet to process.
375 */
376 private void processAdHocCommand(AdHocCommandData requestData) {
377 // Only process requests of type SET
378 if (requestData.getType() != IQ.Type.SET) {
379 return;
380 }
381
382 // Creates the response with the corresponding data
383 AdHocCommandData response = new AdHocCommandData();
384 response.setTo(requestData.getFrom());
385 response.setPacketID(requestData.getPacketID());
386 response.setNode(requestData.getNode());
387 response.setId(requestData.getTo());
388
389 String sessionId = requestData.getSessionID();
390 String commandNode = requestData.getNode();
391
392 if (sessionId == null) {
393 // A new execution request has been received. Check that the
394 // command exists
395 if (!commands.containsKey(commandNode)) {
396 // Requested command does not exist so return
397 // item_not_found error.
398 respondError(response, XMPPError.Condition.item_not_found);
399 return;
400 }
401
402 // Create new session ID
403 sessionId = StringUtils.randomString(15);
404
405 try {
406 // Create a new instance of the command with the
407 // corresponding sessioid
408 LocalCommand command = newInstanceOfCmd(commandNode, sessionId);
409
410 response.setType(IQ.Type.RESULT);
411 command.setData(response);
412
413 // Check that the requester has enough permission.
414 // Answer forbidden error if requester permissions are not
415 // enough to execute the requested command
416 if (!command.hasPermission(requestData.getFrom())) {
417 respondError(response, XMPPError.Condition.forbidden);
418 return;
419 }
420
421 Action action = requestData.getAction();
422
423 // If the action is unknown then respond an error.
424 if (action != null && action.equals(Action.unknown)) {
425 respondError(response, XMPPError.Condition.bad_request,
426 AdHocCommand.SpecificErrorCondition.malformedAction);
427 return;
428 }
429
430 // If the action is not execute, then it is an invalid action.
431 if (action != null && !action.equals(Action.execute)) {
432 respondError(response, XMPPError.Condition.bad_request,
433 AdHocCommand.SpecificErrorCondition.badAction);
434 return;
435 }
436
437 // Increase the state number, so the command knows in witch
438 // stage it is
439 command.incrementStage();
440 // Executes the command
441 command.execute();
442
443 if (command.isLastStage()) {
444 // If there is only one stage then the command is completed
445 response.setStatus(Status.completed);
446 }
447 else {
448 // Else it is still executing, and is registered to be
449 // available for the next call
450 response.setStatus(Status.executing);
451 executingCommands.put(sessionId, command);
452 // See if the session reaping thread is started. If not, start it.
453 if (sessionsSweeper == null) {
454 sessionsSweeper = new Thread(new Runnable() {
455 public void run() {
456 while (true) {
457 for (String sessionId : executingCommands.keySet()) {
458 LocalCommand command = executingCommands.get(sessionId);
459 // Since the command could be removed in the meanwhile
460 // of getting the key and getting the value - by a
461 // processed packet. We must check if it still in the
462 // map.
463 if (command != null) {
464 long creationStamp = command.getCreationDate();
465 // Check if the Session data has expired (default is
466 // 10 minutes)
467 // To remove it from the session list it waits for
468 // the double of the of time out time. This is to
469 // let
470 // the requester know why his execution request is
471 // not accepted. If the session is removed just
472 // after the time out, then whe the user request to
473 // continue the execution he will recieved an
474 // invalid session error and not a time out error.
475 if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000 * 2) {
476 // Remove the expired session
477 executingCommands.remove(sessionId);
478 }
479 }
480 }
481 try {
482 Thread.sleep(1000);
483 }
484 catch (InterruptedException ie) {
485 // Ignore.
486 }
487 }
488 }
489
490 });
491 sessionsSweeper.setDaemon(true);
492 sessionsSweeper.start();
493 }
494 }
495
496 // Sends the response packet
497 connection.sendPacket(response);
498
499 }
500 catch (XMPPException e) {
501 // If there is an exception caused by the next, complete,
502 // prev or cancel method, then that error is returned to the
503 // requester.
504 XMPPError error = e.getXMPPError();
505
506 // If the error type is cancel, then the execution is
507 // canceled therefore the status must show that, and the
508 // command be removed from the executing list.
509 if (XMPPError.Type.CANCEL.equals(error.getType())) {
510 response.setStatus(Status.canceled);
511 executingCommands.remove(sessionId);
512 }
513 respondError(response, error);
514 e.printStackTrace();
515 }
516 }
517 else {
518 LocalCommand command = executingCommands.get(sessionId);
519
520 // Check that a command exists for the specified sessionID
521 // This also handles if the command was removed in the meanwhile
522 // of getting the key and the value of the map.
523 if (command == null) {
524 respondError(response, XMPPError.Condition.bad_request,
525 AdHocCommand.SpecificErrorCondition.badSessionid);
526 return;
527 }
528
529 // Check if the Session data has expired (default is 10 minutes)
530 long creationStamp = command.getCreationDate();
531 if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000) {
532 // Remove the expired session
533 executingCommands.remove(sessionId);
534
535 // Answer a not_allowed error (session-expired)
536 respondError(response, XMPPError.Condition.not_allowed,
537 AdHocCommand.SpecificErrorCondition.sessionExpired);
538 return;
539 }
540
541 /*
542 * Since the requester could send two requests for the same
543 * executing command i.e. the same session id, all the execution of
544 * the action must be synchronized to avoid inconsistencies.
545 */
546 synchronized (command) {
547 Action action = requestData.getAction();
548
549 // If the action is unknown the respond an error
550 if (action != null && action.equals(Action.unknown)) {
551 respondError(response, XMPPError.Condition.bad_request,
552 AdHocCommand.SpecificErrorCondition.malformedAction);
553 return;
554 }
555
556 // If the user didn't specify an action or specify the execute
557 // action then follow the actual default execute action
558 if (action == null || Action.execute.equals(action)) {
559 action = command.getExecuteAction();
560 }
561
562 // Check that the specified action was previously
563 // offered
564 if (!command.isValidAction(action)) {
565 respondError(response, XMPPError.Condition.bad_request,
566 AdHocCommand.SpecificErrorCondition.badAction);
567 return;
568 }
569
570 try {
571 // TODO: Check that all the requierd fields of the form are
572 // TODO: filled, if not throw an exception. This will simplify the
573 // TODO: construction of new commands
574
575 // Since all errors were passed, the response is now a
576 // result
577 response.setType(IQ.Type.RESULT);
578
579 // Set the new data to the command.
580 command.setData(response);
581
582 if (Action.next.equals(action)) {
583 command.incrementStage();
584 command.next(new Form(requestData.getForm()));
585 if (command.isLastStage()) {
586 // If it is the last stage then the command is
587 // completed
588 response.setStatus(Status.completed);
589 }
590 else {
591 // Otherwise it is still executing
592 response.setStatus(Status.executing);
593 }
594 }
595 else if (Action.complete.equals(action)) {
596 command.incrementStage();
597 command.complete(new Form(requestData.getForm()));
598 response.setStatus(Status.completed);
599 // Remove the completed session
600 executingCommands.remove(sessionId);
601 }
602 else if (Action.prev.equals(action)) {
603 command.decrementStage();
604 command.prev();
605 }
606 else if (Action.cancel.equals(action)) {
607 command.cancel();
608 response.setStatus(Status.canceled);
609 // Remove the canceled session
610 executingCommands.remove(sessionId);
611 }
612
613 connection.sendPacket(response);
614 }
615 catch (XMPPException e) {
616 // If there is an exception caused by the next, complete,
617 // prev or cancel method, then that error is returned to the
618 // requester.
619 XMPPError error = e.getXMPPError();
620
621 // If the error type is cancel, then the execution is
622 // canceled therefore the status must show that, and the
623 // command be removed from the executing list.
624 if (XMPPError.Type.CANCEL.equals(error.getType())) {
625 response.setStatus(Status.canceled);
626 executingCommands.remove(sessionId);
627 }
628 respondError(response, error);
629
630 e.printStackTrace();
631 }
632 }
633 }
634 }
635
636 /**
637 * Responds an error with an specific condition.
638 *
639 * @param response the response to send.
640 * @param condition the condition of the error.
641 */
642 private void respondError(AdHocCommandData response,
643 XMPPError.Condition condition) {
644 respondError(response, new XMPPError(condition));
645 }
646
647 /**
648 * Responds an error with an specific condition.
649 *
650 * @param response the response to send.
651 * @param condition the condition of the error.
652 * @param specificCondition the adhoc command error condition.
653 */
654 private void respondError(AdHocCommandData response, XMPPError.Condition condition,
655 AdHocCommand.SpecificErrorCondition specificCondition)
656 {
657 XMPPError error = new XMPPError(condition);
658 error.addExtension(new AdHocCommandData.SpecificError(specificCondition));
659 respondError(response, error);
660 }
661
662 /**
663 * Responds an error with an specific error.
664 *
665 * @param response the response to send.
666 * @param error the error to send.
667 */
668 private void respondError(AdHocCommandData response, XMPPError error) {
669 response.setType(IQ.Type.ERROR);
670 response.setError(error);
671 connection.sendPacket(response);
672 }
673
674 /**
675 * Creates a new instance of a command to be used by a new execution request
676 *
677 * @param commandNode the command node that identifies it.
678 * @param sessionID the session id of this execution.
679 * @return the command instance to execute.
680 * @throws XMPPException if there is problem creating the new instance.
681 */
682 private LocalCommand newInstanceOfCmd(String commandNode, String sessionID)
683 throws XMPPException
684 {
685 AdHocCommandInfo commandInfo = commands.get(commandNode);
686 LocalCommand command;
687 try {
688 command = (LocalCommand) commandInfo.getCommandInstance();
689 command.setSessionID(sessionID);
690 command.setName(commandInfo.getName());
691 command.setNode(commandInfo.getNode());
692 }
693 catch (InstantiationException e) {
694 e.printStackTrace();
695 throw new XMPPException(new XMPPError(
696 XMPPError.Condition.interna_server_error));
697 }
698 catch (IllegalAccessException e) {
699 e.printStackTrace();
700 throw new XMPPException(new XMPPError(
701 XMPPError.Condition.interna_server_error));
702 }
703 return command;
704 }
705
706 /**
707 * Returns the registered commands of this command manager, which is related
708 * to a connection.
709 *
710 * @return the registered commands.
711 */
712 private Collection<AdHocCommandInfo> getRegisteredCommands() {
713 return commands.values();
714 }
715
716 /**
717 * Stores ad-hoc command information.
718 */
719 private static class AdHocCommandInfo {
720
721 private String node;
722 private String name;
723 private String ownerJID;
724 private LocalCommandFactory factory;
725
726 public AdHocCommandInfo(String node, String name, String ownerJID,
727 LocalCommandFactory factory)
728 {
729 this.node = node;
730 this.name = name;
731 this.ownerJID = ownerJID;
732 this.factory = factory;
733 }
734
735 public LocalCommand getCommandInstance() throws InstantiationException,
736 IllegalAccessException
737 {
738 return factory.getInstance();
739 }
740
741 public String getName() {
742 return name;
743 }
744
745 public String getNode() {
746 return node;
747 }
748
749 public String getOwnerJID() {
750 return ownerJID;
751 }
752 }
753}