1 /** 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 21 package org.jivesoftware.smackx.commands; 22 23 import org.jivesoftware.smack.*; 24 import org.jivesoftware.smack.filter.PacketFilter; 25 import org.jivesoftware.smack.filter.PacketTypeFilter; 26 import org.jivesoftware.smack.packet.IQ; 27 import org.jivesoftware.smack.packet.Packet; 28 import org.jivesoftware.smack.packet.PacketExtension; 29 import org.jivesoftware.smack.packet.XMPPError; 30 import org.jivesoftware.smack.util.StringUtils; 31 import org.jivesoftware.smackx.Form; 32 import org.jivesoftware.smackx.NodeInformationProvider; 33 import org.jivesoftware.smackx.ServiceDiscoveryManager; 34 import org.jivesoftware.smackx.commands.AdHocCommand.Action; 35 import org.jivesoftware.smackx.commands.AdHocCommand.Status; 36 import org.jivesoftware.smackx.packet.AdHocCommandData; 37 import org.jivesoftware.smackx.packet.DiscoverInfo; 38 import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; 39 import org.jivesoftware.smackx.packet.DiscoverItems; 40 41 import java.util.ArrayList; 42 import java.util.Collection; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.WeakHashMap; 47 import 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 */ 59 public 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 } 754