/* * 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(); } } }