Skip to content

Commit

Permalink
Improve definition of filterable properties and aliases
Browse files Browse the repository at this point in the history
Move property alias logic in AbstractFilteringVoEnabledDao to hide more
implementation details.

Rename factorValueCharacteristics and bioMaterialCharacteristics to use
a common prefix with already existing paths.
  • Loading branch information
arteymix committed Jan 26, 2023
1 parent e8d84a1 commit d612215
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
*/
public abstract class AbstractCriteriaFilteringVoEnabledDao<O extends Identifiable, VO extends IdentifiableValueObject<O>> extends AbstractFilteringVoEnabledDao<O, VO> {

@Autowired
private PlatformTransactionManager platformTransactionManager;

protected AbstractCriteriaFilteringVoEnabledDao( Class<? extends O> elementClass, SessionFactory sessionFactory ) {
// This is a good default objet alias for Hibernate Criteria since null is used to refer to the root entity.
super( null, elementClass, sessionFactory );
Expand Down Expand Up @@ -232,52 +235,45 @@ public long countPreFilter( @Nullable Filters filters ) {
return ret;
}

@Override
protected FilterablePropertyMeta getFilterablePropertyMeta( String propertyName ) throws IllegalArgumentException {
FilterablePropertyMeta meta = super.getFilterablePropertyMeta( propertyName );
List<FilterablePropertyCriteriaAlias> aliases = getFilterablePropertyCriteriaAliases();
// substitute longest path first
aliases.sort( Comparator.comparing( a -> a.propertyName.length(), Comparator.reverseOrder() ) );
for ( FilterablePropertyCriteriaAlias alias : aliases ) {
if ( propertyName.startsWith( alias.propertyName + "." ) ) {
propertyName = propertyName.replaceFirst( "^" + Pattern.quote( alias.propertyName + "." ), "" );
return meta
.withObjectAlias( alias.alias )
.withPropertyName( propertyName );
}
}
return meta;
}

@Value
protected static class FilterablePropertyAlias {
private static class FilterablePropertyCriteriaAlias {
String propertyName;
String alias;
}

@Autowired
private PlatformTransactionManager platformTransactionManager;

/**
* Unfortunately, because of how criteria API works, you have to explicitly list all aliases.
* TODO: infer this from the criteria.
*/
protected List<FilterablePropertyAlias> getFilterablePropertyAliases() {
private List<FilterablePropertyCriteriaAlias> getFilterablePropertyCriteriaAliases() {
// FIXME: unfortunately, this requires a session...
Criteria criteria = new TransactionTemplate( platformTransactionManager ).execute( ( ts ) -> getFilteringCriteria( Filters.empty() ) );
if ( criteria instanceof CriteriaImpl ) {
//noinspection unchecked
Iterator<CriteriaImpl.Subcriteria> it = ( ( CriteriaImpl ) criteria ).iterateSubcriteria();
List<FilterablePropertyAlias> result = new ArrayList<>();
List<FilterablePropertyCriteriaAlias> result = new ArrayList<>();
while ( it.hasNext() ) {
CriteriaImpl.Subcriteria sc = it.next();
result.add( new FilterablePropertyAlias( sc.getPath(), sc.getAlias() ) );
result.add( new FilterablePropertyCriteriaAlias( sc.getPath(), sc.getAlias() ) );
}
return result;
}
return Collections.emptyList();
}

@Override
protected FilterablePropertyMeta getFilterablePropertyMeta( String propertyName ) throws IllegalArgumentException {
FilterablePropertyMeta meta = super.getFilterablePropertyMeta( propertyName );
List<FilterablePropertyAlias> aliases = getFilterablePropertyAliases();
// substitute longest path first
aliases.sort( Comparator.comparing( a -> a.propertyName.length(), Comparator.reverseOrder() ) );
for ( FilterablePropertyAlias alias : aliases ) {
if ( propertyName.startsWith( alias.propertyName + "." ) ) {
propertyName = propertyName.replaceFirst( "^" + Pattern.quote( alias.propertyName + "." ), "" );
return meta
.withObjectAlias( alias.alias )
.withPropertyName( propertyName );
}
}
return meta;
}

private static void addOrder( Criteria query, Sort sort ) {
String propertyName = formPropertyName( sort.getObjectAlias(), sort.getPropertyName() );
// handle .size ordering
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package ubic.gemma.persistence.service;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.With;
import lombok.*;
import org.apache.commons.lang3.ArrayUtils;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
Expand All @@ -18,8 +15,11 @@
import ubic.gemma.persistence.util.Sort;

import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* Base implementation for {@link FilteringVoEnabledDao}.
Expand All @@ -33,7 +33,7 @@ public abstract class AbstractFilteringVoEnabledDao<O extends Identifiable, VO e
/**
* Maximum depth to explore when enumerating filterable properties via {@link #getFilterableProperties()}.
*/
protected static final int FILTERABLE_PROPERTIES_MAX_DEPTH = 3;
private static final int FILTERABLE_PROPERTIES_MAX_DEPTH = 3;

/**
* Cached partial filterable properties meta, computed as we go.
Expand All @@ -53,12 +53,18 @@ private static class Key {
*/
private final Set<String> filterableProperties;

/**
* Aliases for filterable properties.
*/
private final Set<FilterablePropertyAlias> filterablePropertyAliases;

protected AbstractFilteringVoEnabledDao( @Nullable String objectAlias, Class<? extends O> elementClass, SessionFactory sessionFactory ) {
super( elementClass, sessionFactory );
this.objectAlias = objectAlias;
Set<String> result = new HashSet<>();
addFilterableProperties( "", elementClass, result, FILTERABLE_PROPERTIES_MAX_DEPTH );
this.filterableProperties = Collections.unmodifiableSet( result );
this.filterablePropertyAliases = new HashSet<>();
registerFilterablePropertyAliases( this.filterablePropertyAliases );
this.filterableProperties = new HashSet<>();
registerFilterableProperties( this.filterableProperties );
}

/**
Expand Down Expand Up @@ -122,10 +128,32 @@ public final List<VO> loadAllValueObjects() {
}

@Override
public Set<String> getFilterableProperties() {
public final Set<String> getFilterableProperties() {
return filterableProperties;
}

/**
* Register filterable properties.
* @param properties a collection to which filterable properties are to be added
*/
@OverridingMethodsMustInvokeSuper
protected void registerFilterableProperties( Set<String> properties ) {
addFilterableProperties( "", elementClass, properties, FILTERABLE_PROPERTIES_MAX_DEPTH );
// FIXME: the aliases are not available because they are registered afterward in the constructor
Set<FilterablePropertyAlias> aliases = new HashSet<>();
registerFilterablePropertyAliases( aliases );
for ( FilterablePropertyAlias alias : aliases ) {
addFilterableProperties( alias.prefix, alias.propertyType, properties, FILTERABLE_PROPERTIES_MAX_DEPTH - 1 );
}
}

/**
* Register aliases for filterable properties.
* @param aliases a collection to which aliases are to be added
*/
protected void registerFilterablePropertyAliases( Set<FilterablePropertyAlias> aliases ) {
}

@Override
public Class<?> getFilterablePropertyType( String propertyName ) throws IllegalArgumentException {
return getFilterablePropertyMeta( propertyName ).propertyType;
Expand All @@ -139,7 +167,7 @@ public String getFilterablePropertyDescription( String propertyName ) throws Ill

@Nullable
@Override
public List<Object> getFilterablePropertyAvailableValues( String propertyName ) {
public List<Object> getFilterablePropertyAvailableValues( String propertyName ) throws IllegalArgumentException {
return getFilterablePropertyMeta( propertyName ).availableValues;
}

Expand All @@ -164,7 +192,10 @@ public final Sort getSort( String property, @Nullable Sort.Direction direction )
/**
* Helper that inspects a class and add all the filterable properties with the given prefix.
*/
protected void addFilterableProperties( String prefix, Class<?> entityClass, Set<String> destination, int maxDepth ) {
private void addFilterableProperties( String prefix, Class<?> entityClass, Set<String> destination, int maxDepth ) {
if ( !prefix.isEmpty() && !prefix.endsWith( "." ) ) {
throw new IllegalArgumentException( "A non-empty prefix must end with a '.' character." );
}
if ( maxDepth <= 0 ) {
throw new IllegalArgumentException( String.format( "Maximum depth for adding filterable properties of %s to %s must be strictly positive.",
entityClass.getName(), prefix ) );
Expand Down Expand Up @@ -213,6 +244,22 @@ protected static class FilterablePropertyMeta {
List<Object> availableValues;
}

@Value
@EqualsAndHashCode(of = "prefix")
protected static class FilterablePropertyAlias {
String prefix;
@Nullable
String objectAlias;
Class<?> propertyType;
/**
* If this alias is actual aliasing another alias.
* <p>
* Example: {@code taxon. -> primaryTaxon.}
*/
@Nullable
String aliasFor;
}

/**
* Obtain various meta-information used to infer what to use in a {@link Filter} or {@link Sort}.
* <p>
Expand All @@ -224,6 +271,17 @@ protected static class FilterablePropertyMeta {
* @see #getSort(String, Sort.Direction)
*/
protected FilterablePropertyMeta getFilterablePropertyMeta( String propertyName ) throws IllegalArgumentException {
// replace longer prefix first
List<FilterablePropertyAlias> aliases = filterablePropertyAliases.stream()
.sorted( Comparator.comparing( f -> f.prefix.length(), Comparator.reverseOrder() ) )
.collect( Collectors.toList() );
for ( FilterablePropertyAlias alias : aliases ) {
if ( propertyName.startsWith( alias.prefix ) && !propertyName.equals( alias.prefix + "size" ) ) {
String fieldName = propertyName.replaceFirst( "^" + Pattern.quote( alias.prefix ), "" );
return getFilterablePropertyMeta( alias.objectAlias, fieldName, alias.propertyType )
.withDescription( alias.aliasFor != null ? String.format( "alias for %s.%s", alias.aliasFor, fieldName ) : null );
}
}
return getFilterablePropertyMeta( objectAlias, propertyName, elementClass );
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package ubic.gemma.persistence.service;

import lombok.Value;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.time.StopWatch;
import org.hibernate.Query;
Expand All @@ -13,9 +12,10 @@
import ubic.gemma.persistence.util.Sort;

import javax.annotation.Nullable;
import java.util.*;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -46,54 +46,6 @@ protected AbstractQueryFilteringVoEnabledDao( String objectAlias, Class<O> eleme
super( objectAlias, elementClass, sessionFactory );
}

@Value
protected static class FilterablePropertyQueryAlias {
String prefix;
@Nullable
String objectAlias;
Class<?> propertyType;
}

/**
* Since HQL-based filtering cannot simply detect aliases in the query, you have to declare them explicitly.
*/
protected FilterablePropertyQueryAlias[] getFilterablePropertyQueryAliases() {
return new FilterablePropertyQueryAlias[0];
}

@Override
public Set<String> getFilterableProperties() {
Set<String> results = super.getFilterableProperties();
if ( getFilterablePropertyQueryAliases().length > 0 ) {
results = new HashSet<>( results );
for ( FilterablePropertyQueryAlias alias : getFilterablePropertyQueryAliases() ) {
addFilterableProperties( alias.prefix, alias.propertyType, results, FILTERABLE_PROPERTIES_MAX_DEPTH - 1 );
}
results = Collections.unmodifiableSet( results );
}
return results;
}

/**
* Checks for special properties that are allowed to be referenced on certain objects. E.g. characteristics on EEs.
* {@inheritDoc}
*/
@Override
protected FilterablePropertyMeta getFilterablePropertyMeta( String propertyName ) {
// replace longer prefix first
List<FilterablePropertyQueryAlias> aliases = Arrays.stream( getFilterablePropertyQueryAliases() )
.sorted( Comparator.comparing( f -> f.prefix.length(), Comparator.reverseOrder() ) )
.collect( Collectors.toList() );
for ( FilterablePropertyQueryAlias alias : aliases ) {
if ( propertyName.startsWith( alias.prefix ) && !propertyName.equals( alias.prefix + "size" ) ) {
String fieldName = propertyName.replaceFirst( "^" + Pattern.quote( alias.prefix ), "" );
return getFilterablePropertyMeta( alias.objectAlias, fieldName, alias.propertyType );
}
}
return super.getFilterablePropertyMeta( propertyName );
}


/**
* Produce a query for retrieving value objects after applying a set of filters and a given ordering.
* <p>
Expand All @@ -120,21 +72,21 @@ protected Query getFilteringCountQuery( @Nullable Filters filters ) {
}

/**
* Process a result from {@link #getFilteringQuery(Filters, Sort, EnumSet)} into a {@link O}.
* Process a properties from {@link #getFilteringQuery(Filters, Sort, EnumSet)} into a {@link O}.
* <p>
* The default is to simply cast the result to {@link O}, assuming that it is the only return value of the query.
* The default is to simply cast the properties to {@link O}, assuming that it is the only return value of the query.
*/
protected O processFilteringQueryResultToEntity( Object result ) {
//noinspection unchecked
return ( O ) result;
}

/**
* Process a result from {@link #getFilteringQuery(Filters, Sort, EnumSet)} into a {@link VO} value object.
* Process a properties from {@link #getFilteringQuery(Filters, Sort, EnumSet)} into a {@link VO} value object.
* <p>
* The result is obtained from {@link Query#list()}.
* The properties is obtained from {@link Query#list()}.
* <p>
* By default, it will process the result with {@link #processFilteringQueryResultToEntity(Object)} and then apply
* By default, it will process the properties with {@link #processFilteringQueryResultToEntity(Object)} and then apply
* {@link #doLoadValueObject(Identifiable)} to obtain a value object.
*
* @return a value object, or null, and it will be ignored when constructing the {@link Slice} in {@link #loadValueObjectsPreFilter(Filters, Sort, int, int)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,12 @@ protected Criteria getFilteringCriteria( @Nullable Filters filters ) {
}

@Override
public Set<String> getFilterableProperties() {
Set<String> results = new HashSet<>( super.getFilterableProperties() );
protected void registerFilterableProperties( Set<String> properties ) {
super.registerFilterableProperties( properties );
// these cause a org.hibernate.MappingException: Unknown collection role exception (see https://github.com/PavlidisLab/Gemma/issues/518)
results.remove( "analysis.experimentAnalyzed.characteristics.size" );
results.remove( "analysis.experimentAnalyzed.otherRelevantPublications.size" );
results.remove( "experimentalFactors.size" );
return results;
properties.remove( "analysis.experimentAnalyzed.characteristics.size" );
properties.remove( "analysis.experimentAnalyzed.otherRelevantPublications.size" );
properties.remove( "experimentalFactors.size" );
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@
import ubic.gemma.model.common.auditAndSecurity.curation.AbstractCuratableValueObject;
import ubic.gemma.model.common.auditAndSecurity.curation.Curatable;
import ubic.gemma.model.common.auditAndSecurity.curation.CurationDetails;
import ubic.gemma.model.common.description.DatabaseEntry;
import ubic.gemma.persistence.service.AbstractQueryFilteringVoEnabledDao;
import ubic.gemma.persistence.service.common.auditAndSecurity.CurationDetailsDao;
import ubic.gemma.persistence.util.Filters;
import ubic.gemma.persistence.util.Filter;
import ubic.gemma.persistence.util.Slice;
import ubic.gemma.persistence.util.Filters;

import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.util.*;
import java.util.stream.Collectors;

/**
* Created by tesarst on 07/03/17.
Expand Down Expand Up @@ -91,12 +89,12 @@ protected void addNonTroubledFilter( Filters filters, @Nullable String objectAli
}

@Override
public Set<String> getFilterableProperties() {
Set<String> result = new HashSet<>( super.getFilterableProperties() );
result.addAll( Arrays.asList( "lastUpdated", "troubled", "needsAttention" ) );
return result;
@OverridingMethodsMustInvokeSuper
protected void registerFilterableProperties( Set<String> properties ) {
super.registerFilterableProperties( properties );
properties.addAll( Arrays.asList( "lastUpdated", "troubled", "needsAttention" ) );
}

/**
* {@inheritDoc}
* <p>
Expand Down
Loading

0 comments on commit d612215

Please sign in to comment.