/* * Copyright 2000-2022 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.ObjectInputStream; import java.io.ObjectOutputStream; 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.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.event.MarkedAsDirtyConnectorEvent; import com.vaadin.event.MarkedAsDirtyListener; 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 com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinService; import com.vaadin.server.communication.ConnectorHierarchyWriter; import com.vaadin.shared.Registration; 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 { /** * Cache whether FINE messages are loggable. This is done to avoid * excessively calling getLogger() which might be slightly slow in some * specific environments. Please note that we're not caching the logger * instance itself because of * https://github.com/vaadin/framework/issues/2092. */ private static final boolean fineLogging = getLogger() .isLoggable(Level.FINE); private final Map* 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 (fineLogging) { 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"); } dirtyConnectors.remove(connector); if (!isClientSideInitialized(connector)) { // Client side has never known about this connector so there is no // point in tracking it removeUnregisteredConnector(connector, uI.getSession().getGlobalResourceHandler(false)); } else if (unregisteredConnectors.add(connector)) { // Client side knows about the connector, track it for a while if it // becomes reattached if (fineLogging) { 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 * @returntrue
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 if there are dirty connectors or the force flag is
* true. This should only be called by the framework.
*
* @param force
* {@code true} to force cleaning
* @since 8.2
*/
public void cleanConnectorMap(boolean force) {
if (force || !dirtyConnectors.isEmpty()) {
cleanConnectorMap();
}
}
/**
* Cleans the connector map from all connectors that are no longer attached
* to the application. This should only be called by the framework.
*
* @deprecated use {@link #cleanConnectorMap(boolean)} instead
*/
@Deprecated
public void cleanConnectorMap() {
removeUnregisteredConnectors();
cleanStreamVariables();
// 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.";
Iteratortrue
if the hierarchy is consistent,
* false
otherwise
* @since 8.1
*/
private boolean isHierarchyComplete() {
boolean noErrors = true;
Set* 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 true
and writingResponse
is set to
* false
.
*
* @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) {
// Bump sync id when done writing - the client is not expected to
// know about anything happening after this moment.
currentSyncId++;
}
this.writingResponse = writingResponse;
}
/* Special serialization to JsonObjects which are not serializable */
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Convert JsonObjects in diff state to String representation as
// JsonObject is not serializable
Map-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;
}
/**
* Add a marked as dirty listener that will be called when a client
* connector is marked as dirty.
*
* @param listener
* listener to add
* @since 8.4
* @return registration for removing listener registration
*/
public Registration addMarkedAsDirtyListener(
MarkedAsDirtyListener listener) {
markedDirtyListeners.add(listener);
return () -> markedDirtyListeners.remove(listener);
}
/**
* Notify all registered MarkedAsDirtyListeners the given client connector
* has been marked as dirty.
*
* @param connector
* client connector marked as dirty
* @since 8.4
*/
public void notifyMarkedAsDirtyListeners(ClientConnector connector) {
MarkedAsDirtyConnectorEvent event = new MarkedAsDirtyConnectorEvent(
connector, uI);
new ArrayList<>(markedDirtyListeners).forEach(listener -> {
listener.connectorMarkedAsDirty(event);
});
}
}