Newer
Older
import cz.it4i.qcmp.cli.CliConstants;
import cz.it4i.qcmp.cli.ParseUtils;
import cz.it4i.qcmp.compression.CompressionOptions;
import cz.it4i.qcmp.data.V2i;
import cz.it4i.qcmp.data.V3i;
import cz.it4i.qcmp.fileformat.QuantizationType;
import mpicbg.spim.data.SpimDataException;
import org.apache.commons.cli.*;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Map.Entry;
/**
* Serve XML/HDF5 datasets over HTTP.
*
* <pre>
* usage: BigDataServer [OPTIONS] [NAME XML]...
* Serves one or more XML/HDF5 datasets for remote access over HTTP.
* Provide (NAME XML) pairs on the command line or in a dataset file, where
* NAME is the name under which the dataset should be made accessible and XML
* is the path to the XML file of the dataset.
* -d <FILE> Dataset file: A plain text file specifying one dataset
* per line. Each line is formatted as "NAME <TAB> XML".
* -p <PORT> Listening port. (default: 8080)
* -s <HOSTNAME> Hostname of the server.
* -t <DIRECTORY> Directory to store thumbnails. (new temporary directory
* by default.)
* -m enable statistics and manager context. EXPERIMENTAL!
* To enable the {@code -m} option, build with
* {@link Constants#ENABLE_EXPERIMENTAL_FEATURES} set to {@code true}.
*
* @author Tobias Pietzsch <tobias.pietzsch@gmail.com>
* @author HongKee Moon <moon@mpi-cbg.quantization.de>
public class BigDataServer {
private static final org.eclipse.jetty.util.log.Logger LOG = Log.getLogger(BigDataServer.class);
public final static class ExtendedCompressionOptions extends CompressionOptions {
private int compressFromMipmapLevel;
private boolean enableCodebookTraining = false;
public int getCompressFromMipmapLevel() {
return compressFromMipmapLevel;
}
public void setCompressFromMipmapLevel(final int compressFromMipmapLevel) {
this.compressFromMipmapLevel = compressFromMipmapLevel;
}
public boolean isCodebookTrainingEnabled() {
return enableCodebookTraining;
}
public void setEnableCodebookTraining(final boolean enableCodebookTraining) {
this.enableCodebookTraining = enableCodebookTraining;
}
public ExtendedCompressionOptions copyRequiredParams() {
final ExtendedCompressionOptions copy = new ExtendedCompressionOptions();
copy.setQuantizationType(getQuantizationType());
copy.setQuantizationVector(getQuantizationVector());
copy.setWorkerCount(getWorkerCount());
copy.setCodebookType(getCodebookType());
copy.setCodebookCacheFolder(getCodebookCacheFolder());
copy.setVerbose(isVerbose());
copy.setCompressFromMipmapLevel(getCompressFromMipmapLevel());
return copy;
}
final static class BigDataServerDataset {
private final String xmlFile;
private final ExtendedCompressionOptions compressionOptions;
BigDataServerDataset(final String xmlFile,
final ExtendedCompressionOptions compressionOptions) {
this.xmlFile = xmlFile;
this.compressionOptions = compressionOptions;
}
public String getXmlFile() {
return xmlFile;
}
public ExtendedCompressionOptions getCompressionOptions() {
return compressionOptions;
}
}
static Parameters getDefaultParameters() {
final int port = 8080;
String hostname;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (final UnknownHostException e) {
hostname = "localhost";
}
final String thumbnailDirectory = null;
final String baseUrl = null;
final boolean enableManagerContext = false;
return new Parameters(port,
hostname,
new HashMap<String, BigDataServerDataset>(),
new ExtendedCompressionOptions());
}
public static void main(final String[] args) throws Exception {
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog");
final Parameters params = processOptions(args, getDefaultParameters());
if (params == null)
return;
final String thumbnailsDirectoryName = getThumbnailDirectoryPath(params);
// Threadpool for multiple connections
final Server server = new Server(new QueuedThreadPool(200, 8));
// ServerConnector configuration
final ServerConnector connector = new ServerConnector(server);
connector.setHost(params.getHostname());
connector.setPort(params.getPort());
LOG.info("Set connectors: " + connector);
server.setConnectors(new Connector[]{connector});
final String baseURL = params.getBaseUrl() != null ? params.getBaseUrl() :
"http://" + server.getURI().getHost() + ":" + params.getPort();
System.out.println("baseURL = " + baseURL);
// Handler initialization
final HandlerCollection handlers = new HandlerCollection();
final ContextHandlerCollection datasetHandlers = createHandlers(baseURL,
params.getDatasets(),
thumbnailsDirectoryName);
handlers.addHandler(datasetHandlers);
handlers.addHandler(new JsonDatasetListHandler(server, datasetHandlers));
Handler handler = handlers;
if (params.enableManagerContext()) {
// Add Statistics bean to the connector
final ConnectorStatistics connectorStats = new ConnectorStatistics();
connector.addBean(connectorStats);
// create StatisticsHandler wrapper and ManagerHandler
final StatisticsHandler statHandler = new StatisticsHandler();
handlers.addHandler(new ManagerHandler(baseURL, server, connectorStats, statHandler, datasetHandlers, thumbnailsDirectoryName));
statHandler.setHandler(handlers);
handler = statHandler;
LOG.info("Set handler: " + handler);
server.setHandler(handler);
LOG.info("Server Base URL: " + baseURL);
LOG.info("BigDataServer starting");
server.start();
server.join();
}
/**
* Server parameters: hostname, port, datasets.
*/
private static class Parameters {
private final int port;
private final String hostname;
/**
* maps from dataset name to dataset xml path.
*/
private final Map<String, BigDataServerDataset> datasetNameToXml;
private final String thumbnailDirectory;
private final String baseUrl;
private final boolean enableManagerContext;
private final ExtendedCompressionOptions compressionParam;
Parameters(final int port,
final String hostname,
final Map<String, BigDataServerDataset> datasetNameToXml,
final String thumbnailDirectory,
final String baseUrl,
final boolean enableManagerContext,
final ExtendedCompressionOptions extendedCompressionOptions) {
this.port = port;
this.hostname = hostname;
this.datasetNameToXml = datasetNameToXml;
this.thumbnailDirectory = thumbnailDirectory;
this.baseUrl = baseUrl;
this.enableManagerContext = enableManagerContext;
this.compressionParam = extendedCompressionOptions;
}
public int getPort() {
return port;
}
public String getHostname() {
return hostname;
}
public String getBaseUrl() {
return baseUrl;
}
public String getThumbnailDirectory() {
return thumbnailDirectory;
}
/**
* Get datasets.
*
* @return datasets as a map from dataset name to dataset xml path.
*/
public Map<String, BigDataServerDataset> getDatasets() {
return datasetNameToXml;
}
public boolean enableManagerContext() {
return enableManagerContext;
}
public ExtendedCompressionOptions getCompressionParams() {
return compressionParam;
}
}
@SuppressWarnings("static-access")
static private Parameters processOptions(final String[] args, final Parameters defaultParameters) throws IOException {
final String ENABLE_COMPRESSION = "qcmp";
final String CompressFromShortKey = "cf";
final String CompressFromLongKey = "compress-from";
// create Options object
final Options options = new Options();
final String cmdLineSyntax = "BigDataServer [OPTIONS] [NAME XML] ...\n";
final String description =
"Serves one or more XML/HDF5 datasets for remote access over HTTP.\n" +
"Provide (NAME XML) pairs on the command line or in a dataset file, where NAME is the name under which the " +
"dataset should be made accessible and XML is the path to the XML file of the dataset.\n" +
"If -qcmp option is specified, these options are enabled:\u001b[35m-sq,-vq,-b,-cbc\u001b[0m\n";
options.addOption(OptionBuilder
.withDescription("Hostname of the server.\n(default: " + defaultParameters.getHostname() + ")")
.hasArg()
.withArgName("HOSTNAME")
.create("s"));
options.addOption(OptionBuilder
.withDescription("Listening port.\n(default: " + defaultParameters.getPort() + ")")
.hasArg()
.withArgName("PORT")
.create("p"));
// -d or multiple {name name.xml} pairs
options.addOption(OptionBuilder
.withDescription(
"Dataset file: A plain text file specifying one dataset per line. Each line is formatted as " +
"\"NAME <TAB> XML\".")
.hasArg()
.withArgName("FILE")
.create("d"));
options.addOption(OptionBuilder
.withDescription("Directory to store thumbnails. (new temporary directory by default.)")
.hasArg()
.withArgName("DIRECTORY")
.create("t"));
options.addOption(OptionBuilder
.withDescription("Base URL under which the server will be made visible (e.g., if behind a proxy)")
.hasArg()
.withArgName("BASEURL")
.create("b"));
options.addOption(new OptionWithOrder(OptionBuilder.withDescription("Enable QCMP compression")
.create(ENABLE_COMPRESSION), ++optionOrder));
options.addOption(new OptionWithOrder(CliConstants.createCBCMethod(), ++optionOrder));
// options.addOption(new OptionWithOrder(CliConstants.createSQOption(), ++optionOrder));
options.addOption(new OptionWithOrder(CliConstants.createVQOption(), ++optionOrder));
options.addOption(new OptionWithOrder(CliConstants.createVerboseOption(false), ++optionOrder));
options.addOption(new OptionWithOrder(new Option(CompressFromShortKey, CompressFromLongKey, true,
"Level from which the compression is enabled."), ++optionOrder));
options.addOption(new OptionWithOrder(new Option(CliConstants.WORKER_COUNT_SHORT, CliConstants.WORKER_COUNT_LONG,
true, "Count of worker threads, which are used for codebook training."),
++optionOrder));
if (Constants.ENABLE_EXPERIMENTAL_FEATURES) {
options.addOption(OptionBuilder
.withDescription("enable statistics and manager context. EXPERIMENTAL!")
.create("m"));
}
try {
final CommandLineParser parser = new BasicParser();
final CommandLine cmd = parser.parse(options, args);
// Getting port number option
final String portString = cmd.getOptionValue("p", Integer.toString(defaultParameters.getPort()));
final int port = Integer.parseInt(portString);
// Getting server name option
final String serverName = cmd.getOptionValue("s", defaultParameters.getHostname());
// Getting thumbnail directory option
final String thumbnailDirectory = cmd.getOptionValue("t", defaultParameters.getThumbnailDirectory());
// Getting base url option
final String baseUrl = cmd.getOptionValue("b", defaultParameters.getBaseUrl());
final HashMap<String, BigDataServerDataset> datasets =
new HashMap<String, BigDataServerDataset>(defaultParameters.getDatasets());
final boolean enableQcmpCompression = cmd.hasOption(ENABLE_COMPRESSION);
final ExtendedCompressionOptions baseCompressionOptions = new ExtendedCompressionOptions();
baseCompressionOptions.setWorkerCount(Integer.parseInt(cmd.getOptionValue(CliConstants.WORKER_COUNT_LONG, "1")));
baseCompressionOptions.setQuantizationType(QuantizationType.Invalid);
if (cmd.hasOption(CliConstants.SCALAR_QUANTIZATION_LONG))
baseCompressionOptions.setQuantizationType(QuantizationType.Scalar);
else if (cmd.hasOption(CliConstants.VECTOR_QUANTIZATION_LONG)) {
final String vqValue = cmd.getOptionValue(CliConstants.VECTOR_QUANTIZATION_LONG);
final Optional<V2i> maybeV2 = ParseUtils.tryParseV2i(vqValue, 'x');
baseCompressionOptions.setQuantizationType(QuantizationType.Vector2D);
baseCompressionOptions.setQuantizationVector(new V3i(maybeV2.get().getX(), maybeV2.get().getY(), 1));
final Optional<V3i> maybeV3 = ParseUtils.tryParseV3i(vqValue, 'x');
if (maybeV3.get().getZ() == 1) { // Actually it is 2D quantization!
baseCompressionOptions.setQuantizationType(QuantizationType.Vector2D);
} else {
baseCompressionOptions.setQuantizationType(QuantizationType.Vector3D);
}
baseCompressionOptions.setQuantizationVector(maybeV3.get());
if (baseCompressionOptions.getQuantizationType() == QuantizationType.Invalid) {
throw new ParseException("Invalid quantization type.");
}
baseCompressionOptions.setCodebookType(CompressionOptions.CodebookType.Global);
baseCompressionOptions.setCodebookCacheFolder(cmd.getOptionValue(CliConstants.CODEBOOK_CACHE_FOLDER_LONG));
baseCompressionOptions.setVerbose(cmd.hasOption(CliConstants.VERBOSE_LONG));
if (cmd.hasOption(CompressFromLongKey)) {
baseCompressionOptions.setCompressFromMipmapLevel(Integer.parseInt(cmd.getOptionValue(CompressFromLongKey)));
final StringBuilder compressionReport = new StringBuilder();
compressionReport.append("\u001b[33m");
compressionReport.append("Quantization type: ");
switch (baseCompressionOptions.getQuantizationType()) {
compressionReport.append("Vector2D");
break;
case Vector3D:
compressionReport.append("Vector3D");
compressionReport.append(baseCompressionOptions.getQuantizationVector().toString());
compressionReport.append("Codebook cache folder: ").append(baseCompressionOptions.getCodebookCacheFolder()).append('\n');
compressionReport.append("Verbose mode: ").append(baseCompressionOptions.isVerbose() ? "ON" : "OFF").append('\n');
compressionReport.append("Worker count: ").append(baseCompressionOptions.getWorkerCount()).append('\n');
compressionReport.append("CompressFromMipmapLevel: ").append(baseCompressionOptions.getCompressFromMipmapLevel()).append(
'\n');
System.out.println(compressionReport.toString());
}
boolean enableManagerContext = false;
if (Constants.ENABLE_EXPERIMENTAL_FEATURES) {
if (cmd.hasOption("m"))
enableManagerContext = true;
}
if (cmd.hasOption("d")) {
// process the file given with "-d"
final String datasetFile = cmd.getOptionValue("d");
// check the file presence
final Path path = Paths.get(datasetFile);
if (Files.notExists(path))
throw new IllegalArgumentException("Dataset list file does not exist.");
// Process dataset list file
final List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (final String str : lines) {
final String[] tokens = str.split("\\s*\\t\\s*");
if (tokens.length >= 2 && StringUtils.isNotEmpty(tokens[0].trim()) && StringUtils.isNotEmpty(tokens[1].trim())) {
final String name = tokens[0].trim();
final String xmlPath = tokens[1].trim();
final ExtendedCompressionOptions datasetCompressionOptions = baseCompressionOptions.copyRequiredParams();
if (tokens.length == 3 && StringUtils.isNotEmpty(tokens[2].trim())) {
if (tokens[2].trim().equals("tcb")) {
datasetCompressionOptions.setEnableCodebookTraining(true);
}
}
tryAddDataset(datasets, name, xmlPath, (enableQcmpCompression ? datasetCompressionOptions : null));
} else {
LOG.warn("Invalid dataset file line (will be skipped): {" + str + "}");
}
}
}
// process additional {name, name.xml, [`tcb`]} pair or triplets given on the command-line
final String[] leftoverArgs = cmd.getArgs();
for (int i = 0; i < leftoverArgs.length; ) {
final String name = leftoverArgs[i++];
final String xmlPath = leftoverArgs[i++];
final ExtendedCompressionOptions datasetCompressionOptions = baseCompressionOptions.copyRequiredParams();
if ((i < leftoverArgs.length) && leftoverArgs[i].equals("tcb")) {
datasetCompressionOptions.setEnableCodebookTraining(true);
i++;
}
tryAddDataset(datasets, name, xmlPath, (enableQcmpCompression ? datasetCompressionOptions : null));
}
if (datasets.isEmpty())
throw new IllegalArgumentException("Dataset list is empty.");
return new Parameters(port,
serverName,
datasets,
thumbnailDirectory,
enableQcmpCompression ? baseCompressionOptions : null);
} catch (final ParseException | IllegalArgumentException e) {
LOG.warn(e.getMessage());
System.out.println();
final HelpFormatter formatter = new HelpFormatter();
formatter.setOptionComparator((x, y) -> {
if (x instanceof OptionWithOrder && y instanceof OptionWithOrder) {
return ((OptionWithOrder) x).compareTo((OptionWithOrder) y);
} else if (x instanceof OptionWithOrder) {
return 1;
} else if (y instanceof OptionWithOrder) {
return -1;
} else {
final Option opt1 = (Option) x;
final Option opt2 = (Option) y;
return opt1.getOpt().compareToIgnoreCase(opt2.getOpt());
}
});
formatter.printHelp(cmdLineSyntax, description, options, null);
}
return null;
}
private static void tryAddDataset(final HashMap<String, BigDataServerDataset> datasetNameToXML,
final String xmlPath,
final ExtendedCompressionOptions extendedCompressionOptions) throws IllegalArgumentException {
for (final String reserved : Constants.RESERVED_CONTEXT_NAMES)
if (name.equals(reserved))
throw new IllegalArgumentException("Cannot use dataset name: \"" + name + "\" (reserved for internal use).");
if (datasetNameToXML.containsKey(name))
throw new IllegalArgumentException("Duplicate dataset name: \"" + name + "\"");
if (Files.notExists(Paths.get(xmlPath)))
throw new IllegalArgumentException("Dataset file does not exist: \"" + xmlPath + "\"");
datasetNameToXML.put(name, new BigDataServerDataset(xmlPath, extendedCompressionOptions));
LOG.info("Dataset added: {" + name + ", " + xmlPath +
", QcmpDatasetTraining: " + ((extendedCompressionOptions.isCodebookTrainingEnabled()) ? "ON" : "OFF") + "}");
}
private static String getThumbnailDirectoryPath(final Parameters params) throws IOException {
final String thumbnailDirectoryName = params.getThumbnailDirectory();
if (thumbnailDirectoryName != null) {
Path thumbnails = Paths.get(thumbnailDirectoryName);
if (!Files.exists(thumbnails)) {
try {
thumbnails = Files.createDirectories(thumbnails);
return thumbnails.toFile().getAbsolutePath();
} catch (final IOException e) {
LOG.warn(e.getMessage());
LOG.warn("Could not create thumbnails directory \"" + thumbnailDirectoryName + "\".\n Trying to create temporary " +
"directory.");
}
} else {
if (!Files.isDirectory(thumbnails))
LOG.warn("Thumbnails directory \"" + thumbnailDirectoryName + "\" is not a directory.\n Trying to create temporary " +
"directory.");
else
return thumbnails.toFile().getAbsolutePath();
}
}
final Path thumbnails = Files.createTempDirectory("thumbnails");
thumbnails.toFile().deleteOnExit();
return thumbnails.toFile().getAbsolutePath();
}
private static ContextHandlerCollection createHandlers(final String baseURL,
final Map<String, BigDataServerDataset> dataSet,
final String thumbnailsDirectoryName)
throws SpimDataException, IOException {
final ContextHandlerCollection handlers = new ContextHandlerCollection();
for (final Entry<String, BigDataServerDataset> entry : dataSet.entrySet()) {
final String name = entry.getKey();
final String xmlPath = entry.getValue().getXmlFile();
final CellHandler ctx = new CellHandler(baseURL + context + "/", xmlPath,
name, thumbnailsDirectoryName,
entry.getValue().getCompressionOptions());
ctx.setContextPath(context);
handlers.addHandler(ctx);
}
return handlers;
}