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 35KB

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