/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Lachlan Dowding
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package permafrost.tundra.server;
import com.wm.app.b2b.server.BaseService;
import com.wm.app.b2b.server.InvokeState;
import com.wm.app.b2b.server.ServerAPI;
import com.wm.app.b2b.server.Service;
import com.wm.app.b2b.server.ServiceException;
import com.wm.app.b2b.server.ServiceSetupException;
import com.wm.app.b2b.server.ServiceThread;
import com.wm.app.b2b.server.ns.NSDependencyManager;
import com.wm.app.b2b.server.ns.Namespace;
import com.wm.data.IData;
import com.wm.data.IDataCursor;
import com.wm.data.IDataFactory;
import com.wm.data.IDataUtil;
import com.wm.lang.ns.DependencyManager;
import com.wm.lang.ns.NSName;
import com.wm.lang.ns.NSNode;
import com.wm.lang.ns.NSService;
import com.wm.lang.ns.NSServiceType;
import com.wm.net.HttpHeader;
import permafrost.tundra.data.IDataHelper;
import permafrost.tundra.data.IDataMap;
import permafrost.tundra.lang.BytesHelper;
import permafrost.tundra.lang.ExceptionHelper;
import permafrost.tundra.lang.StringHelper;
import permafrost.tundra.math.IntegerHelper;
import permafrost.tundra.math.NormalDistributionEstimator;
import permafrost.tundra.mime.MIMETypeHelper;
import permafrost.tundra.net.http.HTTPHelper;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* A collection of convenience methods for working with webMethods Integration Server services.
*/
public final class ServiceHelper {
/**
* Disallow instantiation of this class.
*/
private ServiceHelper() {}
/**
* Returns a copy of the call stack for the current invocation.
*
* @return A copy of the call stack for the current invocation.
*/
@SuppressWarnings("unchecked")
public static List<NSService> getCallStack() {
List<NSService> stack = (List<NSService>)InvokeState.getCurrentState().getCallStack();
if (stack == null) stack = Collections.emptyList();
return new ArrayList<NSService>(stack);
}
/**
* Returns true if the calling service is the top-level initiating service of the current thread.
*
* @return True if the calling service is the top-level initiating service of the current thread.
*/
public static boolean isInitiator() {
return getCallStack().size() <= 1;
}
/**
* Creates a new service in the given package with the given name.
*
* @param packageName The name of the package to create the service in.
* @param serviceName The fully-qualified name of the service to be created.
* @param type The type of service to be created.
* @param subtype The subtype of service to be created.
* @throws ServiceException If an error creating the service occurs.
*/
private static void create(String packageName, String serviceName, String type, String subtype) throws ServiceException {
if (!PackageHelper.exists(packageName)) {
throw new IllegalArgumentException("package does not exist: " + packageName);
}
if (NodeHelper.exists(serviceName)) throw new IllegalArgumentException("node already exists: " + serviceName);
NSName service = NSName.create(serviceName);
if (type == null) type = NSServiceType.SVC_FLOW;
if (subtype == null) subtype = NSServiceType.SVCSUB_UNKNOWN;
NSServiceType serviceType = NSServiceType.create(type, subtype);
try {
ServerAPI.registerService(packageName, service, true, serviceType, null, null, null);
} catch (ServiceSetupException ex) {
ExceptionHelper.raise(ex);
}
}
/**
* Creates a new flow service in the given package with the given name.
*
* @param packageName The name of the package to create the service in.
* @param serviceName The fully-qualified name of the service to be created.
* @throws ServiceException If an error creating the service occurs.
*/
public static void create(String packageName, String serviceName) throws ServiceException {
create(packageName, serviceName, null, null);
}
/**
* Returns information about the service with the given name.
*
* @param serviceName The name of the service to be reflected on.
* @return An IData document containing information about the service.
*/
public static IData reflect(String serviceName) {
if (serviceName == null) return null;
BaseService service = Namespace.getService(NSName.create(serviceName));
if (service == null) return null;
IData output = IDataFactory.create();
IDataCursor cursor = output.getCursor();
IDataUtil.put(cursor, "name", serviceName);
IDataUtil.put(cursor, "type", service.getServiceType().getType());
IDataUtil.put(cursor, "package", service.getPackageName());
IDataHelper.put(cursor, "description", service.getComment(), false);
IDataUtil.put(cursor, "references", getReferences(service.getNSName().getFullName()));
IDataUtil.put(cursor, "dependents", getDependents(service.getNSName().getFullName()));
cursor.destroy();
return output;
}
/**
* Returns the list of services that are dependent on the given list of services.
*
* @param services The services to get dependents for.
* @return The list of dependents for the given services.
*/
public static IData getDependents(String ...services) {
DependencyManager manager = NSDependencyManager.current();
Namespace namespace = Namespace.current();
SortedSet<String> packages = new TreeSet<String>();
SortedMap<String, IData> nodes = new TreeMap<String, IData>();
if (services != null) {
for (String service : services) {
if (service != null) {
NSNode node = namespace.getNode(service);
if (node != null) {
IData results = manager.getDependent(node, null);
if (results != null) {
IDataCursor resultsCursor = results.getCursor();
IData[] referencedBy = IDataUtil.getIDataArray(resultsCursor, "referencedBy");
resultsCursor.destroy();
if (referencedBy != null) {
for (IData dependent : referencedBy) {
if (dependent != null) {
IDataCursor dependentCursor = dependent.getCursor();
String name = IDataUtil.getString(dependentCursor, "name");
dependentCursor.destroy();
String[] parts = name.split("\\/");
if (parts.length > 1) {
IData result = IDataFactory.create();
IDataCursor resultCursor = result.getCursor();
IDataUtil.put(resultCursor, "package", parts[0]);
IDataUtil.put(resultCursor, "node", parts[1]);
resultCursor.destroy();
packages.add(parts[0]);
nodes.put(name, result);
}
}
}
}
}
}
}
}
}
IData output = IDataFactory.create();
IDataCursor cursor = output.getCursor();
IDataUtil.put(cursor, "packages", packages.toArray(new String[packages.size()]));
IDataUtil.put(cursor, "packages.length", IntegerHelper.emit(packages.size()));
IDataUtil.put(cursor, "nodes", nodes.values().toArray(new IData[nodes.size()]));
IDataUtil.put(cursor, "nodes.length", IntegerHelper.emit(nodes.size()));
cursor.destroy();
return output;
}
/**
* Returns the list of elements referenced by the given list of services.
*
* @param services The list of services to get references for.
* @return The list of references for the given services.
*/
public static IData getReferences(String ...services) {
DependencyManager manager = NSDependencyManager.current();
Namespace namespace = Namespace.current();
SortedSet<String> packages = new TreeSet<String>();
SortedMap<String, IData> resolved = new TreeMap<String, IData>();
SortedSet<String> unresolved = new TreeSet<String>();
if (services != null) {
for (String service : services) {
if (service != null) {
NSNode node = namespace.getNode(service);
IData results = manager.getReferenced(node, null);
if (results != null) {
IDataCursor resultsCursor = results.getCursor();
IData[] references = IDataUtil.getIDataArray(resultsCursor, "reference");
resultsCursor.destroy();
if (references != null) {
for (IData reference : references) {
if (reference != null) {
IDataCursor referenceCursor = reference.getCursor();
String name = IDataUtil.getString(referenceCursor, "name");
String status = IDataUtil.getString(referenceCursor, "status");
referenceCursor.destroy();
if (status.equals("unresolved")) {
unresolved.add(name);
} else {
String[] parts = name.split("\\/");
if (parts.length > 1) {
IData result = IDataFactory.create();
IDataCursor resultCursor = result.getCursor();
IDataUtil.put(resultCursor, "package", parts[0]);
IDataUtil.put(resultCursor, "node", parts[1]);
resultCursor.destroy();
packages.add(parts[0]);
resolved.put(name, result);
}
}
}
}
}
}
}
}
}
IData output = IDataFactory.create();
IDataCursor cursor = output.getCursor();
IDataUtil.put(cursor, "packages", packages.toArray(new String[packages.size()]));
IDataUtil.put(cursor, "packages.length", IntegerHelper.emit(packages.size()));
IDataUtil.put(cursor, "nodes", resolved.values().toArray(new IData[resolved.size()]));
IDataUtil.put(cursor, "nodes.length", IntegerHelper.emit(resolved.size()));
IDataUtil.put(cursor, "unresolved", unresolved.toArray(new String[unresolved.size()]));
IDataUtil.put(cursor, "unresolved.length", IntegerHelper.emit(unresolved.size()));
cursor.destroy();
return output;
}
/**
* Returns the invoking service.
*
* @return The invoking service.
*/
public static NSService self() {
return Service.getCallingService();
}
/**
* Sets the HTTP response status, headers, and body for the current service invocation.
*
* @param code The HTTP response status code to be returned.
* @param message The HTTP response status message to be returned; if null, the standard message for the given
* code will be used.
* @param headers The HTTP headers to be returned; if null, no custom headers will be added to the response.
* @param content The HTTP response body to be returned.
* @param contentType The MIME content type of the response body being returned.
* @param charset The character set used if a text response is being returned.
* @throws ServiceException If an I/O error occurs.
*/
public static void respond(int code, String message, IData headers, InputStream content, String contentType, Charset charset) throws ServiceException {
try {
HttpHeader response = Service.getHttpResponseHeader();
if (response == null) {
// service was not invoked via HTTP, so throw an exception for HTTP statuses >= 400
if (code >= 400) ExceptionHelper.raise(StringHelper.normalize(content, charset));
} else {
setResponseStatus(response, code, message);
setContentType(response, contentType, charset);
setHeaders(response, headers);
setResponseBody(response, content);
}
} catch (IOException ex) {
ExceptionHelper.raise(ex);
}
}
/**
* Sets the response body in the given HTTP response.
*
* @param response The HTTP response to set the response body in.
* @param content The content to set the response body to.
* @throws ServiceException If an I/O error occurs.
*/
private static void setResponseBody(HttpHeader response, InputStream content) throws ServiceException {
if (response == null) return;
try {
if (content == null) content = new ByteArrayInputStream(new byte[0]);
Service.setResponse(BytesHelper.normalize(content));
} catch (IOException ex) {
ExceptionHelper.raise(ex);
}
}
/**
* Sets the response status code and message in the given HTTP response.
*
* @param response The HTTP response to set the response status in.
* @param code The response status code.
* @param message The response status message.
*/
private static void setResponseStatus(HttpHeader response, int code, String message) {
if (response != null) {
if (message == null) message = HTTPHelper.getResponseStatusMessage(code);
response.setResponse(code, message);
}
}
/**
* Sets the Content-Type header in the given HTTP response.
*
* @param response The HTTP response to set the header in.
* @param contentType The MIME content type.
* @param charset The character set used by the content, or null if not applicable.
* @throws ServiceException If the MIME content type is malformed.
*/
private static void setContentType(HttpHeader response, String contentType, Charset charset) throws ServiceException {
if (contentType == null) contentType = MIMETypeHelper.DEFAULT_MIME_TYPE_STRING;
try {
MimeType mimeType = new MimeType(contentType);
if (charset != null) mimeType.setParameter("charset", charset.displayName());
setHeader(response, "Content-Type", mimeType);
} catch (MimeTypeParseException ex) {
ExceptionHelper.raise(ex);
}
}
/**
* Sets all HTTP header with the given keys to their associated values from the given IData document in the given
* HTTP response.
*
* @param response The HTTP response to add the header to.
* @param headers An IData document containing key value pairs to be set as headers in the given response.
*/
private static void setHeaders(HttpHeader response, IData headers) {
for (Map.Entry<String, Object> entry : IDataMap.of(headers)) {
setHeader(response, entry.getKey(), entry.getValue());
}
}
/**
* Sets the HTTP header with the given key to the given value in the given HTTP response.
*
* @param response The HTTP response to add the header to.
* @param key The header's key.
* @param value The header's value.
*/
private static void setHeader(HttpHeader response, String key, Object value) {
if (response != null && key != null) {
response.clearField(key);
if (value != null) response.addField(key, value.toString());
}
}
/**
* Returns true if the given service exists in Integration Server.
*
* @param service The service to check existence of.
* @return True if the given service exists in Integration Server.
*/
public static boolean exists(String service) {
boolean exists = false;
try {
exists = exists(service, false);
} catch(ServiceException ex) {
// ignore exceptions
}
return exists;
}
/**
* Returns true if the given service exists in Integration Server.
*
* @param service The service to check existence of.
* @param raise If true and the service does not exist, an exception will be thrown.
* @return True if the given service exists in Integration Server.
* @throws ServiceException If raise is true and the given service does not exist.
*/
public static boolean exists(String service, boolean raise) throws ServiceException {
boolean exists = NodeHelper.exists(service) && "service".equals(NodeHelper.getNodeType(service).toString());
if (raise && !exists) ExceptionHelper.raise("Service does not exist: " + service);
return exists;
}
/**
* Invokes the given service with the given pipeline synchronously.
*
* @param service The service to be invoked.
* @param pipeline The input pipeline used when invoking the service.
* @return The output pipeline returned by the service invocation.
* @throws ServiceException If the service throws an exception while being invoked.
*/
public static IData invoke(String service, IData pipeline) throws ServiceException {
return invoke(service, pipeline, true);
}
/**
* Invokes the given service with the given pipeline synchronously.
*
* @param service The service to be invoked.
* @param pipeline The input pipeline used when invoking the service.
* @param raise If true will rethrow exceptions thrown by the invoked service.
* @return The output pipeline returned by the service invocation.
* @throws ServiceException If raise is true and the service throws an exception while being invoked.
*/
public static IData invoke(String service, IData pipeline, boolean raise) throws ServiceException {
return invoke(service, pipeline, raise, true);
}
/**
* Invokes the given service with the given pipeline synchronously.
*
* @param service The service to be invoked.
* @param pipeline The input pipeline used when invoking the service.
* @param raise If true will rethrow exceptions thrown by the invoked service.
* @param clone If true the pipeline will first be cloned before being used by the invocation.
* @return The output pipeline returned by the service invocation.
* @throws ServiceException If raise is true and the service throws an exception while being invoked.
*/
public static IData invoke(String service, IData pipeline, boolean raise, boolean clone) throws ServiceException {
if (service != null) {
if (pipeline == null) pipeline = IDataFactory.create();
try {
IDataUtil.merge(Service.doInvoke(NSName.create(service), normalize(pipeline, clone)), pipeline);
} catch (Exception exception) {
if (raise) {
ExceptionHelper.raise(exception);
} else {
pipeline = addExceptionToPipeline(pipeline, exception);
}
}
}
return pipeline;
}
/**
* Executes a list of services in the order specified.
*
* @param services The list of services to be invoked.
* @param pipeline The input pipeline passed to the first service in the chain.
* @return The output pipeline returned by the final service in the chain.
* @throws ServiceException If an exception is thrown by one of the invoked services.
*/
public static IData chain(String[] services, IData pipeline) throws ServiceException {
return chain(services == null ? null : Arrays.asList(services), pipeline);
}
/**
* Executes a list of services in the order specified.
*
* @param services The list of services to be invoked.
* @param pipeline The input pipeline passed to the first service in the chain.
* @return The output pipeline returned by the final service in the chain.
* @throws ServiceException If an exception is thrown by one of the invoked services.
*/
public static IData chain(Iterable<String> services, IData pipeline) throws ServiceException {
if (services != null) {
for (String service : services) {
pipeline = ServiceHelper.invoke(service, pipeline);
}
}
return pipeline;
}
/**
* Invokes the given service with the given pipeline asynchronously (in another thread).
*
* @param service The service to be invoked.
* @param pipeline The input pipeline used when invoking the service.
* @return The thread on which the service is being invoked.
*/
public static ServiceThread fork(String service, IData pipeline) {
if (service == null) return null;
return Service.doThreadInvoke(NSName.create(service), normalize(pipeline));
}
/**
* Waits for an asynchronously invoked service to complete.
*
* @param serviceThread The service thread to wait on to finish.
* @return The output pipeline from the service invocation executed by the given thread.
* @throws ServiceException If an error occurs when waiting on the thread to finish.
*/
public static IData join(ServiceThread serviceThread) throws ServiceException {
return join(serviceThread, true);
}
/**
* Waits for an asynchronously invoked service to complete.
*
* @param serviceThread The service thread to wait on to finish.
* @param raise If true rethrows any exception thrown by the invoked service.
* @return The output pipeline from the service invocation executed by the given thread.
* @throws ServiceException If raise is true and an error occurs when waiting on the thread to finish.
*/
public static IData join(ServiceThread serviceThread, boolean raise) throws ServiceException {
IData pipeline = null;
if (serviceThread != null) {
try {
pipeline = serviceThread.getIData();
} catch (Throwable exception) {
if (raise) {
ExceptionHelper.raise(exception);
} else {
pipeline = addExceptionToPipeline(null, exception);
}
}
}
return pipeline;
}
/**
* Invokes the given service a given number of times, and returns execution duration statistics. Exceptions thrown
* by the service are ignored / suppressed.
*
* @param service The service to benchmark.
* @param pipeline The input pipeline used when invoking the service.
* @param count The sample count, or in other words the number of times to invoke the service.
* @return Execution duration statistics generated by benchmarking the given service.
*/
public static NormalDistributionEstimator benchmark(String service, IData pipeline, int count) {
NormalDistributionEstimator estimator = null;
try {
estimator = benchmark(service, pipeline, count, false);
} catch(ServiceException ex) {
// ignore exceptions
}
return estimator;
}
/**
* Invokes the given service a given number of times, and returns execution duration statistics.
*
* @param service The service to benchmark.
* @param pipeline The input pipeline used when invoking the service.
* @param count The sample count, or in other words the number of times to invoke the service.
* @param raise If true, exceptions thrown by the service will abort the benchmark.
* @return Execution duration statistics generated by benchmarking the given service.
* @throws ServiceException If the invoked service throws an exception and raise is true.
*/
public static NormalDistributionEstimator benchmark(String service, IData pipeline, int count, boolean raise) throws ServiceException {
if (service == null) throw new NullPointerException("service must not be null");
if (count <= 0) throw new IllegalArgumentException("count must be greater than zero");
if (pipeline == null) pipeline = IDataFactory.create();
NormalDistributionEstimator estimator = new NormalDistributionEstimator("ms");
exists(service, true);
for (int i = 0; i < count; i++) {
IData scope = IDataUtil.clone(pipeline);
long start = System.currentTimeMillis();
try {
invoke(service, scope, raise, false);
} finally {
long end = System.currentTimeMillis();
estimator.add(end - start);
}
}
return estimator;
}
/**
* Provides a try/catch/finally pattern for flow services.
*
* @param tryService The service to be executed in the try clause of the try/catch/finally pattern.
* @param catchService The service to be executed in the catch clause of the try/catch/finally pattern.
* @param finallyService The service to be executed in the finally clause of the try/catch/finally pattern.
* @param pipeline The input pipeline used when invoking the services.
* @return The output pipeline containing the results of the try/catch/finally pattern.
* @throws ServiceException If the service throws an exception while being invoked, and either no catch service is
* specified, or the catch service rethrows the exception.
*/
public static IData ensure(String tryService, String catchService, String finallyService, IData pipeline) throws ServiceException {
try {
pipeline = invoke(tryService, pipeline);
} catch (Throwable exception) {
pipeline = rescue(catchService, pipeline, exception);
} finally {
pipeline = invoke(finallyService, pipeline);
}
return pipeline;
}
/**
* Handles an exception using the given catch service.
*
* @param catchService The service to invoke to handle the given exception.
* @param pipeline The input pipeline for the service.
* @param exception The exception to be handled.
* @return The output pipeline returned by invoking the given catchService.
* @throws ServiceException If the given catchService encounters an error.
*/
public static IData rescue(String catchService, IData pipeline, Throwable exception) throws ServiceException {
if (catchService == null) {
ExceptionHelper.raise(exception);
} else {
pipeline = invoke(catchService, addExceptionToPipeline(pipeline, exception));
}
return pipeline;
}
/**
* Adds the given exception and related variables that describe the exception to the given IData pipeline.
*
* @param pipeline The pipeline to add the exception to.
* @param exception The exception to be added.
* @return The pipeline with the added exception.
*/
private static IData addExceptionToPipeline(IData pipeline, Throwable exception) {
if (pipeline == null) pipeline = IDataFactory.create();
if (exception != null) {
IDataCursor cursor = pipeline.getCursor();
try {
IDataUtil.put(cursor, "$exception", exception);
IDataUtil.put(cursor, "$exception?", "true");
IDataUtil.put(cursor, "$exception.class", exception.getClass().getName());
IDataUtil.put(cursor, "$exception.message", exception.getMessage());
InvokeState invokeState = InvokeState.getCurrentState();
if (invokeState != null) {
IData exceptionInfo = IDataHelper.duplicate(invokeState.getErrorInfoFormatted(), true);
if (exceptionInfo != null) {
IDataCursor ec = exceptionInfo.getCursor();
String exceptionService = IDataUtil.getString(ec, "service");
if (exceptionService != null) {
IDataUtil.put(cursor, "$exception.service", exceptionService);
BaseService baseService = Namespace.getService(NSName.create(exceptionService));
if (baseService != null) {
String packageName = baseService.getPackageName();
if (packageName != null) {
IDataUtil.put(ec, "package", packageName);
IDataUtil.put(cursor, "$exception.package", packageName);
}
}
}
ec.destroy();
IDataUtil.put(cursor, "$exception.info", exceptionInfo);
}
}
IDataUtil.put(cursor, "$exception.stack", ExceptionHelper.getStackTrace(exception));
} finally {
cursor.destroy();
}
}
return pipeline;
}
/**
* Returns a new IData if the given pipeline is null, otherwise returns a clone of the given pipeline.
*
* @param pipeline The pipeline to be normalized.
* @return The normalized pipeline.
*/
private static IData normalize(IData pipeline) {
return normalize(pipeline, true);
}
/**
* Returns a new IData if the given pipeline is null, otherwise returns a clone of the given pipeline.
*
* @param pipeline The pipeline to be normalized.
* @param clone If true the pipeline will be cloned, otherwise it will be returned as is.
* @return The normalized pipeline.
*/
private static IData normalize(IData pipeline, boolean clone) {
if (pipeline == null) {
pipeline = IDataFactory.create();
} else if (clone) {
pipeline = IDataUtil.clone(pipeline);
}
return pipeline;
}
/**
* Returns a BaseService object given a service name.
*
* @param serviceName The name of the service to be returned.
* @return The BaseService object representing the service with the given name.
*/
public static BaseService getService(String serviceName) {
if (serviceName == null) return null;
return Namespace.getService(NodeHelper.getName(serviceName));
}
/**
* Returns the name of the package the given service resides in.
*
* @param serviceName The name of the service whose package is to be returned.
* @return The name of the package the given service resides in.
*/
public static String getPackageName(String serviceName) {
return getPackageName(getService(serviceName));
}
/**
* Returns the name of the package the given service resides in.
*
* @param service The service whose package is to be returned.
* @return The name of the package the given service resides in.
*/
public static String getPackageName(BaseService service) {
if (service == null) return null;
return service.getPackageName();
}
}