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 java.lang.reflect.Constructor; 021import java.lang.reflect.InvocationTargetException; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.WeakHashMap; 028import java.util.concurrent.ConcurrentHashMap; 029import java.util.concurrent.TimeUnit; 030import java.util.logging.Level; 031import java.util.logging.Logger; 032 033import org.jivesoftware.smack.ConnectionCreationListener; 034import org.jivesoftware.smack.Manager; 035import org.jivesoftware.smack.SmackException; 036import org.jivesoftware.smack.SmackException.NoResponseException; 037import org.jivesoftware.smack.SmackException.NotConnectedException; 038import org.jivesoftware.smack.XMPPConnection; 039import org.jivesoftware.smack.XMPPConnectionRegistry; 040import org.jivesoftware.smack.XMPPException; 041import org.jivesoftware.smack.XMPPException.XMPPErrorException; 042import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 043import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 044import org.jivesoftware.smack.packet.IQ; 045import org.jivesoftware.smack.packet.StanzaError; 046import org.jivesoftware.smack.util.StringUtils; 047 048import org.jivesoftware.smackx.commands.packet.AdHocCommandData; 049import org.jivesoftware.smackx.commands.packet.AdHocCommandData.AllowedAction; 050import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder; 051import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider; 052import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 053import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 054import org.jivesoftware.smackx.disco.packet.DiscoverItems; 055import org.jivesoftware.smackx.xdata.form.SubmitForm; 056import org.jivesoftware.smackx.xdata.packet.DataForm; 057 058import org.jxmpp.jid.EntityFullJid; 059import org.jxmpp.jid.Jid; 060 061/** 062 * An AdHocCommandManager is responsible for keeping the list of available 063 * commands offered by a service and for processing commands requests. 064 * 065 * Pass in an XMPPConnection instance to 066 * {@link #getAddHocCommandsManager(XMPPConnection)} in order to 067 * get an instance of this class. 068 * 069 * @author Gabriel Guardincerri 070 * @author Florian Schmaus 071 */ 072public final class AdHocCommandManager extends Manager { 073 public static final String NAMESPACE = "http://jabber.org/protocol/commands"; 074 075 private static final Logger LOGGER = Logger.getLogger(AdHocCommandManager.class.getName()); 076 077 /** 078 * The session time out in seconds. 079 */ 080 private static int DEFAULT_SESSION_TIMEOUT_SECS = 7 * 60; 081 082 /** 083 * Map an XMPPConnection with it AdHocCommandManager. This map have a key-value 084 * pair for every active connection. 085 */ 086 private static final Map<XMPPConnection, AdHocCommandManager> instances = new WeakHashMap<>(); 087 088 /** 089 * Register the listener for all the connection creations. When a new 090 * connection is created a new AdHocCommandManager is also created and 091 * related to that connection. 092 */ 093 static { 094 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 095 @Override 096 public void connectionCreated(XMPPConnection connection) { 097 getInstance(connection); 098 } 099 }); 100 } 101 102 /** 103 * Returns the <code>AdHocCommandManager</code> related to the 104 * <code>connection</code>. 105 * 106 * @param connection the XMPP connection. 107 * @return the AdHocCommandManager associated with the connection. 108 * @deprecated use {@link #getInstance(XMPPConnection)} instead. 109 */ 110 @Deprecated 111 public static AdHocCommandManager getAddHocCommandsManager(XMPPConnection connection) { 112 return getInstance(connection); 113 } 114 115 /** 116 * Returns the <code>AdHocCommandManager</code> related to the 117 * <code>connection</code>. 118 * 119 * @param connection the XMPP connection. 120 * @return the AdHocCommandManager associated with the connection. 121 */ 122 public static synchronized AdHocCommandManager getInstance(XMPPConnection connection) { 123 AdHocCommandManager ahcm = instances.get(connection); 124 if (ahcm == null) { 125 ahcm = new AdHocCommandManager(connection); 126 instances.put(connection, ahcm); 127 } 128 return ahcm; 129 } 130 131 /** 132 * Map a command node with its AdHocCommandInfo. Note: Key=command node, 133 * Value=command. Command node matches the node attribute sent by command 134 * requesters. 135 */ 136 // TODO: Change to Map once Smack's minimum Android API level is 24 or higher. 137 private final ConcurrentHashMap<String, AdHocCommandInfo> commands = new ConcurrentHashMap<>(); 138 139 /** 140 * Map a command session ID with the instance LocalCommand. The LocalCommand 141 * is the an objects that has all the information of the current state of 142 * the command execution. Note: Key=session ID, Value=LocalCommand. Session 143 * ID matches the sessionid attribute sent by command responders. 144 */ 145 private final Map<String, AdHocCommandHandler> executingCommands = new ConcurrentHashMap<>(); 146 147 private final ServiceDiscoveryManager serviceDiscoveryManager; 148 149 private int sessionTimeoutSecs = DEFAULT_SESSION_TIMEOUT_SECS; 150 151 private AdHocCommandManager(XMPPConnection connection) { 152 super(connection); 153 this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); 154 155 // Add the feature to the service discovery manage to show that this 156 // connection supports the AdHoc-Commands protocol. 157 // This information will be used when another client tries to 158 // discover whether this client supports AdHoc-Commands or not. 159 ServiceDiscoveryManager.getInstanceFor(connection).addFeature( 160 NAMESPACE); 161 162 // Set the NodeInformationProvider that will provide information about 163 // which AdHoc-Commands are registered, whenever a disco request is 164 // received 165 ServiceDiscoveryManager.getInstanceFor(connection) 166 .setNodeInformationProvider(NAMESPACE, 167 new AbstractNodeInformationProvider() { 168 @Override 169 public List<DiscoverItems.Item> getNodeItems() { 170 List<DiscoverItems.Item> answer = new ArrayList<>(); 171 Collection<AdHocCommandInfo> commandsList = commands.values(); 172 173 EntityFullJid ourJid = connection().getUser(); 174 if (ourJid == null) { 175 LOGGER.warning("Local connection JID not available, can not respond to " + NAMESPACE + " node information"); 176 return null; 177 } 178 179 for (AdHocCommandInfo info : commandsList) { 180 DiscoverItems.Item item = new DiscoverItems.Item(ourJid); 181 item.setName(info.getName()); 182 item.setNode(info.getNode()); 183 answer.add(item); 184 } 185 186 return answer; 187 } 188 }); 189 190 // The packet listener and the filter for processing some AdHoc Commands 191 // Packets 192 // TODO: This handler being async means that requests for the same command could be handled out of order. Nobody 193 // complained so far, and I could imagine that it does not really matter in practice. But it is certainly 194 // something to keep in mind. 195 connection.registerIQRequestHandler(new AbstractIqRequestHandler(AdHocCommandData.ELEMENT, 196 AdHocCommandData.NAMESPACE, IQ.Type.set, Mode.async) { 197 @Override 198 public IQ handleIQRequest(IQ iqRequest) { 199 AdHocCommandData requestData = (AdHocCommandData) iqRequest; 200 AdHocCommandData response = processAdHocCommand(requestData); 201 assert response.getStatus() != null || response.getType() == IQ.Type.error; 202 return response; 203 } 204 }); 205 } 206 207 /** 208 * Registers a new command with this command manager, which is related to a 209 * connection. The <code>node</code> is an unique identifier of that command for 210 * the connection related to this command manager. The <code>name</code> is the 211 * human readable name of the command. The <code>class</code> is the class of 212 * the command, which must extend {@link AdHocCommandHandler} and have a default 213 * constructor. 214 * 215 * @param node the unique identifier of the command. 216 * @param name the human readable name of the command. 217 * @param clazz the class of the command, which must extend {@link AdHocCommandHandler}. 218 * @throws SecurityException if there was a security violation. 219 * @throws NoSuchMethodException if no such method is declared. 220 */ 221 public void registerCommand(String node, String name, final Class<? extends AdHocCommandHandler> clazz) throws NoSuchMethodException, SecurityException { 222 Constructor<? extends AdHocCommandHandler> constructor = clazz.getConstructor(String.class, String.class, String.class); 223 registerCommand(node, name, new AdHocCommandHandlerFactory() { 224 @Override 225 public AdHocCommandHandler create(String node, String name, String sessionId) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { 226 return constructor.newInstance(node, name, sessionId); 227 } 228 }); 229 } 230 231 /** 232 * Registers a new command with this command manager, which is related to a 233 * connection. The <code>node</code> is an unique identifier of that 234 * command for the connection related to this command manager. The <code>name</code> 235 * is the human readable name of the command. The <code>factory</code> generates 236 * new instances of the command. 237 * 238 * @param node the unique identifier of the command. 239 * @param name the human readable name of the command. 240 * @param factory a factory to create new instances of the command. 241 */ 242 public synchronized void registerCommand(String node, final String name, AdHocCommandHandlerFactory factory) { 243 AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, factory); 244 245 AdHocCommandInfo existing = commands.putIfAbsent(node, commandInfo); 246 if (existing != null) throw new IllegalArgumentException("There is already an ad-hoc command registered for " + node); 247 248 // Set the NodeInformationProvider that will provide information about 249 // the added command 250 serviceDiscoveryManager.setNodeInformationProvider(node, 251 new AbstractNodeInformationProvider() { 252 @Override 253 public List<String> getNodeFeatures() { 254 List<String> answer = new ArrayList<>(); 255 answer.add(NAMESPACE); 256 // TODO: check if this service is provided by the 257 // TODO: current connection. 258 answer.add("jabber:x:data"); 259 return answer; 260 } 261 @Override 262 public List<DiscoverInfo.Identity> getNodeIdentities() { 263 List<DiscoverInfo.Identity> answer = new ArrayList<>(); 264 DiscoverInfo.Identity identity = new DiscoverInfo.Identity( 265 "automation", name, "command-node"); 266 answer.add(identity); 267 return answer; 268 } 269 }); 270 } 271 272 public synchronized boolean unregisterCommand(String node) { 273 AdHocCommandInfo commandInfo = commands.remove(node); 274 if (commandInfo == null) return false; 275 276 serviceDiscoveryManager.removeNodeInformationProvider(node); 277 return true; 278 } 279 280 /** 281 * Discover the commands of an specific JID. The <code>jid</code> is a 282 * full JID. 283 * 284 * @param jid the full JID to retrieve the commands for. 285 * @return the discovered items. 286 * @throws XMPPException if the operation failed for some reason. 287 * @throws SmackException if there was no response from the server. 288 * @throws InterruptedException if the calling thread was interrupted. 289 */ 290 public DiscoverItems discoverCommands(Jid jid) throws XMPPException, SmackException, InterruptedException { 291 return serviceDiscoveryManager.discoverItems(jid, NAMESPACE); 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 AdHocCommand getRemoteCommand(Jid jid, String node) { 305 return new AdHocCommand(connection(), node, jid); 306 } 307 308 /** 309 * Process the AdHoc-Command stanza 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 request the incoming AdHoc command request. 330 */ 331 private AdHocCommandData processAdHocCommand(AdHocCommandData request) { 332 String sessionId = request.getSessionId(); 333 334 final AdHocCommandHandler command; 335 if (sessionId == null) { 336 String commandNode = request.getNode(); 337 338 // A new execution request has been received. Check that the 339 // command exists 340 AdHocCommandInfo commandInfo = commands.get(commandNode); 341 if (commandInfo == null) { 342 // Requested command does not exist so return 343 // item_not_found error. 344 return respondError(request, null, StanzaError.Condition.item_not_found); 345 } 346 347 assert commandInfo.getNode().equals(commandNode); 348 349 // Create a new instance of the command with the 350 // corresponding session ID. 351 try { 352 command = commandInfo.getCommandInstance(); 353 } 354 catch (InstantiationException | IllegalAccessException | IllegalArgumentException 355 | InvocationTargetException e) { 356 LOGGER.log(Level.WARNING, "Could not instanciate ad-hoc command server", e); 357 StanzaError xmppError = StanzaError.getBuilder() 358 .setCondition(StanzaError.Condition.internal_server_error) 359 .setDescriptiveEnText(e.getMessage()) 360 .build(); 361 return respondError(request, null, xmppError); 362 } 363 } else { 364 command = executingCommands.get(sessionId); 365 // Check that a command exists for the specified sessionID 366 // This also handles if the command was removed in the meanwhile 367 // of getting the key and the value of the map. 368 if (command == null) { 369 return respondError(request, null, StanzaError.Condition.bad_request, 370 SpecificErrorCondition.badSessionid); 371 } 372 } 373 374 375 final AdHocCommandDataBuilder responseBuilder = AdHocCommandDataBuilder.buildResponseFor(request) 376 .setSessionId(command.getSessionId()); 377 378 final AdHocCommandData response; 379 /* 380 * Since the requester could send two requests for the same 381 * executing command i.e. the same session id, all the execution of 382 * the action must be synchronized to avoid inconsistencies. 383 */ 384 synchronized (command) { 385 command.addRequest(request); 386 387 if (sessionId == null) { 388 response = processAdHocCommandOfNewSession(request, command, responseBuilder); 389 } else { 390 response = processAdHocCommandOfExistingSession(request, command, responseBuilder); 391 } 392 393 394 AdHocCommandResult commandResult = AdHocCommandResult.from(response); 395 command.addResult(commandResult); 396 } 397 398 return response; 399 } 400 401 private AdHocCommandData createResponseFrom(AdHocCommandData request, AdHocCommandDataBuilder response, XMPPErrorException exception, String sessionId) { 402 StanzaError error = exception.getStanzaError(); 403 404 // If the error type is cancel, then the execution is 405 // canceled therefore the status must show that, and the 406 // command be removed from the executing list. 407 if (error.getType() == StanzaError.Type.CANCEL) { 408 response.setStatus(AdHocCommandData.Status.canceled); 409 410 executingCommands.remove(sessionId); 411 412 return response.build(); 413 } 414 415 return respondError(request, response, error); 416 } 417 418 private static AdHocCommandData createResponseFrom(AdHocCommandData request, AdHocCommandDataBuilder response, Exception exception) { 419 StanzaError error = StanzaError.from(StanzaError.Condition.internal_server_error, exception.getMessage()) 420 .build(); 421 return respondError(request, response, error); 422 } 423 424 private AdHocCommandData processAdHocCommandOfNewSession(AdHocCommandData request, AdHocCommandHandler command, AdHocCommandDataBuilder responseBuilder) { 425 // Check that the requester has enough permission. 426 // Answer forbidden error if requester permissions are not 427 // enough to execute the requested command 428 if (!command.hasPermission(request.getFrom())) { 429 return respondError(request, responseBuilder, StanzaError.Condition.forbidden); 430 } 431 432 AdHocCommandData.Action action = request.getAction(); 433 434 // If the action is not execute, then it is an invalid action. 435 if (action != null && !action.equals(AdHocCommandData.Action.execute)) { 436 return respondError(request, responseBuilder, StanzaError.Condition.bad_request, 437 SpecificErrorCondition.badAction); 438 } 439 440 // Increase the state number, so the command knows in witch 441 // stage it is 442 command.incrementStage(); 443 444 final AdHocCommandData response; 445 try { 446 // Executes the command 447 response = command.execute(responseBuilder); 448 } catch (XMPPErrorException e) { 449 return createResponseFrom(request, responseBuilder, e, command.getSessionId()); 450 } catch (NoResponseException | NotConnectedException | InterruptedException | IllegalStateException e) { 451 return createResponseFrom(request, responseBuilder, e); 452 } 453 454 if (response.isExecuting()) { 455 executingCommands.put(command.getSessionId(), command); 456 // See if the session sweeper thread is scheduled. If not, start it. 457 maybeWindUpSessionSweeper(); 458 } 459 460 return response; 461 } 462 463 private AdHocCommandData processAdHocCommandOfExistingSession(AdHocCommandData request, AdHocCommandHandler command, AdHocCommandDataBuilder responseBuilder) { 464 // Check if the Session data has expired (default is 10 minutes) 465 long creationStamp = command.getCreationDate(); 466 if (System.currentTimeMillis() - creationStamp > sessionTimeoutSecs * 1000) { 467 // Remove the expired session 468 executingCommands.remove(command.getSessionId()); 469 470 // Answer a not_allowed error (session-expired) 471 return respondError(request, responseBuilder, StanzaError.Condition.not_allowed, 472 SpecificErrorCondition.sessionExpired); 473 } 474 475 AdHocCommandData.Action action = request.getAction(); 476 477 // If the user didn't specify an action or specify the execute 478 // action then follow the actual default execute action 479 if (action == null || AdHocCommandData.Action.execute.equals(action)) { 480 AllowedAction executeAction = command.getExecuteAction(); 481 if (executeAction != null) { 482 action = executeAction.action; 483 } 484 } 485 486 // Check that the specified action was previously 487 // offered 488 if (!command.isValidAction(action)) { 489 return respondError(request, responseBuilder, StanzaError.Condition.bad_request, 490 SpecificErrorCondition.badAction); 491 } 492 493 AdHocCommandData response; 494 try { 495 DataForm dataForm; 496 switch (action) { 497 case next: 498 command.incrementStage(); 499 dataForm = request.getForm(); 500 response = command.next(responseBuilder, new SubmitForm(dataForm)); 501 break; 502 case complete: 503 command.incrementStage(); 504 dataForm = request.getForm(); 505 responseBuilder.setStatus(AdHocCommandData.Status.completed); 506 response = command.complete(responseBuilder, new SubmitForm(dataForm)); 507 // Remove the completed session 508 executingCommands.remove(command.getSessionId()); 509 break; 510 case prev: 511 command.decrementStage(); 512 response = command.prev(responseBuilder); 513 break; 514 case cancel: 515 command.cancel(); 516 responseBuilder.setStatus(AdHocCommandData.Status.canceled); 517 response = responseBuilder.build(); 518 // Remove the canceled session 519 executingCommands.remove(command.getSessionId()); 520 break; 521 default: 522 return respondError(request, responseBuilder, StanzaError.Condition.bad_request, 523 SpecificErrorCondition.badAction); 524 } 525 } catch (XMPPErrorException e) { 526 return createResponseFrom(request, responseBuilder, e, command.getSessionId()); 527 } catch (NoResponseException | NotConnectedException | InterruptedException | IllegalStateException e) { 528 return createResponseFrom(request, responseBuilder, e); 529 } 530 531 return response; 532 } 533 534 private boolean sessionSweeperScheduled; 535 536 private int getSessionRemovalTimeoutSecs() { 537 return sessionTimeoutSecs * 2; 538 } 539 540 private void sessionSweeper() { 541 final long currentTime = System.currentTimeMillis(); 542 synchronized (this) { 543 for (Iterator<Map.Entry<String, AdHocCommandHandler>> it = executingCommands.entrySet().iterator(); it.hasNext();) { 544 Map.Entry<String, AdHocCommandHandler> entry = it.next(); 545 AdHocCommandHandler command = entry.getValue(); 546 547 long creationStamp = command.getCreationDate(); 548 // Check if the Session data has expired. 549 // To remove it from the session list it waits for the double of 550 // the of time out time. This is to let 551 // the requester know why his execution request is 552 // not accepted. If the session is removed just 553 // after the time out, then once the user requests to 554 // continue the execution he will received an 555 // invalid session error and not a time out error. 556 if (currentTime - creationStamp > getSessionRemovalTimeoutSecs() * 1000) { 557 // Remove the expired session 558 it.remove(); 559 } 560 } 561 562 sessionSweeperScheduled = false; 563 } 564 565 if (!executingCommands.isEmpty()) { 566 maybeWindUpSessionSweeper(); 567 } 568 }; 569 570 private synchronized void maybeWindUpSessionSweeper() { 571 if (sessionSweeperScheduled) { 572 return; 573 } 574 575 sessionSweeperScheduled = true; 576 schedule(this::sessionSweeper, getSessionRemovalTimeoutSecs() + 1, TimeUnit.SECONDS); 577 } 578 579 /** 580 * Responds an error with an specific condition. 581 * 582 * @param request the request that caused the error response. 583 * @param condition the condition of the error. 584 */ 585 private static AdHocCommandData respondError(AdHocCommandData request, AdHocCommandDataBuilder response, 586 StanzaError.Condition condition) { 587 return respondError(request, response, StanzaError.getBuilder(condition).build()); 588 } 589 590 /** 591 * Responds an error with an specific condition. 592 * 593 * @param request the request that caused the error response. 594 * @param condition the condition of the error. 595 * @param specificCondition the adhoc command error condition. 596 */ 597 private static AdHocCommandData respondError(AdHocCommandData request, AdHocCommandDataBuilder response, StanzaError.Condition condition, 598 SpecificErrorCondition specificCondition) { 599 StanzaError error = StanzaError.getBuilder(condition) 600 .addExtension(new AdHocCommandData.SpecificError(specificCondition)) 601 .build(); 602 return respondError(request, response, error); 603 } 604 605 /** 606 * Responds an error with an specific error. 607 * 608 * @param request the request that caused the error response. 609 * @param error the error to send. 610 */ 611 private static AdHocCommandData respondError(AdHocCommandData request, AdHocCommandDataBuilder response, StanzaError error) { 612 if (response == null) { 613 return AdHocCommandDataBuilder.buildResponseFor(request, IQ.ResponseType.error).setError(error).build(); 614 } 615 616 // Response may be not of IQ type error here, so switch that. 617 return response.ofType(IQ.Type.error) 618 .setError(error) 619 .build(); 620 } 621 622 public static void setDefaultSessionTimeoutSecs(int seconds) { 623 if (seconds < 10) { 624 throw new IllegalArgumentException(); 625 } 626 DEFAULT_SESSION_TIMEOUT_SECS = seconds; 627 } 628 629 public void setSessionTimeoutSecs(int seconds) { 630 if (seconds < 10) { 631 throw new IllegalArgumentException(); 632 } 633 634 sessionTimeoutSecs = seconds; 635 } 636 637 /** 638 * Stores ad-hoc command information. 639 */ 640 private final class AdHocCommandInfo { 641 642 private final String node; 643 private final String name; 644 private final AdHocCommandHandlerFactory factory; 645 646 private static final int MAX_SESSION_GEN_ATTEMPTS = 3; 647 648 private AdHocCommandInfo(String node, String name, AdHocCommandHandlerFactory factory) { 649 this.node = node; 650 this.name = name; 651 this.factory = factory; 652 } 653 654 public AdHocCommandHandler getCommandInstance() throws InstantiationException, 655 IllegalAccessException, IllegalArgumentException, InvocationTargetException { 656 String sessionId; 657 // TODO: The code below contains a race condition. Use CopncurrentHashMap.computeIfAbsent() to remove the 658 // race condition once Smack's minimum Android API level 24 or higher. 659 int attempt = 0; 660 do { 661 attempt++; 662 if (attempt > MAX_SESSION_GEN_ATTEMPTS) { 663 throw new RuntimeException("Failed to compute unique session ID"); 664 } 665 // Create new session ID 666 sessionId = StringUtils.randomString(15); 667 } while (executingCommands.containsKey(sessionId)); 668 669 return factory.create(node, name, sessionId); 670 } 671 672 public String getName() { 673 return name; 674 } 675 676 public String getNode() { 677 return node; 678 } 679 680 } 681}