/*
* Copyright (c) 2009-2012 Clark & Parsia, LLC. <http://www.clarkparsia.com>
*
* 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 com.clarkparsia.empire.impl;
import org.openrdf.model.Graph;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.vocabulary.XMLSchema;
import org.openrdf.model.impl.ValueFactoryImpl;
import org.openrdf.query.BindingSet;
import com.clarkparsia.empire.ds.DataSource;
import com.clarkparsia.empire.ds.ResultSet;
import com.clarkparsia.empire.ds.QueryException;
import com.clarkparsia.empire.Dialect;
import com.clarkparsia.empire.EmpireOptions;
import static com.clarkparsia.empire.util.EmpireUtil.asPrimaryKey;
import com.clarkparsia.empire.util.BeanReflectUtil;
import com.clarkparsia.empire.annotation.RdfGenerator;
import com.clarkparsia.empire.annotation.AnnotationChecker;
import com.clarkparsia.empire.annotation.runtime.Proxy;
import com.clarkparsia.empire.annotation.runtime.ProxyAwareList;
import com.complexible.common.base.Dates;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Implementation of the JPA {@link Query} interface for RDF based query languages.</p>
*
* @author Michael Grove
* @since 0.1
* @version 0.7
*/
public final class RdfQuery implements Query {
/**
* The logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(RdfQuery.class.getName());
/**
* Variable parameter token in queries
*/
public static final String VARIABLE_TOKEN = "??";
/**
* Regex for finding the variable token(s) in a query string
*/
public static final String VT_RE = "\\?\\?";
/**
* The default name expected to be used in queries to denote what is to be returned as objects from the result
* set of the query. Can by changed by specifying a QueryHint with the key {@link #HINT_PROJECTION_VAR}
*/
protected static final String MAGIC_PROJECTION_VAR = "result";
/**
* Key of the {@link javax.persistence.QueryHint} to specify a different projection var
* than specified by the default {@link #MAGIC_PROJECTION_VAR}
*/
public static final String HINT_PROJECTION_VAR = "projection-var";
/**
* Key of the {@link javax.persistence.QueryHint} to specify the bean/entity class to be returned by the query.
*/
public static final String HINT_ENTITY_CLASS = "entity-class";
/**
* The DataSource the query will be executed against
*/
private DataSource mSource;
/**
* The raw query string
*/
private String mQuery;
/**
* The bean class, this is the type of objects returned by this query
*/
private Class mClass;
/**
* Map of parameter index (not string index, their numbered index, eg the first parameter (1), the second (2))
* to the value of that parameter
*/
private Map<Integer, Value> mIndexedParameters = new HashMap<Integer, Value>();
/**
* Map of parameter names to their values
*/
private Map<String, Value> mNamedParameters = new HashMap<String, Value>();
/**
* The current limit of the query, or -1 for no limit
*/
private int mLimit = -1;
/**
* The current result set offset, or -1 for no offset
*/
private int mOffset = -1;
/**
* Whether or not the query results are distinct, the default is true.
*/
private boolean mIsDistinct = true;
/**
* Whether or not this is a construct query.
*/
private boolean mIsConstruct = false;
/**
* The map of asserted query hints.
*/
private Map<String, Object> mHints = new HashMap<String, Object>();
/**
* The dialect of the query represented by this query object.
*/
private Dialect mQueryDialect;
private static String UNAMED_VAR_REGEX = VT_RE + "[\\.\\s})]";
private static String NAMED_VAR_REGEX = VT_RE + "[a-zA-Z0-9_\\-]+";
/**
* Create a new RdfQuery
* @param theSource the data source the query is run against
* @param theQueryString the query string
*/
public RdfQuery(final DataSource theSource, String theQueryString) {
mSource = theSource;
mQuery = theQueryString;
mQueryDialect = theSource.getQueryFactory().getDialect();
mQueryDialect.validateQueryFormat(getQueryString(), getProjectionVarName());
// trying to guess if this is a construct query or not. this is not foolproof, but since the only way of
// definitely specifying this right now is to cast a query object as an RdfQuery and use setConstruct, that
// is not ideal. so we'll take a crack guessing it here.
if (getQueryString().trim().toLowerCase().startsWith("construct")) {
setConstruct(true);
}
parseParameters();
}
/**
* @inheritDoc
*/
@Override
public String toString() {
return query();
}
/**
* Returns the class of Java beans returned as the results of the executed query. When no bean class is specified,
* raw {@link BindingSet} objects are returned.
* @return the class, or null if one is not specified.
*/
public Class getBeanClass() {
if (mClass != null) {
return mClass;
}
else if (getHints().containsKey(HINT_ENTITY_CLASS)) {
Object aValue = getHints().get(HINT_ENTITY_CLASS);
if (aValue instanceof Class) {
return (Class) aValue;
}
else {
try {
return BeanReflectUtil.loadClass(aValue.toString());
}
catch (ClassNotFoundException e) {
LOGGER.error("Invalid Entity class query set, value not found: " + aValue);
return null;
}
}
}
else {
return null;
}
}
/**
* Sets the class of Java beans returned by executions of this query.
* @param theClass the bean class
* @return this query object
*/
public Query setBeanClass(Class<?> theClass) {
mClass = theClass;
return this;
}
/**
* Return the DataSource the query will be run against.
* @return the source
* @see DataSource
*/
DataSource getSource() {
return mSource;
}
/**
* Set the DataSource the query will be run against
* @param theSource the new source
*/
void setSource(final DataSource theSource) {
mSource = theSource;
}
/**
* Return the raw query string as provided by the user. This will contain un-escaped variables and is likely
* to be missing its type (select | construct).
* @return the un-modified query string
*/
protected String getQueryString() {
return mQuery;
}
/**
* Return the result set limit for this query
* @return the limit
*/
public int getMaxResults() {
return mLimit;
}
/**
* Return the current offset of this query
* @return the offset index
*/
public int getFirstResult() {
return mOffset;
}
/**
* Set whether or not to enable the distinct modifier for this query
* @param theDistinct true to enable, false otherwise
* @return this query instance
*/
public Query setDistinct(boolean theDistinct) {
mIsDistinct = theDistinct;
return this;
}
/**
* Return whether or not the distinct modifier is enabled for this query
* @return true if the results will be distinct, false otherwise
*/
public boolean isDistinct() {
return mIsDistinct;
}
/**
* Set whether or not this query object represents a construct query.
* @param theConstruct true to set this as a construct query, false otherwise
* @return this query instance
* @see #isConstruct
*/
public Query setConstruct(boolean theConstruct) {
mIsConstruct = theConstruct;
return this;
}
/**
* Return whether or not this is a construct query. If this is an instance of a construct query, getSingleResult
* will return a {@link Graph} and getResultList will return a List with a single element which is an instance of
* Graph. Otherwise, when it's a select query, these will return a single
* {@link BindingSet}, or a list of Bindings (or instances of
* the Bean class, when specified) respectively.
* @return true if this is a construct query, false otherwise.
*/
public boolean isConstruct() {
return mIsConstruct;
}
/**
* Execute the describe query.
* @return the resulting RDF graph
* @throws QueryException if there is an error while querying
*/
public Graph executeDescribe() throws QueryException {
return getSource().describe(query());
}
/**
* Execute an ask query.
* @return the boolean result of the ask query
* @throws QueryException if there is an error while querying
*/
public boolean executeAsk() throws QueryException {
return getSource().ask(query());
}
/**
* Performs a select query
* @return the result set
* @throws QueryException if there is an error while querying
*/
public ResultSet executeSelect() throws QueryException {
return getSource().selectQuery(query());
}
/**
* Performs a construct query
* @return the result graph
* @throws QueryException if there is an error while querying
*/
public Graph executeConstruct() throws QueryException {
return getSource().graphQuery(query());
}
/**
* @inheritDoc
*/
@SuppressWarnings("unchecked")
public List getResultList() {
List aList = new ProxyAwareList();
try {
if (isConstruct()) {
Graph aGraph = getSource().graphQuery(query());
aList.add(aGraph);
}
else {
ResultSet aResults = getSource().selectQuery(query());
try {
if (getBeanClass() != null) {
// for now, by convention, for this to work like the JPQL stuff where you do something like
// "from Product pr join pr.poc as p where p.id = ?" and expect to get a list of Product instances
// back as the result set, you *MUST* have a var in the projection called 'result' which is
// the URI of the things you want to get back; when you don't do this, we prefix your partial query
// with this string
while (aResults.hasNext()) {
BindingSet aBS = aResults.next();
Object aObj;
String aVarName = getProjectionVarName();
if (aBS.getValue(aVarName) instanceof URI && AnnotationChecker.isValid(getBeanClass())) {
if (EmpireOptions.ENABLE_QUERY_RESULT_PROXY) {
aObj = new Proxy(getBeanClass(), asPrimaryKey(aBS.getValue(aVarName)), getSource());
}
else {
aObj = RdfGenerator.fromRdf(getBeanClass(),
asPrimaryKey(aBS.getValue(aVarName)),
getSource());
}
}
else {
aObj = new RdfGenerator.ValueToObject(getSource(), null,
getBeanClass(), null).apply(aBS.getValue(aVarName));
}
// if the object could not be created, or it was and its not the bean class type, or not a proxy
// for something of the bean class type, then we could not bind the value in the result set
// which is an error.
if (aObj == null
|| !(getBeanClass().isInstance(aObj) || (aObj instanceof Proxy && getBeanClass().isAssignableFrom(((Proxy)aObj).getProxyClass())))) {
throw new PersistenceException("Cannot bind query result to bean: " + getBeanClass());
}
else {
aList.add(aObj);
}
}
}
else {
aList.addAll(Lists.newArrayList(aResults));
}
}
finally {
aResults.close();
}
}
}
catch (Exception e) {
throw new PersistenceException(e);
}
return aList;
}
/**
* Returns the name of the projection variable that is to represent the return value of the query. By default
* this is {@link #MAGIC_PROJECTION_VAR} but you can override this by setting the {@link #HINT_PROJECTION_VAR}
* QueryHint value.
* @return the name of the projection variable to grab
*/
protected String getProjectionVarName() {
if (getHints().containsKey(HINT_PROJECTION_VAR)) {
return getHints().get(HINT_PROJECTION_VAR).toString();
}
else {
return MAGIC_PROJECTION_VAR;
}
}
/**
* @inheritDoc
*/
public Object getSingleResult() {
List aResults = getResultList();
if (aResults == null || aResults.isEmpty()) {
throw new NoResultException();
}
else if (aResults.size() > 1) {
throw new NonUniqueResultException();
}
return aResults.get(0);
}
/**
* @inheritDoc
*/
public int executeUpdate() {
throw new UnsupportedOperationException("Update operations are not supported.");
}
/**
* @inheritDoc
*/
public Query setMaxResults(final int theLimit) {
mLimit = theLimit;
return this;
}
/**
* @inheritDoc
*/
public Query setFirstResult(final int theOffset) {
mOffset = theOffset;
return this;
}
/**
* @inheritDoc
*/
public Query setHint(final String theName, final Object theObj) {
mHints.put(theName, theObj);
return this;
}
/**
* Return a map of the current query hints
* @return the query hints
*/
protected Map<String, Object> getHints() {
return mHints;
}
/**
* @inheritDoc
*/
public Query setParameter(final String theName, final Object theObj) {
validateParameterName(theName);
mNamedParameters.put(theName, validateParameterValue(theObj));
return this;
}
/**
* @inheritDoc
*/
public Query setParameter(final String theName, final Date theDate, final TemporalType theTemporalType) {
Calendar aCal = Calendar.getInstance();
aCal.setTime(theDate);
return setParameter(theName, aCal, theTemporalType);
}
/**
* @inheritDoc
*/
public Query setParameter(final String theName, final Calendar theCalendar, final TemporalType theTemporalType) {
validateParameterName(theName);
Value aValue = asValue(theCalendar, theTemporalType);
mNamedParameters.put(theName, aValue);
return this;
}
/**
* @inheritDoc
*/
public Query setParameter(final int theIndex, final Object theValue) {
validateParameterIndex(theIndex);
mIndexedParameters.put(theIndex, validateParameterValue(theValue));
return this;
}
/**
* @inheritDoc
*/
public Query setParameter(final int theIndex, final Date theDate, final TemporalType theTemporalType) {
validateParameterIndex(theIndex);
return this;
}
/**
* @inheritDoc
*/
public Query setParameter(final int theIndex, final Calendar theCalendar, final TemporalType theTemporalType) {
validateParameterIndex(theIndex);
return this;
}
/**
* @inheritDoc
*/
public Query setFlushMode(final FlushModeType theFlushModeType) {
if (theFlushModeType != FlushModeType.AUTO) {
throw new IllegalArgumentException("Commit style flush mode not supported");
}
return this;
}
/**
* Return the given date object with the specified temporal type as a {@link Value}
* @param theDate the date
* @param theTemporalType the type to extract from the date
* @return the time w.r.t to the TemportalType as a Value
*/
private Value asValue(final Calendar theDate, final TemporalType theTemporalType) {
Value aValue = null;
switch (theTemporalType) {
case DATE:
aValue = ValueFactoryImpl.getInstance().createLiteral(Dates.date(theDate.getTime()), XMLSchema.DATE);
break;
case TIME:
aValue = ValueFactoryImpl.getInstance().createLiteral(Dates.datetime(theDate.getTime()), XMLSchema.TIME);
break;
case TIMESTAMP:
aValue = ValueFactoryImpl.getInstance().createLiteral("" + theDate.getTime().getTime(), XMLSchema.TIME);
break;
}
return aValue;
}
/**
* Validate that a parameter with the given name exists
* @param theName the parameter name to validate
* @throws IllegalArgumentException thrown if a parameter with the given name does not exist
*/
private void validateParameterName(String theName) {
if (!mNamedParameters.containsKey(theName)) {
throw new IllegalArgumentException("Parameter with name '" + theName + "' does not exist");
}
}
/**
* Validate that the specified instance is a {@link Value} or can be
* {@link com.clarkparsia.empire.annotation.RdfGenerator.AsValueFunction turned into one}
* @param theValue the instance to validate
* @return the validated value
*/
private Value validateParameterValue(Object theValue) {
if (!(theValue instanceof Value)) {
try {
return new RdfGenerator.AsValueFunction().apply(theValue);
}
catch (RuntimeException e) {
// this is currently what is thrown when the function cannot transform the value
throw new IllegalArgumentException(e);
}
}
else {
return (Value) theValue;
}
}
/**
* Validate that a parameter at the given index exists
* @param theIndex the index to validate
* @throws IllegalArgumentException if a parameter at the given index does not exist
*/
private void validateParameterIndex(int theIndex) {
if (!mIndexedParameters.containsKey(theIndex)) {
throw new IllegalArgumentException("Parameter at index " + theIndex + " does not exist.");
}
}
/**
* Given the query string fragment, replace all variable parameter tokens with the values specified by the user
* through the various setParameter methods.
* @param theQuery the query fragment
* @return the query string with all parameter variables replaced
* @see #setParameter
*/
private String insertVariables(String theQuery) {
String aBuffer = theQuery;
for (String aName : mNamedParameters.keySet()) {
boolean containsParam = Pattern.compile(VT_RE+aName).matcher(aBuffer).find();
if (mNamedParameters.get(aName) != null && containsParam) {
aBuffer = replaceVariable(aBuffer, aName, mNamedParameters.get(aName));
}
}
int aIndex = 1;
while (aBuffer.indexOf(VARIABLE_TOKEN) != -1) {
boolean containsParam = Pattern.compile(VT_RE).matcher(aBuffer).find();
if (mIndexedParameters.get(aIndex) != null && containsParam) {
aBuffer = aBuffer.replaceFirst(VT_RE, mQueryDialect.asQueryString(mIndexedParameters.get(aIndex++)));
}
else {
break;
}
}
return aBuffer;
}
private String replaceVariable(String theQuery, String theVariable, Value theValue) {
// using this instead of replaceAll -- which keeps giving group does not exist errors. I think my regex must
// be subtly (is that a word?) wrong and I'm just not seeing it. This works, for now.
StringBuffer aQueryBuffer = new StringBuffer();
Matcher m = Pattern.compile(VT_RE+theVariable).matcher(theQuery);
int start = 0;
while (m.find()) {
aQueryBuffer.append(theQuery.substring(start, m.start()));
aQueryBuffer.append(mQueryDialect.asQueryString(theValue));
start = m.start() + m.group(0).length();
}
aQueryBuffer.append(theQuery.substring(start));
return aQueryBuffer.toString();
}
/**
* Given a query fragment from {@link #getQueryString} pull out all the variable parameters
*/
private void parseParameters() {
mNamedParameters.clear();
mIndexedParameters.clear();
Matcher aMatcher = Pattern.compile(UNAMED_VAR_REGEX).matcher(getQueryString());
// i'm pretty sure the JPA stuff is 1-indexed rather than the normal 0-indexed
int aIndex = 1;
while (aMatcher.find()) {
mIndexedParameters.put(aIndex++, null);
}
aMatcher = Pattern.compile(NAMED_VAR_REGEX).matcher(getQueryString());
while (aMatcher.find()) {
mNamedParameters.put(getQueryString().substring(aMatcher.start() + VARIABLE_TOKEN.length(), aMatcher.end()), null);
}
}
protected boolean startsWithKeyword(String theQuery) {
String q = theQuery.toLowerCase().trim();
return q.startsWith("select") || q.startsWith("construct") || q.startsWith("ask") || q.startsWith("describe");
}
/**
* Return a valid, executable query instance from the specified query fragment, and user specified settings such
* as parameter values, limit, offset, etc.
* @return a valid query that can be run against a DataSource
*/
protected String query() {
// use some regexs to look for and remove limits and offsets specified in the query string and store them locally
// these will get postfixed to the query later on.
boolean containsLimit = Pattern.compile("limit(\\s)*[0-9]+[^}]*").matcher(getQueryString()).find();
boolean containsOffset = Pattern.compile("offset(\\s)*[0-9]+[^}]*").matcher(getQueryString()).find();
if (containsLimit) {
String aLimitGrabRegex = "limit(\\s)*[0-9]+";
Matcher m = Pattern.compile(aLimitGrabRegex).matcher(getQueryString());
m.find();
if (getMaxResults() == -1) {
setMaxResults(Integer.parseInt(m.group(0).split(" ")[1]));
}
mQuery = mQuery.replaceAll(aLimitGrabRegex, "");
}
if (containsOffset) {
String aOffsetGrabRegex = "offset(\\s)*[0-9]+";
Matcher m = Pattern.compile(aOffsetGrabRegex).matcher(getQueryString());
m.find();
if (getFirstResult() == -1) {
setFirstResult(Integer.parseInt(m.group(0).split(" ")[1]));
}
mQuery = mQuery.replaceAll(aOffsetGrabRegex, "");
}
String queryStr = insertVariables(getQueryString()).trim();
queryStr = replaceUnusedVariableTokens(queryStr);
// validateVariables();
// TODO: should we get the values for the keywords used here (select, distinct, construct, limit, offset) from
// the subclass rather than hard coding them? or will these be the same for all rdf based query languages?
StringBuffer aQuery = new StringBuffer(queryStr);
if (!aQuery.toString().toLowerCase().startsWith(mQueryDialect.patternKeyword())
&& !startsWithKeyword(aQuery.toString())) {
aQuery.insert(0, mQueryDialect.patternKeyword());
}
StringBuffer aStart = new StringBuffer();
if (!startsWithKeyword(getQueryString())) {
aStart.insert(0, isConstruct() ? "construct " : "select ").append(isDistinct() ? " distinct " : "").append(" ");
if (isConstruct()) {
aStart.append(" * ");
}
else {
aStart.append(mQueryDialect.asProjectionVar(getProjectionVarName())).append(" ");
}
}
aQuery.insert(0, aStart.toString());
if (getMaxResults() != -1) {
aQuery.append(" limit ").append(getMaxResults());
}
if (getFirstResult() != -1) {
aQuery.append(" offset ").append(getFirstResult());
}
mQueryDialect.insertNamespaces(aQuery);
return aQuery.toString();
}
/**
* Replaces all unused variable place holders with syntactically correct replacements. Unnamed variable tokens "??"
* in the query string get replaced with "[]" and named variable tokens "??foo" get turned into normal variables,
* e.g. "?foo"
* @param theQuery the query
* @return the query w/ unused variables replaced w/ the appropriate equivalents.
*/
private String replaceUnusedVariableTokens(String theQuery) {
StringBuffer aQueryBuffer = new StringBuffer();
Matcher m = Pattern.compile(NAMED_VAR_REGEX).matcher(theQuery);
int start = 0;
while (m.find()) {
aQueryBuffer.append(theQuery.substring(start, m.start()));
aQueryBuffer.append(mQueryDialect.asVar(m.group(0).replaceAll(VT_RE, "")));
start = m.start() + m.group(0).length();
}
aQueryBuffer.append(theQuery.substring(start));
return aQueryBuffer.toString().replaceAll(UNAMED_VAR_REGEX, mQueryDialect.asVar(null) + " ");
}
}