Package com.clarkparsia.empire.impl

Source Code of com.clarkparsia.empire.impl.RdfQuery

/*
* 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) + " ");
  }
}
TOP

Related Classes of com.clarkparsia.empire.impl.RdfQuery

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.