/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.usergrid.persistence; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Optional; import org.apache.commons.lang.StringUtils; import org.apache.usergrid.persistence.index.SelectFieldMapping; import org.apache.usergrid.persistence.index.exceptions.QueryParseException; import org.apache.usergrid.persistence.index.query.CounterResolution; import org.apache.usergrid.persistence.index.query.Identifier; import org.apache.usergrid.persistence.index.query.tree.Operand; import org.apache.usergrid.persistence.index.utils.ClassUtils; import org.apache.usergrid.persistence.index.utils.ListUtils; import org.apache.usergrid.persistence.index.utils.MapUtils; import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.*; import java.util.Map.Entry; public class Query { public enum Level { IDS, REFS, CORE_PROPERTIES, ALL_PROPERTIES, LINKED_PROPERTIES } public static final int DEFAULT_LIMIT = 10; public static final int MID_LIMIT = 500; public static final int MAX_LIMIT = 1000; public static final String PROPERTY_UUID = "uuid"; private String type; private Operand rootOperand; private UUID startResult; private Optional<String> cursor = Optional.absent(); private int limit = 0; private boolean mergeSelectResults = false; private Level level = Level.ALL_PROPERTIES; private String connectionType; private List<String> permissions; private boolean reversed; private boolean reversedSet = false; private Long startTime; private Long finishTime; private boolean pad; private boolean connecting = false; private CounterResolution resolution = CounterResolution.ALL; private List<Identifier> identifiers; private List<CounterFilterPredicate> counterFilters; private String collection; private String ql; private Collection<SelectFieldMapping> selectFields; private boolean analyzeOnly = false; private static ObjectMapper mapper = new ObjectMapper(); List<Operand> filterClauses = new ArrayList<Operand>(); public Query() { } /** * Creates a deep copy of a query from another query * @param q */ public Query( Query q ) { if ( q == null ) { return; } ql = q.ql; type = q.type; startResult = q.startResult; cursor = q.cursor; limit = q.limit; mergeSelectResults = q.mergeSelectResults; //level = q.level; connectionType = q.connectionType; permissions = q.permissions != null ? new ArrayList<>( q.permissions ) : null; reversed = q.reversed; reversedSet = q.reversedSet; startTime = q.startTime; finishTime = q.finishTime; resolution = q.resolution; pad = q.pad; rootOperand = q.rootOperand; identifiers = q.identifiers != null ? new ArrayList<>( q.identifiers ) : null; counterFilters = q.counterFilters != null ? new ArrayList<>( q.counterFilters ) : null; collection = q.collection; level = q.level; analyzeOnly = q.analyzeOnly; } /** * Create a query instance from the QL. If the string is null, return an empty query * @param ql * @return */ public static Query fromQLNullSafe(final String ql){ final Query query = fromQL(ql); if(query != null){ return query; } return new Query(); } public static Query fromQL( String ql ) throws QueryParseException { if ( StringUtils.isEmpty(ql) ) { return null; } Query query = new Query( ); query.setQl( ql ); return query; } public static Query all( ){ return fromQL("select *"); } /** * Create a query from a property equals * @param propertyName * @param value * @return */ public static Query fromEquals(final String propertyName, final String value){ return fromQL( propertyName + " = '" + value + "'" ); } private static Query newQueryIfNull( Query query ) { if ( query == null ) { query = new Query(); } return query; } public static Query fromJsonString( String json ) throws QueryParseException { Object o; try { o = mapper.readValue( json, Object.class ); } catch (IOException ex) { throw new QueryParseException("Error parsing JSON query string " + json, ex); } if ( o instanceof Map ) { @SuppressWarnings({ "unchecked", "rawtypes" }) Map<String, List<String>> params = ClassUtils.cast( MapUtils.toMapList( ( Map ) o ) ); return fromQueryParams( params ); } return null; } public static Query fromQueryParams( Map<String, List<String>> params ) throws QueryParseException { Query q = null; CounterResolution resolution = null; List<Identifier> identifiers = null; List<CounterFilterPredicate> counterFilters = null; String ql = QueryUtils.queryStrFrom( params ); String type = ListUtils.first( params.get( "type" ) ); Boolean reversed = ListUtils.firstBoolean( params.get( "reversed" ) ); String connection = ListUtils.first( params.get( "connectionType" ) ); UUID start = ListUtils.firstUuid( params.get( "start" ) ); String cursor = ListUtils.first( params.get( "cursor" ) ); Integer limit = ListUtils.firstInteger( params.get( "limit" ) ); List<String> permissions = params.get( "permission" ); Long startTime = ListUtils.firstLong( params.get( "start_time" ) ); Long finishTime = ListUtils.firstLong( params.get( "end_time" ) ); List<String> l = params.get( "resolution" ); if ( !ListUtils.isEmpty( l ) ) { resolution = CounterResolution.fromString( l.get( 0 ) ); } l = params.get( "counter" ); if ( !ListUtils.isEmpty( l ) ) { counterFilters = CounterFilterPredicate.fromList( l ); } Boolean pad = ListUtils.firstBoolean( params.get( "pad" ) ); for ( Entry<String, List<String>> param : params.entrySet() ) { Identifier identifier = Identifier.from( param.getKey() ); if ( ( param.getValue() == null ) || ( param.getValue().size() == 0 ) || identifier.isUUID() ) { if ( identifier != null ) { if ( identifiers == null ) { identifiers = new ArrayList<Identifier>(); } identifiers.add( identifier ); } } } if ( ql != null ) { ql = ql.replace("+", "%2b"); // ql string supports literal + symbol, encode so it will decode correctly later q = Query.fromQL( decode( ql ) ); } if ( type != null ) { q = newQueryIfNull( q ); q.setEntityType( type ); } if ( connection != null ) { q = newQueryIfNull( q ); q.setConnectionType( connection ); } if ( permissions != null ) { q = newQueryIfNull( q ); q.setPermissions( permissions ); } if ( start != null ) { q = newQueryIfNull( q ); q.setStartResult( start ); } if ( cursor != null ) { q = newQueryIfNull( q ); q.setCursor( cursor ); } if ( limit != null ) { q = newQueryIfNull( q ); q.setLimit( limit ); } if ( startTime != null ) { q = newQueryIfNull( q ); q.setStartTime( startTime ); } if ( finishTime != null ) { q = newQueryIfNull( q ); q.setFinishTime( finishTime ); } if ( resolution != null ) { q = newQueryIfNull( q ); q.setResolution( resolution ); } if ( counterFilters != null ) { q = newQueryIfNull( q ); q.setCounterFilters( counterFilters ); } if ( pad != null ) { q = newQueryIfNull( q ); q.setPad( pad ); } if ( identifiers != null ) { q = newQueryIfNull( q ); q.setIdentifiers( identifiers ); } if ( reversed != null ) { q = newQueryIfNull( q ); q.setReversed( reversed ); } return q; } public static Query fromUUID( UUID uuid ) { Query q = new Query(); q.addIdentifier( Identifier.fromUUID( uuid ) ); return q; } public static Query fromIdentifier( Object id ) { Query q = new Query(); q.addIdentifier( Identifier.from(id) ); return q; } public boolean hasQueryPredicates() { return rootOperand != null; } /** * Return true if the query generated select subjects * @return */ public boolean hasSelectSubjects() { if ( this.selectFields != null ) if ( this.selectFields.size()>0 ) return true; return false; } /** * Set the select subjects from our query results */ public void setSelectSubjects( final Collection<SelectFieldMapping> selectFields ) { this.selectFields = selectFields; } /** * Get the select assignments from our resetus if they were set */ public Collection<SelectFieldMapping> getSelectAssignments() { return this.selectFields; } public boolean containsNameOrEmailIdentifiersOnly() { if ( hasQueryPredicates() ) { return false; } if ( ( identifiers == null ) || identifiers.isEmpty() ) { return false; } for ( Identifier identifier : identifiers ) { if ( !identifier.isEmail() && !identifier.isName() ) { return false; } } return true; } @JsonIgnore public String getSingleNameOrEmailIdentifier() { if ( !containsSingleNameOrEmailIdentifier() ) { return null; } return ( identifiers.get( 0 ).toString() ); } public boolean containsSingleNameOrEmailIdentifier() { return containsNameOrEmailIdentifiersOnly() && ( identifiers.size() == 1 ); } @JsonIgnore public Identifier getSingleIdentifier() { return identifiers != null && identifiers.size() == 1 ? identifiers.get( 0 ) : null; } public boolean containsSingleUuidIdentifier() { return containsUuidIdentifiersOnly() && ( identifiers.size() == 1 ); } boolean containsUuidIdentifiersOnly() { if ( hasQueryPredicates() ) { return false; } if ( ( identifiers == null ) || identifiers.isEmpty() ) { return false; } for ( Identifier identifier : identifiers ) { if ( !identifier.isUUID() ) { return false; } } return true; } @JsonIgnore public UUID getSingleUuidIdentifier() { if ( !containsSingleUuidIdentifier() ) { return null; } return ( identifiers.get( 0 ).getUUID() ); } @JsonIgnore boolean isIdsOnly() { return false; } private void setIdsOnly( boolean idsOnly ) { if ( idsOnly ) { level = Level.IDS; } else if ( isIdsOnly() ) { level = Level.ALL_PROPERTIES; } } public Level getResultsLevel() { isIdsOnly(); return level; } public void setResultsLevel( Level level ) { setIdsOnly( level == Level.IDS ); this.level = level; } public Query withResultsLevel( Level level ) { setIdsOnly( level == Level.IDS ); this.level = level; return this; } public Query withReversed( boolean reversed ) { setReversed( reversed ); return this; } public String getEntityType() { return type; } public void setEntityType( String type ) { this.type = type; } public List<String> getPermissions() { return permissions; } public void setPermissions( List<String> permissions ) { this.permissions = permissions; } public void setAnalyzeOnly(final boolean analyzeOnly){ this.analyzeOnly = analyzeOnly; } public boolean getAnalyzeOnly(){ return analyzeOnly; } public boolean isMergeSelectResults() { return mergeSelectResults; } void setStartResult( UUID startResult ) { this.startResult = startResult; } public Query withStartResult( UUID startResult ) { this.startResult = startResult; return this; } public UUID getStartResult() { return startResult; } public Optional<String> getCursor() { return cursor; } public void setCursor( String cursor ) { this.cursor = Optional.fromNullable( cursor ); } public Query withCursor( String cursor ) { setCursor( cursor ); return this; } public int getLimit() { return getLimit( DEFAULT_LIMIT ); } public int getLimit( int defaultLimit ) { if ( limit <= 0 ) { return defaultLimit > 0 ? defaultLimit : DEFAULT_LIMIT; } return limit; } public void setLimit( int limit ) { // TODO tnine. After users have had time to change their query limits, // this needs to be uncommented and enforced. // if(limit > MAX_LIMIT){ // throw new IllegalArgumentException( // String.format("Query limit must be <= to %d", MAX_LIMIT)); // } if ( limit > MAX_LIMIT ) { limit = MAX_LIMIT; } this.limit = limit; } public Query withLimit( int limit ) { setLimit( limit ); return this; } public boolean isReversed() { return reversed; } public void setReversed( boolean reversed ) { reversedSet = true; this.reversed = reversed; } public boolean isReversedSet() { return reversedSet; } public Long getStartTime() { return startTime; } public void setStartTime( Long startTime ) { this.startTime = startTime; } public Long getFinishTime() { return finishTime; } public void setFinishTime( Long finishTime ) { this.finishTime = finishTime; } public boolean isPad() { return pad; } public void setPad( boolean pad ) { this.pad = pad; } //set the flag to retrieve the edges in the reverse direction. public void setConnecting( boolean connecting ) { this.connecting = connecting; } public boolean isConnecting() { return connecting; } public void setResolution( CounterResolution resolution ) { this.resolution = resolution; } public CounterResolution getResolution() { return resolution; } public void addIdentifier( Identifier identifier ) { if ( identifiers == null ) { identifiers = new ArrayList<Identifier>(); } identifiers.add( identifier ); } void setIdentifiers( List<Identifier> identifiers ) { this.identifiers = identifiers; } public List<CounterFilterPredicate> getCounterFilters() { return counterFilters; } public void addCounterFilter( String counter ) { CounterFilterPredicate p = CounterFilterPredicate.fromString( counter ); if ( p == null ) { return; } if ( counterFilters == null ) { counterFilters = new ArrayList<CounterFilterPredicate>(); } counterFilters.add( p ); } void setCounterFilters( List<CounterFilterPredicate> counterFilters ) { this.counterFilters = counterFilters; } public static final class CounterFilterPredicate implements Serializable { private static final long serialVersionUID = 1L; private final String name; private final Identifier user; private final Identifier group; private final String queue; private final String category; public CounterFilterPredicate( String name, Identifier user, Identifier group, String queue, String category ) { this.name = name; this.user = user; this.group = group; this.queue = queue; this.category = category; } public Identifier getUser() { return user; } public Identifier getGroup() { return group; } public String getQueue() { return queue; } public String getCategory() { return category; } public String getName() { return name; } public static CounterFilterPredicate fromString( String s ) { Identifier user = null; Identifier group = null; String category = null; String name = null; String[] l = StringUtils.split( s, ':' ); if ( l.length > 0 ) { if ( !"*".equals( l[0] ) ) { name = l[0]; } } if ( l.length > 1 ) { if ( !"*".equals( l[1] ) ) { user = Identifier.from( l[1] ); } } if ( l.length > 2 ) { if ( !"*".equals( l[2] ) ) { group = Identifier.from( l[3] ); } } if ( l.length > 3 ) { if ( !"*".equals( l[3] ) ) { category = l[3]; } } if ( ( user == null ) && ( group == null ) && ( category == null ) && ( name == null)) { return null; } return new CounterFilterPredicate( name, user, group, null, category ); } public static List<CounterFilterPredicate> fromList( List<String> l ) { if ( ( l == null ) || ( l.size() == 0 ) ) { return null; } List<CounterFilterPredicate> counterFilters = new ArrayList<CounterFilterPredicate>(); for ( String s : l ) { CounterFilterPredicate filter = CounterFilterPredicate.fromString( s ); if ( filter != null ) { counterFilters.add( filter ); } } if ( counterFilters.size() == 0 ) { return null; } return counterFilters; } } // public List<Object> getSelectionResults( Results rs ) { // // List<Entity> entities = rs.getEntities(); // if ( entities == null ) { // return null; // } // // if ( !hasSelectSubjects() ) { // return cast( entities ); // } // // List<Object> results = new ArrayList<Object>(); // // for ( Entity entity : entities ) { // if ( isMergeSelectResults() ) { // boolean include = false; // Map<String, Object> result = new LinkedHashMap<String, Object>(); // Map<String, String> selects = getSelectAssignments(); // for ( Map.Entry<String, String> select : selects.entrySet() ) { // Object obj = JsonUtils.select( entity, select.getValue(), false ); // if ( obj != null ) { // include = true; // } // result.put( select.getKey(), obj ); // } // if ( include ) { // results.add( result ); // } // } // else { // boolean include = false; // List<Object> result = new ArrayList<Object>(); // Set<String> selects = getSelectSubjects(); // for ( String select : selects ) { // Object obj = JsonUtils.select( entity, select ); // if ( obj != null ) { // include = true; // } // result.add( obj ); // } // if ( include ) { // results.add( result ); // } // } // } // // if ( results.size() == 0 ) { // return null; // } // // return results; // } // public Object getSelectionResult( Results rs ) { // List<Object> r = getSelectionResults( rs ); // if ( ( r != null ) && ( r.size() > 0 ) ) { // return r.get( 0 ); // } // return null; // } private static String decode( String input ) { try { return URLDecoder.decode( input, "UTF-8" ); } catch ( UnsupportedEncodingException e ) { // shouldn't happen, but just in case throw new RuntimeException( e ); } } // note: very likely to be null public String getCollection() { return collection; } public void setCollection( String collection ) { this.collection = collection; } // may be null public Optional<String> getQl() { //if a query exists, but with no ql, we select all return Optional.fromNullable( ql ); } /** * Return true if no query is present and we should perform a graph search * @return */ @JsonIgnore public boolean isGraphSearch(){ return ql == null || ql.trim().toLowerCase().equals("select *"); } public Query setQl( String ql ) { this.ql = ql; return this; } /** * Get the connection type * @return */ public String getConnectionType() { return connectionType; } /** * Set the connection type * @param connection * @return */ public Query setConnectionType( final String connection ) { this.connectionType = connection; return this; } public String getType() { return type; } public Level getLevel() { return level; } }