/************************************************************************* * Copyright 2009-2015 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.compute.common.internal.tags; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString; import static com.eucalyptus.compute.common.internal.tags.FilterSupport.PersistenceFilter.persistenceFilter; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.DetachedCriteria; import org.hibernate.criterion.Junction; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Property; import org.hibernate.criterion.Restrictions; import com.eucalyptus.auth.login.AuthenticationException; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.crypto.util.Timestamps; import com.eucalyptus.util.Parameters; import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; /** * Filter support class overridden for each resource that supports filtering. */ public abstract class FilterSupport<RT> { private static final ConcurrentMap<SupportKey,FilterSupport> supportMap = Maps.newConcurrentMap(); private final Class<RT> resourceClass; private final String qualifier; private Class<? extends Tag> tagClass; private final String tagFieldName; private final String resourceFieldName; private final Set<String> internalFilters; private final Map<String,Function<? super String,Predicate<? super RT>>> predicateFunctions; private final Map<String,String> aliases; private final Map<String,PersistenceFilter> persistenceFilters; /** * Create a new instance. * * @param builder The configuration for the filter support * @see #builderFor */ protected FilterSupport( @Nonnull final Builder<RT> builder ) { this.resourceClass = builder.resourceClass; this.qualifier = builder.qualifier; this.tagClass = builder.tagClass; this.tagFieldName = builder.tagFieldName; this.resourceFieldName = builder.resourceFieldName; this.internalFilters = builder.buildInternalFilters(); this.predicateFunctions = builder.buildPredicateFunctions(); this.aliases = builder.buildAliases(); this.persistenceFilters = builder.buildPersistenceFilters(); } /** * Create a configuration builder for the specified resource class. * * @param resourceClass The resource class * @param <RT> The resource type * @return The builder to use */ protected static <RT> Builder<RT> builderFor( final Class<RT> resourceClass ) { return new Builder<RT>( resourceClass, Filters.DEFAULT_FILTERS ); } /** * Create a configuration builder for the specified resource class. * * @param resourceClass The resource class * @param qualifier The filter set qualifier * @param <RT> The resource type * @return The builder to use */ protected static <RT> Builder<RT> qualifierBuilderFor( final Class<RT> resourceClass, final String qualifier ) { return new Builder<RT>( resourceClass, qualifier ); } /** * Configuration builder for a resource class. * * @param <RT> The resource type */ protected static class Builder<RT> { private final Class<RT> resourceClass; private final String qualifier; private final Set<String> internalFilters = Sets.newHashSet(); private final Map<String,Function<? super String,Predicate<? super RT>>> predicateFunctions = Maps.newHashMap(); private final Map<String,String> aliases = Maps.newHashMap(); private final Map<String,PersistenceFilter> persistenceFilters = Maps.newHashMap(); private Class<? extends Tag> tagClass; private String tagFieldName; // usually "tags" private String resourceFieldName; private Builder( final Class<RT> resourceClass, final String qualifier ) { Parameters.checkParam( "Resource class", resourceClass, notNullValue() ); Parameters.checkParam( "Qualifier", qualifier, not( isEmptyOrNullString() ) ); this.resourceClass = resourceClass; this.qualifier = qualifier; } /** * Enable tag filtering for the resource. * * @param tagClass The TagSupport subclass * @param resourceFieldName The field name linking back to the resource from the tag * @param tagFieldName The name of the tag collection field in the resource class. * @return This builder for call chaining */ public Builder<RT> withTagFiltering( final Class<? extends Tag> tagClass, final String resourceFieldName, final String tagFieldName ) { this.tagClass = tagClass; this.resourceFieldName = resourceFieldName; this.tagFieldName = tagFieldName; return this; } /** * Enable tag filtering for the resource. * * @param tagClass The TagSupport subclass * @param resourceFieldName The field name linking back to the resource from the tag * @return This builder for call chaining */ public Builder<RT> withTagFiltering( final Class<? extends Tag> tagClass, final String resourceFieldName ) { return withTagFiltering( tagClass, resourceFieldName, "tags" ); } /** * Declare a filterable boolean property. * * @param filterName The name of the filter * @param booleanExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withBooleanProperty( final String filterName, final Function<? super RT,Boolean> booleanExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>booleanFilter( booleanExtractor ) ); return this; } /** * Declare an internal filterable boolean property. * * <p>An internal property cannot be accessed via the public API but can be * explicitly added.</p> * * @param filterName The name of the filter * @param booleanExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withInternalBooleanProperty( final String filterName, final Function<? super RT,Boolean> booleanExtractor ) { internalFilters.add( filterName ); predicateFunctions.put( filterName, FilterSupport.<RT>booleanFilter( booleanExtractor ) ); return this; } /** * Declare a filterable multi-valued boolean property. * * @param filterName The name of the filter * @param booleanSetExtractor Function to extract the property values * @return This builder for call chaining */ public Builder<RT> withBooleanSetProperty( final String filterName, final Function<? super RT,Set<Boolean>> booleanSetExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>booleanSetFilter( booleanSetExtractor ) ); return this; } /** * Declare a filterable date property. * * @param filterName The name of the filter * @param dateExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withDateProperty( final String filterName, final Function<? super RT,Date> dateExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>dateFilter( dateExtractor ) ); return this; } /** * Declare a filterable multi-valued date property. * * @param filterName The name of the filter * @param dateSetExtractor Function to extract the property values * @return This builder for call chaining */ public Builder<RT> withDateSetProperty( final String filterName, final Function<? super RT,Set<Date>> dateSetExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>dateSetFilter( dateSetExtractor ) ); return this; } /** * Declare a filterable integer property. * * @param filterName The name of the filter * @param integerExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withIntegerProperty( final String filterName, final Function<? super RT,Integer> integerExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>intFilter( integerExtractor ) ); return this; } /** * Declare a filterable multi-valued integer property. * * @param filterName The name of the filter * @param integerSetExtractor Function to extract the property values * @return This builder for call chaining */ public Builder<RT> withIntegerSetProperty( final String filterName, final Function<? super RT,Set<Integer>> integerSetExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>intSetFilter( integerSetExtractor ) ); return this; } /** * Declare a filterable multi-valued integer property. * * @param filterName The name of the filter * @param integerSetExtractor Function to extract the property values * @param valueFunction Function to convert filter value * @return This builder for call chaining */ public Builder<RT> withIntegerSetProperty( final String filterName, final Function<? super RT,Set<Integer>> integerSetExtractor, final Function<String,Integer> valueFunction ) { predicateFunctions.put( filterName, FilterSupport.<RT>intSetFilter( integerSetExtractor, valueFunction ) ); return this; } /** * Declare a filterable long property. * * @param filterName The name of the filter * @param longExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withLongProperty( final String filterName, final Function<? super RT,Long> longExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>longFilter( longExtractor ) ); return this; } /** * Declare a filterable multi-valued long property. * * @param filterName The name of the filter * @param longSetExtractor Function to extract the property values * @return This builder for call chaining */ public Builder<RT> withLongSetProperty( final String filterName, final Function<? super RT,Set<Long>> longSetExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>longSetFilter( longSetExtractor ) ); return this; } /** * Declare a filterable string property. * * @param filterName The name of the filter * @param stringExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withStringProperty( final String filterName, final Function<? super RT,String> stringExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>stringFilter( stringExtractor ) ); return this; } /** * Declare an internal filterable string property. * * <p>An internal property cannot be accessed via the public API but can be * explicitly added.</p> * * @param filterName The name of the filter * @param stringExtractor Function to extract the property value * @return This builder for call chaining */ public Builder<RT> withInternalStringProperty( final String filterName, final Function<? super RT,String> stringExtractor ) { internalFilters.add( filterName ); predicateFunctions.put( filterName, FilterSupport.<RT>stringFilter( stringExtractor ) ); return this; } /** * Declare a filterable multi-valued string property. * * @param filterName The name of the filter * @param stringSetExtractor Function to extract the property values * @return This builder for call chaining */ public Builder<RT> withStringSetProperty( final String filterName, final Function<? super RT,Set<String>> stringSetExtractor ) { predicateFunctions.put( filterName, FilterSupport.<RT>stringSetFilter( stringSetExtractor ) ); return this; } public Builder<RT> withLikeExplodedProperty( final String filterName, final Function<? super RT, ?> extractor, final Function<String, Collection> explodeFunction ) { predicateFunctions.put( filterName, FilterSupport.<RT>explodedLiteralFilter( extractor, likeWildFunction( explodeFunction ) ) ); return this; } /** * Declare a constant valued string property. * * @param filterName The name of the filter * @param value The property value * @return This builder for call chaining */ public Builder<RT> withConstantProperty( final String filterName, final String value ) { predicateFunctions.put( filterName, FilterSupport.<RT>stringFilter( Functions.compose( Functions.constant( value ), Functions.<RT>identity() ) ) ); return this; } /** * Declare a unsupported property. * * <p>Unsupported properties will fail to match at runtime</p> * * @param filterName The name of the filter * @return This builder for call chaining */ public Builder<RT> withUnsupportedProperty( final String filterName ) { predicateFunctions.put( filterName, FilterSupport.<RT>falseFilter() ); return this; } /** * Declare a persistence alias. * * <p>When a persistence filter traverses tables (join) it is necessary to * declare the alias once globally and reference the alias in each filter * that uses it.</p> * * @param path The path for the alias * @param alias The value for the alias * @return This builder for call chaining */ public Builder<RT> withPersistenceAlias( final String path, final String alias ) { aliases.put( path, alias ); return this; } /** * Declare a property filterable at the persistence layer. * * @param name The name of the filter and the field * @return This builder for call chaining */ public Builder<RT> withPersistenceFilter( final String name ) { return withPersistenceFilter( name, name ); } /** * Declare a property filterable at the persistence layer. * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @return This builder for call chaining */ public Builder<RT> withPersistenceFilter( final String filterName, final String fieldName ) { return withPersistenceFilter( filterName, fieldName, aliases( fieldName ) ); } /** * Declare a property filterable at the persistence layer. * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @param aliases The aliases used by this filter * @return This builder for call chaining */ public Builder<RT> withPersistenceFilter( final String filterName, final String fieldName, final Set<String> aliases ) { persistenceFilters.put( filterName, persistenceFilter( fieldName, aliases ) ); return this; } /** * Declare a property filterable at the persistence layer. * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @param type The field type for this filter * @return This builder for call chaining */ public Builder<RT> withPersistenceFilter( final String filterName, final String fieldName, final PersistenceFilter.Type type ) { return withPersistenceFilter( filterName, fieldName, aliases( fieldName ), type ); } /** * Declare a property filterable at the persistence layer. * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @param aliases The aliases used by this filter * @param type The field type for this filter * @return This builder for call chaining */ public Builder<RT> withPersistenceFilter( final String filterName, final String fieldName, final Set<String> aliases, final PersistenceFilter.Type type ) { persistenceFilters.put( filterName, persistenceFilter( fieldName, aliases, type ) ); return this; } /** * Declare a property filterable at the persistence layer. * * <p>A value function must be provided if the field is not a string value * or one of the supported explicit types. A typical use case is for enum * values.</p> * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @param valueFunction The value conversion function for this filter * @return This builder for call chaining * @see com.google.common.base.Enums#valueOfFunction */ public Builder<RT> withPersistenceFilter( final String filterName, final String fieldName, final Function<String,?> valueFunction ) { return withPersistenceFilter( filterName, fieldName, aliases( fieldName ), valueFunction ); } /** * Declare a property filterable at the persistence layer. * * <p>A value function must be provided if the field is not a string value * or one of the supported explicit types. A typical use case is for enum * values.</p> * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @param aliases The aliases used by this filter * @param valueFunction The value conversion function for this filter * @return This builder for call chaining * @see com.google.common.base.Enums#valueOfFunction */ public Builder<RT> withPersistenceFilter( final String filterName, final String fieldName, final Set<String> aliases, final Function<String,?> valueFunction ) { persistenceFilters.put( filterName, persistenceFilter( fieldName, aliases, valueFunction ) ); return this; } /** * Declare a property filterable at the persistence layer after expansion. * * <p>The given function will be passed a expression containing like * wildcards and is expected to explode to a collection of literal values * to match.</p> * * @param filterName The name of the filter * @param fieldName The path to the field (dot separated) * @param explodeFunction Function to expand a given like expression * @return This builder for call chaining */ public Builder<RT> withLikeExplodingPersistenceFilter( final String filterName, final String fieldName, final Function<String, Collection> explodeFunction ) { persistenceFilters.put( filterName, persistenceFilter( fieldName, aliases( fieldName ), likeWildFunction( explodeFunction ) ) ); return this; } private Set<String> aliases( final String fieldPath ) { final Set<String> aliases = Sets.newHashSet(); final Iterator<String> aliasIterator = Splitter.on( "." ).split( fieldPath ).iterator(); while ( aliasIterator.hasNext() ) { final String value = aliasIterator.next(); if ( aliasIterator.hasNext() ) { // all but last component aliases.add( value ); } } return aliases; } private Map<String,Function<? super String,Predicate<? super RT>>> buildPredicateFunctions() { return ImmutableMap.copyOf( predicateFunctions ); } private Set<String> buildInternalFilters() { return ImmutableSet.copyOf( internalFilters ); } private Map<String,String> buildAliases() { return ImmutableMap.copyOf( aliases ); } private Map<String,PersistenceFilter> buildPersistenceFilters() { return ImmutableMap.copyOf( persistenceFilters ); } } @Nonnull public Class<RT> getResourceClass() { return resourceClass; } @Nonnull public String getQualifier() { return qualifier; } @Nullable public Class<? extends Tag> getTagClass() { return tagClass; } @Nullable public String getTagFieldName() { return tagFieldName; } @Nullable public String getResourceFieldName() { return resourceFieldName; } /** * Generate a Filter for the given filters. * * @param filters The map of filter names to (multiple) values * @param allowInternalFilters True to allow use of internal filters * @return The filter representation * @throws InvalidFilterException If a filter is invalid */ public Filter generate( final Map<String, Set<String>> filters, final boolean allowInternalFilters ) throws InvalidFilterException { final Context ctx = Contexts.lookup(); final String requestAccountId = ctx.getAccountNumber( ); return generate( filters, allowInternalFilters, requestAccountId ); } /** * Generate a Filter for the given filters. * * @param filters The map of filter names to (multiple) values * @param allowInternalFilters True to allow use of internal filters * @return The filter representation * @throws InvalidFilterException If a filter is invalid */ public Filter generate( final Map<String, Set<String>> filters, final boolean allowInternalFilters, final String accountId ) throws InvalidFilterException { // Construct collection filter final List<Predicate<Object>> and = Lists.newArrayList(); for ( final Map.Entry<String,Set<String>> filter : Iterables.filter( filters.entrySet(), Predicates.not( isTagFilter() ) ) ) { final List<Predicate<Object>> or = Lists.newArrayList(); for ( final String value : filter.getValue() ) { final Function<? super String,Predicate<? super RT>> predicateFunction = predicateFunctions.get( filter.getKey() ); if ( predicateFunction == null || (!allowInternalFilters && internalFilters.contains( filter.getKey() ) ) ) { throw InvalidFilterException.forName( filter.getKey() ); } final Predicate<? super RT> valuePredicate = predicateFunction.apply( value ); or.add( typedPredicate( valuePredicate ) ); } and.add( Predicates.or( or ) ); } // Construct database filter and aliases final Junction conjunction = Restrictions.conjunction(); final Map<String,String> aliases = Maps.newHashMap(); for ( final Map.Entry<String,Set<String>> filter : Iterables.filter( filters.entrySet(), Predicates.not( isTagFilter() ) ) ) { final Junction disjunction = Restrictions.disjunction(); for ( final String value : filter.getValue() ) { final PersistenceFilter persistenceFilter = persistenceFilters.get( filter.getKey() ); if ( persistenceFilter != null ) { final Object persistentValue = persistenceFilter.value( value ); if ( persistentValue != null ) { for ( final String alias : persistenceFilter.getAliases() ) aliases.put( alias, this.aliases.get( alias ) ); disjunction.add( buildRestriction( persistenceFilter.getProperty(), persistentValue ) ); } // else, there is no valid DB filter for the given value (e.g. wildcard for integer value) } } conjunction.add( disjunction ); } // Construct database filter and aliases for tags boolean tagPresent = false; final List<Junction> tagJunctions = Lists.newArrayList(); for ( final Map.Entry<String,Set<String>> filter : Iterables.filter( filters.entrySet(), isTagFilter() ) ) { tagPresent = true; final Junction disjunction = Restrictions.disjunction(); final String filterName = filter.getKey(); for ( final String value : filter.getValue() ) { if ( "tag-key".equals( filterName ) ) { disjunction.add( buildTagRestriction( value, null, true ) ); } else if ( "tag-value".equals( filterName ) ) { disjunction.add( buildTagRestriction( null, value, true ) ); } else { disjunction.add( buildTagRestriction( filterName.substring(4), value, false ) ); } } tagJunctions.add( disjunction ); } if ( tagPresent ) conjunction.add( tagCriterion( accountId, tagJunctions ) ); return new Filter( aliases, conjunction, Predicates.and( and ), tagPresent ); } public static FilterSupport forResource( @Nonnull final Class<?> metadataClass, @Nonnull final String qualifier ) { return supportMap.get( supportKey( metadataClass, qualifier ) ); } /** * Will the given like expression match any value? * * @param expression The expression to test * @return True if fully wild */ public static boolean isTotallyWildLikeExpression( final String expression ) { return expression.replace("%","").isEmpty(); } protected static Function<String,String> ignoredValueFunction( final String... ignored ) { return ignoredValueFunction( Sets.newHashSet( ignored ) ); } protected static Function<String,String> ignoredValueFunction( final Set<String> ignored ) { return value -> ignored.contains( value ) ? null : value; } private static <T> Function<? super String, Predicate<? super T>> falseFilter() { return Functions.<Predicate<? super T>>constant( Predicates.alwaysFalse() ); } private static <T> Function<? super String, Predicate<? super T>> explodedLiteralFilter( final Function<? super T,?> extractor, final Function<String, Collection> explodeFunction ) { return new Function<String,Predicate<? super T>>() { @SuppressWarnings( "unchecked" ) @Override public Predicate<T> apply( final String filterValue ) { Collection values = explodeFunction.apply( filterValue ); return values == null ? Predicates.<T>alwaysTrue() : Predicates.compose( Predicates.<Object>in( values ), Functions.compose( extractor, Functions.<T>identity() ) ); } }; } private static <T> Function<? super String, Predicate<? super T>> stringFilter( final Function<? super T, String> extractor ) { return stringSetFilter( Functions.compose( FilterSupport.<String>toSet(), extractor ) ); } private static <T> Function<? super String, Predicate<? super T>> stringSetFilter( final Function<? super T, Set<String>> extractor ) { return new Function<String,Predicate<? super T>>() { @Override public Predicate<T> apply( final String filterValue ) { final Predicate<Set<String>> resourceValuePredicate = resourceValueMatcher( filterValue ); return new Predicate<T>() { @Override public boolean apply( final T resource ) { final Set<String> resourceValues = extractor.apply( resource ); return resourceValuePredicate.apply( resourceValues ); } }; } }; } private static <T> Function<? super String, Predicate<? super T>> dateFilter( final Function<? super T, Date> extractor ) { return dateSetFilter( Functions.compose( FilterSupport.<Date>toSet(), extractor ) ); } private static <T> Function<? super String, Predicate<? super T>> dateSetFilter( final Function<? super T, Set<Date>> extractor ) { return typedSetFilter( extractor, PersistenceFilter.Type.Date ); } private static <T> Function<? super String, Predicate<? super T>> booleanFilter( final Function<? super T, Boolean> extractor ) { return booleanSetFilter( Functions.compose( FilterSupport.<Boolean>toSet(), extractor ) ); } private static <T> Function<? super String, Predicate<? super T>> booleanSetFilter( final Function<? super T, Set<Boolean>> extractor ) { return typedSetFilter( extractor, PersistenceFilter.Type.Boolean ); } private static <T> Function<? super String, Predicate<? super T>> intFilter( final Function<? super T, Integer> extractor ) { return intSetFilter( Functions.compose( FilterSupport.<Integer>toSet(), extractor ) ); } private static <T> Function<? super String, Predicate<? super T>> intSetFilter( final Function<? super T, Set<Integer>> extractor ) { return typedSetFilter( extractor, PersistenceFilter.Type.Integer ); } private static <T> Function<? super String, Predicate<? super T>> intSetFilter( final Function<? super T, Set<Integer>> extractor, final Function<String,Integer> valueFunction ) { return typedSetFilter( extractor, PersistenceFilter.Type.Integer, valueFunction ); } private static <T> Function<? super String, Predicate<? super T>> longFilter( final Function<? super T, Long> extractor ) { return longSetFilter( Functions.compose( FilterSupport.<Long>toSet(), extractor ) ); } private static <T> Function<? super String, Predicate<? super T>> longSetFilter( final Function<? super T, Set<Long>> extractor ) { return typedSetFilter( extractor, PersistenceFilter.Type.Integer ); } private static <T,VT> Function<? super String, Predicate<? super T>> typedSetFilter( final Function<? super T, Set<VT>> extractor, final PersistenceFilter.Type type ) { return typedSetFilter( extractor, type, type.valueFunction( ) ); } private static <T,VT> Function<? super String, Predicate<? super T>> typedSetFilter( final Function<? super T, Set<VT>> extractor, final PersistenceFilter.Type type, final Function<String,?> valueFunction ) { return new Function<String,Predicate<? super T>>() { @Override public Predicate<T> apply( final String filterValue ) { final Predicate<Set<VT>> resourceValuePredicate = resourceValueMatcher( filterValue, type, valueFunction ); return new Predicate<T>() { @Override public boolean apply( final T resource ) { final Set<VT> resourceValues = extractor.apply( resource ); return resourceValuePredicate.apply( resourceValues ); } }; } }; } @SuppressWarnings( "unchecked" ) static void registerFilterSupport( @Nonnull final FilterSupport filterSupport ) { supportMap.put( supportKey( filterSupport.getResourceClass(), filterSupport.getQualifier() ), filterSupport ); } private static SupportKey supportKey( @Nonnull final Class<?> resourceClass, @Nonnull final String qualifier ) { Parameters.checkParam( "Resource class", resourceClass, notNullValue() ); Parameters.checkParam( "Qualifier", qualifier, not( isEmptyOrNullString() ) ); return new SupportKey( resourceClass, qualifier ); } private Predicate<Object> typedPredicate( final Predicate<? super RT> predicate ) { return new Predicate<Object>() { @Override public boolean apply( final Object object ) { return getResourceClass().isInstance( object ) && predicate != null && predicate.apply( getResourceClass().cast( object ) ); } }; } private static <T> Function<T,Set<T>> toSet() { return new Function<T,Set<T>>() { @Override public Set<T> apply( final T value ) { return value == null ? Collections.<T>emptySet() : Collections.singleton( value ); } }; } private static <T> Function<String,T> likeWildFunction( final Function<String,T> delegate ) { return new Function<String, T>() { @Override public T apply( final String expression ) { final StringBuilder likeValueBuilder = new StringBuilder(); translateWildcards( expression, likeValueBuilder, "_", "%", SyntaxEscape.Like ); final String likeExpression = likeValueBuilder.toString(); if ( isTotallyWildLikeExpression( likeExpression ) ) { return null; } else { return delegate.apply( likeExpression ); } } }; } private boolean isTagFilter( final String filter ) { return isTagFilteringEnabled() && ( filter.startsWith("tag:") || "tag-key".equals( filter ) || "tag-value".equals( filter ) ); } private boolean isTagFilteringEnabled() { return tagFieldName != null; } private Predicate<Map.Entry<String,?>> isTagFilter() { return !isTagFilteringEnabled() ? Predicates.<Map.Entry<String,?>>alwaysFalse() : new Predicate<Map.Entry<String,?>>() { @Override public boolean apply( final Map.Entry<String, ?> stringEntry ) { return isTagFilter( stringEntry.getKey() ); } }; } private static <T> Predicate<Set<T>> resourceValueMatcher( final String filterPattern, final PersistenceFilter.Type type, final Function<String,?> valueFunction ) { final Object value = valueFunction.apply( filterPattern ); return value == null ? Predicates.<Set<T>>alwaysFalse() : new Predicate<Set<T>>() { @SuppressWarnings( "SuspiciousMethodCalls" ) @Override public boolean apply( final Set<T> resourceValues ) { boolean match = false; for ( final T resourceValue : resourceValues ) { if ( type.matches( value, resourceValue ) ) { match = true; break; } } return match; } }; } /** * Construct a predicate from a filter pattern. * * A Pattern is constructed from the given filter, as per AWS: * * Filters support the following wildcards: * * *: Matches zero or more characters * ?: Matches exactly one character * * Your search can include the literal values of the wildcard characters; you just need to escape * them with a backslash before the character. For example, a value of \*amazon\?\\ searches for * the literal string *amazon?\. * * Wildcards are translated to regular expression wildcards: * * .*: Matches zero or more characters * . : Matches exactly one character * * Any other regular expression syntax from the filter value is escaped (Pattern.quote) */ private static Predicate<Set<String>> resourceValueMatcher( final String filterPattern ) { final StringBuilder regexBuilder = new StringBuilder(); if ( translateWildcards( filterPattern, regexBuilder, ".", ".*", SyntaxEscape.Regex ) ) { return new Predicate<Set<String>>() { private final Pattern pattern = Pattern.compile( regexBuilder.toString() ); @Override public boolean apply( final Set<String> values ) { return Iterables.any( values, new Predicate<String>() { @Override public boolean apply( final String value ) { return value != null && pattern.matcher( value ).matches(); } } ); } }; } // even if no regex, may contain \ escapes that must be removed final String processedFilterPattern = filterPattern.replaceAll( "\\\\", Matcher.quoteReplacement( "\\" ) ); return new Predicate<Set<String>>() { @Override public boolean apply( final Set<String> values ) { return values.contains( processedFilterPattern ); } }; } /** * Construct a criterion from a filter pattern. * * A Pattern is constructed from the given filter, as per AWS: * * Filters support the following wildcards: * * *: Matches zero or more characters * ?: Matches exactly one character * * Your search can include the literal values of the wildcard characters; you just need to escape * them with a backslash before the character. For example, a value of \*amazon\?\\ searches for * the literal string *amazon?\. * * Wildcards are translated for use with 'like': * * % : Matches zero or more characters * _: Matches exactly one character * * In both cases wildcards can be escaped (to allow literal values) with a backslash (\). * * Translation of wildcards for direct DB filtering must support literal values from each grammar. */ private Criterion buildRestriction( final String property, final Object persistentValue ) { final Object valueObject; if ( persistentValue instanceof String ) { final String value = persistentValue.toString(); final StringBuilder likeValueBuilder = new StringBuilder(); translateWildcards( value, likeValueBuilder, "_", "%", SyntaxEscape.Like ); final String likeValue = likeValueBuilder.toString(); if ( !value.equals( likeValue ) ) { // even if no regex, may contain \ escapes that must be removed return Restrictions.like( property, likeValue ); } valueObject = persistentValue; } else { valueObject = persistentValue; } if ( persistentValue instanceof Collection ) { if ( ((Collection) persistentValue).isEmpty() ) { return Restrictions.not( Restrictions.conjunction() ); // always false } else { return Restrictions.in( property, (Collection) persistentValue ); } } else { return Restrictions.eq( property, valueObject ); } } Map<String, PersistenceFilter> getPersistenceFilters() { return persistenceFilters; } Map<String, String> getAliases() { return aliases; } /** * Build a criterion that uses sub-selects to match the given tag restrictions */ private Criterion tagCriterion( final String accountId, final List<Junction> junctions ) { final Junction conjunction = Restrictions.conjunction(); for ( final Junction criterion : junctions ) { final DetachedCriteria criteria = DetachedCriteria.forClass( tagClass ) .add( Restrictions.eq( "ownerAccountNumber", accountId ) ) .add( criterion ) .setProjection( Projections.property( resourceFieldName ) ); conjunction.add( Property.forName( tagFieldName ).in( criteria ) ); } return conjunction; } /** * Build a restriction for a tag key and/or value. */ private Criterion buildTagRestriction( @Nullable final String key, @Nullable final String value, final boolean keyWildcards ) { final Junction criteria = Restrictions.conjunction(); if ( key != null ) { criteria.add( keyWildcards ? buildRestriction( "displayName", key ) : Restrictions.eq( "displayName", key ) ); } if ( value != null ) { criteria.add( buildRestriction( "value", value ) ); } return criteria; } /** * Translate wildcards from AWS to some other syntax */ static boolean translateWildcards( final String filterPattern, final StringBuilder translated, final String matchOne, final String matchZeroOrMore, final Function<String,String> escapeFunction ) { boolean foundWildcard = false; final CharMatcher syntaxMatcher = CharMatcher.anyOf("\\*?"); if ( syntaxMatcher.matchesAnyOf( filterPattern ) ) { // Process for wildcards boolean escaped = false; for ( final char character : filterPattern.toCharArray() ) { switch ( character ) { case '\\': case '?': case '*': if ( !escaped ) { switch ( character ) { case '\\': escaped = true; break; case '?': foundWildcard = true; translated.append( matchOne ); break; case '*': foundWildcard = true; translated.append( matchZeroOrMore ); break; } break; } escaped = false; default: if ( escaped ) { translated.append( escapeFunction.apply( "\\" ) ); } escaped = false; translated.append( escapeFunction.apply( Character.toString( character ) ) ); } } if ( escaped ) { translated.append( escapeFunction.apply( "\\" ) ); } } else { translated.append( escapeFunction.apply( filterPattern ) ); } return foundWildcard; } /** * Escape wildcards for like literals * * Escapes \ % and _ using a \ */ static String escapeLikeWildcards( final String literalExpression ) { final String escaped; final CharMatcher syntaxMatcher = CharMatcher.anyOf("\\%_"); if ( syntaxMatcher.matchesAnyOf( literalExpression ) ) { final StringBuilder escapedBuffer = new StringBuilder(); for ( final char character : literalExpression.toCharArray() ) { switch ( character ) { case '\\': case '_': case '%': escapedBuffer.append( '\\' ); default: escapedBuffer.append( character ); } } escaped = escapedBuffer.toString(); } else { escaped = literalExpression; } return escaped; } /** * An instance of PersistenceFilter is created for each property. */ public static class PersistenceFilter { public enum Type { Integer { @Override public Function<String, ?> valueFunction() { return new Function<String,Integer>() { @Override public Integer apply( final String textValue ) { try { return java.lang.Integer.valueOf( textValue ); } catch ( NumberFormatException e ) { return null; } } }; } }, Long { @Override public Function<String, ?> valueFunction() { return new Function<String,Long>() { @Override public Long apply( final String textValue ) { try { return java.lang.Long.valueOf( textValue ); } catch ( NumberFormatException e ) { return null; } } }; } }, Date { @Override public Function<String, ?> valueFunction() { return new Function<String,java.util.Date>() { @Override public java.util.Date apply( final String textValue ) { try { return Timestamps.parseIso8601Timestamp( textValue ); } catch ( AuthenticationException e ) { return null; } } }; } @Override boolean matches( final Object targetValue, final Object resourceValue ) { boolean match = false; if ( resourceValue instanceof Date && targetValue instanceof Date ) { match = ((Date) resourceValue).getTime() == ((Date) targetValue).getTime(); } return match; } }, Boolean { @Override public Function<String, ?> valueFunction() { return new Function<String,Boolean>() { @Override public java.lang.Boolean apply( final String textValue ) { java.lang.Boolean value = null; if ( java.lang.Boolean.TRUE.toString().equals( textValue ) ) { value = java.lang.Boolean.TRUE; } else if ( java.lang.Boolean.FALSE.toString().equals( textValue ) ) { value = java.lang.Boolean.FALSE; } return value; } }; } }; public abstract Function<String,?> valueFunction(); boolean matches( final Object targetValue, final Object resourceValue ) { return targetValue.equals( resourceValue ); } } @Nonnull private final String property; @Nonnull private final Set<String> aliases; @Nonnull private final Function<String,?> valueFunction; public static PersistenceFilter persistenceFilter( final String property, final Set<String> aliases ) { return persistenceFilter( property, aliases, Functions.<String>identity() ); } public static PersistenceFilter persistenceFilter( final String property, final Set<String> aliases, final Function<String,?> valueFunction ) { return new PersistenceFilter( property, aliases, valueFunction ); } public static PersistenceFilter persistenceFilter( final String property, final Set<String> aliases, final Type type ) { return new PersistenceFilter( property, aliases, type.valueFunction() ); } @Nonnull public String getProperty() { return property; } @Nonnull public Set<String> getAliases() { return aliases; } @Nullable public Object value( final String textValue ) { return valueFunction.apply( textValue ); } @Nonnull Function<String, ?> getValueFunction() { return valueFunction; } private PersistenceFilter( @Nonnull final String property, @Nonnull final Set<String> aliases, @Nonnull final Function<String,?> valueFunction ) { this.property = property; this.aliases = aliases; this.valueFunction = valueFunction; } } enum SyntaxEscape implements Function<String,String> { Regex { @Override public String apply( final String text ) { return Pattern.quote( text ); } }, Like { @Override public String apply( final String text ) { return escapeLikeWildcards( text ); } } } private static final class SupportKey { private final Class<?> resourceClass; private final String qualifier; private SupportKey( final Class<?> resourceClass, final String qualifier ) { this.resourceClass = resourceClass; this.qualifier = qualifier; } public Class<?> getResourceClass() { return resourceClass; } public String getQualifier() { return qualifier; } @SuppressWarnings( "RedundantIfStatement" ) @Override public boolean equals( final Object o ) { if ( this == o ) return true; if ( o == null || getClass() != o.getClass() ) return false; final SupportKey that = (SupportKey) o; if ( !qualifier.equals( that.qualifier ) ) return false; if ( !resourceClass.equals( that.resourceClass ) ) return false; return true; } @Override public int hashCode() { int result = resourceClass.hashCode(); result = 31 * result + qualifier.hashCode(); return result; } } }