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