You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ConnectorTracker.java 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui;
  17. import java.io.IOException;
  18. import java.io.Serializable;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.Iterator;
  24. import java.util.LinkedList;
  25. import java.util.Map;
  26. import java.util.NavigableMap;
  27. import java.util.Set;
  28. import java.util.TreeMap;
  29. import java.util.UUID;
  30. import java.util.logging.Level;
  31. import java.util.logging.Logger;
  32. import com.vaadin.server.AbstractClientConnector;
  33. import com.vaadin.server.ClientConnector;
  34. import com.vaadin.server.DragAndDropService;
  35. import com.vaadin.server.GlobalResourceHandler;
  36. import com.vaadin.server.LegacyCommunicationManager;
  37. import com.vaadin.server.StreamVariable;
  38. import com.vaadin.server.VaadinRequest;
  39. import com.vaadin.server.VaadinService;
  40. import com.vaadin.server.communication.ConnectorHierarchyWriter;
  41. import elemental.json.Json;
  42. import elemental.json.JsonException;
  43. import elemental.json.JsonObject;
  44. /**
  45. * A class which takes care of book keeping of {@link ClientConnector}s for a
  46. * UI.
  47. * <p>
  48. * Provides {@link #getConnector(String)} which can be used to lookup a
  49. * connector from its id. This is for framework use only and should not be
  50. * needed in applications.
  51. * </p>
  52. * <p>
  53. * Tracks which {@link ClientConnector}s are dirty so they can be updated to the
  54. * client when the following response is sent. A connector is dirty when an
  55. * operation has been performed on it on the server and as a result of this
  56. * operation new information needs to be sent to its
  57. * {@link com.vaadin.client.ServerConnector}.
  58. * </p>
  59. *
  60. * @author Vaadin Ltd
  61. * @since 7.0.0
  62. *
  63. */
  64. public class ConnectorTracker implements Serializable {
  65. private final HashMap<String, ClientConnector> connectorIdToConnector = new HashMap<>();
  66. private final Set<ClientConnector> dirtyConnectors = new HashSet<>();
  67. private final Set<ClientConnector> uninitializedConnectors = new HashSet<>();
  68. /**
  69. * Connectors that have been unregistered and should be cleaned up the next
  70. * time {@link #cleanConnectorMap()} is invoked unless they have been
  71. * registered again.
  72. */
  73. private final Set<ClientConnector> unregisteredConnectors = new HashSet<>();
  74. private boolean writingResponse = false;
  75. private final UI uI;
  76. private transient Map<ClientConnector, JsonObject> diffStates = new HashMap<>();
  77. /** Maps connectorIds to a map of named StreamVariables */
  78. private Map<String, Map<String, StreamVariable>> pidToNameToStreamVariable;
  79. private Map<StreamVariable, String> streamVariableToSeckey;
  80. private int currentSyncId = 0;
  81. /**
  82. * Map to track on which syncId each connector was removed.
  83. *
  84. * @see #getCurrentSyncId()
  85. * @see #cleanConcurrentlyRemovedConnectorIds(long)
  86. */
  87. private final TreeMap<Integer, Set<String>> syncIdToUnregisteredConnectorIds = new TreeMap<>();
  88. /**
  89. * Gets a logger for this class
  90. *
  91. * @return A logger instance for logging within this class
  92. *
  93. */
  94. private static Logger getLogger() {
  95. return Logger.getLogger(ConnectorTracker.class.getName());
  96. }
  97. /**
  98. * Creates a new ConnectorTracker for the given uI. A tracker is always
  99. * attached to a uI and the uI cannot be changed during the lifetime of a
  100. * {@link ConnectorTracker}.
  101. *
  102. * @param uI
  103. * The uI to attach to. Cannot be null.
  104. */
  105. public ConnectorTracker(UI uI) {
  106. this.uI = uI;
  107. }
  108. /**
  109. * Register the given connector.
  110. * <p>
  111. * The lookup method {@link #getConnector(String)} only returns registered
  112. * connectors.
  113. * </p>
  114. *
  115. * @param connector
  116. * The connector to register.
  117. */
  118. public void registerConnector(ClientConnector connector) {
  119. boolean wasUnregistered = unregisteredConnectors.remove(connector);
  120. String connectorId = connector.getConnectorId();
  121. ClientConnector previouslyRegistered = connectorIdToConnector
  122. .get(connectorId);
  123. if (previouslyRegistered == null) {
  124. connectorIdToConnector.put(connectorId, connector);
  125. uninitializedConnectors.add(connector);
  126. if (getLogger().isLoggable(Level.FINE)) {
  127. getLogger().log(Level.FINE, "Registered {0} ({1})",
  128. new Object[] { connector.getClass().getSimpleName(),
  129. connectorId });
  130. }
  131. } else if (previouslyRegistered != connector) {
  132. throw new RuntimeException("A connector with id " + connectorId
  133. + " is already registered!");
  134. } else if (!wasUnregistered) {
  135. getLogger().log(Level.WARNING,
  136. "An already registered connector was registered again: {0} ({1})",
  137. new Object[] { connector.getClass().getSimpleName(),
  138. connectorId });
  139. }
  140. dirtyConnectors.add(connector);
  141. }
  142. /**
  143. * Unregister the given connector.
  144. *
  145. * <p>
  146. * The lookup method {@link #getConnector(String)} only returns registered
  147. * connectors.
  148. * </p>
  149. *
  150. * @param connector
  151. * The connector to unregister
  152. */
  153. public void unregisterConnector(ClientConnector connector) {
  154. String connectorId = connector.getConnectorId();
  155. if (!connectorIdToConnector.containsKey(connectorId)) {
  156. getLogger().log(Level.WARNING,
  157. "Tried to unregister {0} ({1}) which is not registered",
  158. new Object[] { connector.getClass().getSimpleName(),
  159. connectorId });
  160. return;
  161. }
  162. if (connectorIdToConnector.get(connectorId) != connector) {
  163. throw new RuntimeException("The given connector with id "
  164. + connectorId
  165. + " is not the one that was registered for that id");
  166. }
  167. Set<String> unregisteredConnectorIds = syncIdToUnregisteredConnectorIds
  168. .get(currentSyncId);
  169. if (unregisteredConnectorIds == null) {
  170. unregisteredConnectorIds = new HashSet<>();
  171. syncIdToUnregisteredConnectorIds.put(currentSyncId,
  172. unregisteredConnectorIds);
  173. }
  174. unregisteredConnectorIds.add(connectorId);
  175. dirtyConnectors.remove(connector);
  176. if (unregisteredConnectors.add(connector)) {
  177. if (getLogger().isLoggable(Level.FINE)) {
  178. getLogger().log(Level.FINE, "Unregistered {0} ({1})",
  179. new Object[] { connector.getClass().getSimpleName(),
  180. connectorId });
  181. }
  182. } else {
  183. getLogger().log(Level.WARNING,
  184. "Unregistered {0} ({1}) that was already unregistered.",
  185. new Object[] { connector.getClass().getSimpleName(),
  186. connectorId });
  187. }
  188. }
  189. /**
  190. * Checks whether the given connector has already been initialized in the
  191. * browser. The given connector should be registered with this connector
  192. * tracker.
  193. *
  194. * @param connector
  195. * the client connector to check
  196. * @return <code>true</code> if the initial state has previously been sent
  197. * to the browser, <code>false</code> if the client-side doesn't
  198. * already know anything about the connector.
  199. */
  200. public boolean isClientSideInitialized(ClientConnector connector) {
  201. assert connectorIdToConnector.get(connector
  202. .getConnectorId()) == connector : "Connector should be registered with this ConnectorTracker";
  203. return !uninitializedConnectors.contains(connector);
  204. }
  205. /**
  206. * Marks the given connector as initialized, meaning that the client-side
  207. * state has been initialized for the connector.
  208. *
  209. * @see #isClientSideInitialized(ClientConnector)
  210. *
  211. * @param connector
  212. * the connector that should be marked as initialized
  213. */
  214. public void markClientSideInitialized(ClientConnector connector) {
  215. uninitializedConnectors.remove(connector);
  216. }
  217. /**
  218. * Marks all currently registered connectors as uninitialized. This should
  219. * be done when the client-side has been reset but the server-side state is
  220. * retained.
  221. *
  222. * @see #isClientSideInitialized(ClientConnector)
  223. */
  224. public void markAllClientSidesUninitialized() {
  225. uninitializedConnectors.addAll(connectorIdToConnector.values());
  226. diffStates.clear();
  227. }
  228. /**
  229. * Gets a connector by its id.
  230. *
  231. * @param connectorId
  232. * The connector id to look for
  233. * @return The connector with the given id or null if no connector has the
  234. * given id
  235. */
  236. public ClientConnector getConnector(String connectorId) {
  237. ClientConnector connector = connectorIdToConnector.get(connectorId);
  238. // Ignore connectors that have been unregistered but not yet cleaned up
  239. if (unregisteredConnectors.contains(connector)) {
  240. return null;
  241. } else if (connector != null) {
  242. return connector;
  243. } else {
  244. DragAndDropService service = uI.getSession()
  245. .getDragAndDropService();
  246. if (connectorId.equals(service.getConnectorId())) {
  247. return service;
  248. }
  249. }
  250. return null;
  251. }
  252. /**
  253. * Cleans the connector map from all connectors that are no longer attached
  254. * to the application. This should only be called by the framework.
  255. */
  256. public void cleanConnectorMap() {
  257. if (!unregisteredConnectors.isEmpty()) {
  258. removeUnregisteredConnectors();
  259. }
  260. // Do this expensive check only with assertions enabled
  261. assert isHierarchyComplete() : "The connector hierarchy is corrupted. "
  262. + "Check for missing calls to super.setParent(), super.attach() and super.detach() "
  263. + "and that all custom component containers call child.setParent(this) when a child is added and child.setParent(null) when the child is no longer used. "
  264. + "See previous log messages for details.";
  265. // remove detached components from paintableIdMap so they
  266. // can be GC'ed
  267. Iterator<ClientConnector> iterator = connectorIdToConnector.values()
  268. .iterator();
  269. GlobalResourceHandler globalResourceHandler = uI.getSession()
  270. .getGlobalResourceHandler(false);
  271. while (iterator.hasNext()) {
  272. ClientConnector connector = iterator.next();
  273. assert connector != null;
  274. if (connector.getUI() != uI) {
  275. // If connector is no longer part of this uI,
  276. // remove it from the map. If it is re-attached to the
  277. // application at some point it will be re-added through
  278. // registerConnector(connector)
  279. // This code should never be called as cleanup should take place
  280. // in detach()
  281. getLogger().log(Level.WARNING,
  282. "cleanConnectorMap unregistered connector {0}. This should have been done when the connector was detached.",
  283. getConnectorAndParentInfo(connector));
  284. if (globalResourceHandler != null) {
  285. globalResourceHandler.unregisterConnector(connector);
  286. }
  287. uninitializedConnectors.remove(connector);
  288. diffStates.remove(connector);
  289. iterator.remove();
  290. } else if (!uninitializedConnectors.contains(connector)
  291. && !LegacyCommunicationManager
  292. .isConnectorVisibleToClient(connector)) {
  293. uninitializedConnectors.add(connector);
  294. diffStates.remove(connector);
  295. assert isRemovalSentToClient(connector) : "Connector "
  296. + connector + " (id = " + connector.getConnectorId()
  297. + ") is no longer visible to the client, but no corresponding hierarchy change is being sent.";
  298. if (getLogger().isLoggable(Level.FINE)) {
  299. getLogger().log(Level.FINE,
  300. "cleanConnectorMap removed state for {0} as it is not visible",
  301. getConnectorAndParentInfo(connector));
  302. }
  303. }
  304. }
  305. cleanStreamVariables();
  306. }
  307. private boolean isRemovalSentToClient(ClientConnector connector) {
  308. VaadinRequest request = VaadinService.getCurrentRequest();
  309. if (request == null) {
  310. // Probably run from a unit test without normal request handling
  311. return true;
  312. }
  313. String attributeName = ConnectorHierarchyWriter.class.getName()
  314. + ".hierarchyInfo";
  315. Object hierarchyInfoObj = request.getAttribute(attributeName);
  316. if (hierarchyInfoObj instanceof JsonObject) {
  317. JsonObject hierachyInfo = (JsonObject) hierarchyInfoObj;
  318. ClientConnector firstVisibleParent = findFirstVisibleParent(
  319. connector);
  320. if (firstVisibleParent == null) {
  321. // Connector is detached, not our business
  322. return true;
  323. }
  324. if (!hierachyInfo.hasKey(firstVisibleParent.getConnectorId())) {
  325. /*
  326. * No hierarchy change about to be sent, but this might be
  327. * because of an optimization that omits explicit hierarchy
  328. * changes for empty connectors that have state changes.
  329. */
  330. if (hasVisibleChild(firstVisibleParent)) {
  331. // Not the optimization case if the parent has visible
  332. // children
  333. return false;
  334. }
  335. attributeName = ConnectorHierarchyWriter.class.getName()
  336. + ".stateUpdateConnectors";
  337. Object stateUpdateConnectorsObj = request
  338. .getAttribute(attributeName);
  339. if (stateUpdateConnectorsObj instanceof Set<?>) {
  340. Set<?> stateUpdateConnectors = (Set<?>) stateUpdateConnectorsObj;
  341. if (!stateUpdateConnectors
  342. .contains(firstVisibleParent.getConnectorId())) {
  343. // Not the optimization case if the parent is not marked
  344. // as dirty
  345. return false;
  346. }
  347. } else {
  348. getLogger().warning("Request attribute " + attributeName
  349. + " is not a Set");
  350. }
  351. }
  352. } else {
  353. getLogger().warning("Request attribute " + attributeName
  354. + " is not a JsonObject");
  355. }
  356. return true;
  357. }
  358. private static boolean hasVisibleChild(ClientConnector parent) {
  359. Iterator<? extends ClientConnector> iterator = AbstractClientConnector
  360. .getAllChildrenIterable(parent).iterator();
  361. while (iterator.hasNext()) {
  362. ClientConnector child = iterator.next();
  363. if (LegacyCommunicationManager.isConnectorVisibleToClient(child)) {
  364. return true;
  365. }
  366. }
  367. return false;
  368. }
  369. private ClientConnector findFirstVisibleParent(ClientConnector connector) {
  370. while (connector != null) {
  371. connector = connector.getParent();
  372. if (LegacyCommunicationManager
  373. .isConnectorVisibleToClient(connector)) {
  374. return connector;
  375. }
  376. }
  377. return null;
  378. }
  379. private void removeUnregisteredConnectors() {
  380. GlobalResourceHandler globalResourceHandler = uI.getSession()
  381. .getGlobalResourceHandler(false);
  382. for (ClientConnector connector : unregisteredConnectors) {
  383. ClientConnector removedConnector = connectorIdToConnector
  384. .remove(connector.getConnectorId());
  385. assert removedConnector == connector;
  386. if (globalResourceHandler != null) {
  387. globalResourceHandler.unregisterConnector(connector);
  388. }
  389. uninitializedConnectors.remove(connector);
  390. diffStates.remove(connector);
  391. }
  392. unregisteredConnectors.clear();
  393. }
  394. private boolean isHierarchyComplete() {
  395. boolean noErrors = true;
  396. Set<ClientConnector> danglingConnectors = new HashSet<>(
  397. connectorIdToConnector.values());
  398. LinkedList<ClientConnector> stack = new LinkedList<>();
  399. stack.add(uI);
  400. while (!stack.isEmpty()) {
  401. ClientConnector connector = stack.pop();
  402. danglingConnectors.remove(connector);
  403. Iterable<? extends ClientConnector> children = AbstractClientConnector
  404. .getAllChildrenIterable(connector);
  405. for (ClientConnector child : children) {
  406. stack.add(child);
  407. if (!connector.equals(child.getParent())) {
  408. noErrors = false;
  409. getLogger().log(Level.WARNING,
  410. "{0} claims that {1} is its child, but the child claims {2} is its parent.",
  411. new Object[] { getConnectorString(connector),
  412. getConnectorString(child),
  413. getConnectorString(child.getParent()) });
  414. }
  415. }
  416. }
  417. for (ClientConnector dangling : danglingConnectors) {
  418. noErrors = false;
  419. getLogger().log(Level.WARNING,
  420. "{0} claims that {1} is its parent, but the parent does not acknowledge the parenthood.",
  421. new Object[] { getConnectorString(dangling),
  422. getConnectorString(dangling.getParent()) });
  423. }
  424. return noErrors;
  425. }
  426. /**
  427. * Mark the connector as dirty. This should not be done while the response
  428. * is being written.
  429. *
  430. * @see #getDirtyConnectors()
  431. * @see #isWritingResponse()
  432. *
  433. * @param connector
  434. * The connector that should be marked clean.
  435. */
  436. public void markDirty(ClientConnector connector) {
  437. if (isWritingResponse()) {
  438. throw new IllegalStateException(
  439. "A connector should not be marked as dirty while a response is being written.");
  440. }
  441. if (getLogger().isLoggable(Level.FINE)) {
  442. if (!dirtyConnectors.contains(connector)) {
  443. getLogger().log(Level.FINE, "{0} is now dirty",
  444. getConnectorAndParentInfo(connector));
  445. }
  446. }
  447. dirtyConnectors.add(connector);
  448. }
  449. /**
  450. * Mark the connector as clean.
  451. *
  452. * @param connector
  453. * The connector that should be marked clean.
  454. */
  455. public void markClean(ClientConnector connector) {
  456. if (getLogger().isLoggable(Level.FINE)) {
  457. if (dirtyConnectors.contains(connector)) {
  458. getLogger().log(Level.FINE, "{0} is no longer dirty",
  459. getConnectorAndParentInfo(connector));
  460. }
  461. }
  462. dirtyConnectors.remove(connector);
  463. }
  464. /**
  465. * Returns {@link #getConnectorString(ClientConnector)} for the connector
  466. * and its parent (if it has a parent).
  467. *
  468. * @param connector
  469. * The connector
  470. * @return A string describing the connector and its parent
  471. */
  472. private String getConnectorAndParentInfo(ClientConnector connector) {
  473. String message = getConnectorString(connector);
  474. if (connector.getParent() != null) {
  475. message += " (parent: " + getConnectorString(connector.getParent())
  476. + ")";
  477. }
  478. return message;
  479. }
  480. /**
  481. * Returns a string with the connector name and id. Useful mostly for
  482. * debugging and logging.
  483. *
  484. * @param connector
  485. * The connector
  486. * @return A string that describes the connector
  487. */
  488. private String getConnectorString(ClientConnector connector) {
  489. if (connector == null) {
  490. return "(null)";
  491. }
  492. String connectorId;
  493. try {
  494. connectorId = connector.getConnectorId();
  495. } catch (RuntimeException e) {
  496. // This happens if the connector is not attached to the application.
  497. // SHOULD not happen in this case but theoretically can.
  498. connectorId = "@" + Integer.toHexString(connector.hashCode());
  499. }
  500. return connector.getClass().getName() + "(" + connectorId + ")";
  501. }
  502. /**
  503. * Mark all connectors in this uI as dirty.
  504. */
  505. public void markAllConnectorsDirty() {
  506. markConnectorsDirtyRecursively(uI);
  507. getLogger().fine("All connectors are now dirty");
  508. }
  509. /**
  510. * Mark all connectors in this uI as clean.
  511. */
  512. public void markAllConnectorsClean() {
  513. dirtyConnectors.clear();
  514. getLogger().fine("All connectors are now clean");
  515. }
  516. /**
  517. * Marks all visible connectors dirty, starting from the given connector and
  518. * going downwards in the hierarchy.
  519. *
  520. * @param c
  521. * The component to start iterating downwards from
  522. */
  523. private void markConnectorsDirtyRecursively(ClientConnector c) {
  524. if (c instanceof Component && !((Component) c).isVisible()) {
  525. return;
  526. }
  527. markDirty(c);
  528. for (ClientConnector child : AbstractClientConnector
  529. .getAllChildrenIterable(c)) {
  530. markConnectorsDirtyRecursively(child);
  531. }
  532. }
  533. /**
  534. * Returns a collection of all connectors which have been marked as dirty.
  535. * <p>
  536. * The state and pending RPC calls for dirty connectors are sent to the
  537. * client in the following request.
  538. * </p>
  539. *
  540. * @return A collection of all dirty connectors for this uI. This list may
  541. * contain invisible connectors.
  542. */
  543. public Collection<ClientConnector> getDirtyConnectors() {
  544. return dirtyConnectors;
  545. }
  546. /**
  547. * Checks if there a dirty connectors.
  548. *
  549. * @return true if there are dirty connectors, false otherwise
  550. */
  551. public boolean hasDirtyConnectors() {
  552. return !getDirtyConnectors().isEmpty();
  553. }
  554. /**
  555. * Returns a collection of those {@link #getDirtyConnectors() dirty
  556. * connectors} that are actually visible to the client.
  557. *
  558. * @return A list of dirty and visible connectors.
  559. */
  560. public ArrayList<ClientConnector> getDirtyVisibleConnectors() {
  561. Collection<ClientConnector> dirtyConnectors = getDirtyConnectors();
  562. ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<>(
  563. dirtyConnectors.size());
  564. for (ClientConnector c : dirtyConnectors) {
  565. if (LegacyCommunicationManager.isConnectorVisibleToClient(c)) {
  566. dirtyVisibleConnectors.add(c);
  567. }
  568. }
  569. return dirtyVisibleConnectors;
  570. }
  571. public JsonObject getDiffState(ClientConnector connector) {
  572. assert getConnector(connector.getConnectorId()) == connector;
  573. return diffStates.get(connector);
  574. }
  575. public void setDiffState(ClientConnector connector, JsonObject diffState) {
  576. assert getConnector(connector.getConnectorId()) == connector;
  577. diffStates.put(connector, diffState);
  578. }
  579. public boolean isDirty(ClientConnector connector) {
  580. return dirtyConnectors.contains(connector);
  581. }
  582. /**
  583. * Checks whether the response is currently being written. Connectors can
  584. * not be marked as dirty when a response is being written.
  585. *
  586. * @see #setWritingResponse(boolean)
  587. * @see #markDirty(ClientConnector)
  588. *
  589. * @return <code>true</code> if the response is currently being written,
  590. * <code>false</code> if outside the response writing phase.
  591. */
  592. public boolean isWritingResponse() {
  593. return writingResponse;
  594. }
  595. /**
  596. * Sets the current response write status. Connectors can not be marked as
  597. * dirty when the response is written.
  598. * <p>
  599. * This method has a side-effect of incrementing the sync id by one (see
  600. * {@link #getCurrentSyncId()}), if {@link #isWritingResponse()} returns
  601. * <code>true</code> and <code>writingResponse</code> is set to
  602. * <code>false</code>.
  603. *
  604. * @param writingResponse
  605. * the new response status.
  606. *
  607. * @see #markDirty(ClientConnector)
  608. * @see #isWritingResponse()
  609. * @see #getCurrentSyncId()
  610. *
  611. * @throws IllegalArgumentException
  612. * if the new response status is the same as the previous value.
  613. * This is done to help detecting problems caused by missed
  614. * invocations of this method.
  615. */
  616. public void setWritingResponse(boolean writingResponse) {
  617. if (this.writingResponse == writingResponse) {
  618. throw new IllegalArgumentException(
  619. "The old value is same as the new value");
  620. }
  621. /*
  622. * the right hand side of the && is unnecessary here because of the
  623. * if-clause above, but rigorous coding is always rigorous coding.
  624. */
  625. if (!writingResponse && this.writingResponse) {
  626. // Bump sync id when done writing - the client is not expected to
  627. // know about anything happening after this moment.
  628. currentSyncId++;
  629. }
  630. this.writingResponse = writingResponse;
  631. }
  632. /* Special serialization to JsonObjects which are not serializable */
  633. private void writeObject(java.io.ObjectOutputStream out)
  634. throws IOException {
  635. out.defaultWriteObject();
  636. // Convert JsonObjects in diff state to String representation as
  637. // JsonObject is not serializable
  638. HashMap<ClientConnector, String> stringDiffStates = new HashMap<>(
  639. diffStates.size() * 2);
  640. for (ClientConnector key : diffStates.keySet()) {
  641. stringDiffStates.put(key, diffStates.get(key).toString());
  642. }
  643. out.writeObject(stringDiffStates);
  644. }
  645. /* Special serialization to JsonObjects which are not serializable */
  646. private void readObject(java.io.ObjectInputStream in)
  647. throws IOException, ClassNotFoundException {
  648. in.defaultReadObject();
  649. // Read String versions of JsonObjects and parse into JsonObjects as
  650. // JsonObject is not serializable
  651. diffStates = new HashMap<>();
  652. @SuppressWarnings("unchecked")
  653. HashMap<ClientConnector, String> stringDiffStates = (HashMap<ClientConnector, String>) in
  654. .readObject();
  655. diffStates = new HashMap<>(stringDiffStates.size() * 2);
  656. for (ClientConnector key : stringDiffStates.keySet()) {
  657. try {
  658. diffStates.put(key, Json.parse(stringDiffStates.get(key)));
  659. } catch (JsonException e) {
  660. throw new IOException(e);
  661. }
  662. }
  663. }
  664. /**
  665. * Checks if the indicated connector has a StreamVariable of the given name
  666. * and returns the variable if one is found.
  667. *
  668. * @param connectorId
  669. * @param variableName
  670. * @return variable if a matching one exists, otherwise null
  671. */
  672. public StreamVariable getStreamVariable(String connectorId,
  673. String variableName) {
  674. if (pidToNameToStreamVariable == null) {
  675. return null;
  676. }
  677. Map<String, StreamVariable> map = pidToNameToStreamVariable
  678. .get(connectorId);
  679. if (map == null) {
  680. return null;
  681. }
  682. StreamVariable streamVariable = map.get(variableName);
  683. return streamVariable;
  684. }
  685. /**
  686. * Adds a StreamVariable of the given name to the indicated connector.
  687. *
  688. * @param connectorId
  689. * @param variableName
  690. * @param variable
  691. */
  692. public void addStreamVariable(String connectorId, String variableName,
  693. StreamVariable variable) {
  694. assert getConnector(connectorId) != null;
  695. if (pidToNameToStreamVariable == null) {
  696. pidToNameToStreamVariable = new HashMap<>();
  697. }
  698. Map<String, StreamVariable> nameToStreamVariable = pidToNameToStreamVariable
  699. .get(connectorId);
  700. if (nameToStreamVariable == null) {
  701. nameToStreamVariable = new HashMap<>();
  702. pidToNameToStreamVariable.put(connectorId, nameToStreamVariable);
  703. }
  704. nameToStreamVariable.put(variableName, variable);
  705. if (streamVariableToSeckey == null) {
  706. streamVariableToSeckey = new HashMap<>();
  707. }
  708. String seckey = streamVariableToSeckey.get(variable);
  709. if (seckey == null) {
  710. /*
  711. * Despite section 6 of RFC 4122, this particular use of UUID *is*
  712. * adequate for security capabilities. Type 4 UUIDs contain 122 bits
  713. * of random data, and UUID.randomUUID() is defined to use a
  714. * cryptographically secure random generator.
  715. */
  716. seckey = UUID.randomUUID().toString();
  717. streamVariableToSeckey.put(variable, seckey);
  718. }
  719. }
  720. /**
  721. * Removes StreamVariables that belong to connectors that are no longer
  722. * attached to the session.
  723. */
  724. private void cleanStreamVariables() {
  725. if (pidToNameToStreamVariable != null) {
  726. ConnectorTracker connectorTracker = uI.getConnectorTracker();
  727. Iterator<String> iterator = pidToNameToStreamVariable.keySet()
  728. .iterator();
  729. while (iterator.hasNext()) {
  730. String connectorId = iterator.next();
  731. if (connectorTracker.getConnector(connectorId) == null) {
  732. // Owner is no longer attached to the session
  733. Map<String, StreamVariable> removed = pidToNameToStreamVariable
  734. .get(connectorId);
  735. for (String key : removed.keySet()) {
  736. streamVariableToSeckey.remove(removed.get(key));
  737. }
  738. iterator.remove();
  739. }
  740. }
  741. }
  742. }
  743. /**
  744. * Removes any StreamVariable of the given name from the indicated
  745. * connector.
  746. *
  747. * @param connectorId
  748. * @param variableName
  749. */
  750. public void cleanStreamVariable(String connectorId, String variableName) {
  751. if (pidToNameToStreamVariable == null) {
  752. return;
  753. }
  754. Map<String, StreamVariable> nameToStreamVar = pidToNameToStreamVariable
  755. .get(connectorId);
  756. nameToStreamVar.remove(variableName);
  757. if (nameToStreamVar.isEmpty()) {
  758. pidToNameToStreamVariable.remove(connectorId);
  759. }
  760. }
  761. /**
  762. * Returns the security key associated with the given StreamVariable.
  763. *
  764. * @param variable
  765. * @return matching security key if one exists, null otherwise
  766. */
  767. public String getSeckey(StreamVariable variable) {
  768. if (streamVariableToSeckey == null) {
  769. return null;
  770. }
  771. return streamVariableToSeckey.get(variable);
  772. }
  773. /**
  774. * Check whether a connector was present on the client when the it was
  775. * creating this request, but was removed server-side before the request
  776. * arrived.
  777. *
  778. * @since 7.2
  779. * @param connectorId
  780. * The connector id to check for whether it was removed
  781. * concurrently or not.
  782. * @param lastSyncIdSeenByClient
  783. * the most recent sync id the client has seen at the time the
  784. * request was sent, or -1 to ignore potential problems
  785. * @return <code>true</code> if the connector was removed before the client
  786. * had a chance to react to it.
  787. */
  788. public boolean connectorWasPresentAsRequestWasSent(String connectorId,
  789. long lastSyncIdSeenByClient) {
  790. assert getConnector(connectorId) == null : "Connector " + connectorId
  791. + " is still attached";
  792. if (lastSyncIdSeenByClient == -1) {
  793. // Ignore potential problems
  794. return true;
  795. }
  796. /*
  797. * Use non-inclusive tail map to find all connectors that were removed
  798. * after the reported sync id was sent to the client.
  799. */
  800. NavigableMap<Integer, Set<String>> unregisteredAfter = syncIdToUnregisteredConnectorIds
  801. .tailMap(Integer.valueOf((int) lastSyncIdSeenByClient), false);
  802. for (Set<String> unregisteredIds : unregisteredAfter.values()) {
  803. if (unregisteredIds.contains(connectorId)) {
  804. // Removed with a higher sync id, so it was most likely present
  805. // when this sync id was sent.
  806. return true;
  807. }
  808. }
  809. return false;
  810. }
  811. /**
  812. * Gets the most recently generated server sync id.
  813. * <p>
  814. * The sync id is incremented by one whenever a new response is being
  815. * written. This id is then sent over to the client. The client then adds
  816. * the most recent sync id to each communication packet it sends back to the
  817. * server. This way, the server knows at what state the client is when the
  818. * packet is sent. If the state has changed on the server side since that,
  819. * the server can try to adjust the way it handles the actions from the
  820. * client side.
  821. * <p>
  822. * The sync id value <code>-1</code> is ignored to facilitate testing with
  823. * pre-recorded requests.
  824. *
  825. * @see #setWritingResponse(boolean)
  826. * @see #connectorWasPresentAsRequestWasSent(String, long)
  827. * @since 7.2
  828. * @return the current sync id
  829. */
  830. public int getCurrentSyncId() {
  831. return currentSyncId;
  832. }
  833. /**
  834. * Maintains the bookkeeping connector removal and concurrency by removing
  835. * entries that have become too old.
  836. * <p>
  837. * <em>It is important to run this call for each transmission from the
  838. * client</em> , otherwise the bookkeeping gets out of date and the results
  839. * form {@link #connectorWasPresentAsRequestWasSent(String, long)} will
  840. * become invalid (that is, even though the client knew the component was
  841. * removed, the aforementioned method would start claiming otherwise).
  842. * <p>
  843. * Entries that both client and server agree upon are removed. Since
  844. * argument is the last sync id that the client has seen from the server, we
  845. * know that entries earlier than that cannot cause any problems anymore.
  846. * <p>
  847. * The sync id value <code>-1</code> is ignored to facilitate testing with
  848. * pre-recorded requests.
  849. *
  850. * @see #connectorWasPresentAsRequestWasSent(String, long)
  851. * @since 7.2
  852. * @param lastSyncIdSeenByClient
  853. * the sync id the client has most recently received from the
  854. * server.
  855. */
  856. public void cleanConcurrentlyRemovedConnectorIds(
  857. int lastSyncIdSeenByClient) {
  858. if (lastSyncIdSeenByClient == -1) {
  859. // Sync id checking is not in use, so we should just clear the
  860. // entire map to avoid leaking memory
  861. syncIdToUnregisteredConnectorIds.clear();
  862. return;
  863. }
  864. /*
  865. * We remove all entries _older_ than the one reported right now,
  866. * because the remaining still contain components that might cause
  867. * conflicts. In any case, it's better to clean up too little than too
  868. * much, especially as the data will hardly grow into the kilobytes.
  869. */
  870. syncIdToUnregisteredConnectorIds.headMap(lastSyncIdSeenByClient, true)
  871. .clear();
  872. }
  873. }