Java tutorial
/* * Copyright 2008 Alin Dreghiciu. * * 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 org.qi4j.index.rdf.query.internal; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import org.json.JSONException; import org.json.JSONStringer; import org.qi4j.api.common.QualifiedName; import org.qi4j.api.entity.Entity; import org.qi4j.api.property.StateHolder; import org.qi4j.api.query.grammar.AssociationIsNullPredicate; import org.qi4j.api.query.grammar.AssociationNullPredicate; import org.qi4j.api.query.grammar.BooleanExpression; import org.qi4j.api.query.grammar.ComparisonPredicate; import org.qi4j.api.query.grammar.Conjunction; import org.qi4j.api.query.grammar.ContainsAllPredicate; import org.qi4j.api.query.grammar.ContainsPredicate; import org.qi4j.api.query.grammar.Disjunction; import org.qi4j.api.query.grammar.EqualsPredicate; import org.qi4j.api.query.grammar.GreaterOrEqualPredicate; import org.qi4j.api.query.grammar.GreaterThanPredicate; import org.qi4j.api.query.grammar.LessOrEqualPredicate; import org.qi4j.api.query.grammar.LessThanPredicate; import org.qi4j.api.query.grammar.ManyAssociationContainsPredicate; import org.qi4j.api.query.grammar.MatchesPredicate; import org.qi4j.api.query.grammar.Negation; import org.qi4j.api.query.grammar.NotEqualsPredicate; import org.qi4j.api.query.grammar.OrderBy; import org.qi4j.api.query.grammar.Predicate; import org.qi4j.api.query.grammar.PropertyIsNullPredicate; import org.qi4j.api.query.grammar.PropertyNullPredicate; import org.qi4j.api.query.grammar.PropertyReference; import org.qi4j.api.query.grammar.SingleValueExpression; import org.qi4j.api.query.grammar.ValueExpression; import org.qi4j.api.value.ValueComposite; import org.qi4j.index.rdf.query.RdfQueryParser; import org.qi4j.runtime.types.SerializableType; import org.qi4j.runtime.types.ValueTypeFactory; import org.qi4j.spi.property.PropertyType; import org.qi4j.spi.property.ValueType; import org.slf4j.LoggerFactory; import static java.lang.String.*; /** * JAVADOC Add JavaDoc */ public class RdfQueryParserImpl implements RdfQueryParser { private static ThreadLocal<DateFormat> ISO8601_UTC = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; } }; private static final Map<Class<? extends Predicate>, String> m_operators; private static final Set<Character> reservedChars; private Namespaces namespaces = new Namespaces(); private Triples triples = new Triples(namespaces); static { m_operators = new HashMap<Class<? extends Predicate>, String>(); m_operators.put(EqualsPredicate.class, "="); m_operators.put(GreaterOrEqualPredicate.class, ">="); m_operators.put(GreaterThanPredicate.class, ">"); m_operators.put(LessOrEqualPredicate.class, "<="); m_operators.put(LessThanPredicate.class, "<"); m_operators.put(NotEqualsPredicate.class, "!="); m_operators.put(ManyAssociationContainsPredicate.class, "="); reservedChars = new HashSet<Character>( Arrays.asList('\"', '^', '.', '\\', '?', '*', '+', '{', '}', '(', ')', '|', '$', '[', ']')); } public String getQuery(final Class<?> resultType, final BooleanExpression whereClause, final OrderBy[] orderBySegments, final Integer firstResult, final Integer maxResults) { // Add type+identity triples last. This makes queries faster since the query engine can reduce the number of triples // to check faster triples.addDefaultTriples(resultType.getName()); // and collect namespaces final String filter = processFilter(whereClause, true); final String orderBy = processOrderBy(orderBySegments); StringBuilder query = new StringBuilder(); for (String namespace : namespaces.getNamespaces()) { query.append(format("PREFIX %s: <%s> %n", namespaces.getNamespacePrefix(namespace), namespace)); } query.append("SELECT DISTINCT ?identity\n"); if (triples.hasTriples()) { query.append("WHERE {\n"); StringBuilder optional = new StringBuilder(); for (Triples.Triple triple : triples) { final String subject = triple.getSubject(); final String predicate = triple.getPredicate(); final String value = triple.getValue(); if (triple.isOptional()) { optional.append(format("OPTIONAL {%s %s %s}. ", subject, predicate, value)); optional.append('\n'); } else { query.append(format("%s %s %s. ", subject, predicate, value)); query.append('\n'); } } // Add OPTIONAL statements last if (optional.length() > 0) query.append(optional.toString()); if (filter.length() > 0) { query.append("FILTER ").append(filter); } query.append("\n}"); } if (orderBy != null) { query.append("\nORDER BY ").append(orderBy); } if (firstResult != null) { query.append("\nOFFSET ").append(firstResult); } if (maxResults != null) { query.append("\nLIMIT ").append(maxResults); } LoggerFactory.getLogger(getClass()).debug("Query:\n" + query); return query.toString(); } private String processFilter(final BooleanExpression expression, boolean allowInline) { if (expression == null) { return ""; } if (expression instanceof Conjunction) { final Conjunction conjunction = (Conjunction) expression; String left = processFilter(conjunction.leftSideExpression(), allowInline); String right = processFilter(conjunction.rightSideExpression(), allowInline); if (left.equals("")) { return right; } else if (right.equals("")) { return left; } else { return format("(%s && %s)", left, right); } } if (expression instanceof Disjunction) { final Disjunction disjunction = (Disjunction) expression; String left = processFilter(disjunction.leftSideExpression(), false); String right = processFilter(disjunction.rightSideExpression(), false); if (left.equals("")) { return right; } else if (right.equals("")) { return left; } else { return format("(%s || %s)", left, right); } } if (expression instanceof Negation) { return format("(!%s)", processFilter(((Negation) expression).expression(), false)); } if (expression instanceof MatchesPredicate) { return processMatchesPredicate((MatchesPredicate) expression); } if (expression instanceof ComparisonPredicate) { return processComparisonPredicate((ComparisonPredicate) expression, allowInline); } if (expression instanceof ManyAssociationContainsPredicate) { return processManyAssociationContainsPredicate((ManyAssociationContainsPredicate) expression, allowInline); } if (expression instanceof PropertyNullPredicate) { return processNullPredicate((PropertyNullPredicate) expression); } if (expression instanceof AssociationNullPredicate) { return processNullPredicate((AssociationNullPredicate) expression); } if (expression instanceof ContainsPredicate<?, ?>) { return processContainsPredicate((ContainsPredicate<?, ?>) expression); } if (expression instanceof ContainsAllPredicate<?, ?>) { return processContainsAllPredicate((ContainsAllPredicate<?, ?>) expression); } throw new UnsupportedOperationException("Expression " + expression + " is not supported"); } private static String join(String[] strings, String delimiter) { StringBuilder builder = new StringBuilder(); for (Integer x = 0; x < strings.length; ++x) { builder.append(strings[x]); if (x + 1 < strings.length) { builder.append(delimiter); } } return builder.toString(); } private String createAndEscapeJSONString(Object value, PropertyReference<?> propertyRef) throws JSONException { ValueType type = ValueTypeFactory.instance().newValueType(value.getClass(), propertyRef.propertyType(), propertyRef.propertyDeclaringType()); JSONStringer json = new JSONStringer(); json.array(); this.createJSONString(value, type, json); json.endArray(); String result = json.toString(); result = result.substring(1, result.length() - 1); result = this.escapeJSONString(result); return result; } private String createRegexStringForContaining(String valueVariable, String containedString) { // The matching value must start with [, then contain something (possibly nothing), // then our value, then again something (possibly nothing), and end with ] return format("regex(str(%s), \"^\\\\u005B.*%s.*\\\\u005D$\", \"s\")", valueVariable, containedString); } private void createJSONString(Object value, ValueType type, JSONStringer stringer) throws JSONException { // TODO the sole purpose of this method is to get rid of "_type" information, which ValueType.toJSON // produces for value composites // So, change toJSON(...) to be configurable so that the caller can decide whether he wants type // information into json string or not if (type.isValue() || (type instanceof SerializableType && value instanceof ValueComposite)) { stringer.object(); // Rest is partial copypasta from ValueCompositeType.toJSON(Object, JSONStringer) ValueComposite valueComposite = (ValueComposite) value; StateHolder state = valueComposite.state(); final Map<QualifiedName, Object> values = new HashMap<QualifiedName, Object>(); state.visitProperties(new StateHolder.StateVisitor<RuntimeException>() { public void visitProperty(QualifiedName name, Object value) { values.put(name, value); } }); List<PropertyType> actualTypes = type.types(); for (PropertyType propertyType : actualTypes) { stringer.key(propertyType.qualifiedName().name()); Object propertyValue = values.get(propertyType.qualifiedName()); if (propertyValue == null) { stringer.value(null); } else { this.createJSONString(propertyValue, propertyType.type(), stringer); } } stringer.endObject(); } else { type.toJSON(value, stringer); } } private String escapeJSONString(String jsonStr) { StringBuilder builder = new StringBuilder(); for (Character c : jsonStr.toCharArray()) { if (reservedChars.contains(c)) { builder.append("\\\\u").append(format("%04X", (int) c)); } else { builder.append(c); } } return builder.toString(); } private String processContainsAllPredicate(final ContainsAllPredicate<?, ?> predicate) { ValueExpression<?> valueExpression = predicate.valueExpression(); if (valueExpression instanceof SingleValueExpression<?>) { String valueVariable = triples.addTriple(predicate.propertyReference(), false).getValue(); final SingleValueExpression<?> singleValueExpression = (SingleValueExpression<?>) valueExpression; String[] strings = new String[((Collection<?>) singleValueExpression.value()).size()]; Integer x = 0; for (Object o : (Collection<?>) singleValueExpression.value()) { String jsonStr = ""; if (o != null) { try { jsonStr = this.createAndEscapeJSONString(o, predicate.propertyReference()); } catch (JSONException jsone) { throw new UnsupportedOperationException("Error when JSONing value", jsone); } } strings[x] = this.createRegexStringForContaining(valueVariable, jsonStr); x++; } StringBuilder regex = new StringBuilder(); if (strings.length > 0) { // For some reason, just "FILTER ()" causes error in SPARQL query regex.append("("); regex.append(join(strings, " && ")); regex.append(")"); } else { regex.append(this.createRegexStringForContaining(valueVariable, "")); } return regex.toString(); } else { throw new UnsupportedOperationException("Value " + valueExpression + " is not supported."); } } private String processContainsPredicate(final ContainsPredicate<?, ?> predicate) { ValueExpression<?> valueExpression = predicate.valueExpression(); if (valueExpression instanceof SingleValueExpression<?>) { String valueVariable = triples.addTriple(predicate.propertyReference(), false).getValue(); SingleValueExpression<?> singleValueExpression = (SingleValueExpression<?>) valueExpression; try { return this.createRegexStringForContaining(valueVariable, this .createAndEscapeJSONString(singleValueExpression.value(), predicate.propertyReference())); } catch (JSONException jsone) { throw new UnsupportedOperationException("Error when JSONing value", jsone); } } else { throw new UnsupportedOperationException("Value " + valueExpression + " is not supported."); } } private String processMatchesPredicate(final MatchesPredicate predicate) { ValueExpression valueExpression = predicate.valueExpression(); if (valueExpression instanceof SingleValueExpression) { String valueVariable = triples.addTriple(predicate.propertyReference(), false).getValue(); final SingleValueExpression singleValueExpression = (SingleValueExpression) valueExpression; return format("regex(%s,\"%s\")", valueVariable, singleValueExpression.value()); } else { throw new UnsupportedOperationException("Value " + valueExpression + " is not supported"); } } private String processComparisonPredicate(final ComparisonPredicate predicate, boolean allowInline) { ValueExpression valueExpression = predicate.valueExpression(); if (valueExpression instanceof SingleValueExpression) { Triples.Triple triple = triples.addTriple(predicate.propertyReference(), false); // Don't use FILTER for equals-comparison. Do direct match instead if (predicate instanceof EqualsPredicate && allowInline) { final SingleValueExpression singleValueExpression = (SingleValueExpression) valueExpression; triple.setValue("\"" + toString(singleValueExpression.value()) + "\""); return ""; } else { String valueVariable = triple.getValue(); final SingleValueExpression singleValueExpression = (SingleValueExpression) valueExpression; return String.format("(%s %s \"%s\")", valueVariable, getOperator(predicate.getClass()), toString(singleValueExpression.value())); } } else { throw new UnsupportedOperationException("Value " + valueExpression + " is not supported"); } } private String processManyAssociationContainsPredicate(ManyAssociationContainsPredicate predicate, boolean allowInline) { ValueExpression valueExpression = predicate.valueExpression(); if (valueExpression instanceof SingleValueExpression) { Triples.Triple triple = triples.addTriple(predicate.associationReference(), false); if (allowInline) { final SingleValueExpression singleValueExpression = (SingleValueExpression) valueExpression; triple.setValue("<" + toString(singleValueExpression.value()) + ">"); return ""; } else { String valueVariable = triple.getValue(); final SingleValueExpression singleValueExpression = (SingleValueExpression) valueExpression; return String.format("(%s %s <%s>)", valueVariable, getOperator(predicate.getClass()), toString(singleValueExpression.value())); } } else { throw new UnsupportedOperationException("Value " + valueExpression + " is not supported"); } } private String processNullPredicate(final PropertyNullPredicate predicate) { final String value = triples.addTriple(predicate.propertyReference(), true).getValue(); if (predicate instanceof PropertyIsNullPredicate) { return format("(! bound(%s))", value); } else { return format("(bound(%s))", value); } } private String processNullPredicate(final AssociationNullPredicate predicate) { final String value = triples.addTriple(predicate.associationReference(), true).getValue(); if (predicate instanceof AssociationIsNullPredicate) { return format("(! bound(%s))", value); } else { return format("(bound(%s))", value); } } private String processOrderBy(OrderBy[] orderBySegments) { if (orderBySegments != null && orderBySegments.length > 0) { final StringBuilder orderBy = new StringBuilder(); for (OrderBy orderBySegment : orderBySegments) { if (orderBySegment != null) { final String valueVariable = triples.addTriple(orderBySegment.propertyReference(), false) .getValue(); if (orderBySegment.order() == OrderBy.Order.ASCENDING) { orderBy.append(format("ASC(%s)", valueVariable)); } else { orderBy.append(format("DESC(%s)", valueVariable)); } } } return orderBy.length() > 0 ? orderBy.toString() : null; } return null; } private String getOperator(final Class<? extends Predicate> predicateClass) { String operator = null; for (Map.Entry<Class<? extends Predicate>, String> entry : m_operators.entrySet()) { if (entry.getKey().isAssignableFrom(predicateClass)) { operator = entry.getValue(); break; } } if (operator == null) { throw new UnsupportedOperationException( "Predicate [" + predicateClass.getName() + "] is not supported"); } return operator; } private String toString(Object value) { if (value == null) { return null; } if (value instanceof Date) { return ISO8601_UTC.get().format((Date) value); } else if (value instanceof Entity) { return "urn:qi4j:entity:" + value.toString(); } else { return value.toString(); } } }