/**
* Copyright 2014 SAP AG
*
* 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 org.spotter.eclipse.ui;
import java.io.InputStream;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.lpe.common.config.ConfigParameterDescription;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spotter.client.SpotterServiceClient;
import org.spotter.eclipse.ui.model.ExtensionMetaobject;
import org.spotter.eclipse.ui.util.DialogUtils;
import org.spotter.eclipse.ui.util.SpotterProjectSupport;
import org.spotter.shared.configuration.JobDescription;
import org.spotter.shared.configuration.SpotterExtensionType;
import org.spotter.shared.hierarchy.model.RawHierarchyFactory;
import org.spotter.shared.hierarchy.model.XPerformanceProblem;
import org.spotter.shared.status.SpotterProgress;
import com.sun.jersey.api.client.ClientHandlerException;
/**
* <p>
* A wrapper for the Spotter Service Client that delegates to the client and
* handles arising exceptions by providing meaningful screen messages to the
* user. Exceptions thrown during a request are stored and can be retrieved
* afterwards via {@link #getLastException()}.
* </p>
* <p>
* This wrapper class also caches requested information from the server for
* future use. Whenever the client settings change, the cache will also be
* cleared automatically or it can be cleared on demand.
* </p>
*
* @author Denis Knoepfle
*
*/
public class ServiceClientWrapper {
public static final String DEFAULT_SERVICE_HOST = "localhost";
public static final String DEFAULT_SERVICE_PORT = "8080";
public static final String KEY_SERVICE_HOST = "spotter.service.host";
public static final String KEY_SERVICE_PORT = "spotter.service.port";
private static final Logger LOGGER = LoggerFactory.getLogger(ServiceClientWrapper.class);
private static enum HandlerStyle {
SILENT, LOG_ONLY, SHOW
}
private static final String DIALOG_TITLE = "DynamicSpotter Service Client";
private static final String ERR_MSG_CONN = "Connection to '%s' at port %s could not be established!";
private static final String MSG_FAIL_SAFE = "Error while storing preferences. Please try again.";
private static final String MSG_NO_ACTION = "Action cannot be performed without connection to DynamicSpotter Service. "
+ "Please check connection settings and try again.";
private static final String MSG_START_DIAGNOSIS = "Could not start diagnosis!";
private static final String MSG_REQU_RESULTS = "Could not retrieve diagnosis results!";
private static final String MSG_NO_STATUS = "Could not retrieve status.";
private static final String MSG_NO_RUN_EXCEPTION = "Could not retrieve the last run exception.";
private static final String MSG_NO_CONFIG_PARAMS = "Could not retrieve configuration parameters.";
private static final String MSG_NO_EXTENSIONS = "Could not retrieve list of extensions.";
private static final String MSG_NO_DEFAULT_HIER = "Could not retrieve the default hierarchy.";
private static final String MSG_NO_SATTELITE_TEST = "Could not test satellite connection.";
private final String projectName;
private final SpotterServiceClient client;
private Exception lastClientException;
private String host;
private String port;
// used for caching
private Set<ConfigParameterDescription> cachedSpotterConfParameters;
private Map<String, ConfigParameterDescription> cachedSpotterConfParamsMap;
private Map<SpotterExtensionType, Set<String>> cachedExtensionNames = new HashMap<>();
private Map<SpotterExtensionType, ExtensionMetaobject[]> cachedExtensionMetaobjects = new HashMap<>();
private Map<String, Set<ConfigParameterDescription>> cachedExtensionConfParamters = new HashMap<>();
private Map<String, String> cachedExtensionDescriptions = new HashMap<>();
private long lastClearTime;
/**
* Creates a new instance using the default host and port.
*/
public ServiceClientWrapper() {
this(null, true);
}
/**
* Creates a new instance. Service client settings will be stored using the
* given project name as identifier. Providing <code>null</code> means that
* those settings will be saved in the plugin's root scope instead.
*
* @param projectName
* The project the settings should be saved for or
* <code>null</code> to store them at the plugin's root scope
*/
public ServiceClientWrapper(String projectName) {
this(projectName, false);
}
private ServiceClientWrapper(String projectName, boolean useDefaults) {
if (useDefaults) {
this.host = DEFAULT_SERVICE_HOST;
this.port = DEFAULT_SERVICE_PORT;
} else {
Preferences prefs;
if (projectName == null) {
prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID);
} else {
prefs = SpotterProjectSupport.getProjectPreferences(projectName);
}
this.host = prefs.get(KEY_SERVICE_HOST, DEFAULT_SERVICE_HOST);
this.port = prefs.get(KEY_SERVICE_PORT, DEFAULT_SERVICE_PORT);
}
this.projectName = projectName;
this.client = new SpotterServiceClient(host, port);
this.lastClientException = null;
this.lastClearTime = System.currentTimeMillis();
}
/**
* @return The name of the project this client is associated with.
*/
public String getProjectName() {
return this.projectName;
}
/**
* @return The host of this client.
*/
public String getHost() {
return this.host;
}
/**
* @return The port of this client.
*/
public String getPort() {
return this.port;
}
/**
* Returns a value that determines the last time the cache was cleared. If a
* previous received value is smaller than the current one it means that the
* cache has been cleared in the meantime.
*
* @return a number that can be used for comparison to check if cache has
* been cleared meanwhile
*/
public long getLastClearTime() {
return lastClearTime;
}
/**
* Updates the URL of the wrapped client.
*
* @param newHost
* The new host
* @param newPort
* The new port
*/
public void updateUrl(String newHost, String newPort) {
if (!host.equals(newHost) || !port.equals(newPort)) {
client.updateUrl(newHost, newPort);
host = newHost;
port = newPort;
clearCache();
}
}
/**
* Saves preferences for the service client for the given project at the
* workspace level and updates the plugin's Spotter Service Client.
*
* @param newHost
* The new host
* @param newPort
* The new port
* @return <code>true</code> on success, otherwise <code>false</code>
*/
public boolean saveServiceClientSettings(String newHost, String newPort) {
Preferences prefs;
if (projectName == null) {
prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID);
} else {
prefs = SpotterProjectSupport.getProjectPreferences(projectName);
}
String oldHost = prefs.get(KEY_SERVICE_HOST, DEFAULT_SERVICE_HOST);
String oldPort = prefs.get(KEY_SERVICE_PORT, DEFAULT_SERVICE_PORT);
prefs.put(KEY_SERVICE_HOST, newHost);
prefs.put(KEY_SERVICE_PORT, newPort);
// force save
try {
prefs.flush();
// update current client
updateUrl(newHost, newPort);
return true;
} catch (BackingStoreException e) {
LOGGER.error("Saving Service Client settings failed.", e);
// restore old values
prefs.put(KEY_SERVICE_HOST, oldHost);
prefs.put(KEY_SERVICE_PORT, oldPort);
DialogUtils.handleError(MSG_FAIL_SAFE, e);
}
return false;
}
/**
* Starts the diagnosis using the given job description. Returns the
* retrieved job id or <code>null</code> on failure.
*
* @param jobDescription
* The job description to use.
* @return The retrieved job id or <code>null</code> on failure.
*/
public Long startDiagnosis(final JobDescription jobDescription) {
lastClientException = null;
try {
return client.startDiagnosis(jobDescription);
} catch (Exception e) {
handleException("startDiagnosis", MSG_START_DIAGNOSIS, e, HandlerStyle.SHOW, false);
}
return null;
}
/**
* Requests the results of a the run with the given job id.
*
* @param jobId
* the job id of the diagnosis run
* @return input stream containing the zipped run result folder or
* <code>null</code> if none found
*/
public InputStream requestResults(final String jobId) {
lastClientException = null;
try {
return client.requestResults(jobId);
} catch (Exception e) {
handleException("requestResults", MSG_REQU_RESULTS, e, HandlerStyle.SHOW, false);
}
return null;
}
/**
* Returns <code>true</code> if DynamicSpotter diagnostics is currently
* running, otherwise <code>false</code>.
*
* @param silent
* <code>true</code> to disable dialog pop-up and logging
* @return <code>true</code> if currently running, otherwise
* <code>false</code>
*/
public boolean isRunning(boolean silent) {
lastClientException = null;
try {
return client.isRunning();
} catch (Exception e) {
HandlerStyle style = silent ? HandlerStyle.SILENT : HandlerStyle.SHOW;
handleException("isRunning", MSG_NO_STATUS, e, style, true);
}
return false;
}
/**
* Returns the exception thrown during the last diagnosis run or
* <code>null</code> if none.
*
* @param silent
* <code>true</code> to disable dialog pop-up and logging
* @return the exception thrown during the last diagnosis run or
* <code>null</code> if none
*/
public Exception getLastRunException(boolean silent) {
lastClientException = null;
try {
return client.getLastRunException();
} catch (Exception e) {
HandlerStyle style = silent ? HandlerStyle.SILENT : HandlerStyle.SHOW;
handleException("getLastRunException", MSG_NO_RUN_EXCEPTION, e, style, true);
}
return null;
}
/**
* @return set of configuration parameter descriptions for DynamicSpotter
* configuration.
*/
public Set<ConfigParameterDescription> getConfigurationParameters() {
lastClientException = null;
if (cachedSpotterConfParameters != null) {
return cachedSpotterConfParameters;
}
try {
cachedSpotterConfParameters = client.getConfigurationParameters();
} catch (Exception e) {
handleException("getConfigurationParameters", MSG_NO_CONFIG_PARAMS, e, HandlerStyle.SHOW, false);
}
return cachedSpotterConfParameters;
}
/**
* Returns the <code>ConfigParameterDescription</code> that suits the given
* name.
*
* @param name
* name of the description object to retrieve
* @return the matching description object or <code>null</code> if not found
*/
public ConfigParameterDescription getSpotterConfigParam(String name) {
lastClientException = null;
if (cachedSpotterConfParamsMap == null) {
cachedSpotterConfParamsMap = initSpotterConfParamsMap();
if (cachedSpotterConfParamsMap == null) {
return null;
}
}
return cachedSpotterConfParamsMap.get(name);
}
/**
* Returns an array of extension meta objects for the given extension type
* or <code>null</code> on failure.
*
* @param extType
* extension type of interest
* @return array of extension meta objects for the given extension type. In
* the case of an error <code>null</code> is returned.
*/
public ExtensionMetaobject[] getAvailableExtensions(SpotterExtensionType extType) {
lastClientException = null;
ExtensionMetaobject[] metaobjects = cachedExtensionMetaobjects.get(extType);
if (metaobjects != null) {
return metaobjects;
}
Set<String> extNames = getAvailableExtensionNames(extType);
if (extNames == null) {
return null;
}
List<ExtensionMetaobject> list = new ArrayList<ExtensionMetaobject>();
for (String extName : extNames) {
// force caching and ignore invalid extensions
if (getExtensionConfigParamters(extName, HandlerStyle.LOG_ONLY) == null) {
continue;
}
list.add(new ExtensionMetaobject(projectName, extName));
}
metaobjects = list.toArray(new ExtensionMetaobject[list.size()]);
cachedExtensionMetaobjects.put(extType, metaobjects);
return metaobjects;
}
/**
* Returns a set of extension names for the given extension type.
*
* @param extType
* extension type of interest
* @return a set of extension names for the given extension type
*/
public Set<String> getAvailableExtensionNames(SpotterExtensionType extType) {
lastClientException = null;
Set<String> extNames = cachedExtensionNames.get(extType);
if (extNames != null) {
return extNames;
}
try {
extNames = client.getAvailableExtensions(extType);
cachedExtensionNames.put(extType, extNames);
} catch (Exception e) {
handleException("getAvailableExtensions", MSG_NO_EXTENSIONS, e, HandlerStyle.SHOW, false);
}
return extNames;
}
/**
* Returns the extension configuration parameters for the given extension or
* <code>null</code> if the extension doesn't exist.
*
* @param extName
* The name of the extension
* @return the extension configuration parameters for the extension or
* <code>null</code>
*/
public Set<ConfigParameterDescription> getExtensionConfigParamters(String extName) {
return getExtensionConfigParamters(extName, HandlerStyle.SHOW);
}
private Set<ConfigParameterDescription> getExtensionConfigParamters(String extName, HandlerStyle style) {
lastClientException = null;
Set<ConfigParameterDescription> confParams = cachedExtensionConfParamters.get(extName);
if (confParams != null) {
if (!cachedExtensionDescriptions.containsKey(extName)) {
cachedExtensionDescriptions.put(extName, findExtensionDescription(confParams));
}
return confParams;
}
try {
confParams = client.getExtensionConfigParamters(extName);
if (confParams != null) {
cachedExtensionConfParamters.put(extName, confParams);
cachedExtensionDescriptions.put(extName, findExtensionDescription(confParams));
}
} catch (Exception e) {
handleException("getExtensionConfigParameters", MSG_NO_CONFIG_PARAMS, e, style, false);
}
return confParams;
}
/**
* Returns the textual description of the given extension.
*
* @param extName
* The name of the extension
* @return the textual description of the given extension
*/
public String getExtensionDescription(String extName) {
lastClientException = null;
if (!cachedExtensionConfParamters.containsKey(extName)) {
// force caching of the extension description
getExtensionConfigParamters(extName);
}
return cachedExtensionDescriptions.get(extName);
}
/**
* Returns the default hierarchy.
*
* @return the default hierarchy
*/
public XPerformanceProblem getDefaultHierarchy() {
lastClientException = null;
try {
return client.getDefaultHierarchy();
} catch (Exception e) {
handleException("getDefaultHierarchy", MSG_NO_DEFAULT_HIER, e, HandlerStyle.LOG_ONLY, true);
}
return RawHierarchyFactory.getInstance().createEmptyHierarchy();
}
/**
* Returns a report on the progress of the current job.
*
* @return a report on the progress of the current job
*/
public SpotterProgress getCurrentProgressReport() {
lastClientException = null;
try {
return client.getCurrentProgressReport();
} catch (Exception e) {
handleException("getCurrentProgressReport", MSG_NO_STATUS, e, HandlerStyle.SHOW, false);
}
return null;
}
/**
* Returns the id of the currently running job.
*
* @return the id of the currently running job
*/
public Long getCurrentJobId() {
lastClientException = null;
try {
return client.getCurrentJobId();
} catch (Exception e) {
handleException("getCurrentJobId", MSG_NO_STATUS, e, HandlerStyle.SHOW, false);
}
return null;
}
/**
* Returns the root problem of the currently running job.
*
* @param silent
* <code>true</code> to disable dialog pop-up and logging
* @return the root problem
*/
public XPerformanceProblem getCurrentRootProblem(boolean silent) {
lastClientException = null;
try {
return client.getCurrentRootProblem();
} catch (Exception e) {
HandlerStyle style = silent ? HandlerStyle.SILENT : HandlerStyle.SHOW;
handleException("getCurrentRootProblem", MSG_NO_STATUS, e, style, false);
}
return null;
}
/**
* Tests connection to the satellite specified by the given extension name,
* host and port. If extension is not a satellite this method returns
* <code>false</code>!
*
* @param extName
* The name of the extension
* @param host
* The host/ip to connect to
* @param port
* The port to connect to
* @return <code>true</code> if connection could have been established,
* otherwise <code>false</code>
*/
public boolean testConnectionToSattelite(String extName, String host, String port) {
lastClientException = null;
try {
return client.testConnectionToSattelite(extName, host, port);
} catch (Exception e) {
handleException("testConnectionToSattelite", MSG_NO_SATTELITE_TEST, e, HandlerStyle.SHOW, false);
}
return false;
}
/**
* Tests connection to the DS Service.
*
* @param showErrorDialog
* <code>true</code> to show an error dialog on failure
*
* @return <code>true</code> if connection could have been established,
* otherwise <code>false</code>
*/
public boolean testConnection(boolean showErrorDialog) {
lastClientException = null;
boolean connection;
try {
connection = client.testConnection();
} catch (Exception e) {
connection = false;
lastClientException = e;
}
if (showErrorDialog && !connection) {
showConnectionProblemMessage(MSG_NO_ACTION, host, port, false);
}
return connection;
}
/**
* Returns the last exception thrown on the client side during a request.
*
* @return the last exception thrown
*/
public Exception getLastClientException() {
return lastClientException;
}
/**
* Returns whether the reason for the last exception was a connection issue.
*
* @return <code>true</code> if it was connection issue, <code>false</code>
* otherwise
*/
public boolean isConnectionIssue() {
if (lastClientException instanceof ClientHandlerException) {
if (lastClientException.getCause() instanceof ConnectException) {
return true;
}
}
return false;
}
/**
* Clears the cache deleting all data that was fetched from the server.
*/
public void clearCache() {
// collections as a whole are returned or they are related,
// so set to null after clear
if (cachedSpotterConfParameters != null) {
cachedSpotterConfParameters.clear();
cachedSpotterConfParameters = null;
}
if (cachedSpotterConfParamsMap != null) {
cachedSpotterConfParamsMap.clear();
cachedSpotterConfParamsMap = null;
}
// only values of these maps are returned, so clear() is enough
cachedExtensionNames.clear();
cachedExtensionMetaobjects.clear();
cachedExtensionConfParamters.clear();
cachedExtensionDescriptions.clear();
lastClearTime = System.currentTimeMillis();
}
/**
* Shows a message on the screen explaining the connection problem. If cause
* is not <code>null</code> it will be appended to the message.
*
* @param cause
* The cause of the problem
* @param host
* The host used for the connection
* @param port
* The port used for the connection
* @param warning
* <code>true</code> to show a warning only, otherwise an error
* is shown
*/
public static void showConnectionProblemMessage(String cause, String host, String port, boolean warning) {
String msg = String.format(ERR_MSG_CONN, host, port);
if (cause != null) {
msg += "\n\n" + cause;
}
if (warning) {
DialogUtils.openWarning(DIALOG_TITLE, msg);
} else {
DialogUtils.openError(DIALOG_TITLE, msg);
}
}
/**
* Handles an exception and stores the given exception which can then be
* retrieved later via {@link #getLastException()}.
*
* @param requestName
* The name of the requester. This name is only used for logging.
* @param requestErrorMsg
* The error message for the request failure. This message is
* included in the screen message in case of a connection issue.
* @param exception
* The exception thrown in the request
* @param style
* the style how the exception should be communicated
* @param warning
* <code>true</code> for warning, <code>false</code> for error
*/
private void handleException(String requestName, String requestErrorMsg, Exception exception, HandlerStyle style,
boolean warning) {
lastClientException = exception;
if (style == HandlerStyle.SILENT) {
return;
}
if (warning) {
LOGGER.warn("{} request failed! Cause: {}", requestName, exception.toString());
} else {
LOGGER.error("{} request failed! Cause: {}", requestName, exception.toString());
}
if (style == HandlerStyle.LOG_ONLY) {
return;
}
if (isConnectionIssue()) {
showConnectionProblemMessage(requestErrorMsg, host, port, warning);
} else {
// illegal response state or server error
String header = "DS Service returned with an error!";
String message = exception.getMessage();
if (message == null) {
message = "Exception (" + exception.getClass().getName() + ") contains no message.";
}
String fullMessage = header + "\n\n" + message;
if (warning) {
DialogUtils.openWarning(DIALOG_TITLE, fullMessage);
} else {
DialogUtils.handleError(header, exception);
}
}
}
private Map<String, ConfigParameterDescription> initSpotterConfParamsMap() {
Map<String, ConfigParameterDescription> map = new HashMap<String, ConfigParameterDescription>();
Set<ConfigParameterDescription> settings = getConfigurationParameters();
if (settings == null) {
return null;
}
for (ConfigParameterDescription desc : settings) {
map.put(desc.getName(), desc);
}
return map;
}
private String findExtensionDescription(Set<ConfigParameterDescription> confParams) {
for (ConfigParameterDescription desc : confParams) {
if (desc.getName().equals(ConfigParameterDescription.EXT_DESCRIPTION_KEY)) {
return desc.getDefaultValue();
}
}
return null;
}
}