/*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is the Kowari Metadata Store.
*
* The Initial Developer of the Original Code is Plugged In Software Pty
* Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions
* created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002
* Plugged In Software Pty Ltd. All Rights Reserved.
*
* Contributor(s):
* Move to java-generics copyright Netymon Pty Ltd
*
* [NOTE: The text of this Exhibit A may differ slightly from the text
* of the notices in the Source Code files of the Original Code. You
* should use the text of this Exhibit A rather than the text found in the
* Original Code Source Code for Your Modifications.]
*
*/
package org.mulgara.query;
// Java 2 standard packages;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.log4j.Logger;
import org.mulgara.connection.Connection;
import org.mulgara.connection.Connection.SessionOp;
import org.mulgara.query.operation.Command;
import org.mulgara.server.Session;
/**
* An ITQL query. This is a data structure used as an argument to the
* {@link org.mulgara.server.Session#query} method.
*
* @created 2001-08-12
*
* @author <a href="http://staff.pisoftware.com/raboczi">Simon Raboczi</a>
*
* @company <A href="mailto:info@PIsoftware.com">Plugged In Software</A>
*
* @copyright © 2001-2003 <A href="http://www.PIsoftware.com/">Plugged In
* Software Pty Ltd</A>
*
* @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a>
*/
public class Query implements Cloneable, Serializable, Command {
/**
* Allow newer compiled version of the stub to operate when changes
* have not occurred with the class.
* NOTE : update this serialVersionUID when a method or a public member is
* deleted.
*/
private static final long serialVersionUID = 5165341858790210479L;
/** Logger. */
private static final Logger logger = Logger.getLogger(Query.class.getName());
/**
* The variable list. This may only contain {@link Variable}s. It corresponds
* to the <code>select</code> clause. If it is <code>null</code>, it indicates
* that there is no <code>select</code> clause and that no projection will be
* performed.
*/
private List<SelectElement> variableList;
/**
* Mutable version of the variable list. This isn't exposed via {@link
* #getVariableList} the way {@link #variableList} is.
*/
private List<SelectElement> mutableVariableList;
/** The model expression. It corresponds to the <code>from</code> clause. */
private GraphExpression graphExpression;
/** The constraint expression. It corresponds to the <code>where</code> clause. */
private ConstraintExpression constraintExpression;
/** The having expression. It corresponds to the <code>having</code> clause. */
private ConstraintHaving havingConstraint;
/**
* The sort ordering. The elements of this list should be {@link Order}s, with
* major orderings preceding minor orderings. It's only sensible for this to
* contain orders on variables in the {@link #variableList}.
*/
private List<Order> orderList;
/**
* The limit on rows in the result. If this is <code>null</code>, it indicates
* that there is no limit.
*/
private Integer limit;
/** The offset on rows in the result. This value is never negative. */
private int offset;
/** Indicates that the results must be without duplicates. */
private boolean distinct = true;
/** The accumulated solutions. This can be <code>null</code>, indicating no solutions. */
private Answer answer;
/** A UI message describing the result of this message. */
private String resultMessage = "";
/** A lexical form for the query, set manually rather than constructed in {@link #toString()}. */
private String textualForm = "";
//
// Constructors
//
/**
* Construct a query.
*
* @param variableList {@link Variable}s or node values to appear as bindings
* in the solution (i.e. columns of the result {@link Answer});
* <code>null</code> indicates that all columns are to be retained.
* This is a list of: Variable; ConstantValue; Count; Subquery.
* @param graphExpression an expression defining the model to query, never
* <code>null</code>
* @param constraintExpression an expression defining the constraints to
* satisfy, never <code>null</code>
* @param havingExpression an expression defining the conditions to apply to
* aggregate functions or null if not given.
* @param orderList sort order column names. This is a list of {@link Order}s
* which is a wrapper around Variable, with an "ascending" flag.
* @param limit the maximum number of rows to return, which must be
* non-negative; <code>null</code> indicates no limit
* @param offset the number of rows to skip from the beginning of the result,
* never negative
* @param answer an existing solution set to which results must belong, or
* {@link UnconstrainedAnswer} for no constraints; never
* <code>null</code> is
* @throws IllegalArgumentException if <var>limit</var> or <var>offset</var>
* are negative, or if <var>graphExpression</var>,
* <var>constraintExpression</var>, <var>orderList<var> or
* <var>answer</var> are <code>null</code>
*/
public Query(List<? extends SelectElement> variableList, GraphExpression graphExpression,
ConstraintExpression constraintExpression,
ConstraintHaving havingExpression, List<Order> orderList, Integer limit,
int offset, boolean distinct, Answer answer) {
// Validate parameters
if (graphExpression == null) {
throw new IllegalArgumentException("Null \"graphExpression\" parameter");
} else if (constraintExpression == null) {
throw new IllegalArgumentException("Null \"constraintExpression\" parameter");
} else if ((limit != null) && (limit.intValue() < 0)) {
throw new IllegalArgumentException("Negative \"limit\" parameter");
} else if (orderList == null) {
throw new IllegalArgumentException("Null \"orderList\" parameter");
} else if (offset < 0) {
throw new IllegalArgumentException("Negative \"offset\" parameter");
} else if (answer == null) {
throw new IllegalArgumentException("Null \"answer\" parameter");
} else if (variableList != null) {
Set<Variable> variableSet = new HashSet<Variable>(constraintExpression.getVariables());
variableSet.addAll(Arrays.asList(answer.getVariables()));
for (Object o: variableList) {
if (o instanceof Variable) {
Variable var = (Variable)o;
if (!var.isBnodeVar() && !variableSet.contains(var)) {
if (logger.isDebugEnabled()) logger.debug("Failed to find " + var + " in " + variableSet);
throw new IllegalArgumentException("Failed to constrain all variables: " + var +
" not constrained in WHERE or GIVEN clauses");
}
if (var.isBnodeVar() && !(this instanceof ConstructQuery)) {
if (logger.isDebugEnabled()) logger.debug("BNode variable in non-CONSTRUCT query: " + var);
throw new IllegalArgumentException("BNode variable in non-CONSTRUCT query: " + var);
}
}
}
}
// Initialize fields
this.mutableVariableList = (variableList == null) ? null : new ArrayList<SelectElement>(variableList);
this.variableList = (variableList == null) ? null : Collections.unmodifiableList(mutableVariableList);
this.graphExpression = graphExpression;
this.constraintExpression = constraintExpression;
this.havingConstraint = havingExpression;
this.orderList = Collections.unmodifiableList(new ArrayList<Order>(orderList));
this.limit = limit;
this.offset = offset;
this.distinct = distinct;
this.answer = answer;
}
/**
* Construct a new query equivalent to substituing 'where' for the
* where-clause in the original query.
*/
public Query(Query query, ConstraintExpression where) {
this.mutableVariableList = query.mutableVariableList;
this.variableList = query.variableList;
this.graphExpression = query.graphExpression;
this.constraintExpression = where;
this.havingConstraint = query.havingConstraint;
this.orderList = query.orderList;
this.limit = query.limit;
this.offset = query.offset;
this.distinct = query.distinct;
this.answer = (query.answer != null) ?(Answer)query.answer.clone() : new UnconstrainedAnswer();
/*
this(query.getVariableList(),
query.getModelExpression(),
where,
query.getHavingExpression(),
query.getOrderList(),
query.getLimit(),
query.getOffset(),
(Answer)query.getGiven().clone());
*/
}
/**
* Cloning must always be supported.
*/
public Object clone() {
Query cloned;
try {
cloned = (Query) super.clone();
}
catch (CloneNotSupportedException e) {
throw new RuntimeException("Query subclass "+getClass()+" not cloneable");
}
// Copy mutable fields by value
if (variableList == null) {
cloned.mutableVariableList = null;
cloned.variableList = null;
} else {
cloned.variableList = new ArrayList<SelectElement>();
for (SelectElement o : variableList) {
if (o instanceof Subquery) {
Subquery s = (Subquery)o;
cloned.variableList.add(new Subquery(s.getVariable(), (Query)s.getQuery().clone()));
} else if (o instanceof Count) {
Count a = (Count)o;
cloned.variableList.add(new Count(a.getVariable(), (Query)a.getQuery().clone()));
} else {
cloned.variableList.add(o);
}
}
cloned.mutableVariableList = Collections.unmodifiableList(cloned.variableList);
}
cloned.graphExpression = graphExpression; // FIXME: should be cloned
cloned.answer = (Answer)answer.clone();
// Copy immutable fields by reference
cloned.orderList = orderList;
cloned.limit = limit;
cloned.offset = offset;
return cloned;
}
//
// API methods
//
/**
* Accessor for the <code>variableList</code> property.
*
* @return a {@link List} containing one or more {@link Variable}s, {@link ConstantValue}s,
* {@link Count}s or {@link Subquery}
*/
public List<SelectElement> getVariableList() {
return variableList;
}
/**
* Accessor for the <code>constraintExpression</code> property.
* @return a {@link ConstraintExpression}
*/
public ConstraintExpression getConstraintExpression() {
return constraintExpression;
}
/**
* Accessor for the <code>havingExpression</code> property.
* @return a {@link ConstraintExpression} containing only
* {@link ConstraintHaving} or <code>null</code> to indicate an empty
* having clause.
*/
public ConstraintHaving getHavingExpression() {
return havingConstraint;
}
/**
* Accessor for the <code>graphExpression</code> property.
* @return a {@link GraphExpression}, or <code>null</code> to indicate the empty model
*/
public GraphExpression getModelExpression() {
return graphExpression;
}
/**
* Accessor for the <code>orderList</code> property.
* @return a {@link List} containing one or more {@link Order}s
* (which wrap {@link Variable}s)
*/
public List<Order> getOrderList() {
return orderList;
}
/**
* Accessor for the <code>limit</code> property.
* @return the limit for this query, or <code>null</code> if unlimited
*/
public Integer getLimit() {
return limit;
}
/**
* Accessor for the <code>offset</code> property.
* @return the offset for this query, a non-negative integer
*/
public int getOffset() {
return offset;
}
/**
* Accessor for the <code>answer</code> property. If the <var>
* constraintExpression</var> property is <code>null</code>, this is the
* answer to the entire query.
* @return an {@link Answer}, or <code>null</code> to indicate the set of all statements
*/
public Answer getGiven() {
return answer;
}
/**
* Accessor for the DISTINCT property on this query.
* @return <code>true</code> if results of the query should not contain duplicates.
*/
public boolean isDistinct() {
return distinct;
}
//
// Methods overriding Object
//
/**
* Equality is by value.
* @param object The object to compare to
* @return <code>true</code> if object is functionaly equivalent to this object.
*/
public boolean equals(Object object) {
if (object == this) return true;
if (object == null) return false;
if (!(object instanceof Query)) return false;
Query query = (Query)object;
// Check the variableList field
if (!variableList.equals(query.variableList)) return false;
// Check the graphExpression field
if ((graphExpression == null) ?
(query.graphExpression != null) :
(!graphExpression.equals(query.graphExpression))) {
return false;
}
// Check the constraintExpression field
if ((constraintExpression == null) ?
(query.constraintExpression != null) :
(!constraintExpression.equals(query.constraintExpression))) {
return false;
}
if ((havingConstraint == null) ?
(query.havingConstraint != null) :
(!havingConstraint.equals(query.havingConstraint))) {
return false;
}
// Check the orderList field
if ((orderList == null) ^ (query.orderList == null)) return false;
if ((orderList != null) && !orderList.equals(query.orderList)) return false;
// Check the limit field
if ((limit == null) ^ (query.limit == null)) return false;
if ((limit != null) && !limit.equals(query.limit)) return false;
// Check the offset field
if (offset != query.offset) return false;
// Finally, it comes down to the answer field
return answer.equals(query.answer);
}
public int hashCode() {
return variableList.hashCode() + 3 * graphExpression.hashCode() +
5 * constraintExpression.hashCode() + 7 * havingConstraint.hashCode() +
11 * ((orderList == null) ? 0 : orderList.hashCode()) +
13 * ((limit == null) ? 0 : limit.hashCode()) +
17 * offset + answer.hashCode();
}
/**
* Close this {@link Query}, and the underlying {@link Answer} objects.
*/
public void close() throws TuplesException {
answer.close();
answer = null;
if (mutableVariableList != null) {
for (SelectElement v: mutableVariableList) {
if (v instanceof AggregateFunction) {
((AggregateFunction)v).getQuery().close();
}
}
}
}
/** @see org.mulgara.query.operation.Command#setText(java.lang.String) */
public void setText(String text) {
textualForm = text;
}
/**
* Returns the textual representation of this Command.
* @return The text of the command. This comes from the lexer, rather than reconstruction
* from the structure of the query.
*/
public String getText() {
return textualForm;
}
/**
* Generate a legible representation of the query.
*
* @return A string representing all the elements of a query.
*/
public String toString() {
StringBuffer buffer = new StringBuffer();
// SELECT
if (variableList != null) {
buffer.append("SELECT");
for (Object i: variableList) buffer.append(" ").append(i);
buffer.append(" ");
}
// FROM
buffer.append("FROM ").append(graphExpression);
// WHERE
buffer.append(" WHERE ").append(constraintExpression);
// HAVING
if (havingConstraint != null) buffer.append(" HAVING ").append(havingConstraint);
// ORDER BY
if (!orderList.isEmpty()) {
buffer.append(" ORDER BY");
for (Order o: orderList) buffer.append(" ").append(o);
}
// LIMIT
if (limit != null) buffer.append(" LIMIT ").append(limit.intValue());
// OFFSET
if (offset != 0) buffer.append(" OFFSET ").append(offset);
// GIVEN
if (answer != null) buffer.append(" GIVEN ").append(answer);
return buffer.toString();
}
/**
* Serializes the current object to a stream.
* @param out The stream to write to.
* @throws IOException If an I/O error occurs while writing.
*/
private void writeObject(ObjectOutputStream out) throws IOException {
// convert answer to be serializable if needed
if (!(answer instanceof Serializable)) {
// TODO: use a remote answer object if the given is too large
try {
Answer tmpAnswer = answer;
answer = new ArrayAnswer(answer);
tmpAnswer.close();
} catch (TuplesException e) {
throw new IOException("Unable to serialize GIVEN clause");
}
}
out.defaultWriteObject();
}
//
// Command interface methods
//
/**
* Operation can only be run by a server.
* @return <code>false</code> as this is AST for a server.
*/
public boolean isLocalOperation() {
return false;
}
/**
* Operation is not restricted to a user interface.
* @return <code>false</code> as this operation has no effect on a UI.
*/
public boolean isUICommand() {
return false;
}
/**
* Indicates that this command returns an Answer. Saves the overhead of checking
* the return type of execute.
* @return <code>true</code>.
*/
public boolean isAnswerable() {
return true;
}
/**
* Indicates that the command modifies the state in a transaction.
* @return <code>true</code> If the transaction state is to be modified.
*/
public boolean isTxCommitRollback() {
return false;
}
/**
* Gets the associated server for a non-local operation.
* @return the server URI, or <code>null</code> if the data should be found locally.
*/
public URI getServerURI() {
Set<URI> dbURIs = getModelExpression().getDatabaseURIs();
return dbURIs.isEmpty() ? null : dbURIs.iterator().next();
}
/**
* Gets a message text relevant to the operation. Useful for the UI.
* Consider changing this to a serialization of the result.
* @return A text message associated with the result of this operation. Usually empty.
*/
public String getResultMessage() {
return resultMessage;
}
/**
* Sets message text relevant to the operation. Useful for the UI.
* @return The set text.
*/
public String setResultMessage(String resultMessage) {
return this.resultMessage = resultMessage;
}
/**
* Executes this query on a connection.
* @param conn The connection to a database session to execute the query against.
* @return The answer to this query. This must be closed by the calling code.
*/
public Answer execute(Connection conn) throws QueryException, TuplesException {
if (logger.isDebugEnabled()) logger.debug("Executing query " + toString());
Answer answer = conn.execute(new SessionOp<Answer,QueryException>() {
public Answer fn(Session session) throws QueryException {
return session.query(Query.this);
}
});
if (answer == null) throw new QueryException("Invalid answer received");
if (logger.isDebugEnabled()) logger.debug("Successfully executed query");
// move to the first row
answer.beforeFirst();
return answer;
}
}