All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.ui.ConnectorTracker Maven / Gradle / Ivy

There is a newer version: 8.27.1
Show newest version
/*
 * Copyright 2000-2014 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.vaadin.ui;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.vaadin.server.AbstractClientConnector;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.DragAndDropService;
import com.vaadin.server.GlobalResourceHandler;
import com.vaadin.server.LegacyCommunicationManager;
import com.vaadin.server.StreamVariable;

import elemental.json.Json;
import elemental.json.JsonException;
import elemental.json.JsonObject;

/**
 * A class which takes care of book keeping of {@link ClientConnector}s for a
 * UI.
 * 

* Provides {@link #getConnector(String)} which can be used to lookup a * connector from its id. This is for framework use only and should not be * needed in applications. *

*

* Tracks which {@link ClientConnector}s are dirty so they can be updated to the * client when the following response is sent. A connector is dirty when an * operation has been performed on it on the server and as a result of this * operation new information needs to be sent to its * {@link com.vaadin.client.ServerConnector}. *

* * @author Vaadin Ltd * @since 7.0.0 * */ public class ConnectorTracker implements Serializable { private final HashMap connectorIdToConnector = new HashMap(); private Set dirtyConnectors = new HashSet(); private Set uninitializedConnectors = new HashSet(); /** * Connectors that have been unregistered and should be cleaned up the next * time {@link #cleanConnectorMap()} is invoked unless they have been * registered again. */ private final Set unregisteredConnectors = new HashSet(); private boolean writingResponse = false; private UI uI; private transient Map diffStates = new HashMap(); /** Maps connectorIds to a map of named StreamVariables */ private Map> pidToNameToStreamVariable; private Map streamVariableToSeckey; private int currentSyncId = 0; /** * Map to track on which syncId each connector was removed. * * @see #getCurrentSyncId() * @see #cleanConcurrentlyRemovedConnectorIds(long) */ private TreeMap> syncIdToUnregisteredConnectorIds = new TreeMap>(); /** * Gets a logger for this class * * @return A logger instance for logging within this class * */ public static Logger getLogger() { return Logger.getLogger(ConnectorTracker.class.getName()); } /** * Creates a new ConnectorTracker for the given uI. A tracker is always * attached to a uI and the uI cannot be changed during the lifetime of a * {@link ConnectorTracker}. * * @param uI * The uI to attach to. Cannot be null. */ public ConnectorTracker(UI uI) { this.uI = uI; } /** * Register the given connector. *

* The lookup method {@link #getConnector(String)} only returns registered * connectors. *

* * @param connector * The connector to register. */ public void registerConnector(ClientConnector connector) { boolean wasUnregistered = unregisteredConnectors.remove(connector); String connectorId = connector.getConnectorId(); ClientConnector previouslyRegistered = connectorIdToConnector .get(connectorId); if (previouslyRegistered == null) { connectorIdToConnector.put(connectorId, connector); uninitializedConnectors.add(connector); if (getLogger().isLoggable(Level.FINE)) { getLogger().log( Level.FINE, "Registered {0} ({1})", new Object[] { connector.getClass().getSimpleName(), connectorId }); } } else if (previouslyRegistered != connector) { throw new RuntimeException("A connector with id " + connectorId + " is already registered!"); } else if (!wasUnregistered) { getLogger() .log(Level.WARNING, "An already registered connector was registered again: {0} ({1})", new Object[] { connector.getClass().getSimpleName(), connectorId }); } dirtyConnectors.add(connector); } /** * Unregister the given connector. * *

* The lookup method {@link #getConnector(String)} only returns registered * connectors. *

* * @param connector * The connector to unregister */ public void unregisterConnector(ClientConnector connector) { String connectorId = connector.getConnectorId(); if (!connectorIdToConnector.containsKey(connectorId)) { getLogger().log( Level.WARNING, "Tried to unregister {0} ({1}) which is not registered", new Object[] { connector.getClass().getSimpleName(), connectorId }); return; } if (connectorIdToConnector.get(connectorId) != connector) { throw new RuntimeException("The given connector with id " + connectorId + " is not the one that was registered for that id"); } Set unregisteredConnectorIds = syncIdToUnregisteredConnectorIds .get(currentSyncId); if (unregisteredConnectorIds == null) { unregisteredConnectorIds = new HashSet(); syncIdToUnregisteredConnectorIds.put(currentSyncId, unregisteredConnectorIds); } unregisteredConnectorIds.add(connectorId); dirtyConnectors.remove(connector); if (unregisteredConnectors.add(connector)) { if (getLogger().isLoggable(Level.FINE)) { getLogger().log( Level.FINE, "Unregistered {0} ({1})", new Object[] { connector.getClass().getSimpleName(), connectorId }); } } else { getLogger().log( Level.WARNING, "Unregistered {0} ({1}) that was already unregistered.", new Object[] { connector.getClass().getSimpleName(), connectorId }); } } /** * Checks whether the given connector has already been initialized in the * browser. The given connector should be registered with this connector * tracker. * * @param connector * the client connector to check * @return true if the initial state has previously been sent * to the browser, false if the client-side doesn't * already know anything about the connector. */ public boolean isClientSideInitialized(ClientConnector connector) { assert connectorIdToConnector.get(connector.getConnectorId()) == connector : "Connector should be registered with this ConnectorTracker"; return !uninitializedConnectors.contains(connector); } /** * Marks the given connector as initialized, meaning that the client-side * state has been initialized for the connector. * * @see #isClientSideInitialized(ClientConnector) * * @param connector * the connector that should be marked as initialized */ public void markClientSideInitialized(ClientConnector connector) { uninitializedConnectors.remove(connector); } /** * Marks all currently registered connectors as uninitialized. This should * be done when the client-side has been reset but the server-side state is * retained. * * @see #isClientSideInitialized(ClientConnector) */ public void markAllClientSidesUninitialized() { uninitializedConnectors.addAll(connectorIdToConnector.values()); diffStates.clear(); } /** * Gets a connector by its id. * * @param connectorId * The connector id to look for * @return The connector with the given id or null if no connector has the * given id */ public ClientConnector getConnector(String connectorId) { ClientConnector connector = connectorIdToConnector.get(connectorId); // Ignore connectors that have been unregistered but not yet cleaned up if (unregisteredConnectors.contains(connector)) { return null; } else if (connector != null) { return connector; } else { DragAndDropService service = uI.getSession() .getDragAndDropService(); if (connectorId.equals(service.getConnectorId())) { return service; } } return null; } /** * Cleans the connector map from all connectors that are no longer attached * to the application. This should only be called by the framework. */ public void cleanConnectorMap() { if (!unregisteredConnectors.isEmpty()) { removeUnregisteredConnectors(); } // Do this expensive check only with assertions enabled assert isHierarchyComplete() : "The connector hierarchy is corrupted. " + "Check for missing calls to super.setParent(), super.attach() and super.detach() " + "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. " + "See previous log messages for details."; // remove detached components from paintableIdMap so they // can be GC'ed Iterator iterator = connectorIdToConnector.values() .iterator(); GlobalResourceHandler globalResourceHandler = uI.getSession() .getGlobalResourceHandler(false); while (iterator.hasNext()) { ClientConnector connector = iterator.next(); assert connector != null; if (connector.getUI() != uI) { // If connector is no longer part of this uI, // remove it from the map. If it is re-attached to the // application at some point it will be re-added through // registerConnector(connector) // This code should never be called as cleanup should take place // in detach() getLogger() .log(Level.WARNING, "cleanConnectorMap unregistered connector {0}. This should have been done when the connector was detached.", getConnectorAndParentInfo(connector)); if (globalResourceHandler != null) { globalResourceHandler.unregisterConnector(connector); } uninitializedConnectors.remove(connector); diffStates.remove(connector); iterator.remove(); } else if (!uninitializedConnectors.contains(connector) && !LegacyCommunicationManager .isConnectorVisibleToClient(connector)) { uninitializedConnectors.add(connector); diffStates.remove(connector); if (getLogger().isLoggable(Level.FINE)) { getLogger() .log(Level.FINE, "cleanConnectorMap removed state for {0} as it is not visible", getConnectorAndParentInfo(connector)); } } } cleanStreamVariables(); } private void removeUnregisteredConnectors() { GlobalResourceHandler globalResourceHandler = uI.getSession() .getGlobalResourceHandler(false); for (ClientConnector connector : unregisteredConnectors) { ClientConnector removedConnector = connectorIdToConnector .remove(connector.getConnectorId()); assert removedConnector == connector; if (globalResourceHandler != null) { globalResourceHandler.unregisterConnector(connector); } uninitializedConnectors.remove(connector); diffStates.remove(connector); } unregisteredConnectors.clear(); } private boolean isHierarchyComplete() { boolean noErrors = true; Set danglingConnectors = new HashSet( connectorIdToConnector.values()); LinkedList stack = new LinkedList(); stack.add(uI); while (!stack.isEmpty()) { ClientConnector connector = stack.pop(); danglingConnectors.remove(connector); Iterable children = AbstractClientConnector .getAllChildrenIterable(connector); for (ClientConnector child : children) { stack.add(child); if (!connector.equals(child.getParent())) { noErrors = false; getLogger() .log(Level.WARNING, "{0} claims that {1} is its child, but the child claims {2} is its parent.", new Object[] { getConnectorString(connector), getConnectorString(child), getConnectorString(child .getParent()) }); } } } for (ClientConnector dangling : danglingConnectors) { noErrors = false; getLogger() .log(Level.WARNING, "{0} claims that {1} is its parent, but the parent does not acknowledge the parenthood.", new Object[] { getConnectorString(dangling), getConnectorString(dangling.getParent()) }); } return noErrors; } /** * Mark the connector as dirty. This should not be done while the response * is being written. * * @see #getDirtyConnectors() * @see #isWritingResponse() * * @param connector * The connector that should be marked clean. */ public void markDirty(ClientConnector connector) { if (isWritingResponse()) { throw new IllegalStateException( "A connector should not be marked as dirty while a response is being written."); } if (getLogger().isLoggable(Level.FINE)) { if (!dirtyConnectors.contains(connector)) { getLogger().log(Level.FINE, "{0} is now dirty", getConnectorAndParentInfo(connector)); } } dirtyConnectors.add(connector); } /** * Mark the connector as clean. * * @param connector * The connector that should be marked clean. */ public void markClean(ClientConnector connector) { if (getLogger().isLoggable(Level.FINE)) { if (dirtyConnectors.contains(connector)) { getLogger().log(Level.FINE, "{0} is no longer dirty", getConnectorAndParentInfo(connector)); } } dirtyConnectors.remove(connector); } /** * Returns {@link #getConnectorString(ClientConnector)} for the connector * and its parent (if it has a parent). * * @param connector * The connector * @return A string describing the connector and its parent */ private String getConnectorAndParentInfo(ClientConnector connector) { String message = getConnectorString(connector); if (connector.getParent() != null) { message += " (parent: " + getConnectorString(connector.getParent()) + ")"; } return message; } /** * Returns a string with the connector name and id. Useful mostly for * debugging and logging. * * @param connector * The connector * @return A string that describes the connector */ private String getConnectorString(ClientConnector connector) { if (connector == null) { return "(null)"; } String connectorId; try { connectorId = connector.getConnectorId(); } catch (RuntimeException e) { // This happens if the connector is not attached to the application. // SHOULD not happen in this case but theoretically can. connectorId = "@" + Integer.toHexString(connector.hashCode()); } return connector.getClass().getName() + "(" + connectorId + ")"; } /** * Mark all connectors in this uI as dirty. */ public void markAllConnectorsDirty() { markConnectorsDirtyRecursively(uI); getLogger().fine("All connectors are now dirty"); } /** * Mark all connectors in this uI as clean. */ public void markAllConnectorsClean() { dirtyConnectors.clear(); getLogger().fine("All connectors are now clean"); } /** * Marks all visible connectors dirty, starting from the given connector and * going downwards in the hierarchy. * * @param c * The component to start iterating downwards from */ private void markConnectorsDirtyRecursively(ClientConnector c) { if (c instanceof Component && !((Component) c).isVisible()) { return; } markDirty(c); for (ClientConnector child : AbstractClientConnector .getAllChildrenIterable(c)) { markConnectorsDirtyRecursively(child); } } /** * Returns a collection of all connectors which have been marked as dirty. *

* The state and pending RPC calls for dirty connectors are sent to the * client in the following request. *

* * @return A collection of all dirty connectors for this uI. This list may * contain invisible connectors. */ public Collection getDirtyConnectors() { return dirtyConnectors; } /** * Checks if there a dirty connectors. * * @return true if there are dirty connectors, false otherwise */ public boolean hasDirtyConnectors() { return !getDirtyConnectors().isEmpty(); } /** * Returns a collection of those {@link #getDirtyConnectors() dirty * connectors} that are actually visible to the client. * * @return A list of dirty and visible connectors. */ public ArrayList getDirtyVisibleConnectors() { Collection dirtyConnectors = getDirtyConnectors(); ArrayList dirtyVisibleConnectors = new ArrayList( dirtyConnectors.size()); for (ClientConnector c : dirtyConnectors) { if (LegacyCommunicationManager.isConnectorVisibleToClient(c)) { dirtyVisibleConnectors.add(c); } } return dirtyVisibleConnectors; } public JsonObject getDiffState(ClientConnector connector) { assert getConnector(connector.getConnectorId()) == connector; return diffStates.get(connector); } public void setDiffState(ClientConnector connector, JsonObject diffState) { assert getConnector(connector.getConnectorId()) == connector; diffStates.put(connector, diffState); } public boolean isDirty(ClientConnector connector) { return dirtyConnectors.contains(connector); } /** * Checks whether the response is currently being written. Connectors can * not be marked as dirty when a response is being written. * * @see #setWritingResponse(boolean) * @see #markDirty(ClientConnector) * * @return true if the response is currently being written, * false if outside the response writing phase. */ public boolean isWritingResponse() { return writingResponse; } /** * Sets the current response write status. Connectors can not be marked as * dirty when the response is written. *

* This method has a side-effect of incrementing the sync id by one (see * {@link #getCurrentSyncId()}), if {@link #isWritingResponse()} returns * false and writingResponse is set to * true. * * @param writingResponse * the new response status. * * @see #markDirty(ClientConnector) * @see #isWritingResponse() * @see #getCurrentSyncId() * * @throws IllegalArgumentException * if the new response status is the same as the previous value. * This is done to help detecting problems caused by missed * invocations of this method. */ public void setWritingResponse(boolean writingResponse) { if (this.writingResponse == writingResponse) { throw new IllegalArgumentException( "The old value is same as the new value"); } /* * the right hand side of the && is unnecessary here because of the * if-clause above, but rigorous coding is always rigorous coding. */ if (writingResponse && !this.writingResponse) { currentSyncId++; } this.writingResponse = writingResponse; } /* Special serialization to JsonObjects which are not serializable */ private void writeObject(java.io.ObjectOutputStream out) throws IOException { out.defaultWriteObject(); // Convert JsonObjects in diff state to String representation as // JsonObject is not serializable HashMap stringDiffStates = new HashMap( diffStates.size() * 2); for (ClientConnector key : diffStates.keySet()) { stringDiffStates.put(key, diffStates.get(key).toString()); } out.writeObject(stringDiffStates); } /* Special serialization to JsonObjects which are not serializable */ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // Read String versions of JsonObjects and parse into JsonObjects as // JsonObject is not serializable diffStates = new HashMap(); @SuppressWarnings("unchecked") HashMap stringDiffStates = (HashMap) in .readObject(); diffStates = new HashMap( stringDiffStates.size() * 2); for (ClientConnector key : stringDiffStates.keySet()) { try { diffStates.put(key, Json.parse(stringDiffStates.get(key))); } catch (JsonException e) { throw new IOException(e); } } } /** * Checks if the indicated connector has a StreamVariable of the given name * and returns the variable if one is found. * * @param connectorId * @param variableName * @return variable if a matching one exists, otherwise null */ public StreamVariable getStreamVariable(String connectorId, String variableName) { if (pidToNameToStreamVariable == null) { return null; } Map map = pidToNameToStreamVariable .get(connectorId); if (map == null) { return null; } StreamVariable streamVariable = map.get(variableName); return streamVariable; } /** * Adds a StreamVariable of the given name to the indicated connector. * * @param connectorId * @param variableName * @param variable */ public void addStreamVariable(String connectorId, String variableName, StreamVariable variable) { assert getConnector(connectorId) != null; if (pidToNameToStreamVariable == null) { pidToNameToStreamVariable = new HashMap>(); } Map nameToStreamVariable = pidToNameToStreamVariable .get(connectorId); if (nameToStreamVariable == null) { nameToStreamVariable = new HashMap(); pidToNameToStreamVariable.put(connectorId, nameToStreamVariable); } nameToStreamVariable.put(variableName, variable); if (streamVariableToSeckey == null) { streamVariableToSeckey = new HashMap(); } String seckey = streamVariableToSeckey.get(variable); if (seckey == null) { seckey = UUID.randomUUID().toString(); streamVariableToSeckey.put(variable, seckey); } } /** * Removes StreamVariables that belong to connectors that are no longer * attached to the session. */ private void cleanStreamVariables() { if (pidToNameToStreamVariable != null) { ConnectorTracker connectorTracker = uI.getConnectorTracker(); Iterator iterator = pidToNameToStreamVariable.keySet() .iterator(); while (iterator.hasNext()) { String connectorId = iterator.next(); if (connectorTracker.getConnector(connectorId) == null) { // Owner is no longer attached to the session Map removed = pidToNameToStreamVariable .get(connectorId); for (String key : removed.keySet()) { streamVariableToSeckey.remove(removed.get(key)); } iterator.remove(); } } } } /** * Removes any StreamVariable of the given name from the indicated * connector. * * @param connectorId * @param variableName */ public void cleanStreamVariable(String connectorId, String variableName) { if (pidToNameToStreamVariable == null) { return; } Map nameToStreamVar = pidToNameToStreamVariable .get(connectorId); nameToStreamVar.remove(variableName); if (nameToStreamVar.isEmpty()) { pidToNameToStreamVariable.remove(connectorId); } } /** * Returns the security key associated with the given StreamVariable. * * @param variable * @return matching security key if one exists, null otherwise */ public String getSeckey(StreamVariable variable) { if (streamVariableToSeckey == null) { return null; } return streamVariableToSeckey.get(variable); } /** * Check whether a connector was present on the client when the it was * creating this request, but was removed server-side before the request * arrived. * * @since 7.2 * @param connectorId * The connector id to check for whether it was removed * concurrently or not. * @param lastSyncIdSeenByClient * the most recent sync id the client has seen at the time the * request was sent, or -1 to ignore potential problems * @return true if the connector was removed before the client * had a chance to react to it. */ public boolean connectorWasPresentAsRequestWasSent(String connectorId, long lastSyncIdSeenByClient) { assert getConnector(connectorId) == null : "Connector " + connectorId + " is still attached"; boolean clientRequestIsTooOld = lastSyncIdSeenByClient < currentSyncId && lastSyncIdSeenByClient != -1; if (clientRequestIsTooOld) { /* * The headMap call is present here because we're only interested in * connectors removed "in the past" (i.e. the server has removed * them before the client ever knew about that), since those are the * ones that we choose to handle as a special case. */ /*- * Server Client * [#1 add table] ---------. * \ * [push: #2 remove table]-. `--> [adding table, storing #1] * \ .- [table from request #1 needs more data] * \/ * /`-> [removing table, storing #2] * [#1 < #2 - ignoring] <---´ */ for (Set unregisteredConnectors : syncIdToUnregisteredConnectorIds .headMap(currentSyncId).values()) { if (unregisteredConnectors.contains(connectorId)) { return true; } } } return false; } /** * Gets the most recently generated server sync id. *

* The sync id is incremented by one whenever a new response is being * written. This id is then sent over to the client. The client then adds * the most recent sync id to each communication packet it sends back to the * server. This way, the server knows at what state the client is when the * packet is sent. If the state has changed on the server side since that, * the server can try to adjust the way it handles the actions from the * client side. *

* The sync id value -1 is ignored to facilitate testing with * pre-recorded requests. * * @see #setWritingResponse(boolean) * @see #connectorWasPresentAsRequestWasSent(String, long) * @since 7.2 * @return the current sync id */ public int getCurrentSyncId() { return currentSyncId; } /** * Maintains the bookkeeping connector removal and concurrency by removing * entries that have become too old. *

* It is important to run this call for each transmission from the client * , otherwise the bookkeeping gets out of date and the results form * {@link #connectorWasPresentAsRequestWasSent(String, long)} will become * invalid (that is, even though the client knew the component was removed, * the aforementioned method would start claiming otherwise). *

* Entries that both client and server agree upon are removed. Since * argument is the last sync id that the client has seen from the server, we * know that entries earlier than that cannot cause any problems anymore. *

* The sync id value -1 is ignored to facilitate testing with * pre-recorded requests. * * @see #connectorWasPresentAsRequestWasSent(String, long) * @since 7.2 * @param lastSyncIdSeenByClient * the sync id the client has most recently received from the * server. */ public void cleanConcurrentlyRemovedConnectorIds(int lastSyncIdSeenByClient) { if (lastSyncIdSeenByClient == -1) { // Sync id checking is not in use, so we should just clear the // entire map to avoid leaking memory syncIdToUnregisteredConnectorIds.clear(); return; } /* * We remove all entries _older_ than the one reported right now, * because the remaining still contain components that might cause * conflicts. In any case, it's better to clean up too little than too * much, especially as the data will hardly grow into the kilobytes. */ syncIdToUnregisteredConnectorIds.headMap(lastSyncIdSeenByClient) .clear(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy