package org.dcache.services.info;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.TimeoutCacheException;
import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellMessageSender;
import dmg.cells.nucleus.CellPath;
import dmg.cells.nucleus.NoRouteToCellException;
import dmg.util.HttpException;
import dmg.util.HttpRequest;
import dmg.util.HttpResponseEngine;
import org.dcache.cells.CellStub;
import org.dcache.services.info.serialisation.JsonSerialiser;
import org.dcache.services.info.serialisation.PrettyPrintTextSerialiser;
import org.dcache.services.info.serialisation.XmlSerialiser;
import org.dcache.util.Args;
import org.dcache.vehicles.InfoGetSerialisedDataMessage;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.find;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* This class provides support for querying the info cell via the admin
* web-interface. It implements the HttpResponseEngine to handle requests at a
* particular point (a specific alias).
* <p>
* Users may query the complete tree, or select a subtree by specifying the
* path.
* <p>
* It supports several serialisers from which the user may chose, either by
* specifying the query parameter 'format', by specifying the HTTP Accept
* header. XML is the default if neither indicates which serialiser to use.
* <p>
* The implementation caches serialised data for one second. This is a safety
* feature to reducing the impact on info of pathologically broken clients that
* make many requests per second.
*/
public class InfoHttpEngine implements HttpResponseEngine, CellMessageSender
{
private static final Logger LOGGER = LoggerFactory.getLogger(HttpResponseEngine.class);
private static final List<String> ENTIRE_TREE = new ArrayList<>();
private final SerialisationHandler xmlSerialiser =
new SerialisationHandler(XmlSerialiser.NAME, "text/xml");
private final SerialisationHandler jsonSerialiser =
new SerialisationHandler(JsonSerialiser.NAME, "text/json");
private final SerialisationHandler prettyPrintSerialiser =
new SerialisationHandler(PrettyPrintTextSerialiser.NAME, "text/x-ascii-art");
private final Map<String,SerialisationHandler> mimetypeToSerialiser =
ImmutableMap.<String,SerialisationHandler>builder().
put("application/xml", xmlSerialiser).
put("text/xml", xmlSerialiser).
put("application/json", jsonSerialiser).
put("text/x-ascii-art", prettyPrintSerialiser).
build();
private final Map<String,SerialisationHandler> queryParameterToSerialiser =
ImmutableMap.<String,SerialisationHandler>builder().
put("xml", xmlSerialiser).
put("json", jsonSerialiser).
put("pretty", prettyPrintSerialiser).
build();
private final String _infoCellName;
private CellStub _info;
/**
* httpd-side class for each info-side serialiser.
*/
private class SerialisationHandler
{
private final String _name;
private final String _mimeType;
LoadingCache<List<String>, String> resultCache = CacheBuilder.newBuilder()
.maximumSize(10)
.expireAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<List<String>, String>() {
@Override
public String load(List<String> path) throws InterruptedException, CacheException, NoRouteToCellException
{
InfoGetSerialisedDataMessage message =
(path == ENTIRE_TREE) ? new InfoGetSerialisedDataMessage(_name)
: new InfoGetSerialisedDataMessage(path, _name);
message = _info.sendAndWait(message);
return message.getSerialisedData();
}
});
public SerialisationHandler(String name, String mimeType)
{
_name = name;
_mimeType = mimeType;
}
public void handleRequest(HttpRequest request) throws HttpException
{
String[] urlItems = request.getRequestTokens();
OutputStream out = request.getOutputStream();
List<String> path = urlItems.length == 1 ? ENTIRE_TREE :
Arrays.asList(urlItems).subList(1, urlItems.length);
try {
byte[] raw = resultCache.get(path).getBytes(Charsets.UTF_8);
request.printHttpHeader(raw.length);
request.setContentType(this._mimeType);
out.write(raw);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof TimeoutCacheException) {
throw new HttpException(503, "The info cell took too " +
"long to reply, suspect trouble (" +
cause.getMessage() + ")");
}
if (cause instanceof CacheException) {
throw new HttpException(500, "Error when requesting " +
"info from info cell. (" + cause.getMessage() + ")");
}
if (cause instanceof InterruptedException) {
throw new HttpException(503, "Received interrupt " +
"whilst processing data. Please try again later.");
}
propagate(cause);
} catch (IOException e) {
LOGGER.error("Failed to send response: {}", e.getMessage());
}
}
}
/**
* The constructor simply creates a new nucleus for us to use when sending messages.
*/
public InfoHttpEngine(String[] args)
{
_infoCellName = new Args(args).getOption("cell");
checkArgument(_infoCellName != null, "-cell option is required for InfoHttpEngine handler.");
}
@Override
public void setCellEndpoint(CellEndpoint endpoint)
{
_info = new CellStub(endpoint, new CellPath(_infoCellName), 4000, MILLISECONDS);
}
/**
* Handle a request for data. This either returns the cached contents (if
* still valid), or queries the info cell for information.
*/
@Override
public void queryUrl(HttpRequest request) throws HttpException
{
LOGGER.info("Received request: {}", request);
SerialisationHandler handler = find(asList(
serialiserFromUri(request),
serialiserFromHttpHeaders(request),
xmlSerialiser), notNull());
handler.handleRequest(request);
}
private SerialisationHandler serialiserFromUri(HttpRequest request) throws HttpException
{
SerialisationHandler serialiser = null;
String argument = request.getParameter("format");
if (argument != null) {
serialiser = queryParameterToSerialiser.get(argument);
if (serialiser == null) {
throw new HttpException(415, "specified format does not exist");
}
}
return serialiser;
}
private SerialisationHandler serialiserFromHttpHeaders(HttpRequest request)
{
String accept = request.getRequestAttributes().get("Accept");
if (accept == null) {
return null;
}
SerialisationHandler bestHandler = null;
/*
* Choose the best mime-type that the client will accept, taking
* into account which formats we support, the client's preferences
* (q values) and choosing the most specific (i.e. longest) mime-type.
* Here is an example value (should be one line)
*
* application/xml;q=0.5,application/json;q=0.8,
* application/x-proprietary-format
*
* For details, see
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*/
double bestQ = 0;
String bestEntry = "";
for (String entry : Splitter.on(',').trimResults().split(accept)) {
List<String> items = Splitter.on(';').trimResults().splitToList(entry);
String mimeType = items.get(0);
List<String> args = items.subList(1, items.size());
StringBuilder sb = new StringBuilder().append(mimeType);
double q = 1;
for (String arg : args) {
if (arg.startsWith("q=")) {
try {
q = Double.parseDouble(arg.substring(2));
} catch (NumberFormatException e) {
LOGGER.debug("malformed q value ('{}') in Accept: {}", q, e.toString());
q = 0;
}
} else {
sb.append(';').append(arg);
}
}
String entryWithoutQ = sb.toString();
// REVISIT: no wildcard support for mimetypes; e.g. text/* or */*
SerialisationHandler handler = mimetypeToSerialiser.get(mimeType);
if (q >= bestQ && entryWithoutQ.length() > bestEntry.length() && handler != null) {
bestHandler = handler;
bestQ = q;
bestEntry = entryWithoutQ;
}
}
return bestHandler;
}
@Override
public void startup()
{
// This class has no background activity.
}
@Override
public void shutdown()
{
// No background activity to shutdown.
}
}