// 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.communication;
import com.google.collide.client.bootstrap.BootstrapSession;
import com.google.collide.client.communication.MessageFilter.MessageRecipient;
import com.google.collide.client.status.StatusManager;
import com.google.collide.client.util.logging.Log;
import com.google.collide.clientlibs.vertx.VertxBus.ReplyHandler;
import com.google.collide.dto.ClientToServerDocOp;
import com.google.collide.dto.CodeErrors;
import com.google.collide.dto.CodeErrorsRequest;
import com.google.collide.dto.CodeGraphRequest;
import com.google.collide.dto.CodeGraphResponse;
import com.google.collide.dto.EmptyMessage;
import com.google.collide.dto.GetDirectory;
import com.google.collide.dto.GetDirectoryResponse;
import com.google.collide.dto.GetFileContents;
import com.google.collide.dto.GetFileContentsResponse;
import com.google.collide.dto.GetWorkspaceMetaData;
import com.google.collide.dto.GetWorkspaceMetaDataResponse;
import com.google.collide.dto.GetWorkspaceParticipants;
import com.google.collide.dto.GetWorkspaceParticipantsResponse;
import com.google.collide.dto.KeepAlive;
import com.google.collide.dto.LogFatalRecord;
import com.google.collide.dto.LogFatalRecordResponse;
import com.google.collide.dto.RecoverFromMissedDocOps;
import com.google.collide.dto.RecoverFromMissedDocOpsResponse;
import com.google.collide.dto.RoutingTypes;
import com.google.collide.dto.Search;
import com.google.collide.dto.SearchResponse;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.dto.ServerToClientDocOps;
import com.google.collide.dto.UpdateWorkspaceRunTargets;
import com.google.collide.dto.WorkspaceTreeUpdate;
import com.google.collide.dto.client.DtoClientImpls.ServerErrorImpl;
import com.google.collide.dto.shared.JsonFieldConstants;
import com.google.collide.dtogen.client.RoutableDtoClientImpl;
import com.google.collide.dtogen.shared.ClientToServerDto;
import com.google.collide.dtogen.shared.RoutableDto;
import com.google.collide.dtogen.shared.ServerToClientDto;
import com.google.collide.json.client.Jso;
import com.google.collide.json.shared.JsonStringMap;
import com.google.collide.json.shared.JsonStringMap.IterationCallback;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.annotations.VisibleForTesting;
/**
* The EventBus APIs for the Collide server.
*
* See {@package com.google.collide.dto} for data objects.
*
*/
public class FrontendApi {
/**
* EventBus API that documents the message types sent to the frontend. This API is fire and
* forget, since it does not expect a response.
*
* @param <REQ> The outgoing message type.
*/
public static interface SendApi<REQ extends ClientToServerDto> {
public void send(REQ msg);
}
/**
* EventBus API that documents the message types sent to the frontend, and the message type
* expected to be returned as a response.
*
* @param <REQ> The outgoing message type.
* @param <RESP> The incoming message type.
*/
public static interface RequestResponseApi<
REQ extends ClientToServerDto, RESP extends ServerToClientDto> {
public void send(REQ msg, final ApiCallback<RESP> callback);
}
/**
* Callback interface for receiving a matched response for requests to a frontend API.
*/
public interface ApiCallback<T extends ServerToClientDto> extends MessageRecipient<T> {
void onFail(FailureReason reason);
}
@VisibleForTesting
protected class ApiImpl<REQ extends ClientToServerDto, RESP extends ServerToClientDto>
implements
RequestResponseApi<REQ, RESP>,
SendApi<REQ> {
private final String address;
protected ApiImpl(String address) {
this.address = address;
}
@Override
public void send(REQ msg) {
RoutableDtoClientImpl messageImpl = (RoutableDtoClientImpl) msg;
addCustomFields(messageImpl);
pushChannel.send(address, messageImpl.serialize());
}
@Override
public void send(REQ msg, final ApiCallback<RESP> callback) {
RoutableDtoClientImpl messageImpl = (RoutableDtoClientImpl) msg;
addCustomFields(messageImpl);
pushChannel.send(address, messageImpl.serialize(), new ReplyHandler() {
@Override
public void onReply(String message) {
Jso jso = Jso.deserialize(message);
Log.info(getClass(), message);
if (RoutingTypes.SERVERERROR == jso.getIntField(RoutableDto.TYPE_FIELD)) {
ServerErrorImpl serverError = (ServerErrorImpl) jso;
callback.onFail(serverError.getFailureReason());
return;
}
ServerToClientDto messageDto = (ServerToClientDto) jso;
@SuppressWarnings("unchecked")
RESP resp = (RESP) messageDto;
callback.onMessageReceived(resp);
}
});
}
private void addCustomFields(final RoutableDtoClientImpl messageImpl) {
customHeaders.iterate(new IterationCallback<String>() {
@Override
public void onIteration(String header, String value) {
messageImpl.<Jso>cast().addField(header, value);
}
});
}
}
// ///////////////////////////////
// BEGIN AVAILABLE FRONTEND APIS
// ///////////////////////////////
/*
* IMPORTANT!
*
* By convention (and ignore the entries that ignore this convention :) ) we try to have
* GetDto/ResponseDto pairs for each unique servlet path. This helps us guard against
* client/frontend API version skew via a simple hash of all of the DTO messages.
*
* So if you add a new Servlet Path, please also add a new Get/Response Dto pair.
*/
public final RequestResponseApi<ClientToServerDocOp, ServerToClientDocOps> MUTATE_FILE =
makeApi("documents.mutate");
/**
* Lets a client re-synchronize with the server's version of a file after being offline or missing
* a doc op broadcast.
*/
public final RequestResponseApi<RecoverFromMissedDocOps, RecoverFromMissedDocOpsResponse>
RECOVER_FROM_MISSED_DOC_OPS = makeApi("documents.recoverMissedDocop");
/**
* Get the contents of a file and provisions an edit session so that it can be edited.
*/
public final RequestResponseApi<GetFileContents, GetFileContentsResponse> GET_FILE_CONTENTS =
makeApi("documents.createEditSession");
/**
* Get a subdirectory. Just the subtree rooted at that path. No associated meta data.
*/
public final RequestResponseApi<GetDirectory, GetDirectoryResponse> GET_DIRECTORY =
makeApi("tree.get");
/** Sends an ADD_FILE, ADD_DIR, COPY, MOVE, or DELETE tree mutation. */
public final RequestResponseApi<WorkspaceTreeUpdate, EmptyMessage>
MUTATE_WORKSPACE_TREE = makeApi("tree.mutate");
/**
* Send a keep-alive for the client in a workspace.
*/
public final SendApi<KeepAlive> KEEP_ALIVE = makeApi("participants.keepAlive");
/**
* Gets the list of workspace participants.
*/
public final RequestResponseApi<GetWorkspaceParticipants, GetWorkspaceParticipantsResponse>
GET_WORKSPACE_PARTICIPANTS = makeApi("participants.getParticipants");
/** Requests that we get updated information about a workspace's run targets. */
public final SendApi<UpdateWorkspaceRunTargets> UPDATE_WORKSPACE_RUN_TARGETS =
makeApi("workspace.updateRunTarget");
/** Requests workspace state like the last opened files and run targets. */
public final RequestResponseApi<GetWorkspaceMetaData, GetWorkspaceMetaDataResponse>
GET_WORKSPACE_META_DATA = makeApi("workspace.getMetaData");
/**
* Retrieves code errors for a file.
*/
public final RequestResponseApi<CodeErrorsRequest, CodeErrors> GET_CODE_ERRORS =
makeApi("todo.implementMe");
/**
* Retrieves code parsing results.
*/
public final RequestResponseApi<CodeGraphRequest, CodeGraphResponse> GET_CODE_GRAPH =
makeApi("todo.implementMe");
/**
* Log an exception to the server and potentially receive an unobfuscated response.
*/
public final RequestResponseApi<LogFatalRecord, LogFatalRecordResponse> LOG_REMOTE =
makeApi("todo.implementMe");
// TODO: this may want to move to browser channel instead, for
// search-as-you-type streaming. No sense to it yet until we have a real
// backend, though.
public final RequestResponseApi<Search, SearchResponse> SEARCH = makeApi("todo.implementMe");
// /////////////////////////////
// END AVAILABLE FRONTEND APIS
// /////////////////////////////
/**
* Creates a FrontendApi and initializes it.
*/
public static FrontendApi create(PushChannel pushChannel, StatusManager statusManager) {
FrontendApi frontendApi = new FrontendApi(pushChannel, statusManager);
frontendApi.initCustomFields();
return frontendApi;
}
private final JsonStringMap<String> customHeaders = JsonCollections.createMap();
private final PushChannel pushChannel;
private final StatusManager statusManager;
public FrontendApi(PushChannel pushChannel, StatusManager statusManager) {
this.pushChannel = pushChannel;
this.statusManager = statusManager;
}
private void initCustomFields() {
customHeaders.put(
JsonFieldConstants.SESSION_USER_ID, BootstrapSession.getBootstrapSession().getUserId());
}
/**
* Makes an API given the URL.
*
* @param <REQ> the request object
* @param <RESP> the response object
*/
protected <
REQ extends ClientToServerDto, RESP extends ServerToClientDto> ApiImpl<REQ, RESP> makeApi(
String url) {
return new ApiImpl<REQ, RESP>(url);
}
}