package org.exist.fluent;
import java.io.IOException;
import java.text.*;
import java.util.*;
import java.util.regex.*;
import org.apache.log4j.Logger;
import org.exist.dom.*;
import org.exist.security.xacml.AccessContext;
import org.exist.source.*;
import org.exist.storage.*;
import org.exist.util.LockException;
import org.exist.xquery.*;
import org.exist.xquery.functions.*;
import org.exist.xquery.value.*;
/**
* Provides facilities for performing queries on a database. It cannot
* be instantiated directly; you must obtain an instance from a resource or the database.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public class QueryService implements Cloneable {
private static final Pattern PRE_SUB_PATTERN = Pattern.compile("\\$(\\d+)");
private static final Logger LOG = Logger.getLogger(QueryService.class);
private static final Statistics STATS = new Statistics();
/**
* Get the process-wide performance statistics gathering facet.
*
* @return the performance statistics facet
*/
public static Statistics statistics() {return STATS;}
private NamespaceMap namespaceBindings;
private Map<String, Document> moduleMap = new TreeMap<String, Document>();
private final Database db;
protected DocumentSet docs, overrideDocs;
protected Sequence base;
protected AnyURIValue baseUri;
private Map<QName, Object> bindings = new HashMap<QName, Object>();
private boolean presub;
/**
* Use this constructor when the docs and base are not constant for the query
* service and need to be set just before the query. You must also override
* the prepareContext method.
*
* @param origin
*/
QueryService(Resource origin) {
this.namespaceBindings = origin.namespaceBindings().extend();
this.db = origin.database();
}
QueryService(Resource origin, DocumentSet docs, Sequence base) {
this(origin);
this.docs = docs;
this.base = base;
}
private QueryService() {
this.namespaceBindings = null;
this.db = null;
}
boolean isFreshFrom(Resource origin) {
return !presub && bindings.isEmpty() && moduleMap.isEmpty() && (namespaceBindings == null || namespaceBindings.isFreshFrom(origin.namespaceBindings()));
}
static final QueryService NULL = new QueryService() {
@Override protected ItemList executeQuery(String query, WrapperFactory wrappeFactory, Object[] params) {
return ItemList.NULL;
}
@Override public QueryAnalysis analyze(String query, Object... params) {
throw new UnsupportedOperationException("NULL query service");
}
@Override public QueryService let(String var, Object value) {return this;}
@Override public QueryService namespace(String key, String uri) {return this;}
@Override public NamespaceMap namespaceBindings() {throw new UnsupportedOperationException("NULL query service");}
@Override public QueryService importModule(Document module) {return this;}
@Override public Item single(String query, Object... params) {throw new DatabaseException("expected 1 result item, got 0 (NULL query)");}
};
void prepareContext(DBBroker broker) {
// do nothing by default, override this if you need to set docs and base just before
// a query is evaluated
}
/**
* Return the database to which the resource that provides the context for
* this query service belongs. The returned database will inherit its
* namespace bindings from this query service.
*
* @return the database that contains this object
*/
public Database database() {
return new Database(db, namespaceBindings);
}
/**
* Bind a variable to the given value within all query expression evaluated subsequently.
*
* @param variableName the qualified name of the variable to bind;
* prefixes are taken from the namespace mappings of the folder that provided this service;
* if the name starts with a <code>$</code>, it will be stripped automatically
* @param value the value the variable should take
* @return this service, to chain calls
*/
public QueryService let(String variableName, Object value) {
if (variableName == null) throw new NullPointerException("null variable name");
if (variableName.startsWith("$")) variableName = variableName.substring(1);
if (variableName.length() == 0) throw new IllegalArgumentException("empty variable name");
return let(QName.parse(variableName, namespaceBindings, ""), value);
}
/**
* Bind a variable to the given value within all query expression evaluated subsequently.
*
* @param variableName the qualified name of the variable to bind
* @param value the value the variable should take
* @return this service, to chain calls
*/
public QueryService let(QName variableName, Object value) {
bindings.put(variableName, value);
return this;
}
/**
* Declare a namespace binding within the scope of this query.
*
* @param key the key to bind
* @param uri the namespace uri
* @return this service, to chain calls
*/
public QueryService namespace(String key, String uri) {
namespaceBindings.put(key, uri);
return this;
}
/**
* Return this query service's namespace bindings for inspection or modification.
*
* @return this query service's namespace bindings
*/
public NamespaceMap namespaceBindings() {
return namespaceBindings;
}
final Pattern MODULE_DECLARATION_DQUOTE = Pattern.compile("\\A\\s*module\\s+namespace\\s+[\\p{Alpha}_][\\w.-]*\\s*=\\s*\"(([^\"]*(\"\")?)*)\"\\s*;");
final Pattern MODULE_DECLARATION_SQUOTE = Pattern.compile("\\A\\s*module\\s+namespace\\s+[\\p{Alpha}_][\\w.-]*\\s*=\\s*'(([^']*('')?)*)'\\s*;");
/**
* Import an XQuery library module from the given document. The namespace and preferred
* prefix of the module are extracted from the module itself. The MIME type of the document
* is set to "application/xquery" as a side-effect.
*
* @param module the non-XML document that holds the library module's source
* @return this service, to chain calls
* @throws DatabaseException if the module is an XML document, or the module declaration
* cannot be found at the top of the document
*/
public QueryService importModule(Document module) {
if (module instanceof XMLDocument) throw new DatabaseException("module cannot be an XML document: " + module);
Matcher matcher = MODULE_DECLARATION_DQUOTE.matcher(module.contentsAsString());
if (!matcher.find()) {
matcher = MODULE_DECLARATION_SQUOTE.matcher(module.contentsAsString());
if (!matcher.find()) throw new DatabaseException("couldn't find a module declaration at the top of " + module);
}
module.metadata().setMimeType("application/xquery");
String moduleNamespace = matcher.group(1);
// TODO: should do URILiteral processing here to replace entity and character references and normalize
// whitespace, but since it seems that eXist doesn't do it either (bug?) there's no reason to rush.
Document prevModule = moduleMap.get(moduleNamespace);
if (prevModule != null && !prevModule.equals(module)) throw new DatabaseException("module " + moduleNamespace + " already bound to " + prevModule + ", can't rebind to " + module);
moduleMap.put(moduleNamespace, module);
return this;
}
/**
* Import the same modules into this query service as imported by the given query service.
* This is a one-time copy; further imports into either query service won't affect the other one.
* @param that the query service to copy module imports from
* @return this service, to chain calls
*/
public QueryService importSameModulesAs(QueryService that) {
moduleMap.putAll(that.moduleMap);
return this;
}
/**
* Limit the root documents accessible to the query to the given list, overriding the any set
* derived from the query's context. The query will still be able to access other documents
* through bound variables or by naming them directly, though.
*
* @param rootDocs the list of root documents to limit the query to
* @return this service, to chain calls
*/
public QueryService limitRootDocuments(XMLDocument... rootDocs) {
return limitRootDocuments(Arrays.asList(rootDocs));
}
/**
* Limit the root documents accessible to the query to the given list, overriding the any set
* derived from the query's context. The query will still be able to access other documents
* through bound variables or by naming them directly, though.
*
* @param rootDocs the list of root documents to limit the query to
* @return this service, to chain calls
*/
public QueryService limitRootDocuments(Collection<XMLDocument> rootDocs) {
overrideDocs = new DefaultDocumentSet();
for (XMLDocument doc : rootDocs) ((MutableDocumentSet) overrideDocs).add(doc.doc);
return this;
}
/**
* Pre-substitute variables of the form '$n' where n is an integer in all query expressions
* evaluated subsequently. The values are taken from the usual postional parameter list.
* Parameters that are presubbed are also bound to the usual $_n variables and can be
* used normally as such. Pre-subbing is useful for element and attribute names, where
* XQuery doesn't allow variables.
*
* @return this service, to chain calls
*/
public QueryService presub() {
presub = true;
return this;
}
@Override public QueryService clone() {
return clone(null, null);
}
/**
* Clone this query service, optionally overriding the clone's namespace and variable bindings.
* If the namespace bindings override or variable bindings override is specified, then that object
* is cloned and used for its respective purpose. If an override is not specified, the bindings
* are cloned from the original query service.
*
* @param nsBindingsOverride the namespace bindings to clone, or <code>null</code> to clone from the original
* @param varBindingsOverride the variable bindings to clone, or <code>null</code> to clone from the original
* @return a clone of this query service with bindings optionally overridden
*/
public QueryService clone(NamespaceMap nsBindingsOverride, Map<QName, ?> varBindingsOverride) {
try {
QueryService that = (QueryService) super.clone();
that.namespaceBindings = nsBindingsOverride != null
? nsBindingsOverride.clone() : that.namespaceBindings.clone();
if (varBindingsOverride == null) {
that.bindings = new HashMap<QName, Object>(that.bindings);
} else {
that.bindings = new HashMap<QName, Object>();
for (Map.Entry<QName, ?> entry : varBindingsOverride.entrySet()) {
that.let(entry.getKey(), entry.getValue());
}
}
that.moduleMap = new TreeMap<String, Document>(moduleMap);
return that;
} catch (CloneNotSupportedException e) {
throw new RuntimeException("unexpected exception", e);
}
}
/**
* Get all items that match the given query in the context of this object.
* @param query the query to match
* @param params parameters to the query, will be substituted for $_1, $_2, etc.
* @return a collection of all items that match the query
*/
public ItemList all(String query, Object... params) {
return executeQuery(query, null, params);
}
/**
* Run the given query, ignoring the results. Useful for running update "queries" --
* see eXist's <a href="http://exist-db.org/update_ext.html">XQuery Update Extensions</a>.
* @param query the query to run
* @param params parameters to the query, will be substituted for $_1, $_2, etc.
*/
public void run(String query, Object... params) {
executeQuery(query, null, params);
}
private interface WrapperFactory {
Function createWrapper(XQueryContext context);
}
ItemList executeQuery(String query, WrapperFactory wrapperFactory, Object[] params) {
long t1 = System.currentTimeMillis(), t2 = 0, t3 = 0, t4 = 0;
if (presub) query = presub(query, params);
DBBroker broker = null;
try {
broker = db.acquireBroker();
prepareContext(broker);
if (overrideDocs != null) docs = overrideDocs;
final org.exist.source.Source source = buildQuerySource(query, params, "execute");
final XQuery xquery = broker.getXQueryService();
final XQueryPool pool = xquery.getXQueryPool();
CompiledXQuery compiledQuery = pool.borrowCompiledXQuery(broker, source);
MutableDocumentSet docsToLock = new DefaultDocumentSet();
if (docs != null) docsToLock.addAll(docs);
if (base != null) docsToLock.addAll(base.getDocumentSet());
try {
XQueryContext context;
if (compiledQuery == null) {
context = xquery.newContext(AccessContext.INTERNAL_PREFIX_LOOKUP);
buildXQueryStaticContext(context, true);
} else {
context = compiledQuery.getContext();
// static context already set
}
buildXQueryDynamicContext(context, params, docsToLock, true);
t2 = System.currentTimeMillis();
if (compiledQuery == null) {
compiledQuery = xquery.compile(context, source);
t3 = System.currentTimeMillis();
}
docsToLock.lock(broker, false, false);
try {
return new ItemList(xquery.execute(wrap(compiledQuery, wrapperFactory, context), base), namespaceBindings.extend(), db);
} finally {
docsToLock.unlock(false);
t4 = System.currentTimeMillis();
}
} finally {
if (compiledQuery != null) pool.returnCompiledXQuery(source, compiledQuery);
}
} catch (XPathException e) {
LOG.debug("query execution failed -- " + query + " -- " + (params == null ? "" : " with params " + Arrays.asList(params)) + (bindings.isEmpty() ? "" : " and bindings " + bindings));
throw new DatabaseException("failed to execute query", e);
} catch (IOException e) {
throw new DatabaseException("unexpected exception", e);
} catch (LockException e) {
throw new DatabaseException("deadlock", e);
} finally {
db.releaseBroker(broker);
STATS.update(query, t1, t2, t3, t4, System.currentTimeMillis());
}
}
@SuppressWarnings("unchecked")
private CompiledXQuery wrap(CompiledXQuery expr, WrapperFactory wrapperFactory, XQueryContext context) throws XPathException {
if (wrapperFactory == null) return expr;
Function wrapper = wrapperFactory.createWrapper(context);
wrapper.setArguments(Collections.singletonList(expr));
// wrapper.setSource(expr.getSource());
return wrapper;
}
private org.exist.source.Source buildQuerySource(String query, Object[] params, String cookie) {
Map<String, String> combinedMap = namespaceBindings.getCombinedMap();
for (Map.Entry<String, Document> entry : moduleMap.entrySet()) {
combinedMap.put("<module> " + entry.getKey(), entry.getValue().path());
}
for (Map.Entry<QName, Object> entry : bindings.entrySet()) {
combinedMap.put("<var> " + entry.getKey(), null); // don't care about values, as long as the same vars are bound
}
combinedMap.put("<posvars> " + params.length, null);
combinedMap.put("<cookie>", cookie);
// TODO: should include statically known documents and baseURI too?
return new StringSourceWithMapKey(query, combinedMap);
}
private void buildXQueryDynamicContext(XQueryContext context, Object[] params, MutableDocumentSet docsToLock, boolean bindVariables) throws XPathException {
context.setBackwardsCompatibility(false);
context.setStaticallyKnownDocuments(docs);
context.setBaseURI(baseUri == null ? new AnyURIValue("/db") : baseUri);
if (bindVariables) {
for (Map.Entry<QName, Object> entry : bindings.entrySet()) {
context.declareVariable(
new org.exist.dom.QName(entry.getKey().getLocalPart(), entry.getKey().getNamespaceURI(), entry.getKey().getPrefix()),
convertValue(entry.getValue()));
}
if (params != null) for (int i = 0; i < params.length; i++) {
Object convertedValue = convertValue(params[i]);
if (docsToLock != null && convertedValue instanceof Sequence) {
docsToLock.addAll(((Sequence) convertedValue).getDocumentSet());
}
context.declareVariable("_"+(i+1), convertedValue);
}
}
}
private void buildXQueryStaticContext(XQueryContext context, boolean importModules) throws XPathException {
context.declareNamespaces(namespaceBindings.getCombinedMap());
for (Map.Entry<String, Document> entry : moduleMap.entrySet()) {
context.importModule(entry.getKey(), null, "xmldb:exist:///db" + entry.getValue().path());
}
}
/**
* Convert the given object into a value appropriate for being defined as
* the value of a variable in an XQuery. This will extract a sequence out
* of all database objects, convert collections and arrays into sequences
* recursively, convert <code>null</code> into an empty sequence, and
* pass other objects through untouched.
* Convertible objects that are defined in the JDK will be automatically
* converted by eXist.
* @see org.exist.xquery.XPathUtil#javaObjectToXPath(Object, XQueryContext, boolean)
*
* @param o the object to convert to a database value
* @return the converted value, ready for assignment to an XQuery variable
*/
@SuppressWarnings("unchecked")
private Object convertValue(Object o) {
if (o == null) return Collections.emptyList();
if (o instanceof Resource) {
try {
return ((Resource) o).convertToSequence();
} catch (UnsupportedOperationException e) {
return o;
}
}
List<Object> list = null;
if (o instanceof Collection) list = new ArrayList<Object>((Collection) o);
else if (o instanceof Object[]) list = new ArrayList<Object>(Arrays.asList((Object[]) o));
if (list != null) {
for (ListIterator<Object> it = list.listIterator(); it.hasNext(); ) {
it.set(convertValue(it.next()));
}
return list;
}
return DataUtils.toXMLObject(o);
}
private String presub(String query, Object[] params) {
if (params == null) return query;
StringBuffer buf = new StringBuffer();
Matcher matcher = PRE_SUB_PATTERN.matcher(query);
while(matcher.find()) {
matcher.appendReplacement(buf, ((String) params[Integer.parseInt(matcher.group(1))-1]).replace("\\", "\\\\").replace("$", "\\$"));
}
matcher.appendTail(buf);
return buf.toString();
}
/**
* Get all items that match the given query in the context of this object,
* without regard for the order of the results. This can sometimes make a query
* run faster.
* @param query the query to match
* @param params
* @return a collection of all items that match the query
*/
public ItemList unordered(String query, Object... params) {
// TODO: put expression in unordered context once eXist supports it
// TODO: verify that callers to 'all' could not use 'unordered'
// return all("declare ordering unordered; " + query, params);
return all(query, params);
}
private static final WrapperFactory EXACTLY_ONE = new WrapperFactory() {
public Function createWrapper(XQueryContext context) {return new FunExactlyOne(context);}
};
/**
* Get the one and only item that matches the given query in the context of
* this object.
* @param query the query to match
* @param params
* @return the unique item that matches the query
*/
public Item single(String query, Object... params) {
ItemList result = executeQuery(query, EXACTLY_ONE, params);
assert result.size() == 1 : "expected single result, got " + result.size();
return result.get(0);
}
private static final WrapperFactory ZERO_OR_ONE = new WrapperFactory() {
public Function createWrapper(XQueryContext context) {return new FunZeroOrOne(context);}
};
/**
* Get no more than one item that matches the given query in the context
* of this object.
* @param query the query to match
* @param params
* @return the item that matches this query, or <code>Item.NULL</code> if none
*/
public Item optional(String query, Object... params) {
ItemList result = executeQuery(query, ZERO_OR_ONE, params);
assert result.size() <= 1 : "expected zero or one results, got " + result.size();
return result.size() == 0 ? Item.NULL : result.get(0);
}
public boolean flag(String query, boolean defaultValue) {
Item item = optional(query);
if (item != Item.NULL) {
try {
return item.booleanValue();
} catch (Exception e) {
LOG.error("illegal flag value '" + item +"' found for query " + query + "; using default '" + defaultValue + "'");
}
}
return defaultValue;
}
private static final WrapperFactory EXISTS = new WrapperFactory() {
public Function createWrapper(XQueryContext context) {return new FunExists(context);}
};
/**
* Return whether at least one item matches the given query in the context
* of this object.
* @param query the query to match
* @param params
* @return <code>true</code> if at least one item matches, <code>false</code> otherwise
*/
public boolean exists(String query, Object... params) {
return executeQuery(query, EXISTS, params).get(0).booleanValue();
}
/**
* Statically analyze a query for various properties.
*
* @param query the query to analyze
* @param params parameters for the query; if necessary parameters are left out they will be listed as required variables in the analysis
* @return a query analysis facet
*/
public QueryAnalysis analyze(String query, Object... params) {
if (presub) query = presub(query, params);
long t1 = System.currentTimeMillis(), t2 = 0, t3 = 0;
DBBroker broker = null;
try {
broker = db.acquireBroker();
prepareContext(broker);
final org.exist.source.Source source = buildQuerySource(query, params, "analyze");
final XQuery xquery = broker.getXQueryService();
final XQueryPool pool = xquery.getXQueryPool();
CompiledXQuery compiledQuery = pool.borrowCompiledXQuery(broker, source);
try {
AnalysisXQueryContext context;
if (compiledQuery == null) {
context = new AnalysisXQueryContext(broker, AccessContext.INTERNAL_PREFIX_LOOKUP);
buildXQueryStaticContext(context, false);
buildXQueryDynamicContext(context, params, null, false);
t2 = System.currentTimeMillis();
compiledQuery = xquery.compile(context, source);
t3 = System.currentTimeMillis();
} else {
context = (AnalysisXQueryContext) compiledQuery.getContext();
t2 = System.currentTimeMillis();
}
return new QueryAnalysis(
compiledQuery, Collections.unmodifiableSet(context.requiredVariables), Collections.unmodifiableSet(context.requiredFunctions));
} finally {
if (compiledQuery != null) pool.returnCompiledXQuery(source, compiledQuery);
}
} catch (XPathException e) {
LOG.warn("query compilation failed -- " + query + " -- " + (params == null ? "" : " with params " + Arrays.asList(params)) + (bindings.isEmpty() ? "" : " and bindings " + bindings));
throw new DatabaseException("failed to compile query", e);
} catch (IOException e) {
throw new DatabaseException("unexpected exception", e);
} finally {
db.releaseBroker(broker);
STATS.update(query, t1, t2, t3, 0, System.currentTimeMillis());
}
}
private static final class AnalysisXQueryContext extends XQueryContext {
final Set<QName> requiredFunctions = new TreeSet<QName>();
final Set<QName> requiredVariables = new TreeSet<QName>();
private AnalysisXQueryContext(DBBroker broker, AccessContext accessCtx) {
super(broker, accessCtx);
}
@Override public Variable resolveVariable(org.exist.dom.QName qname) throws XPathException {
Variable var = super.resolveVariable(qname);
if (var == null) {
requiredVariables.add(new QName(qname.getNamespaceURI(), qname.getLocalName(), qname.getPrefix()));
var = new Variable(qname);
}
return var;
}
@Override public UserDefinedFunction resolveFunction(org.exist.dom.QName qname, int argCount) throws XPathException {
UserDefinedFunction func = super.resolveFunction(qname, argCount);
if (func == null) {
requiredFunctions.add(new QName(qname.getNamespaceURI(), qname.getLocalName(), qname.getPrefix()));
func = new UserDefinedFunction(this, new FunctionSignature(qname, null, new SequenceType(Type.ITEM, org.exist.xquery.Cardinality.ZERO_OR_MORE), true));
func.setFunctionBody(new SequenceConstructor(this));
}
return func;
}
}
/**
* An access point for running various analyses on a query.
*/
public static class QueryAnalysis {
private final CompiledXQuery query;
private final Set<QName> requiredVariables;
private final Set<QName> requiredFunctions;
private QueryAnalysis(CompiledXQuery query, Set<QName> requiredVariables, Set<QName> requiredFunctions) {
this.query = query;
this.requiredVariables = requiredVariables;
this.requiredFunctions = requiredFunctions;
}
/**
* Return the name of the statically determined return type of the query expression.
* The name is in a standard form, see {@link org.exist.xquery.value.Type} for a list
* of possible values. If the return type cannot be statically determined, it defaults to
* <code>Type.ITEM</code>, the universal supertype in XQuery.
*
* @return the name of the return type of the query being analyzed
*/
public String returnTypeName() {
return org.exist.xquery.value.Type.getTypeName(
query instanceof Expression ? ((PathExpr) query).returnsType() : org.exist.xquery.value.Type.ITEM);
}
/**
* The enumeration of recognized cardinalities for parameter and return types.
*/
public static enum Cardinality {ZERO, ZERO_OR_ONE, ONE, ZERO_OR_MORE, ONE_OR_MORE}
/**
* Return the statically determined cardinality of the return type of the query expression.
* If the cardinality cannot be statically determined, it defaults to <code>ZERO_OR_MORE</code>,
* the least restrictive cardinality.
*
* @return the cardinality of the return type of the query being analyzed
*/
public Cardinality cardinality() {
if (query instanceof Expression) {
int cardinality = ((Expression) query).getCardinality();
switch (cardinality) {
case org.exist.xquery.Cardinality.EMPTY: return Cardinality.ZERO;
case org.exist.xquery.Cardinality.EXACTLY_ONE: return Cardinality.ONE;
case org.exist.xquery.Cardinality.ZERO_OR_ONE: return Cardinality.ZERO_OR_ONE;
case org.exist.xquery.Cardinality.ZERO_OR_MORE: return Cardinality.ZERO_OR_MORE;
case org.exist.xquery.Cardinality.ONE_OR_MORE: return Cardinality.ONE_OR_MORE;
default:
LOG.error("unexpected eXist cardinality flag " + cardinality);
}
}
return Cardinality.ZERO_OR_MORE;
}
/**
* Return a list of variables that are required to be defined by this query, excluding any
* positional variables that were provided to the {@link QueryService#analyze(String, Object[]) analyze}
* method. The variable names will not include the leading '$'.
*
* @return a list of variables required by this query
*/
public Set<QName> requiredVariables() {
return requiredVariables;
}
/**
* Return a list of functions that are required to be defined by this query, beyond the
* standard XPath/XQuery ones.
*
* @return a list of functions required by this query
*/
public Set<QName> requiredFunctions() {
return requiredFunctions;
}
}
public static class Statistics {
private static final NumberFormat COUNT_FORMAT = NumberFormat.getIntegerInstance();
private static final MessageFormat FULL_ENTRY_FORMAT = new MessageFormat(
"{1} uses in {3,number,0.000}s ({11,number,percent}, {7,number,0.00}ms avg) [" +
"{4,number,0.000}s compiling ({8,number,0.00}ms avg, {2,number,percent} cache hits), " +
"{5,number,0.000}s preparing ({9,number,0.00}ms avg), {6,number,0.000}s executing ({10,number,0.00}ms avg)" +
"]: {0}");
private static final MessageFormat STAND_ALONE_ENTRY_FORMAT = new MessageFormat(
"{1,number,integer} uses in {3,number,0.000}s ({7,number,0.00}ms avg) [" +
"{4,number,0.000}s compiling ({8,number,0.00}ms avg, {2,number,percent} cache hits), " +
"{5,number,0.000}s preparing ({9,number,0.00}ms avg), {6,number,0.000}s executing ({10,number,0.00}ms avg)" +
"]: {0}");
private static final Comparator<Entry> TOTAL_TIME_DESCENDING = new Comparator<Entry>() {
public int compare(Entry e1, Entry e2) {
return e1.queryTime == e2.queryTime ? 0 : (e1.queryTime > e2.queryTime ? -1 : 1);
}
};
private final Map<String, Entry> entries = new HashMap<String, Entry>();
void update(String query, long t1, long t2, long t3, long t4, long t5) {
long tQuery = t5 - t1, tPreparation = t2 > 0 ? t2 - t1 : -1, tCompilation = t3 > 0 ? t3 - t2 : -1, tExecution = t4 > 0 ? t4 - (t3 > 0 ? t3 : t2) : -1;
get(null).update(tQuery, tPreparation, tCompilation, tExecution);
get(query).update(tQuery, tPreparation, tCompilation, tExecution);
}
synchronized Entry get(String query) {
Entry entry = entries.get(query);
if (entry == null) entries.put(query, entry = new Entry(query));
return entry;
}
/**
* Get a list of all statistics entries for which data has been gathered. The list is a copy
* and can be further manipulating without affecting the service.
*
* @return a list of all statistics entries
*/
public synchronized List<Entry> entries() {
return new ArrayList<Entry>(entries.values());
}
/**
* Get the entry that aggregates statistics over all the queries.
*
* @return the totals entry
*/
public Entry totals() {
return get(null);
}
/**
* Reset all gathered statistics back to zero.
*/
public synchronized void reset() {
entries.clear();
}
/**
* Return a string that describes the statistics gathered for all the entries.
*
* @return a string describing the statistics gathered so far
*/
public synchronized String toString() {
return toStringTop(entries.size());
}
/**
* Return a string that describes the statistics for the top n entries, sorted by
* descending order of total time spent dealing with the query. This will always
* include the totals entry in the first position.
*
* @param n the desired number of entries to describe
* @return a string describing the statistics for the top n entries
*/
public synchronized String toStringTop(int n) {
StringBuilder out = new StringBuilder();
List<Entry> list = entries();
if (list.isEmpty()) return "<no queries executed>";
Collections.sort(list, TOTAL_TIME_DESCENDING);
int maxCountLength = COUNT_FORMAT.format(list.get(0).numQueries).length();
double totalDuration = list.get(0).queryTime;
for (Entry entry : list.subList(0, Math.min(n, list.size()))) out.append(entry.toString(maxCountLength, totalDuration)).append('\n');
return out.toString();
}
/**
* Performance counters for a single query. The fields are public for convenience (and to avoid
* a forest of accessors) but should be considered as read-only.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public static class Entry {
/**
* The query string (after pre-substitution) that this entry is about. If <code>null</code>
* then this is the totals entry.
*/
public final String query;
// These are simple counters
public long numQueries, queriesPrepared, queriesCompiled, queriesRun;
// All times are in seconds.
public double queryTime, queryPreparationTime, queryCompilationTime, queryRunTime;
Entry(String query) {
this.query = query;
}
synchronized void update(long tQuery, long tPreparation, long tCompilation, long tRun) {
numQueries++;
queryTime += tQuery / 1000.0;
if (tPreparation >= 0) {
queriesPrepared++;
queryPreparationTime += tPreparation / 1000.0;
}
if (tCompilation >= 0) {
queriesCompiled++;
queryCompilationTime += tCompilation / 1000.0;
}
if (tRun >= 0) {
queryRunTime += tRun / 1000.0;
queriesRun++;
}
}
public synchronized String toString(int maxCountLength, double totalDuration) {
String formattedCount = String.format("%" + maxCountLength + "s", COUNT_FORMAT.format(numQueries));
return FULL_ENTRY_FORMAT.format(new Object[] {
query == null ? "TOTALS" : query,
formattedCount,
(queriesPrepared - queriesCompiled) / (double) queriesPrepared,
queryTime,
queryCompilationTime,
queryPreparationTime,
queryRunTime,
queryTime * 1000 / numQueries,
queriesCompiled == 0 ? 0 : queryCompilationTime * 1000 / queriesCompiled,
queriesPrepared == 0 ? 0 : queryPreparationTime * 1000 / queriesPrepared,
queriesRun == 0 ? 0 : queryRunTime * 1000 / queriesRun,
queryTime / totalDuration
});
}
@Override public synchronized String toString() {
return STAND_ALONE_ENTRY_FORMAT.format(new Object[] {
query == null ? "TOTALS" : query,
numQueries,
(queriesPrepared - queriesCompiled) / (double) queriesPrepared,
queryTime,
queryCompilationTime,
queryPreparationTime,
queryRunTime,
queryTime * 1000 / numQueries,
queriesCompiled == 0 ? 0 : queryCompilationTime * 1000 / queriesCompiled,
queriesPrepared == 0 ? 0 : queryPreparationTime * 1000 / queriesPrepared,
queriesRun == 0 ? 0 : queryRunTime * 1000 / queriesRun
});
}
}
}
}