// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.clientlibs.invalidation;
import com.google.collide.client.status.StatusManager;
import com.google.collide.client.util.ClientTimer;
import com.google.collide.dto.RecoverFromDroppedTangoInvalidationResponse.RecoveredPayload;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonStringMap;
import com.google.collide.shared.invalidations.InvalidationObjectId;
import com.google.collide.shared.invalidations.InvalidationUtils;
import com.google.collide.shared.invalidations.InvalidationObjectId.VersioningRequirement;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;
import javax.annotation.Nullable;
/**
* A manager which is responsible for registering and unregistering objects with Tango and notifying
* the appropriate listener.
*
*/
public class InvalidationManager implements InvalidationRegistrar {
/**
* The version used when calling {@link InvalidationRegistrar.Listener#onInvalidated} when Tango
* reports that the invalidation's version was unknown.
*/
public static InvalidationManager create(StatusManager statusManager, Recoverer recoverer) {
return new InvalidationManager(recoverer, new DropRecoveringInvalidationControllerFactory(
logger, ClientTimer.FACTORY));
}
/**
* Allows for clients to recover from missing or squelched payloads.
*/
public interface Recoverer {
/**
* Called from {@link Recoverer#recoverPayloads(InvalidationObjectId, int, Callback)} with the
* response.
*/
public interface Callback {
void onPayloadsRecovered(JsonArray<RecoveredPayload> payloads, int currentObjectVersion);
void onError();
}
/**
* Called when a recovery is needed.
*
* @param objectId the ID of the object that needs to be recovered
* @param currentClientVersion the last version of the object that was delivered to the client
*/
void recoverPayloads(InvalidationObjectId<?> objectId, int currentClientVersion, Callback callback);
}
private static final InvalidationLogger logger = InvalidationLogger.create();
// Listener-scoped
/** Keyed by the object name */
private final JsonStringMap<InvalidationRegistrar.Listener> listenerMap =
JsonCollections.createMap();
/** Keyed by the object name */
private final JsonStringMap<DropRecoveringInvalidationController>
dropRecoveringInvalidationControllers = JsonCollections.createMap();
// Misc
private final Recoverer recoverer;
private final DropRecoveringInvalidationControllerFactory
dropRecoveringInvalidationControllerFactory;
private InvalidationManager(Recoverer recoverer,
DropRecoveringInvalidationControllerFactory dropRecoveringInvalidationControllerFactory) {
if (recoverer == null) {
recoverer = new Recoverer() {
@Override
public void recoverPayloads(
InvalidationObjectId<?> objectId, int currentClientVersion, Callback callback) {
// null operation
}
};
}
this.recoverer = recoverer;
this.dropRecoveringInvalidationControllerFactory = dropRecoveringInvalidationControllerFactory;
}
/**
* Registers a new listener for a specific object.
*/
@Override
public RemovableHandle register(final InvalidationObjectId<?> objectId, Listener eventListener) {
logger.fine("Registering object: %s", objectId);
final String objectName = objectId.getName();
listenerMap.put(objectName, eventListener);
final DropRecoveringInvalidationController dropRecoveringInvalidationController;
if (objectId.getVersioningRequirement() == VersioningRequirement.PAYLOADS) {
dropRecoveringInvalidationController =
dropRecoveringInvalidationControllerFactory.create(objectId, eventListener, recoverer);
dropRecoveringInvalidationControllers.put(objectName, dropRecoveringInvalidationController);
} else {
dropRecoveringInvalidationController = null;
}
return new RemovableHandle() {
@Override
public void initializeRecoverer(long nextExpectedVersion) {
Preconditions.checkState(dropRecoveringInvalidationController != null,
"You did need initialize a recoverer for this object: " + objectId);
Preconditions.checkArgument(nextExpectedVersion >= InvalidationUtils.INITIAL_OBJECT_VERSION,
"You can't initialize the recoverer to a value < " + InvalidationUtils.INITIAL_OBJECT_VERSION
+ ": " + objectId + "(" + nextExpectedVersion + ")");
// TODO: Fix up the long vs int debacle.
dropRecoveringInvalidationController.setNextExpectedVersion((int) nextExpectedVersion);
}
@Override
public void remove() {
unregister(objectId);
}
};
}
/**
* Unregisters an object Id.
*/
@Override
public void unregister(InvalidationObjectId<?> objectId) {
logger.fine("Unregistering object Id: %s", objectId);
final String objectName = objectId.getName();
listenerMap.remove(objectName);
DropRecoveringInvalidationController dropRecoveringInvalidationController =
dropRecoveringInvalidationControllers.remove(objectName);
if (dropRecoveringInvalidationController != null) {
dropRecoveringInvalidationController.cleanup();
}
}
public void handleInvalidation(String name, long version, @Nullable String payload) {
logger.fine("Invalidation for %s: Version: %s Payload: %s", name, version, payload);
InvalidationRegistrar.Listener listener = listenerMap.get(name);
if (listener == null) {
logger.severe("The listener does not exist for this objectId: %s", name);
return;
}
DropRecoveringInvalidationController dropRecoveringInvalidationController =
dropRecoveringInvalidationControllers.get(name);
if (dropRecoveringInvalidationController != null) {
// we check if the payload is the special string indicating null
boolean isEmptyPayload = payload != null && payload.equals(InvalidationObjectId.EMPTY_PAYLOAD);
dropRecoveringInvalidationController.handleInvalidated(
isEmptyPayload ? null : payload, version, isEmptyPayload);
} else {
listener.onInvalidated(name, version, payload, null);
}
}
}