// 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.client.codeunderstanding;
import static com.google.collide.shared.util.StringUtils.isNullOrEmpty;
import com.google.collide.client.communication.FrontendApi;
import com.google.collide.client.util.DeferredCommandExecutor;
import com.google.collide.client.util.logging.Log;
import com.google.collide.dto.CodeBlock;
import com.google.collide.dto.CodeGraph;
import com.google.collide.dto.CodeGraphFreshness;
import com.google.collide.dto.CodeGraphRequest;
import com.google.collide.dto.CodeGraphResponse;
import com.google.collide.dto.CodeReferences;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.dto.client.DtoClientImpls.CodeBlockImpl;
import com.google.collide.dto.client.DtoClientImpls.CodeGraphFreshnessImpl;
import com.google.collide.dto.client.DtoClientImpls.CodeGraphImpl;
import com.google.collide.dto.client.DtoClientImpls.CodeGraphRequestImpl;
import com.google.collide.dto.client.DtoClientImpls.CodeReferencesImpl;
import com.google.collide.json.client.Jso;
import com.google.common.base.Preconditions;
/**
* An object that holds the current state of communication with Cube-service.
*
* <p>Request to service are sequenced and "collapsed".
* It means that newer request is hold until the previous response comes,
* and that if there are several new requests, only the last of them
* survives.
*
* <p>Also this class is responsible for merging updates coming from the server.
*
*/
public class CubeState implements FrontendApi.ApiCallback<CodeGraphResponse> {
/**
* An interface of a callback that is called when appropriate update
* is received.
*/
interface CubeResponseDistributor {
/**
* Notifies instances interested in Cube data.
*
* @param updates indicates which data has been changed
*/
void notifyListeners(CubeDataUpdates updates);
}
/**
* A command that gently asks outer object to perform "retry" action
* after specified timeout.
*
* <p>The command itself is executed once a second to be able to be
* dismissed.
*
* <p>When time is elapsed command calls outer object
* {@link CubeState#retryIfReady()} each iteration until it
* responds that action is taken.
*/
private class RetryExecutor extends DeferredCommandExecutor {
protected RetryExecutor() {
super(1000);
}
@Override
protected boolean execute() {
return !retryIfReady();
}
}
/**
* Cube API.
*/
private final FrontendApi.RequestResponseApi<CodeGraphRequest, CodeGraphResponse> api;
/**
* Last (merged) Cube data.
*/
private CubeData data;
/**
* Actual merged freshness of data.
*/
private CodeGraphFreshness freshness;
/**
* File path to be requested on next request.
*/
private String activeFilePath;
/**
* File path in last response.
*
* <p>If filePath to be requested is not the same as in last response -
* we reset fileTree and it's freshness.
*/
private String lastResponseFilePath;
/**
* File path in last request for which he hasn't got response yet.
*/
private String requestedFilePath;
/**
* Flag indicating that we should make a new request after
* we receive response.
*/
private boolean deferredRefresh;
/**
* Indicates that we should not further do more requests / process responses.
*/
private boolean isDismissed;
/**
* Instance that distributes data to consumers.
*/
private final CubeResponseDistributor distributor;
/**
* Number of times the retry command has been consequently (re)scheduled.
*/
private int retryRound;
/**
* Retry command executor.
*/
private final RetryExecutor retryExecutor = new RetryExecutor();
public CubeState(FrontendApi.RequestResponseApi<CodeGraphRequest, CodeGraphResponse> api,
CubeResponseDistributor distributor) {
this.api = api;
this.distributor = distributor;
data = CubeData.EMPTY_DATA;
freshness = CodeGraphFreshnessImpl.make()
.setFullGraph("0")
.setLibsSubgraph("0")
.setWorkspaceTree("0")
.setFileTree("0")
.setFileReferences("0");
}
/**
* Prevents further instance activity.
*/
public void dismiss() {
isDismissed = true;
retryExecutor.cancel();
}
public CubeData getData() {
return data;
}
/**
* Makes a next request if there is a deferred one or
* schedule retry if required.
*
* <p>This method must be called after network response or
* failure is processed to schedule next network activity.
*/
private void processDeferredActions() {
requestedFilePath = null;
if (deferredRefresh) {
deferredRefresh = false;
refresh();
} else if (retryRound > 0) {
// We should schedule retry if hasn't done it yet.
if (!retryExecutor.isScheduled()) {
retryRound++;
retryExecutor.schedule(2 + retryRound);
}
}
}
/**
* Enqueues or collapses request to Cube-service.
*/
public void refresh() {
if (isDismissed) {
return;
}
if (activeFilePath == null) {
return;
}
// Refresh is already deferred.
if (deferredRefresh) {
return;
}
// Waiting for response of equal request.
if (activeFilePath.equals(requestedFilePath)) {
return;
}
boolean activeIsCodeFilePath = checkFilePathIsCodeFile(activeFilePath);
// Will send request after response come.
if (requestedFilePath != null) {
// do not defer if we do not need fileTree
if (activeIsCodeFilePath) {
deferredRefresh = true;
}
return;
}
// Else reset fileTree, if needed.
if (!activeFilePath.equals(lastResponseFilePath) || !activeIsCodeFilePath) {
resetContextFileData();
}
String filePathToRequest = activeIsCodeFilePath ? activeFilePath : null;
// And send request.
CodeGraphRequest request = CodeGraphRequestImpl.make()
.setFreshness(freshness)
.setFilePath(filePathToRequest);
api.send(request, this);
requestedFilePath = activeFilePath;
}
/**
* Forgets stale fileTree data and freshness.
*/
private void resetContextFileData() {
freshness = CodeGraphFreshnessImpl.make()
.setLibsSubgraph(freshness.getLibsSubgraph())
.setFileTree("0")
.setFullGraph(freshness.getFullGraph())
.setWorkspaceTree(freshness.getWorkspaceTree())
.setFileReferences("0");
data = new CubeData(activeFilePath, null, data.getFullGraph(), data.getLibsSubgraph(),
data.getWorkspaceTree(), null);
}
/**
* Merges fresh server data with stored one and notifies consumers.
*
* @param message fresh Cube data.
*/
@Override
public void onMessageReceived(CodeGraphResponse message) {
if (isDismissed) {
Log.debug(getClass(), "Ignored CUBE response");
return;
}
retryRound = 0;
retryExecutor.cancel();
boolean requestedIsCode = checkFilePathIsCodeFile(requestedFilePath);
CodeGraphFreshnessImpl merged = CodeGraphFreshnessImpl.make();
CodeGraphFreshness serverFreshness = message.getFreshness();
CodeGraph libsSubgraph = data.getLibsSubgraph();
merged.setLibsSubgraph(this.freshness.getLibsSubgraph());
boolean libsSubgraphUpdated = false;
CodeBlock fileTree = data.getFileTree();
merged.setFileTree(this.freshness.getFileTree());
merged.setFileTreeHash(this.freshness.getFileTreeHash());
boolean fileTreeUpdated = false;
CodeGraph workspaceTree = data.getWorkspaceTree();
merged.setWorkspaceTree(this.freshness.getWorkspaceTree());
boolean workspaceTreeUpdated = false;
CodeGraph fullGraph = data.getFullGraph();
merged.setFullGraph(this.freshness.getFullGraph());
boolean fullGraphUpdated = false;
CodeReferences fileReferences = data.getFileReferences();
merged.setFileReferences(this.freshness.getFileReferences());
boolean fileReferencesUpdated = false;
if (!isNullOrEmpty(message.getLibsSubgraphJson())
&& compareFreshness(serverFreshness.getLibsSubgraph(), merged.getLibsSubgraph()) > 0) {
libsSubgraph = Jso.<CodeGraphImpl>deserialize(message.getLibsSubgraphJson());
merged.setLibsSubgraph(serverFreshness.getLibsSubgraph());
libsSubgraphUpdated = true;
}
if (!isNullOrEmpty(message.getFileTreeJson()) && requestedIsCode
&& isServerFileTreeMoreFresh(merged, serverFreshness)) {
fileTree = Jso.<CodeBlockImpl>deserialize(message.getFileTreeJson());
merged.setFileTree(serverFreshness.getFileTree());
merged.setFileTreeHash(serverFreshness.getFileTreeHash());
fileTreeUpdated = true;
}
if (!isNullOrEmpty(message.getWorkspaceTreeJson())
&& compareFreshness(serverFreshness.getWorkspaceTree(), merged.getWorkspaceTree()) > 0) {
workspaceTree = Jso.<CodeGraphImpl>deserialize(message.getWorkspaceTreeJson());
merged.setWorkspaceTree(serverFreshness.getWorkspaceTree());
workspaceTreeUpdated = true;
}
if (!isNullOrEmpty(message.getFullGraphJson())
&& compareFreshness(serverFreshness.getFullGraph(), merged.getFullGraph()) > 0) {
fullGraph = Jso.<CodeGraphImpl>deserialize(message.getFullGraphJson());
merged.setFullGraph(serverFreshness.getFullGraph());
fullGraphUpdated = true;
}
if (!isNullOrEmpty(message.getFileReferencesJson())
&& compareFreshness(serverFreshness.getFileReferences(), merged.getFileReferences()) > 0) {
fileReferences = Jso.<CodeReferencesImpl>deserialize(message.getFileReferencesJson());
merged.setFileReferences(serverFreshness.getFileReferences());
fileReferencesUpdated = true;
}
if (!requestedFilePath.equals(activeFilePath)) {
fileTree = null;
fileTreeUpdated = false;
fileReferences = null;
fileReferencesUpdated = false;
}
CubeDataUpdates updates = new CubeDataUpdates(fileTreeUpdated, fullGraphUpdated,
libsSubgraphUpdated, workspaceTreeUpdated, fileReferencesUpdated);
// At this moment consumers are waiting for *activeFilePath* updates.
// If update for it is not received yet (requested != active), then
// fileTree is set to null, and consumers could try to use data from
// fullGraph using filePath stored in data.
data = new CubeData(activeFilePath, fileTree, fullGraph, libsSubgraph, workspaceTree,
fileReferences);
freshness = merged;
Log.debug(getClass(), "CUBE data updated", updates, data);
lastResponseFilePath = requestedFilePath;
distributor.notifyListeners(updates);
processDeferredActions();
}
@Override
public void onFail(FailureReason reason) {
if (isDismissed) {
return;
}
processDeferredActions(true);
}
/**
* Makes a next request if there is a deferred one or
* schedule retry if required.
*
* <p>This method must be called after network response or
* failure is processed to schedule next network activity.
*
* @param afterFail {@code true} indicates that this method
* is invoked from {@link #onFail(FailureReason)}
*/
private void processDeferredActions(boolean afterFail) {
requestedFilePath = null;
if (deferredRefresh) {
deferredRefresh = false;
refresh();
} else if (afterFail || retryRound > 0) {
// We should schedule retry if hasn't done it yet.
if (!retryExecutor.isScheduled()) {
retryRound++;
retryExecutor.schedule(2 + retryRound);
}
}
}
/**
* Compares server and client freshness.
*
* <p>The point is that server can send no freshness, which means that
* data is not ready.
*
* <p>In common case freshness is an integer written as string.
*
* @param serverFreshness string that represents server side freshness
* @param clientFreshness string that represents client side freshness
* @return {@code 0}, {@code 1}, or {@code -1} according to freshness
* relationship
*/
private static int compareFreshness(String serverFreshness, String clientFreshness) {
if (isNullOrEmpty(clientFreshness)) {
throw new IllegalArgumentException("client freshness should never be undefined");
}
if (isNullOrEmpty(serverFreshness)) {
// Assume server don't know better than we do.
return -1;
}
return Long.valueOf(serverFreshness).compareTo(Long.valueOf(clientFreshness));
}
/**
* Sets active file path and requests fresh data from service.
*
* @param filePath new active file path
*/
void setFilePath(String filePath) {
Preconditions.checkNotNull(filePath);
activeFilePath = filePath;
refresh();
}
/**
* Checks if the specified file path is acceptable for Cube.
*
* @return {@code false} if we shouldn't send request for the specified file
*/
private static boolean checkFilePathIsCodeFile(String filePath) {
Preconditions.checkNotNull(filePath);
// TODO: should we ignore case?
return filePath.endsWith(".js") || filePath.endsWith(".py") || filePath.endsWith(".dart");
}
private static boolean isServerFileTreeMoreFresh(
CodeGraphFreshness clientFreshness, CodeGraphFreshness serverFreshness) {
return compareFreshness(serverFreshness.getFileTree(), clientFreshness.getFileTree()) > 0
|| (compareFreshness(serverFreshness.getFileTree(), clientFreshness.getFileTree()) == 0
&& !serverFreshness.getFileTreeHash().equals(clientFreshness.getFileTreeHash()));
}
/**
* Resends last request.
*
* @return {@code false} if object is not ready to perform that action
* and should be notified again later
*/
private boolean retryIfReady() {
// Waiting for response.
if (requestedFilePath != null) {
return false;
}
// Force request.
deferredRefresh = false;
refresh();
return true;
}
}