/*
* 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.invoke;
import com.wm.app.b2b.server.BaseService;
import com.wm.app.b2b.server.invoke.ServiceStatus;
import com.wm.data.IData;
import com.wm.data.IDataUtil;
import com.wm.util.ServerException;
import com.wm.util.coder.IDataCodable;
import com.wm.util.coder.IDataXMLCoder;
import permafrost.tundra.data.IDataMap;
import permafrost.tundra.io.FileHelper;
import permafrost.tundra.lang.BooleanHelper;
import permafrost.tundra.time.DateTimeHelper;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
/**
* A service invocation processor that saves input and output pipelines to disk.
*/
public class PipelineCaptureProcessor extends AbstractInvokeChainProcessor implements IDataCodable {
/**
* The default service pattern: matches all services.
*/
public static Pattern DEFAULT_SERVICE_PATTERN = Pattern.compile(".*");
/**
* The default pipeline directory.
*/
public static File DEFAULT_DIRECTORY = new File("./pipeline");
/**
* A thread-pool for asynchronously saving pipelines to disk, so that invocation performance is minimally affected.
*/
protected ExecutorService executor;
/**
* A regular expression which if matching the invoked service will save the pipeline to disk.
*/
protected volatile Pattern servicePattern;
/**
* The directory in which the pipelines are saved.
*/
protected volatile File directory;
/**
* When the capture was started.
*/
protected volatile long startTime;
/**
* The local host name, used when naming pipeline files.
*/
protected volatile String localhost;
/**
* Atomic counter incremented for each saved pipeline.
*/
protected AtomicLong count = new AtomicLong(0);
/**
* The datetime format used in the saved pipeline file names.
*/
protected static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMddHHmmssSSS");
/**
* Creates a new pipeline capture processor using default settings.
*/
public PipelineCaptureProcessor() {
this(DEFAULT_SERVICE_PATTERN, DEFAULT_DIRECTORY);
}
/**
* Creates a new pipeline capture processor.
* @param servicePattern The regular expression that if matching an invoked service will save that service's pipeline.
* @param directory The directory to which to save the pipelines.
*/
public PipelineCaptureProcessor(Pattern servicePattern, File directory) {
setServicePattern(servicePattern);
setDirectory(directory);
}
/**
* Returns the regular expression used to match service names.
* @return the regular expression used to match service names.
*/
public Pattern getServicePattern() {
return servicePattern;
}
/**
* Sets the regular expression used to match service names.
* @param servicePattern The regular expression used to match service names.
*/
public synchronized void setServicePattern(Pattern servicePattern) {
if (servicePattern == null) throw new NullPointerException("servicePattern must not be null");
this.servicePattern = servicePattern;
}
/**
* Returns the directory the pipeline files are saved to.
* @return the directory the pipeline files are saved to.
*/
public File getDirectory() {
return new File(directory.getAbsolutePath());
}
/**
* Sets the directory the pipeline files are saved to.
* @param directory The directory the pipeline files are saved to.
*/
public synchronized void setDirectory(File directory) {
if (directory == null) throw new NullPointerException("directory must not be null");
this.directory = directory;
this.directory.mkdirs();
}
/**
* Sets the local host name member variable using a DNS lookup.
*/
private void resolveLocalHost() {
try {
localhost = sanitize(InetAddress.getLocalHost().getHostName().toLowerCase());
} catch(UnknownHostException ex) {
localhost = "unknown";
}
}
/**
* Registers this class as an invocation handler and starts saving pipelines.
*/
public synchronized void start() {
if (!started) {
resolveLocalHost();
executor = Executors.newSingleThreadExecutor(new NamedThreadFactory());
startTime = System.currentTimeMillis();
super.start();
}
}
/**
* Unregisters this class as an invocation handler and stops saving pipelines.
*/
public synchronized void stop() {
if (started) {
super.stop();
// disable new tasks from being submitted
executor.shutdown();
try {
// wait a while for existing tasks to terminate
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// cancel currently executing tasks
executor.shutdownNow();
}
} catch(InterruptedException ex) {
// cancel if current thread also interrupted
executor.shutdownNow();
// preserve interrupt status
Thread.currentThread().interrupt();
}
count.set(0);
}
}
/**
* Processes a service invocation by saving the input and output pipeline to disk.
*
* @param iterator Invocation chain.
* @param baseService The invoked service.
* @param pipeline The input pipeline for the service.
* @param serviceStatus The status of the service invocation.
* @throws ServerException If the service invocation fails.
*/
@Override
public void process(Iterator iterator, BaseService baseService, IData pipeline, ServiceStatus serviceStatus) throws ServerException {
File directory = this.directory; // cache this locally, so that input and output pipelines are always written together to same directory
String serviceName = baseService.getNSName().getFullName();
String sanitizedServiceName = null;
String startDateTime = null;
long id = 0;
boolean matches = servicePattern.matcher(serviceName).matches();
if (matches) {
sanitizedServiceName = sanitize(serviceName);
startDateTime = DATE_FORMATTER.format(new Date());
id = count.incrementAndGet();
}
try {
if (matches) {
try {
executor.execute(new SavePipelineToFileRunnable(pipeline, generatePipelineFilename(directory, sanitizedServiceName, startDateTime, id, "input")));
} catch(RejectedExecutionException ex) {
// do nothing, executor has been shutdown
}
}
super.process(iterator, baseService, pipeline, serviceStatus);
} finally {
if (matches) {
try {
executor.execute(new SavePipelineToFileRunnable(pipeline, generatePipelineFilename(directory, sanitizedServiceName, startDateTime, id, "output")));
} catch(RejectedExecutionException ex) {
// do nothing, executor has been shutdown
}
}
}
}
/**
* Returns a string sanitized for use in a file name.
*
* @param string The string to sanitize.
* @return The sanitized string.
*/
protected String sanitize(String string) {
return string == null ? "" : string.replaceAll("\\W+", "-");
}
/**
* Returns a new file name for saving a pipeline to.
*
* @param directory The parent directory for the pipeline files.
* @param serviceName The name of the service the pipeline belongs to.
* @param startDateTime The formatted start datetime of the service.
* @param id The ID of the invocation.
* @param suffix A suffix for the file name, such as "input" or "output".
* @return A file name suitable for saving the pipeline to.
*/
protected File generatePipelineFilename(File directory, String serviceName, String startDateTime, long id, String suffix) {
return new File(directory, String.format("%s_%019d_%019d_%s_%s_%s.%s", localhost, startTime, id, startDateTime, serviceName, suffix, "xml"));
}
/**
* Sets the regular expression pattern used for matching service name and the directory in which
* pipeline files are saved from the given IData document.
*
* @param document An IData document containing the keys: pattern, directory.
*/
@Override
public void setIData(IData document) {
IDataMap map = new IDataMap(document);
String pattern = (String)map.get("pattern");
String directory = (String)map.get("directory");
setServicePattern(Pattern.compile(pattern));
setDirectory(FileHelper.construct(directory));
}
/**
* Returns an IData representation of this object.
* @return An IData representation of this object.
*/
@Override
public IData getIData() {
IDataMap map = new IDataMap();
map.put("pattern", getServicePattern().toString());
map.put("directory", FileHelper.normalize(getDirectory()));
map.put("started?", BooleanHelper.emit(started));
if (started) {
map.put("start", DateTimeHelper.format(startTime));
map.put("count", "" + count.get());
}
return map;
}
/**
* Thread factory that names the returned threads.
*/
private class NamedThreadFactory implements ThreadFactory {
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setName("Tundra/PipelineCaptureProcessor#" + thread.getId());
return thread;
}
}
/**
* A runnable which saves a pipeline to a file.
*/
private static class SavePipelineToFileRunnable implements Runnable {
/**
* The IData pipeline to be saved.
*/
protected IData pipeline;
/**
* The file to save the pipeline to.
*/
protected File target;
/**
* Creates a new runnable that saves the given pipeline to the given file.
* @param pipeline The pipeline to be saved.
* @param target The file to save the pipeline to.
*/
public SavePipelineToFileRunnable(IData pipeline, File target) {
if (pipeline == null) throw new NullPointerException("pipeline must not be null");
if (target == null) throw new NullPointerException("target must not be null");
try {
this.pipeline = IDataUtil.deepClone(pipeline);
this.target = target;
} catch(IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* Saves the IData pipeline to the target file.
*/
@Override
public void run() {
try {
IDataXMLCoder coder = new IDataXMLCoder();
coder.writeToFile(target, pipeline);
} catch(IOException ex) {
throw new RuntimeException(ex);
}
}
}
}