/* * 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* 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"); } Settrue
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* 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 Collectiontrue
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
*
* 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
* 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 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
HashMaptrue
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-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.
* -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();
}
}