/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.logger;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.che.api.analytics.shared.dto.EventParameters;
import org.eclipse.che.api.user.gwt.client.UserServiceClient;
import org.eclipse.che.api.user.shared.dto.UserDescriptor;
import org.eclipse.che.ide.api.app.AppContext;
import org.eclipse.che.ide.api.app.CurrentProject;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.rest.AsyncRequestCallback;
import org.eclipse.che.ide.rest.DtoUnmarshallerFactory;
import org.eclipse.che.ide.util.Config;
import org.eclipse.che.ide.util.loging.Log;
import org.eclipse.che.ide.websocket.Message;
import org.eclipse.che.ide.websocket.MessageBuilder;
import org.eclipse.che.ide.websocket.MessageBus;
import org.eclipse.che.ide.websocket.events.ConnectionOpenedHandler;
import org.eclipse.che.ide.websocket.rest.RequestCallback;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import static com.google.gwt.http.client.RequestBuilder.POST;
import static org.eclipse.che.ide.MimeType.APPLICATION_JSON;
import static org.eclipse.che.ide.rest.HTTPHeader.CONTENTTYPE;
/**
* API to track Analytics events.
*
* @author Anatoliy Bazko
*/
@Singleton
public class AnalyticsEventLoggerImpl implements AnalyticsEventLoggerExt {
private static final int MAX_PENDING_MESSAGES = 1000;
private static final String IDE_EVENT = "ide-usage";
private static final String API_ANALYTICS_PATH = "/analytics/log/";
protected static final String WS_PARAM = "WS";
protected static final String USER_PARAM = "USER";
protected static final String SOURCE_PARAM = "SOURCE";
protected static final String ACTION_PARAM = "ACTION";
protected static final String PROJECT_NAME_PARAM = "PROJECT";
protected static final String PROJECT_TYPE_PARAM = "TYPE";
private static final String EMPTY_PARAM_VALUE = "";
private final DtoFactory dtoFactory;
private final UserServiceClient user;
private final AppContext appContext;
private final MessageBus messageBus;
private final DtoUnmarshallerFactory dtoUnmarshallerFactory;
private final Queue<Message> pendingMessages;
private volatile boolean isOpenedMessageBus;
private String currentUser;
@Inject
public AnalyticsEventLoggerImpl(DtoFactory dtoFactory,
UserServiceClient user,
AppContext appContext,
MessageBus messageBus,
DtoUnmarshallerFactory dtoUnmarshallerFactory) {
this.dtoFactory = dtoFactory;
this.user = user;
this.appContext = appContext;
this.messageBus = messageBus;
this.dtoUnmarshallerFactory = dtoUnmarshallerFactory;
saveCurrentUser();
this.pendingMessages = new LinkedList<Message>() {
@Override
public boolean add(Message message) {
return size() < MAX_PENDING_MESSAGES && super.add(message);
}
};
this.messageBus.addOnOpenHandler(new ConnectionOpenedHandler() {
@Override
public void onOpen() {
isOpenedMessageBus = true;
Message message;
while ((message = pendingMessages.poll()) != null) {
doSend(message);
}
}
});
}
@Override
public void log(Object action) {
doLog(IDE_EVENT, action, null, null);
}
@Override
public void log(Object action, String actionName) {
doLog(IDE_EVENT, action, actionName, null);
}
@Override
public void log(Object action, String actionName, Map<String, String> additionalParams) {
doLog(IDE_EVENT, action, actionName, additionalParams);
}
@Override
public void logEvent(String event, Map<String, String> additionalParams) {
doLog(event, null, null, additionalParams);
}
@Override
@Deprecated
public void log(String action) {
if (action != null) {
Map<String, String> parameters = new HashMap<>();
parameters.put(SOURCE_PARAM, action);
doLog(IDE_EVENT, null, null, parameters);
}
}
@Override
@Deprecated
public void log(Class<?> actionClass, String actionName) {
doLog(IDE_EVENT, actionClass, actionName, null);
}
@Override
@Deprecated
public void log(Class<?> actionClass, String actionName, Map<String, String> additionalParams) {
doLog(IDE_EVENT, actionClass, actionName, additionalParams);
}
private void doLog(@Nullable String event,
@Nullable Object action,
@Nullable String actionName,
@Nullable Map<String, String> additionalParams) {
// we can put here additional params depending on action class
doLog(event, action == null ? null : action.getClass(), actionName, additionalParams);
}
private void doLog(@Nullable String event,
@Nullable Class<?> actionClass,
@Nullable String actionName,
@Nullable Map<String, String> additionalParams) {
if (event == null) {
return;
}
additionalParams = additionalParams == null ? new HashMap<String, String>() : new HashMap<>(additionalParams);
validate(additionalParams);
if (actionName != null) {
validate(actionName, MAX_PARAM_VALUE_LENGTH);
additionalParams.put(ACTION_PARAM, actionName);
}
if (actionClass != null) {
additionalParams.put(SOURCE_PARAM, actionClass.getName());
}
putReservedParameters(additionalParams);
send(event, additionalParams);
}
private void putReservedParameters(Map<String, String> additionalParams) {
CurrentProject project = appContext.getCurrentProject();
if (project != null) {
putIfNotNull(PROJECT_NAME_PARAM, project.getRootProject().getName(), additionalParams);
putIfNotNull(PROJECT_TYPE_PARAM, project.getRootProject().getType(), additionalParams);
}
putIfNotNull(USER_PARAM, currentUser, additionalParams);
putIfNotNull(WS_PARAM, getWorkspace(), additionalParams);
}
protected String getWorkspace() {
return Config.getWorkspaceName();
}
private void putIfNotNull(String key,
@Nullable String value,
Map<String, String> additionalParams) {
if (value != null) {
additionalParams.put(key, value);
}
}
private void validate(Map<String, String> additionalParams) throws IllegalArgumentException {
if (additionalParams.size() > MAX_PARAMS_NUMBER) {
throw new IllegalArgumentException("The number of parameters exceeded the limit in " + MAX_PARAMS_NUMBER);
}
for (Map.Entry<String, String> entry : additionalParams.entrySet()) {
String param = entry.getKey();
String value = entry.getValue();
validate(param, MAX_PARAM_NAME_LENGTH);
validate(value, MAX_PARAM_VALUE_LENGTH);
}
}
private void validate(String s, int maxLength) {
if (s.length() > maxLength) {
throw new IllegalArgumentException(
"The length of '" + s.substring(0, maxLength) + "...' exceeded the maximum in " + maxLength + " characters");
}
}
private void saveCurrentUser() {
user.getCurrentUser(new AsyncRequestCallback<UserDescriptor>(dtoUnmarshallerFactory.newUnmarshaller(UserDescriptor.class)) {
@Override
protected void onSuccess(UserDescriptor result) {
if (result != null) {
currentUser = result.getEmail();
} else {
currentUser = EMPTY_PARAM_VALUE;
}
}
@Override
protected void onFailure(Throwable exception) {
currentUser = EMPTY_PARAM_VALUE;
}
});
}
protected void send(String event, Map<String, String> parameters) {
EventParameters additionalParams = dtoFactory.createDto(EventParameters.class).withParams(parameters);
final String json = dtoFactory.toJson(additionalParams);
MessageBuilder builder = new MessageBuilder(POST, API_ANALYTICS_PATH + event);
builder.data(json);
builder.header(CONTENTTYPE, APPLICATION_JSON);
Message message = builder.build();
if (isOpenedMessageBus) {
doSend(message);
} else {
pendingMessages.offer(message);
}
}
private void doSend(final Message message) {
try {
messageBus.send(message, new RequestCallback() {
@Override
protected void onSuccess(Object result) {
}
@Override
protected void onFailure(Throwable exception) {
Log.error(getClass(), exception.getMessage());
}
});
} catch (Exception e) {
Log.error(getClass(), e.getMessage());
}
}
}