/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.topicmaps.query.parser;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.ontopia.utils.CompactHashSet;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.xml.AbstractTopicMapExporter;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.impl.utils.BindingContext;
import net.ontopia.topicmaps.query.impl.utils.QueryAnalyzer;
/**
* INTERNAL: Used to represent parsed SELECT queries.
*/
public class TologQuery extends TologStatement {
protected List clauses; // the actual predicates of the query part, if any
protected Map arguments; // external args used to resolve param refs
protected Map vartypemap; // variable -> Object[] containing possible types
protected Map ptypemap; // parameter -> Object[] containing possible types
protected List<Variable> variables; // select * from variables
protected Set<Variable> countVariables;
protected Set<Variable> allVariables;
protected List orderBy;
protected Set<String> orderDescending;
protected int limit;
protected int offset;
protected List selected_variables; // cached unmodifiable collection
public TologQuery() {
clauses = new ArrayList();
orderBy = new ArrayList();
countVariables = new CompactHashSet<Variable>();
variables = new ArrayList();
orderDescending = new CompactHashSet<String>();
limit = -1;
offset = -1;
}
public List getClauses() {
return clauses;
}
public void setClauseList(List clauses) {
this.clauses = clauses;
}
public Map getArguments() {
// NOTE: needed by the RDBMS implementation.
return arguments;
}
public void setArguments(Map arguments) {
this.arguments = arguments;
}
public Object getArgument(String name) throws InvalidQueryException {
if (arguments == null)
throw new InvalidQueryException("Tried to get value for query parameter '" +
name + "', but no arguments provided");
Object value = arguments.get(name);
if (value == null)
throw new InvalidQueryException("No value supplied for query parameter '" +
name + "'");
return value;
}
/// query introspection
public Map getVariableTypes() {
return vartypemap;
}
public Map getParameterTypes() {
return ptypemap;
}
/// ParsedQueryIF implementation [the class does not implement the interface]
public List getSelectedVariables() {
if (selected_variables == null) {
// Need to resolve the list of selected variables once, because
// allVariables is unordered.
if (variables.size() > 0)
selected_variables = Collections.unmodifiableList(variables);
else
selected_variables = Collections.unmodifiableList(new ArrayList(allVariables));
}
return selected_variables;
}
/**
* Used to override the actual query string and set the projection from
* code. Not usually used, but used by the tolog INSERT statement.
*/
public void setSelectedVariables(List<Variable> vars) {
selected_variables = null;
variables = vars;
}
public boolean hasSelectClause() {
return !variables.isEmpty() || !countVariables.isEmpty();
}
public String[] getSelectedVariableNames() {
List selected = getSelectedVariables();
int width = selected.size();
String[] colnames = new String[width];
for (int i=0; i < width; i++) {
colnames[i] = ((Variable)selected.get(i)).getName();
}
return colnames;
}
public Collection getAllVariables() {
return allVariables;
}
public Collection getCountedVariables() {
return countVariables;
}
public List getOrderBy() {
return orderBy;
}
public boolean isOrderedAscending(String name) {
return !orderDescending.contains(name);
}
/// Object implementation
public String toString() {
StringBuilder buf = new StringBuilder();
// select clause
buf.append("select ");
List selected = getSelectedVariables();
if (!selected.isEmpty()) {
for (int ix = 0; ix < selected.size(); ix++) {
if (ix > 0) buf.append(", ");
if (countVariables.contains(selected.get(ix)))
buf.append("count(" + selected.get(ix) + ")");
else
buf.append(selected.get(ix));
}
buf.append(" from \n");
}
// predicates
buf.append(toStringFromPart());
// order by
if (!orderBy.isEmpty()) {
buf.append(" order by ");
for (int ix = 0; ix < orderBy.size(); ix++) {
if (ix > 0) buf.append(", ");
buf.append(orderBy.get(ix));
}
buf.append("\n");
}
// limit/offset
if (limit != -1)
buf.append(" limit " + limit);
if (offset != -1)
buf.append(" offset " + offset);
buf.append("?");
// variable types
// buf.append("\n\n");
// Iterator it = vartypemap.keySet().iterator();
// while (it.hasNext()) {
// Object name = it.next();
// buf.append("$" + name + " -> " + net.ontopia.utils.DebugUtils.toString((Object[]) vartypemap.get(name)) + "\n");
// }
return buf.toString();
}
public static String toString(List clauses) {
StringBuilder buf = new StringBuilder();
Set rules = new CompactHashSet();
for (int ix = 0; ix < clauses.size(); ix++) {
if (ix > 0) buf.append(", ");
AbstractClause theClause = (AbstractClause) clauses.get(ix);
if (theClause instanceof PredicateClause) {
PredicateClause clause = (PredicateClause) theClause;
PredicateIF predicate = clause.getPredicate();
buf.append(predicate.getName() + "(" +
argumentsToString(clause.getArguments()) + ")");
if (predicate instanceof net.ontopia.topicmaps.query.impl.basic.RulePredicate)
rules.add(predicate);
} else if (theClause instanceof OrClause) {
OrClause clause = (OrClause) theClause;
buf.append("{ ");
List alts = clause.getAlternatives();
for (int i = 0; i < alts.size(); i++) {
if (i > 0) buf.append(" | ");
buf.append(toString((List) alts.get(i)));
}
buf.append(" }");
} else if (theClause instanceof NotClause) {
NotClause clause = (NotClause) theClause;
buf.append("not(" + toString(clause.getClauses()) + ")");
} else
throw new OntopiaRuntimeException("Unknown clause type:" + theClause);
buf.append("\n");
}
// Iterator it = rules.iterator();
// while (it.hasNext()) {
// PredicateIF predicate = (PredicateIF) it.next();
// buf.append("\n\n\n---" + predicate.getName() + "\n");
// buf.append(toString(((net.ontopia.topicmaps.query.impl.basic.RulePredicate) predicate).getClauses()));
// }
return buf.toString();
}
protected String toStringFromPart() {
return toString(clauses);
}
private static String argumentsToString(List arguments) {
StringBuilder buf = new StringBuilder();
for (int ix = 0; ix < arguments.size(); ix++) {
if (ix > 0) buf.append(", ");
valueToString(arguments.get(ix), buf);
}
return buf.toString();
}
private static void valueToString(Object arg, StringBuilder buf) {
if (arg instanceof String)
buf.append('"').append(arg).append('"');
else if (arg instanceof TopicIF)
buf.append(topicToString((TopicIF) arg));
else if (arg instanceof Pair) {
Pair pair = (Pair) arg;
valueToString(pair.getFirst(), buf);
buf.append(" : ");
buf.append(topicToString((TopicIF) pair.getSecond()));
} else
buf.append(arg);
}
private static String topicToString(TopicIF topic) {
String fallbackid = null; // bad IDs, only used in worst case
Iterator it = topic.getItemIdentifiers().iterator();
while (it.hasNext()) {
LocatorIF loc = (LocatorIF) it.next();
String addr = loc.getAddress();
int pos = addr.lastIndexOf('#');
if (pos != -1) {
String id = addr.substring(pos + 1);
if (AbstractTopicMapExporter.mayCollide(id))
fallbackid = id;
else
return id;
}
}
it = topic.getSubjectIdentifiers().iterator();
if (it.hasNext())
return "i\"" + ((LocatorIF) it.next()).getAddress() + "\"";
it = topic.getSubjectLocators().iterator();
if (it.hasNext())
return "a\"" + ((LocatorIF) it.next()).getAddress() + "\"";
if (fallbackid != null)
return fallbackid;
return "@" + topic.getObjectId();
}
/// Modifiers
public void addVariable(Variable variable) throws AntlrWrapException {
if (variables.contains(variable))
throw new AntlrWrapException(
new InvalidQueryException("Variable " + variable +
" appears twice in select clause"));
variables.add(variable);
}
public void addCountVariable(Variable variable) throws AntlrWrapException {
if (variables.contains(variable))
throw new AntlrWrapException(
new InvalidQueryException("Variable " + variable +
" appears twice in select clause"));
variables.add(variable);
countVariables.add(variable);
}
public void addOrderBy(Variable variable, boolean ascending) {
orderBy.add(variable);
if (!ascending)
orderDescending.add(variable.getName());
}
public void close() throws InvalidQueryException {
// compute the variables we calculate
allVariables = new CompactHashSet<Variable>();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
Iterator it = clause.getAllVariables().iterator();
while (it.hasNext())
allVariables.add((Variable) it.next());
}
// verify that SELECT variables actually exist
for (int ix = 0; ix < variables.size(); ix++)
if (!allVariables.contains(variables.get(ix)))
throw new InvalidQueryException("Variable " + variables.get(ix) + " in select clause not used in query");
// verify that ORDER BY variables actually exist (and are selected)
for (int ix = 0; ix < orderBy.size(); ix++) {
if (!allVariables.contains(orderBy.get(ix)))
throw new InvalidQueryException("Variable " + orderBy.get(ix) + " in order by clause not used in query");
if (!(variables.isEmpty() && countVariables.isEmpty()) &&
!variables.contains(orderBy.get(ix)) &&
!countVariables.contains(orderBy.get(ix)))
throw new InvalidQueryException("Variable " + orderBy.get(ix) + " in order by clause not in select list");
}
// type analysis
BindingContext bc = QueryAnalyzer.analyzeTypes(this);
vartypemap = bc.getVariableTypes();
Iterator it = vartypemap.keySet().iterator();
while (it.hasNext()) {
String key = (String) it.next();
Object[] value = (Object[]) vartypemap.get(key);
// verify that types are compatible
if (value.length > 1) {
int seedType = getTypeIdentifier((Class)value[0]);
for (int ix = 1; ix < value.length; ix++) {
if (!isCompatibleTypes(seedType, getTypeIdentifier((Class)value[ix])))
throw new InvalidQueryException("Variable " + key + " can be bound to incompatible types: '" +
value[0] + "' and '" + value[ix] + "'");
}
}
}
ptypemap = bc.getParameterTypes();
}
// Note: class types mapped to type identifiers in order to make it
// easier and faster to check for incompatibilities.
protected int TYPE_TMObjectIF = 1;
protected int TYPE_String = 2;
protected int TYPE_Number = 4;
private int getTypeIdentifier(Class type) throws InvalidQueryException {
if (TMObjectIF.class.isAssignableFrom(type))
return TYPE_TMObjectIF;
else if (String.class.equals(type))
return TYPE_String;
else if (Integer.class.equals(type))
return TYPE_Number;
else if (Float.class.equals(type))
return TYPE_Number;
else
throw new InvalidQueryException("Unsupported variable type: " + type);
}
private boolean isCompatibleTypes(int type1, int type2) {
if (type1 == type2)
return true;
else
return false;
}
private boolean isTMObject(Class aclass) {
Class[] ifs = aclass.getInterfaces();
for (int ix = 0; ix < ifs.length; ix++)
if (ifs[ix].equals(TMObjectIF.class) || isTMObject(ifs[ix]))
return true;
return false;
}
public void setLimit(int limit) {
this.limit = limit;
}
public int getLimit() {
return limit;
}
public void setOffset(int offset) throws InvalidQueryException {
this.offset = offset;
}
public int getOffset() {
return offset;
}
}