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

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