/*******************************************************************************
* Copyright (c) 2004, 2007 IBM Corporation and Cambridge Semantics Incorporated.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* File: $Source: /cvsroot/slrp/glitter/com.ibm.adtech.glitter/src/com/ibm/adtech/glitter/query/QueryController.java,v $
* Created by: Lee Feigenbaum (<a href="mailto:feigenbl@us.ibm.com">feigenbl@us.ibm.com</a>)
* Created on: 10/23/06
* Revision: $Id: QueryController.java 164 2007-07-31 14:11:09Z mroy $
*
* Contributors: IBM Corporation - initial API and implementation
* Cambridge Semantics Incorporated - Fork to Anzo
*******************************************************************************/
package org.openanzo.glitter.query;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.collections15.BidiMap;
import org.apache.commons.collections15.MapIterator;
import org.apache.commons.collections15.OrderedMap;
import org.apache.commons.collections15.bidimap.TreeBidiMap;
import org.apache.commons.collections15.map.ListOrderedMap;
import org.openanzo.glitter.Engine;
import org.openanzo.glitter.dataset.DefaultQueryDataset;
import org.openanzo.glitter.dataset.QueryDataset;
import org.openanzo.glitter.exception.UnknownPrefixException;
import org.openanzo.glitter.syntax.abstrakt.GraphPattern;
import org.openanzo.glitter.syntax.concrete.ParseException;
import org.openanzo.rdf.BlankNode;
import org.openanzo.rdf.Constants;
import org.openanzo.rdf.Literal;
import org.openanzo.rdf.MemURI;
import org.openanzo.rdf.TriplePatternComponent;
import org.openanzo.rdf.URI;
import org.openanzo.rdf.Variable;
import org.openanzo.rdf.utils.PrettyPrintable;
/**
* The {@link QueryController} is a central point for much of the information that characterizes a parsed and prepared query. A QueryController is populated as
* a query is parsed, and its information is used to feed other parts of the query system (e.g. the {@link RDFDataset})
*
* @author lee <lee@cambridgesemantics.com>
*
*/
public class QueryController implements QueryInformation, PrettyPrintable {
protected URI baseUri = null;
protected java.net.URI baseJavaUri = null;
protected OrderedMap<String, URI> prefixMap = new ListOrderedMap<String, URI>();
protected OrderedMap<String, URI> queryOptions = new ListOrderedMap<String, URI>();
protected QueryDataset queryDataset;
protected QueryDataset parsedDataset = new DefaultQueryDataset();
protected QueryResultForm resultForm;
protected GraphPattern queryPattern;
protected QueryResults queryResults;
protected SolutionGenerator solutionGenerator;
protected Engine engine;
// query modifiers
protected int limit = -1;
protected int offset = -1;
protected ArrayList<OrderingCondition> ordering = new ArrayList<OrderingCondition>();
protected boolean datasetFromQuery = false;
protected boolean cancelled = false;
/**
* Default constructor.
*/
public QueryController() {
}
/**
* @param engine
* the engine to set
*/
public void setEngine(Engine engine) {
this.engine = engine;
}
/**
* @return the engine
*/
public Engine getEngine() {
return engine;
}
/**
* Notes an {@link OrderingCondition} (expression + sort direction) found in a query in an <tt>ORDER BY</tt> clause. Note that the order that
* {@link #addOrderingCondition(OrderingCondition)} is called is significant and should match the order the conditions are present in the query.
*
* @param condition
*/
public void addOrderingCondition(OrderingCondition condition) {
this.ordering.add(condition);
}
/* (non-Javadoc)
* @see com.ibm.adtech.glitter.query.QueryInformation#getBaseUri()
*/
public URI getBaseUri() {
return this.baseUri;
}
public int getLimit() {
return this.limit;
}
/**
* @return the queryDataset
*/
public QueryDataset getQueryDataset() {
return (datasetFromQuery) ? parsedDataset : queryDataset;
}
/**
* @return the queryDataset
*/
public QueryDataset getParsedQueryDataset() {
return parsedDataset;
}
/**
* @param queryDataset
* the queryDataset to set
*/
public void setQueryDataset(QueryDataset queryDataset) {
this.queryDataset = queryDataset;
}
public int getOffset() {
return this.offset;
}
public List<OrderingCondition> getOrderingConditions() {
return this.ordering;
}
public GraphPattern getQueryPattern() {
return this.queryPattern;
}
public QueryResultForm getQueryResultForm() {
return this.resultForm;
}
/** Pretty print options */
static public enum QueryStringPrintOptions {
/** Indent the query lines */
INDENT,
/** Use prefixes in query string */
USE_PREFIXES,
/** Generate new prefixes based on query string */
GENERATE_NEW_PREFIXES,
/** Remove unused prefixes from query */
REMOVE_UNUSED_PREFIXES;
}
/**
* Pretty print a new line
*
* @param printFlags
* flags for printing
* @param indentLevel
* level of indentation for this line
* @param sb
* output builder
*/
static public void printSeparator(EnumSet<QueryStringPrintOptions> printFlags, int indentLevel, StringBuilder sb) {
if (printFlags.contains(QueryStringPrintOptions.INDENT)) {
sb.append('\n');
for (int i = 0; i < indentLevel; i++)
sb.append('\t');
} else {
sb.append(' ');
}
}
/**
* Pretty print a URI
*
* @param u
* URI to print
* @param printFlags
* flags to printing
* @param uri2prefix
* map of uris and prefixes
* @param sb
* output builder
*/
static private void printURI(URI u, EnumSet<QueryStringPrintOptions> printFlags, Map<String, String> uri2prefix, StringBuilder sb) {
if (printFlags.contains(QueryStringPrintOptions.USE_PREFIXES)) {
// first, try the whole URI
String prefix = uri2prefix.get(u.toString());
if (prefix != null) {
sb.append(prefix);
sb.append(':');
return;
}
prefix = uri2prefix.get(u.getNamespace());
if (prefix != null) {
sb.append(prefix);
sb.append(':');
sb.append(u.getLocalName());
return;
}
}
sb.append('<');
sb.append(u.toString());
sb.append('>');
return;
}
/**
* Print a triple pattern
*
* @param tpc
* triple pattern to print
* @param printFlags
* flags to printing
* @param uri2prefix
* uri prefixes
* @param sb
* output builder
*/
static public void printTriplePatternComponent(TriplePatternComponent tpc, EnumSet<QueryStringPrintOptions> printFlags, Map<String, String> uri2prefix, StringBuilder sb) {
if (tpc instanceof URI)
printURI((URI) tpc, printFlags, uri2prefix, sb);
else if (tpc instanceof BlankNode)
sb.append(tpc.toString());
else if (tpc instanceof Literal)
sb.append(tpc.toString());
else if (tpc instanceof Variable)
sb.append(tpc.toString());
else
sb.append(tpc.toString());
}
/**
* Pretty print query string
*
* @param printFlags
* print flags
* @return pretty print version of query
*/
public String prettyPrintQueryString(EnumSet<QueryStringPrintOptions> printFlags) {
return prettyPrintQueryString(printFlags, 0);
}
/**
* Pretty print query string
*
* @param printFlags
* print flags
* @param startIndentLevel
* @return pretty print version of query
*/
@SuppressWarnings("all")
public String prettyPrintQueryString(EnumSet<QueryStringPrintOptions> printFlags, int startIndentLevel) {
QueryResultForm queryResultForm = this.getQueryResultForm();
StringBuilder s = new StringBuilder();
// output the base, if any
if (this.baseUri != null) {
s.append("BASE <");
s.append(this.baseUri);
s.append(">");
printSeparator(printFlags, 0, s);
}
// add prefixes for all URIs mentioned in the query
final BidiMap<String, String> prefix2uri = new TreeBidiMap<String, String>();
Map<String, String> uri2prefix = prefix2uri.inverseBidiMap();
for (Entry<String, URI> e : this.prefixMap.entrySet())
prefix2uri.put(e.getKey(), e.getValue().toString());
for (Entry<String, URI> e : this.queryOptions.entrySet())
prefix2uri.put(e.getKey(), e.getValue().toString());
if (printFlags.contains(QueryStringPrintOptions.GENERATE_NEW_PREFIXES)) {
visitURIs(new URIVisitor() {
private int p = 0;
public boolean visitURI(URI u) {
if (!prefix2uri.containsValue(u.getNamespace())) {
// TODO - could use next-to-last URI component to name prefix
while (prefix2uri.containsKey("p" + ++p))
;
prefix2uri.put("p" + p, u.getNamespace());
}
return true;
}
}, false, true);
}
// output prefixes
MapIterator<String, String> it = prefix2uri.mapIterator();
while (it.hasNext()) {
String key = it.next();
String value = it.getValue();
s.append("PREFIX ");
s.append(key);
s.append(": <");
s.append(value);
s.append(">");
printSeparator(printFlags, 0, s);
}
this.resultForm.prettyPrintQueryPart(printFlags, startIndentLevel, uri2prefix, s);
printSeparator(printFlags, startIndentLevel, s);
if (isDatasetFromQuery()) {
for (URI u : getQueryDataset().getDefaultGraphURIs()) {
s.append("FROM ");
printURI(u, printFlags, uri2prefix, s);
printSeparator(printFlags, startIndentLevel, s);
}
for (URI u : getQueryDataset().getNamedGraphURIs()) {
s.append("FROM NAMED ");
printURI(u, printFlags, uri2prefix, s);
printSeparator(printFlags, startIndentLevel, s);
}
for (URI u : getQueryDataset().getNamedDatasetURIs()) {
s.append("FROM DATASET ");
printURI(u, printFlags, uri2prefix, s);
printSeparator(printFlags, startIndentLevel, s);
}
}
s.append("WHERE {");
printSeparator(printFlags, startIndentLevel + 1, s);
this.queryPattern.prettyPrintQueryPart(printFlags, startIndentLevel + 1, uri2prefix, s);
printSeparator(printFlags, startIndentLevel, s);
s.append("}");
if (queryResultForm instanceof Projection) {
Projection projection = (Projection) queryResultForm;
if (!projection.getGroupByVariables().isEmpty()) {
printSeparator(printFlags, startIndentLevel, s);
projection.prettyPrintGroupByQueryPart(printFlags, startIndentLevel, uri2prefix, s);
}
}
if (this.ordering.size() > 0) {
printSeparator(printFlags, startIndentLevel, s);
s.append("ORDER BY ");
for (int i = 0; i < this.ordering.size(); i++) {
OrderingCondition c = this.ordering.get(i);
if (i != 0)
s.append(' ');
c.prettyPrintQueryPart(printFlags, startIndentLevel, uri2prefix, s);
}
printSeparator(printFlags, startIndentLevel, s);
}
if (this.limit > -1) {
printSeparator(printFlags, startIndentLevel, s);
s.append("LIMIT ");
s.append(this.limit);
}
if (this.offset > -1) {
printSeparator(printFlags, startIndentLevel, s);
s.append("OFFSET ");
s.append(this.offset);
}
if (printFlags.contains(QueryStringPrintOptions.REMOVE_UNUSED_PREFIXES)) {
String q = s.toString();
s = new StringBuilder();
String[] lines = q.split("\n");
Pattern p = Pattern.compile("^PREFIX\\s*(\\w+:)", Pattern.CASE_INSENSITIVE);
boolean first = true;
for (String line : lines) {
Matcher m = p.matcher(line);
if (m.find()) {
Pattern prefix = Pattern.compile("\\W" + m.group(1));
if (prefix.split(q).length <= 2)
continue;
}
if (!first)
s.append('\n');
s.append(line);
first = false;
}
}
return s.toString();
}
public String getQueryString(boolean includeFromClause) {
QueryResultForm queryResultForm = this.getQueryResultForm();
StringBuilder s = new StringBuilder(this.resultForm.toString());
if (includeFromClause && isDatasetFromQuery()) {
for (URI u : getQueryDataset().getDefaultGraphURIs())
s.append(" FROM <" + u + ">");
for (URI u : getQueryDataset().getNamedGraphURIs())
s.append(" FROM NAMED <" + u + ">");
for (URI u : getQueryDataset().getNamedDatasetURIs()) {
s.append(" FROM DATASET <" + u + ">");
}
}
s.append(" WHERE { ");
s.append(this.queryPattern);
s.append(" }");
if (queryResultForm instanceof Projection) {
Projection projection = (Projection) queryResultForm;
List<Variable> vars = projection.getGroupByVariables();
if (vars != null && !vars.isEmpty()) {
s.append(" GROUP BY");
for (Variable v : vars)
s.append(" " + v);
}
}
if (this.ordering.size() > 0) {
s.append(" ORDER BY");
for (OrderingCondition c : this.ordering)
s.append(" " + c.toString());
}
if (this.limit > -1)
s.append(" LIMIT " + this.limit);
if (this.offset > -1)
s.append(" OFFSET " + this.offset);
return s.toString();
}
public QueryType getQueryType() {
if (this.resultForm instanceof Projection)
return QueryType.SELECT;
else if (this.resultForm instanceof Ask)
return QueryType.ASK;
else if (this.resultForm instanceof Construct)
return QueryType.CONSTRUCT;
else
return null;
}
/**
*
* @return Whether or not the query is ordered; i.e., if it has any {@link OrderingCondition}s
*/
public boolean isOrdered() {
return getOrderingConditions().size() > 0;
}
/**
* @return the queryOptions
*/
public OrderedMap<String, URI> getQueryOptions() {
return queryOptions;
}
/**
* Records a mapping from a given prefix to its {@link URI} expansion. See {@link #resolveQName(String, String)}
*
* @param prefix
* @param uri
*/
public void mapPrefix(String prefix, URI uri) {
if (uri.getNamespace().equals(Constants.NAMESPACES.GLITTER_QUERYOPTION_NAMESPACE)) {
this.queryOptions.put(prefix, uri);
} else {
this.prefixMap.put(prefix, uri);
}
}
/**
* Fully resolves a prefixed name into the full {@link URI} that it represents
*
* @param prefix
* The prefix part of the prefixed name (before the ':')
* @param localPart
* The local name part of the prefixed name (after the ':')
* @return A fully resolved (absolute) {@link URI}
* @throws ParseException
*/
public URI resolveQName(String prefix, String localPart) throws ParseException {
return resolveQName(prefix, localPart, true);
}
/**
* Resolves a prefixed name into the full {@link URI} that it represents.
*
* @param prefix
* The prefix part of the prefixed name (before the ':')
* @param localPart
* The local name part of the prefixed name (after the ':')
* @param fullyResolve
* If <tt>false</tt>, simply resolve the <tt>prefix</tt>. If <tt>true</tt>, also resolve a relative URI based on our base URI. See
* {@link #resolveUri(String)}.
* @return The resolved prefixed name.
* @throws ParseException
*/
private URI resolveQName(String prefix, String localPart, boolean fullyResolve) throws ParseException {
URI u = this.prefixMap.get(prefix);
if (u == null)
throw new UnknownPrefixException(prefix);
try {
if (fullyResolve && baseJavaUri != null)
return resolveUri(new java.net.URI(u.toString() + localPart));
else
return MemURI.create(u.toString() + localPart);
} catch (URISyntaxException e) {
throw new ParseException(e.getMessage());
}
}
/**
*
* @param u
* Anzo URI form of the URI to resolve.
* @return The resolved {@link URI}.
*/
public URI resolveUri(String u) {
if (getBaseUri() != null) {
java.net.URI uri = java.net.URI.create(u.toString());
return resolveUri(uri);
} else {
return MemURI.create(u);
}
}
/**
* @param u
* @return uri
*/
public URI resolveUri(java.net.URI u) {
if (baseJavaUri != null) {
return MemURI.create(baseJavaUri.resolve(u));
} else {
return MemURI.create(u.toString());
}
}
/**
* Sets the base URI against which relative URIs are resolved.
*
* @param u
* the bsae {@link URI}
*/
public void setBaseUri(URI u) {
this.baseUri = u;
this.baseJavaUri = java.net.URI.create(u.toString());
}
/**
* Sets the solution limit as given by a <tt>LIMIT</tt> clause
*
* @param limit
*/
public void setLimit(int limit) {
this.limit = limit;
}
/**
* Sets the offset into the solution sequence as given by a <tt>OFFSET</tt> clause
*
* @param offset
*/
public void setOffset(int offset) {
this.offset = offset;
}
/**
* Sets the root node in the <tt>WHERE</tt> clause of the query
*
* @param pattern
*/
public void setQueryPattern(GraphPattern pattern) {
this.queryPattern = pattern;
}
/**
* Sets the query form. One of {@link Projection}, {@link Ask}, or {@link Construct}.
*
* @param resultForm
*/
public void setQueryResultForm(QueryResultForm resultForm) {
this.resultForm = resultForm;
}
/**
* Retrieves the query results. This is only valid after {@link #setQueryResults(QueryResults)} has been called.
*
* @return The {@link QueryResults}
*/
public QueryResults getQueryResults() {
return this.queryResults;
}
/**
* Sets the results of executing the query.
*
* @param queryResults
*/
public void setQueryResults(QueryResults queryResults) {
this.queryResults = queryResults;
}
public SolutionGenerator getSolutionGenerator() {
return this.solutionGenerator;
}
/**
* Sets the {@link SolutionGenerator} used for generating bindings while executing the query.
*
* @param solutionGenerator
*/
public void setSolutionGenerator(SolutionGenerator solutionGenerator) {
this.solutionGenerator = solutionGenerator;
}
/**
*
* @return true if the dataset (graphs) for this query came from the query itself (as opposed to the protocol / API)
*/
public boolean isDatasetFromQuery() {
return datasetFromQuery;
}
/**
*
* @param datasetFromQuery
* true if the dataset (graphs) for this query came from the query itself (as opposed to the protocol / API)
*/
public void setDatasetFromQuery(boolean datasetFromQuery) {
this.datasetFromQuery = datasetFromQuery;
}
public void prettyPrint(StringBuilder buffer) {
buffer.append("Query(");
if (this.getBaseUri() != null) {
buffer.append("BaseUri(");
buffer.append(this.getBaseUri());
buffer.append("), ");
}
Set<URI> graphs = getQueryDataset().getDefaultGraphURIs();
if (graphs.size() > 0) {
buffer.append("DefaultGraphs(");
int i = 0;
for (URI graph : graphs) {
buffer.append(graph);
if (++i < graphs.size())
buffer.append(", ");
}
buffer.append("), ");
}
Set<URI> namedGraphs = getQueryDataset().getNamedGraphURIs();
if (namedGraphs.size() > 0) {
buffer.append("NamedGraphs(");
{
int i = 0;
for (URI graph : namedGraphs) {
buffer.append(graph);
if (++i < graphs.size())
buffer.append(", ");
}
}
buffer.append("), ");
}
int limit = this.getLimit();
if (limit > -1)
buffer.append("LIMIT(" + limit + "), ");
int offset = this.getOffset();
if (offset > -1)
buffer.append("OFFSET(" + offset + "), ");
List<OrderingCondition> orderingConditions = this.getOrderingConditions();
if (orderingConditions.size() > 0) {
buffer.append("OrderBy(");
int i = 0;
for (OrderingCondition condition : orderingConditions) {
condition.getCondition().prettyPrint(buffer);
if (++i < orderingConditions.size())
buffer.append(", ");
}
buffer.append("), ");
}
this.getQueryResultForm().prettyPrint(buffer);
buffer.append(", GraphPattern(");
queryPattern.prettyPrint(buffer);
buffer.append(")");
buffer.append(")");
}
protected boolean visitURI(URIVisitor v, URI u) {
if (u != null)
return v.visitURI(u);
return true;
}
// private boolean visitVariable(VariableVisitor v, Variable var) {
// if (v != null)
// return v.visitVariable(var);
// return true;
// }
protected void visitURIs(URIVisitor v, boolean includeBaseAndPrefixes, boolean includeGraphsAndDatasets) {
if (includeBaseAndPrefixes) {
if (!visitURI(v, this.baseUri))
return;
for (URI u : this.prefixMap.values())
if (!visitURI(v, u))
return;
}
if (includeGraphsAndDatasets) {
for (URI u : getQueryDataset().getDefaultGraphURIs())
if (!visitURI(v, u))
return;
for (URI u : getQueryDataset().getNamedGraphURIs())
if (!visitURI(v, u))
return;
for (URI u : getQueryDataset().getNamedDatasetURIs())
if (!visitURI(v, u))
return;
}
ArrayList<QueryPart> queryParts = new ArrayList<QueryPart>();
queryParts.add(this.resultForm);
queryParts.add(this.queryPattern);
queryParts.addAll(this.ordering);
for (QueryPart part : queryParts)
for (URI u : part.getReferencedURIs())
if (!visitURI(v, u))
return;
}
static interface URIVisitor {
public boolean visitURI(URI u);
}
static interface VariableVisitor {
public boolean visitVariable(Variable v);
}
/**
* @return the cancelled
*/
public boolean isCancelled() {
return cancelled;
}
/**
* @param cancelled
* the cancelled to set
*/
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}