Changeset 1820


Ignore:
Timestamp:
May 6, 2011, 5:41:49 PM (12 years ago)
Author:
robert@…
Message:

Adjusted querying process in order to improve speed with large amounts of data. The querying is now performed directly in HQL, instead of fetching all possible objects from the database and filtering them.

Location:
trunk/src/groovy/dbnp/query
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/groovy/dbnp/query/AssaySearch.groovy

    r1526 r1820  
    3030
    3131                this.entity = "Assay";
    32         }
    33 
    34         /**
    35          * Searches for assays based on the given criteria. All criteria have to be satisfied and
    36          * criteria for the different entities are satisfied as follows:
    37          *
    38          *              Study.title = 'abc'             
    39          *                              Only assays are returned from studies with title 'abc'
    40          *             
    41          *              Subject.species = 'human'
    42          *                              Only assays are returned with samples from subjects with species = 'human' 
    43          *
    44          *              Sample.name = 'sample 1'
    45          *                              Only assays are returned with samples with name = 'sample 1'
    46          *
    47          *              Event.startTime = '0s'
    48          *                              Only assays are returned with samples from subjects that have had an event with start time = '0s' 
    49          *
    50          *              SamplingEvent.startTime = '0s'
    51          *                              Only assays are returned with samples that have originated from a sampling event with start time = '0s' 
    52          *
    53          *              Assay.module = 'metagenomics'
    54          *                              Only assays are returned with module = metagenomics 
    55          *
    56          * When searching for more than one criterion per entity, these are taken combined. Searching for
    57          *
    58          *              Subject.species = 'human'
    59          *              Subject.name = 'Jan'
    60          *
    61          *  will result in all samples from a human subject named 'Jan'. Samples from a mouse subject
    62          *  named 'Jan' or a human subject named 'Kees' won't satisfy the criteria.
    63          *     
    64          */
    65         @Override
    66         protected void executeAnd() {
    67                 def assays = Assay.list().findAll { it.parent?.canRead( this.user ) };
    68 
    69                 executeAnd( assays );
    70         }
    71 
    72         /**
    73          * Searches for samples based on the given criteria. Only one of the criteria have to be satisfied and
    74          * criteria for the different entities are satisfied as follows:
    75          *
    76          *              Study.title = 'abc'             
    77          *                              Only assays are returned from studies with title 'abc'
    78          *             
    79          *              Subject.species = 'human'
    80          *                              Only assays are returned with samples from subjects with species = 'human' 
    81          *
    82          *              Sample.name = 'sample 1'
    83          *                              Only assays are returned with samples with name = 'sample 1'
    84          *
    85          *              Event.startTime = '0s'
    86          *                              Only assays are returned with samples from subjects that have had an event with start time = '0s' 
    87          *
    88          *              SamplingEvent.startTime = '0s'
    89          *                              Only assays are returned with samples that have originated from a sampling event with start time = '0s' 
    90          *
    91          *              Assay.module = 'metagenomics'
    92          *                              Only assays are returned with module = metagenomics 
    93          *
    94          * When searching for more than one criterion per entity, these are taken separately. Searching for
    95          *
    96          *              Subject.species = 'human'
    97          *              Subject.name = 'Jan'
    98          *
    99          *  will result in all samples from a human subject or a subject named 'Jan'. Samples from a mouse subject
    100          *  named 'Jan' or a human subject named 'Kees' will also satisfy the criteria.
    101          *
    102          */
    103         @Override
    104         void executeOr() {
    105                 def allAssays = Assay.list().findAll { it.parent?.canRead( this.user ) }.toList();
    106                 executeOr( allAssays );
    10732        }
    10833
     
    14570        }
    14671
     72       
     73        /**
     74         * Returns the HQL name for the element or collections to be searched in, for the given entity name
     75         * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50.
     76         * But when searching for Sample results, the system must search in sample.parentSubject for age > 50
     77         *
     78         * @param entity        Name of the entity of the criterion
     79         * @return                      HQL name for this element or collection of elements
     80         */
     81        protected String elementName( String entity ) {
     82                switch( entity ) {
     83                        case "Assay":                   return "assay"
     84                        case "Sample":                  return "assay.samples"
     85                        case "Study":                   return "assay.parent"
     86                       
     87                        case "Subject":                 return "assay.samples.parentSubject"                    // Will not be used, since entityClause() is overridden
     88                        case "SamplingEvent":   return "assay.samples.parentEvent"                              // Will not be used, since entityClause() is overridden
     89                        case "Event":                   return "assay.samples.parentEventGroup.events"  // Will not be used, since entityClause() is overridden
     90                        default:                                return null;
     91                }
     92        }
     93
     94        /**
     95         * Returns the a where clause for the given entity name
     96         * For example: when searching for Subject.age > 50 with Study results, the system must search
     97         *
     98         *      WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...)
     99         *
     100         * The returned string is fed to sprintf with 3 string parameters:
     101         *              from (in this case 'study.subjects'
     102         *              alias (in this case 'subject'
     103         *              paramName (in this case '...')
     104         *
     105         * @param entity                Name of the entity of the criterion
     106         * @return                      HQL where clause for this element or collection of elements
     107         */
     108        protected String entityClause( String entity ) {
     109                switch( entity ) {
     110                        case "Subject":
     111                                return 'EXISTS( FROM assay.samples sample WHERE sample.parentSubject IN (:%3$s) )'
     112                        case "SamplingEvent":
     113                                return 'EXISTS( FROM assay.samples sample WHERE sample.parentEvent IN (:%3$s) )'
     114                        case "Event":
     115                                return 'EXISTS( FROM assay.samples sample WHERE EXISTS( FROM sample.parentEventGroup.events event WHERE event IN (:%3$s) ) )'
     116                        default:
     117                                return super.entityClause( entity );
     118                }
     119        }
     120
     121        /**
     122         * Returns true iff the given entity is accessible by the user currently logged in
     123         *
     124         * @param entity                Study to determine accessibility for.
     125         * @return                      True iff the user is allowed to access this study
     126         */
     127        protected boolean isAccessible( def entity ) {
     128                return entity?.parent?.canRead( this.user );
     129        }
     130
     131       
    147132        /**
    148133         * Returns the saved field data that could be shown on screen. This means, the data
  • trunk/src/groovy/dbnp/query/Criterion.groovy

    r1800 r1820  
    3030
    3131        /**
     32         * Returns the class for the entity of this criterion
     33         * @return     
     34         */
     35        public Class entityClass() {
     36                if( this.entity == '*' )
     37                        return null;
     38               
     39                       
     40                try {
     41                        return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity)
     42                } catch( Exception e ) {
     43                        throw new Exception( "Unknown entity for criterion " + this, e );
     44                }
     45        }
     46       
     47        /**
    3248         * Retrieves a combination of the entity and field
    3349         * @return
     
    3652                return entity.toString() + ( field ? "." + field.toString() : "" );
    3753        }
    38        
     54
    3955        /**
    4056         * Retrieves a human readable description of the combination of the entity and field
     
    5268                }
    5369        }
     70
     71        /**
     72         * Returns the type of criterion when searching. Multiple types can be returned, since fields
     73         * with the same name might have different types.
     74         * 
     75         * @return      List of strings determining the type of this criterion. Possibilities are:
     76         *              [STRING,BOOLEAN,..]:The criterion references a template field that contains a 'simple'
     77         *                                                      value (boolean, double, long, string, reltime, date)
     78         *              [STRINGLIST,...]:       The criterion references a template field that contains a 'complex'
     79         *                                                      value (listitem, ontologyterm, template, module) referencing another
     80         *                                                      database table
     81         *              Wildcard:                       The criterion references all fields
     82         */
     83        protected List<String> criterionType() {
     84                if( this.entity == '*' || this.field == '*' ) {
     85                        return [
     86                                 'String',
     87                                 'Text',
     88                                 'File',
     89                                 'Date',
     90                                 'RelTime',
     91                                 'Double',
     92                                 'Long',
     93                                 'Boolean',
     94                                 'StringList',
     95                                 'ExtendableStringList',
     96                                 'Term',
     97                                 'Template',
     98                                 'Module'
     99                        ]
     100                }
     101
     102                // Determine domain fields of the entity
     103                def domainFields = entityClass().giveDomainFields();
     104                def domainField = domainFields.find { it.name == this.field };
     105                if( domainField )
     106                        return [domainField.type?.casedName];
     107
     108                // If this field is not a domain field, search for the field in the database
     109                def entityClass = entityClass()
     110
     111                if( !entityClass || !this.field )
     112                        return null;
     113
     114                // Find all fields with this name and entity
     115                def fields = TemplateField.findAllByName( this.field ).findAll { it.entity == entityClass };
     116
     117                // If the field is not found, return null
     118                if( !fields )
     119                        return null
     120
     121                // Return the (unique) String value of the types
     122                return fields*.type.unique()*.casedName;
     123        }
     124
     125        /**
     126         * Determines whether the field in this criterion is a domain field
     127         *
     128         * @return      True iff the field is a domain field, false otherwise
     129         */
     130        protected boolean isDomainCriterion() {
     131                def entityClass = entityClass()
     132               
     133                if( !entityClass )
     134                        return false;
     135                       
     136                // Determine domain fields of the entity
     137                def domainFields = entityClass.giveDomainFields();
     138                def domainField = domainFields.find { it.name == this.field };
     139
     140                return (domainField ? true : false)
     141        }
    54142       
     143        /**
     144         * Determines whether this criterion references a 'complex' field (i.e. a field that
     145         * contains a complex type like Term, ListItem etc.)
     146         *
     147         * @return
     148         */
     149        public boolean isComplexCriterion() {
     150                if( isDomainCriterion() )
     151                        return false;
     152                       
     153                def types = criterionType();
     154               
     155                return types.any { type ->
     156                        switch( type ) {
     157                                case 'StringList':
     158                                case 'ExtendableStringList':
     159                                case 'Term':
     160                                case 'Template':
     161                                case 'Module':
     162                                        return true;
     163                        }
     164                       
     165                        return false;
     166                }
     167        }
     168
     169        /**
     170         * Case the field value to search on to the given type
     171         * @param fieldType     Name of the template field type
     172         * @return                      Value casted to the right value
     173         */
     174        protected def castValue( String fieldType ) {
     175                switch( fieldType ) {
     176
     177                        case 'String':
     178                        case 'Text':
     179                        case 'StringList':
     180                        case 'ExtendableStringList':
     181                        case 'Term':
     182                        case 'Template':
     183                        case 'Module':
     184                                return value?.toString();
     185                        case 'File':
     186                                return null; // Never search in filenames, since they are not very descriptive
     187                        case 'Date':
     188                                // The comparison with date values should only be performed iff the value
     189                                // contains a parsable date
     190                                // and the operator is equals, gte, gt, lt or lte
     191                                if( operator == Operator.insearch || operator == Operator.contains )
     192                                        return null
     193
     194                                try {
     195                                        Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value );
     196                                        return dateCriterion
     197                                } catch( Exception e ) {
     198                                        return null;
     199                                }
     200
     201                        case 'RelTime':
     202                                // The comparison with date values should only be performed iff the value
     203                                // contains a long number
     204                                // and the operator is equals, gte, gt, lt or lte
     205                                if( operator == Operator.insearch || operator == Operator.contains )
     206                                        return null
     207
     208                                try {
     209                                        RelTime rt
     210
     211                                        // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it
     212                                        if( value.toString().isLong() ) {
     213                                                rt = new RelTime( Long.parseLong( value.toString() ) );
     214                                        } else {
     215                                                rt = new RelTime( value.toString() );
     216                                        }
     217
     218                                        return rt.getValue()
     219                                } catch( Exception e ) {
     220                                        return null;
     221                                }
     222                        case 'Double':
     223                                // The comparison with date values should only be performed iff the value
     224                                // contains a double number
     225                                // and the operator is equals, gte, gt, lt or lte
     226                                if( operator == Operator.insearch || operator == Operator.contains )
     227                                        return null
     228
     229                                if( value.isDouble() ) {
     230                                        return Double.parseDouble( value )
     231                                } else {
     232                                        return null;
     233                                }
     234                        case 'Long':
     235                                // The comparison with date values should only be performed iff the value
     236                                // contains a long number
     237                                // and the operator is equals, gte, gt, lt or lte
     238                                if( operator == Operator.insearch || operator == Operator.contains )
     239                                        return null
     240
     241                                if( value.isLong() ) {
     242                                        return Long.parseLong( value )
     243                                } else {
     244                                        return null;
     245                                }
     246                        case 'Boolean':
     247                                // The comparison with boolean values should only be performed iff the value
     248                                // contains 'true' or 'false' (case insensitive)
     249                                // and the operator is equals
     250                                if( operator != Operator.equals )
     251                                        return null
     252
     253                                def lowerCaseValue = value.toString().toLowerCase();
     254                                if( lowerCaseValue == 'true' || lowerCaseValue == 'false' ) {
     255                                        return Boolean.parseBoolean( this.value )
     256                                } else {
     257                                        return null
     258                                }
     259                }
     260        }
     261
     262        /**
     263         * Create a HQL where clause from this criterion, in order to be used within a larger HQL statement
     264         *
     265         * @param       objectToSearchIn        HQL name of the object to search in
     266         * @return      Map with 3 keys:   'join' and'where' with the HQL join and where clause for this criterion and 'parameters' for the query named parameters
     267         */
     268        public Map toHQL( String prefix, String objectToSearchIn = "object" ) {
     269                List whereClause = []
     270                String joinClause = "";
     271                Map parameters = [:];
     272                def emptyCriterion = [ "join": null, "where": null, "parameters": null ];
     273
     274                // If this criterion is used to search within another search result, we use a special piece of HQL
     275                if( this.operator == Operator.insearch ) {
     276                        if( this.value?.results ) {
     277                                parameters[ prefix + "SearchResults" ] = this.value?.results
     278
     279                                return [ "join": "", "where": "( " + objectToSearchIn + " in (:" + prefix + "SearchResults) )" , "parameters": parameters ];
     280                        } else {
     281                                return emptyCriterion;
     282                        }
     283                }
     284               
     285                // If no value is given, don't do anything
     286                if( !value )
     287                        return emptyCriterion;
     288               
     289                // Check whether the field is a domain field
     290                if( isDomainCriterion() ) {
     291                        // Determine the types of this criterion, but there will be only 1 for a domain field
     292                        def criterionType = criterionType()[0];
     293                       
     294                        // Some domain fields don't contain a value, but a reference to another table
     295                        // These should be handled differently
     296                        def fieldName = this.field
     297                       
     298                        if(
     299                                ( objectToSearchIn == "subject" && fieldName == "species" ) ||
     300                                ( objectToSearchIn == "sample" && fieldName == "material" ) ||
     301                                ( objectToSearchIn == "assay" && fieldName == "module" ) ||
     302                                ( objectToSearchIn == "samplingEvent" && fieldName == "sampleTemplate" ) ) {
     303                                fieldName += ".name"
     304                        }
     305                               
     306                        def query = extendWhereClause( "( %s )", objectToSearchIn + "." + fieldName, prefix, criterionType, castValue( criterionType ) );
     307                        return [ "join": "", "where": query.where, "parameters": query.parameters  ]
     308                }
     309
     310                // Determine the type of this criterion
     311                def criterionTypes = criterionType();
     312               
     313                if( !criterionTypes )
     314                        return emptyCriterion;                 
     315
     316               
     317                // Several types of criteria are handled differently.
     318                // The 'wildcard' is handled by searching for all types.
     319                // The 'simple' types (string, double) are handled by searching in the associated table
     320                // The 'complex' types (stringlist, template etc., referencing another
     321                // database table) can't be handled correctly, since the HQL INDEX() function doesn't work on those relations.
     322                // We do a search for these types to see whether any field with that type fits this criterion, in order to
     323                // filter out false positives later on.
     324                criterionTypes.findAll { it }.each { criterionType ->
     325                        // Cast criterion value to the right type
     326                        def currentValue = castValue( criterionType );
     327
     328                        // Determine field name
     329                        def fieldName = "template" + criterionType + 'Fields'
     330                       
     331                        switch( criterionType ) {
     332                                case "Wildcard":
     333                                        // Wildcard search is handled by
     334                                        break;
     335
     336                                case 'String':
     337                                case 'Text':
     338                                case 'File':
     339                                case 'Date':
     340                                case 'RelTime':
     341                                case 'Double':
     342                                case 'Long':
     343                                case 'Boolean':
     344                                        // 'Simple' field types
     345                                        if( currentValue != null ) {
     346                                                joinClause += " left join " + objectToSearchIn + "." + fieldName + " as " + prefix + "_" + fieldName + " ";
     347       
     348                                                def condition = this.oneToManyWhereCondition( prefix + "_" + fieldName, prefix, criterionType, currentValue )
     349                                                whereClause += condition[ "where" ];
     350       
     351                                                condition[ "parameters" ].each {
     352                                                        parameters[ it.key ] = it.value;
     353                                                }
     354                                        }
     355                                        break;
     356                                       
     357                                case 'StringList':
     358                                case 'ExtendableStringList':
     359                                case 'Term':
     360                                case 'Template':
     361                                case 'Module':
     362                                        // 'Complex' field types
     363                                        def condition = this.manyToManyWhereCondition( objectToSearchIn, fieldName, prefix, "name", currentValue )
     364                                        whereClause += condition[ "where" ];
     365       
     366                                        condition[ "parameters" ].each {
     367                                                parameters[ it.key ] = it.value;
     368                                        }
     369                                default:
     370                                        break;
     371                        }
     372                }
     373
     374                def where = whereClause?.findAll { it } ? "( " + whereClause.join( " OR " ) + " )" : ""
     375               
     376                return [ "join": joinClause, "where": where , "parameters": parameters ];
     377        }
     378
     379        /**
     380         * Extends a given condition with a where clause of this criterion. If you supply "select * from Study where %s", %s will
     381         * be replaced by the where clause for the given field. Also, the parameters map will be extended (if needed)
     382         *
     383         * @param hql                   Initial HQL string where the clause will be put into
     384         * @param fieldName             Name of the field that should be referenced
     385         * @param uniquePrefix  Unique prefix for this criterion
     386         * @param fieldType             Type of field value to search for
     387         * @param fieldValue    Field value to search for
     388         * @return                              Map with 'where' key referencing the extended where clause and 'parameters' key referencing a map with parameters.
     389         */
     390        protected Map extendWhereClause( String hql, String fieldName, String uniquePrefix, String fieldType, def fieldValue ) {
     391                def parameters = [:]
     392
     393                switch( this.operator ) {
     394                        case Operator.contains:
     395                                hql = sprintf( hql, fieldName + " like :" + uniquePrefix + "ValueLike" );
     396                                parameters[ uniquePrefix + "ValueLike" ] = "%" + fieldValue + "%"
     397                                break;
     398                        case Operator.equals:
     399                        case Operator.gte:
     400                        case Operator.gt:
     401                        case Operator.lte:
     402                        case Operator.lt:
     403                                hql = sprintf( hql, fieldName + " "  + this.operator.name + " :" + uniquePrefix + "Value" + fieldType );
     404                                parameters[ uniquePrefix + "Value" + fieldType ] = fieldValue
     405                                break;
     406                }
     407
     408                return [ "where": hql, "parameters": parameters]
     409        }
     410
     411        /**
     412         * Creates a condition for this criterion, for a given fieldName and value. The fieldName should reference a collection that has a one-to-many
     413         * relation with the object being sought
     414         * 
     415         * @param fieldName             Name to search in
     416         * @param uniquePrefix  Unique prefix for this criterion
     417         * @param currentValue  Map with 'value' referencing the value being sought and 'type' referencing
     418         *                                              the type of the value as string. The value should be be casted to the right class for this field.
     419         * @return                              Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters.
     420         */
     421        protected Map oneToManyWhereCondition( String fieldName, String uniquePrefix, String fieldType, def fieldValue ) {
     422                // Create the where condition for checking the value
     423                // First check the name of the field, if needed
     424                def condition
     425                def parameters = [:]
     426
     427                if( this.field != '*' ) {
     428                        condition = "( %s AND index(" + fieldName + ") = :" + uniquePrefix + "Field )"
     429                        parameters[ uniquePrefix + "Field" ] = this.field
     430                } else {
     431                        condition = "%s";
     432                }
     433
     434                def whereClause = extendWhereClause( condition, fieldName, uniquePrefix, fieldType, fieldValue );
     435                parameters.each {
     436                        whereClause.parameters[ it.key ] = it.value;
     437                }
     438
     439                return whereClause;
     440        }
     441
     442        /**
     443         * Creates a condition for this criterion, for a given fieldName and value. The fieldName should
     444         * reference a collection that has a many-to-many relation with the object being sought (e.g. templateTermFields).
     445         *
     446         * Unfortunately, there is no way to determine the name of the field in HQL for this many-to-many collections, since the
     447         * INDEX() function in HQL doesn't work for many-to-many collections.
     448         * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4879
     449         * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     450         * 
     451         * @param fieldName             Name to search in
     452         * @param uniquePrefix  Unique prefix for this criterion
     453         * @param currentValue  Map with 'value' referencing the value being sought and 'type' referencing
     454         *                                              the type of the value as string. The value should be be casted to the right class for this field.
     455         * @return                              Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters.
     456         */
     457        protected Map manyToManyWhereCondition( String objectToSearchIn, String collection, String uniquePrefix, String searchField, def value ) {
     458                // exists( FROM [objectToSearchIn].[collection] as [uniquePrefix][collection] WHERE [searchField] LIKE [value] )
     459                // Create the where condition for checking the value
     460                def condition = "exists ( FROM " + objectToSearchIn + "." + collection + " as " + uniquePrefix + "_" + collection + " WHERE %s )";
     461
     462                return extendWhereClause( condition, uniquePrefix + "_" + collection + "." + searchField, uniquePrefix, "STRING", value );
     463        }
     464
    55465        /**
    56466         * Retrieves the correct value for this criterion in the given object (with template)
     
    71481                                fieldValue = entity.template?.name
    72482                        } else if( field == "*" ) {
    73                                 fieldValue = entity.giveFields().collect{ 
     483                                fieldValue = entity.giveFields().collect{
    74484                                        if( it && it.name ) {
    75                                                 Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) ) 
     485                                                Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) )
    76486                                        }
    77487                                }
     
    79489                                fieldValue = Search.prepare( entity.getFieldValue( field ), entity.giveFieldType( field ) )
    80490                        }
    81                        
     491
    82492                        return fieldValue
    83493                } catch( Exception e ) {
     
    89499
    90500        /**
    91          * Checks if the given object (with template) that satisfies the given criterion.
    92          *
    93          * @param entity                Entity to check for criterion satisfaction. Should be a child of template entity
    94          * @param criterion     Criterion to match on
    95          * @return                      True iff there the entity satisfies the given criterion.
    96          */
    97         public boolean matchOneEntity( TemplateEntity entity ) {
    98                 def fieldValue = this.getFieldValue( entity );
    99 
    100                 // Null is returned, the given field doesn't exist. In that case, this criterion will fail.
    101                 // TODO: Maybe give the user a choice whether he want's to include these studies or not
    102                 if( fieldValue == null )
    103                         return false;
    104 
    105                 return this.match( fieldValue );
    106         }
    107 
    108         /**
    109          * Checks for all entities in the given entityList, if there is any object that satisfies the given criterion.
    110          *
    111          * @param entityList    List with entities. The entities should be child classes of TemplateEntity
    112          * @param criterion             Criterion to match on
    113          * @return                              True iff there is any entity in the list that satisfies the given criterion.
    114          */
    115         public boolean matchAnyEntity( List<TemplateEntity> entityList ) {
    116                 for( entity in entityList ) {
    117                         if( matchOneEntity( entity ) )
    118                                 return true;
    119                 }
    120                 return false;
    121         }
    122 
    123         /**
    124          * Checks for all entities in the given entityList, if all objects satisfy the given criterion.
    125          *
    126          * @param entityList    List with entities. The entities should be child classes of TemplateEntity
    127          * @param criterion             Criterion to match on
    128          * @return                              True iff all entities satisfy the given criterion.
    129          */
    130         public boolean matchAllEntities( List<TemplateEntity> entityList ) {
    131                 for( entity in entityList ) {
    132                         if( !matchOneEntity( entity ) )
    133                                 return false;
    134                 }
    135                 return true;
    136         }
    137 
    138         /**
    139          * Checks for all values in the given List, if there is any value that satisfies the given criterion.
    140          *
    141          * @param entityList            List with values.
    142          * @param criterion             Criterion to match on
    143          * @return                              True iff there is any value in the list that satisfies the given criterion.
    144          */
    145         public boolean matchAny( List valueList ) {
    146                 for( value in valueList ) {
    147                         if( match( value ) )
    148                                 return true;
    149                 }
    150                 return false;
    151         }
    152 
    153         /**
    154          * Checks for all values in the given List, if all values satisfy the given criterion.
    155          *
    156          * @param entityList            List with values.
    157          * @param criterion             Criterion to match on
    158          * @return                              True iff all values satisfy the given criterion.
    159          */
    160         public boolean matchAll( List entityList ) {
    161                 for( value in valueList ) {
    162                         if( !match( value ) )
    163                                 return false;
    164                 }
    165                 return true;
    166         }
    167 
    168         /**
    169501         * Tries to match a value against a criterion and returns true if it matches
    170502         *
     
    175507                if( fieldValue == null )
    176508                        return false;
    177                
     509
    178510                // in-search criteria have to be handled separately
    179511                if( this.operator == Operator.insearch ) {
    180512                        return this.value?.getResults()?.contains( fieldValue );
    181                 }       
    182                
     513                }
     514
    183515                // Other criteria are handled based on the class of the value given.
    184516                def classname = fieldValue.class.getName();
     
    202534                                default:                                                matches = compareValues( fieldValue.toString().trim().toLowerCase(), this.operator, value.toString().toLowerCase().trim() ); break;
    203535                        }
    204                        
     536
    205537                        return matches;
    206538                } catch( Exception e ) {
     
    229561                                return fieldValue <= criterionValue;
    230562                        case Operator.contains:
    231                                 // Contains operator can only be used on string values
     563                        // Contains operator can only be used on string values
    232564                                return fieldValue.toString().contains( criterionValue.toString() );
    233565                        case Operator.equals:
     
    314646                        if( lowerCaseValue != 'true' && lowerCaseValue != 'false' )
    315647                                return false;
    316                                
     648
    317649                        Boolean booleanCriterion = Boolean.parseBoolean( value );
    318650                        return compareValues( fieldValue, this.operator, booleanCriterion );
     
    346678                }
    347679        }
    348        
     680
    349681        public static Operator parseOperator( String name ) throws Exception {
    350682                switch( name.trim() ) {
    351                         case "=": 
    352                         case "equals":          return Operator.equals;
    353                         case "contains":        return Operator.contains; 
    354                         case ">=": 
    355                         case "gte":                     return Operator.gte;
    356                         case ">": 
    357                         case "gt":                      return Operator.gt;
    358                         case "<=": 
    359                         case "lte":                     return Operator.lte;
    360                         case "<": 
    361                         case "lt":                      return Operator.lt;
     683                        case "=":
     684                                case "equals":          return Operator.equals;
     685                        case "contains":        return Operator.contains;
     686                        case ">=":
     687                                case "gte":                     return Operator.gte;
     688                        case ">":
     689                                case "gt":                      return Operator.gt;
     690                        case "<=":
     691                                case "lte":                     return Operator.lte;
     692                        case "<":
     693                                case "lt":                      return Operator.lt;
    362694                        case "in":                      return Operator.insearch;
    363695                        default:
    364                                 throw new Exception( "Operator not found" ); 
     696                                throw new Exception( "Operator not found" );
    365697                }
    366698        }
     
    369701                return "[Criterion " + entityField() + " " + operator + " " + value + "]";
    370702        }
    371        
     703
    372704        public boolean equals( Object o ) {
    373705                if( o == null )
    374706                        return false;
    375                
    376                 if( !( o instanceof Criterion ) ) 
    377                         return false;
    378                        
     707
     708                if( !( o instanceof Criterion ) )
     709                        return false;
     710
    379711                Criterion otherCriterion = (Criterion) o;
    380712                return  this.entity == otherCriterion.entity &&
    381                                 this.field == otherCriterion.field &&
    382                                 this.operator == otherCriterion.operator &&
    383                                 this.value == otherCriterion.value;
     713                this.field == otherCriterion.field &&
     714                this.operator == otherCriterion.operator &&
     715                this.value == otherCriterion.value;
    384716        }
    385717}
  • trunk/src/groovy/dbnp/query/SampleSearch.groovy

    r1800 r1820  
    3030
    3131                this.entity = "Sample";
    32         }
    33 
    34         /**
    35          * Searches for samples based on the given criteria. All criteria have to be satisfied and
    36          * criteria for the different entities are satisfied as follows:
    37          *
    38          *              Sample.title = 'abc'           
    39          *                              Only samples are returned from studies with title 'abc'
    40          *             
    41          *              Subject.species = 'human'
    42          *                              Only samples are returned from subjects with species = 'human' 
    43          *
    44          *              Sample.name = 'sample 1'
    45          *                              Only samples are returned with name = 'sample 1'
    46          *
    47          *              Event.startTime = '0s'
    48          *                              Only samples are returned from subjects that have had an event with start time = '0s' 
    49          *
    50          *              SamplingEvent.startTime = '0s'
    51          *                              Only samples are returned that have originated from a sampling event with start time = '0s' 
    52          *
    53          *              Assay.module = 'metagenomics'
    54          *                              Only samples are returned that have been processed in an assay with module = metagenomics 
    55          *
    56          * When searching for more than one criterion per entity, these are taken combined. Searching for
    57          *
    58          *              Subject.species = 'human'
    59          *              Subject.name = 'Jan'
    60          *
    61          *  will result in all samples from a human subject named 'Jan'. Samples from a mouse subject
    62          *  named 'Jan' or a human subject named 'Kees' won't satisfy the criteria.
    63          *     
    64          */
    65         @Override
    66         void executeAnd() {
    67                 // If no criteria are found, return all samples
    68                 if( !criteria || criteria.size() == 0 ) {
    69                         results = Sample.list().findAll { it.parent?.canRead( this.user ) };
    70                         return;
    71                 }
    72 
    73                 // We expect the study criteria to be the most discriminative, and discard
    74                 // the most samples. (e.g. by searching on study title or study type). For
    75                 // that reason we first look through the list of studies. However, when the
    76                 // user didn't enter any study criteria, this will be an extra step, but doesn't
    77                 // cost much time to process.
    78                 def samples = []
    79                 if( getEntityCriteria( 'Study' ).size() > 0 ) {
    80                         def studies = Study.list()
    81                         if( studies )
    82                                 studies = studies.findAll { it.canRead( this.user ) };
    83 
    84                         studies = filterStudiesOnStudyCriteria( studies );
    85 
    86                         if( studies.size() == 0 ) {
    87                                 results = [];
    88                                 return;
    89                         }
    90 
    91                         def c = Sample.createCriteria()
    92                         samples = c.list {
    93                                 'in'( 'parent', studies )
    94                         }
    95 
    96                         // Save data about the resulting studies in the
    97                         // result fields array. The data that is now in the array
    98                         // is saved based on the study id, not based on the sample id
    99                         clearResultFields();
    100                         saveResultFields( samples, getEntityCriteria( "Study" ), { sample, criterion ->
    101                                 return criterion.getFieldValue( sample.parent );
    102                         });
    103                 } else {
    104                         samples = Sample.list()
    105                         if( samples )
    106                                 samples = samples.findAll { it.parent?.canRead( this.user ) }
    107                 }
    108 
    109                 samples = filterOnSubjectCriteria( samples );
    110                 samples = filterOnSampleCriteria( samples );
    111                 samples = filterOnEventCriteria( samples );
    112                 samples = filterOnSamplingEventCriteria( samples );
    113                 samples = filterOnAssayCriteria( samples );
    114                
    115                 // Filter on criteria for which the entity is unknown
    116                 samples = filterOnAllFieldsCriteria( samples );
    117                
    118                 // Filter on module criteria
    119                 samples = filterOnModuleCriteria( samples );
    120 
    121                 // Save matches
    122                 results = samples;
    123         }
    124 
    125         /**
    126          * Searches for samples based on the given criteria. Only one of the criteria have to be satisfied and
    127          * criteria for the different entities are satisfied as follows:
    128          *
    129          *              Sample.title = 'abc'
    130          *                              Samples are returned from studies with title 'abc'
    131          *
    132          *              Subject.species = 'human'
    133          *                              Samples are returned from subjects with species = 'human'
    134          *
    135          *              Sample.name = 'sample 1'
    136          *                              Samples are returned with name = 'sample 1'
    137          *
    138          *              Event.startTime = '0s'
    139          *                              Samples are returned from subjects that have had an event with start time = '0s'
    140          *
    141          *              SamplingEvent.startTime = '0s'
    142          *                              Samples are returned that have originated from a sampling event with start time = '0s'
    143          *
    144          *              Assay.module = 'metagenomics'
    145          *                              Samples are returned that have been processed in an assay with module = metagenomics
    146          *
    147          * When searching for more than one criterion per entity, these are taken separately. Searching for
    148          *
    149          *              Subject.species = 'human'
    150          *              Subject.name = 'Jan'
    151          *
    152          *  will result in all samples from a human subject or a subject named 'Jan'. Samples from a mouse subject
    153          *  named 'Jan' or a human subject named 'Kees' will also satisfy the criteria.
    154          *
    155          */
    156         @Override
    157         void executeOr() {
    158                 def allSamples = Sample.list().findAll { it.parent?.canRead( this.user ) }.toList();
    159                 executeOr( allSamples );
    16032        }
    16133
     
    20274
    20375        /**
    204          * Filters the given list of studies on the study criteria
    205          * @param studies       Original list of studies
    206          * @return                      List with all samples that match the Study-criteria
     76         * Returns the HQL name for the element or collections to be searched in, for the given entity name
     77         * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50.
     78         * But when searching for Sample results, the system must search in sample.parentSubject for age > 50
     79         *
     80         * @param entity        Name of the entity of the criterion
     81         * @return                      HQL name for this element or collection of elements
    20782         */
    208         protected List filterStudiesOnStudyCriteria( List studies ) {
    209                 return filterOnTemplateEntityCriteria(studies, "Study", { study, criterion -> return criterion.getFieldValue( study ) })
     83        protected String elementName( String entity ) {
     84                switch( entity ) {
     85                        case "Sample":                  return "sample"
     86                        case "Study":                   return "sample.parent"
     87                        case "Subject":                 return "sample.parentSubject"
     88                        case "SamplingEvent":   return "sample.parentEvent"
     89                        case "Event":                   return "sample.parentEventGroup.events"
     90                        case "Assay":                   return "sample.assays"                  // Will not be used, since entityClause() is overridden
     91                        default:                                return null;
     92                }
    21093        }
    21194
    21295        /**
    213          * Filters the given list of samples on the assay criteria
    214          * @param samples       Original list of samples
    215          * @return                      List with all samples that match the assay-criteria
     96         * Returns the a where clause for the given entity name
     97         * For example: when searching for Subject.age > 50 with Study results, the system must search
     98         *
     99         *      WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...)
     100         *
     101         * The returned string is fed to sprintf with 3 string parameters:
     102         *              from (in this case 'study.subjects'
     103         *              alias (in this case 'subject'
     104         *              paramName (in this case '...')
     105         *
     106         * @param entity                Name of the entity of the criterion
     107         * @return                      HQL where clause for this element or collection of elements
    216108         */
    217         @Override
    218         protected List filterOnAssayCriteria( List samples ) {
    219                 if( !samples?.size() )
    220                         return [];
     109        protected String entityClause( String entity ) {
     110                switch( entity ) {
     111                        case "Assay":
     112                                return 'EXISTS( FROM Assay assay WHERE assay IN (:%3$s) AND EXISTS( FROM assay.samples assaySample WHERE assaySample = sample ) ) '
     113                        default:
     114                                return super.entityClause( entity );
     115                }
     116        }
    221117
    222                 // There is no sample.assays property, so we have to look for assays another way: just find
    223                 // all assays that match the criteria
    224                 def criteria = getEntityCriteria( 'Assay' );
    225 
    226                 if( getEntityCriteria( 'Assay' ).size() == 0 ) {
    227                         if( searchMode == SearchMode.and )
    228                                 return samples
    229                         else if( searchMode == SearchMode.or )
    230                                 return [];
    231                 }
    232 
    233                 def assays = filterEntityList( Assay.list(), criteria, { assay, criterion ->
    234                         if( !assay )
    235                                 return false
    236 
    237                         return criterion.matchOneEntity( assay );
    238                 });
    239 
    240                 println "Matching assays: " + assays
    241        
    242                 // If no assays match these criteria, then no samples will match either
    243                 if( assays.size() == 0 )
    244                         return [];
    245 
    246                 // Now filter the samples on whether they are attached to the filtered assays
    247                 def matchingSamples = samples.findAll { sample ->
    248                         if( !sample.parent )
    249                                 return false;
    250 
    251                         def studyAssays = assays.findAll { it.parent.equals( sample.parent ); }
    252                        
    253                         println "Assays for " + sample + " (based on study): " + studyAssays
    254                        
    255                         // See if this sample is present in any of the matching assays. If so,
    256                         // this sample matches the criteria
    257                         for( def assay in studyAssays ) {
    258                                 if( assay.samples?.contains( sample ) )
    259                                         return true;
    260                                
    261                                 println "Assay " + assay + " with samples " + assay.samples + " doesn't contain " + sample;
    262                         }
    263 
    264                         return false;
    265                 }
    266 
    267                 return matchingSamples;
     118        /**
     119         * Returns true iff the given entity is accessible by the user currently logged in
     120         *
     121         * @param entity                Study to determine accessibility for.
     122         * @return                      True iff the user is allowed to access this study
     123         */
     124        protected boolean isAccessible( def entity ) {
     125                return entity?.parent?.canRead( this.user );
    268126        }
    269127
  • trunk/src/groovy/dbnp/query/Search.groovy

    r1800 r1820  
    112112                this.executionDate = new Date();
    113113
    114                 switch( searchMode ) {
    115                         case SearchMode.and:
    116                                 executeAnd();
    117                                 break;
    118                         case SearchMode.or:
    119                                 executeOr();
    120                                 break;
    121                 }
     114                // Execute the search
     115                executeSearch();
    122116
    123117                // Save the value of this results for later use
     
    126120
    127121        /**
    128          * Executes an inclusive (AND) search based on the given criteria. Should be filled in by
    129          * subclasses searching for a specific entity
    130          */
    131         protected void executeAnd() {
    132 
    133         }
    134 
    135         /**
    136          * Executes an exclusive (OR) search based on the given criteria. Should be filled in by
    137          * subclasses searching for a specific entity
    138          */
    139         protected void executeOr() {
    140 
    141         }
    142 
    143         /**
    144          * Default implementation of an inclusive (AND) search. Can be called by subclasses in order
    145          * to simplify searches.
    146          *
    147          * Filters the list of objects on study, subject, sample, event, samplingevent and assaycriteria,
    148          * based on the closures defined in valueCallback. Afterwards, the objects are filtered on module
    149          * criteria
    150          *
    151          * @param objects       List of objects to search in
    152          */
    153         protected void executeAnd( List objects ) {
    154                 // If no criteria are found, return all studies
    155                 if( !criteria || criteria.size() == 0 ) {
    156                         results = objects;
    157                         return;
    158                 }
    159 
    160                 // Perform filters
    161                 objects = filterOnStudyCriteria( objects );
    162                 objects = filterOnSubjectCriteria( objects );
    163                 objects = filterOnSampleCriteria( objects );
    164                 objects = filterOnEventCriteria( objects );
    165                 objects = filterOnSamplingEventCriteria( objects );
    166                 objects = filterOnAssayCriteria( objects );
    167 
    168                 // Filter on criteria for which the entity is unknown
    169                 objects = filterOnAllFieldsCriteria( objects );
    170                
    171                 // Filter on module criteria
    172                 objects = filterOnModuleCriteria( objects );
    173 
    174                 // Save matches
    175                 results = objects;
    176         }
    177 
    178         /**
    179         * Default implementation of an exclusive (OR) search. Can be called by subclasses in order
    180         * to simplify searches.
    181         *
    182         * Filters the list of objects on study, subject, sample, event, samplingevent and assaycriteria,
    183         * based on the closures defined in valueCallback. Afterwards, the objects are filtered on module
    184         * criteria
    185         *
    186         * @param allObjects     List of objects to search in
    187         */
    188    protected void executeOr( List allObjects ) {
    189                 // If no criteria are found, return all studies
    190                 if( !criteria || criteria.size() == 0 ) {
    191                         results = allObjects;
    192                         return;
    193                 }
    194 
    195                 // Perform filters on those objects not yet found by other criteria
    196                 def objects = []
    197                 objects = ( objects + filterOnStudyCriteria( allObjects - objects ) ).unique();
    198                 objects = ( objects + filterOnSubjectCriteria( allObjects - objects ) ).unique();
    199                 objects = ( objects + filterOnSampleCriteria( allObjects - objects ) ).unique();
    200                 objects = ( objects + filterOnEventCriteria( allObjects - objects ) ).unique();
    201                 objects = ( objects + filterOnSamplingEventCriteria( allObjects - objects ) ).unique();
    202                 objects = ( objects + filterOnAssayCriteria( allObjects - objects ) ).unique();
    203                
    204                 // Filter on criteria for which the entity is unknown
    205                 objects = ( objects + filterOnAllFieldsCriteria( allObjects - objects ) ).unique();
    206                
    207                 // All objects (including the ones already found by another criterion) are sent to
    208                 // be filtered on module criteria, in order to have the module give data about all
    209                 // objects (for showing purposes later on)
    210                 objects = ( objects + filterOnModuleCriteria( allObjects ) ).unique();
    211                
    212                 // Save matches
    213                 results = objects;
    214    }
    215 
     122         * Executes a query
     123         */
     124        protected void executeSearch() {
     125                // Create HQL query for criteria for the entity being sought
     126                def selectClause = ""
     127                def fullHQL = createHQLForEntity( this.entity );
     128
     129                // Create SQL for other entities, by executing a subquery first, and
     130                // afterwards selecting the study based on the entities found
     131                def resultsFound
     132
     133                def entityNames = [ "Study", "Subject", "Sample", "Assay", "Event", "SamplingEvent" ];
     134                for( entityToSearch in entityNames ) {
     135                        // Add conditions for all criteria for the given entity. However,
     136                        // the conditions for the 'main' entity (the entity being sought) are already added
     137                        if( entity != entityToSearch ) {
     138                                resultsFound = addEntityConditions(
     139                                        entityToSearch,                                                                                                                 // Name of the entity to search in
     140                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
     141                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
     142                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
     143                                        fullHQL                                                                                                                                 // Current HQL statement
     144                                )
     145                               
     146                                // If no results are found, and we are searching 'inclusive', there will be no
     147                                // results whatsoever. So we can quit this method now.
     148                                if( !resultsFound && searchMode == SearchMode.and ) {
     149                                        return
     150                                }
     151                        }
     152                }
     153                       
     154                // Search in all entities
     155                resultsFound = addWildcardConditions( fullHQL, entityNames )
     156                if( !resultsFound && searchMode == SearchMode.and ) {
     157                        return
     158                }
     159               
     160                // Combine all parts to generate a full HQL query
     161                def hqlQuery = selectClause + " " + fullHQL.from + ( fullHQL.where ? "  WHERE " + fullHQL.where.join( " " + searchMode.toString() + " "  ) : "" );
     162               
     163                // Find all objects
     164                def entities = entityClass().findAll( hqlQuery, fullHQL.parameters );
     165               
     166                // Find criteria that match one or more 'complex' fields
     167                // These criteria must be checked extra, since they are not correctly handled
     168                // by the HQL criteria. See also Criterion.manyToManyWhereCondition and
     169                // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     170                entities = filterForComplexCriteria( entities, getEntityCriteria( this.entity ) );
     171               
     172                // Filter on module criteria. If the search is 'and', only the entities found until now
     173                // should be queried in the module. Otherwise, all entities are sent, in order to retrieve
     174                // data (to show on screen) for all entities
     175                if( hasModuleCriteria() ) {
     176                        if( searchMode == SearchMode.and ) {
     177                                entities = filterOnModuleCriteria( entities );
     178                        } else {
     179                                entities = filterOnModuleCriteria( entityClass().list().findAll { this.isAccessible( it ) } )
     180                        }
     181                }
     182               
     183                // Determine which studies can be read
     184                results = entities;
     185               
     186        }
    216187               
    217188        /************************************************************************
     
    279250                }
    280251        }
    281 
     252       
     253        /**
     254        * Returns the HQL name for the element or collections to be searched in, for the given entity name
     255        * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50.
     256        * But when searching for Sample results, the system must search in sample.parentSubject for age > 50
     257        *
     258        * This method should be overridden in child classes
     259        *
     260        * @param entity Name of the entity of the criterion
     261        * @return                       HQL name for this element or collection of elements
     262        */
     263   protected String elementName( String entity ) {
     264           switch( entity ) {
     265                   case "Study":                       
     266                   case "Subject":                     
     267                   case "Sample":                       
     268                   case "Event":                       
     269                   case "SamplingEvent":       
     270                   case "Assay":                       
     271                                return entity[ 0 ].toLowerCase() + entity[ 1 .. -1 ]
     272                   default:                             return null;
     273           }
     274   }
     275   
     276        /**
     277        * Returns the a where clause for the given entity name
     278        * For example: when searching for Subject.age > 50 with Study results, the system must search
     279        *               
     280        *       WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...)
     281        *
     282        * The returned string is fed to sprintf with 3 string parameters:
     283        *               from (in this case 'study.subjects'
     284        *               alias (in this case 'subject'
     285        *               paramName (in this case '...')
     286        *
     287        * This method can be overridden in child classes to enable specific behaviour
     288        *
     289        * @param entity         Name of the entity of the criterion
     290        * @return                       HQL where clause for this element or collection of elements
     291        */
     292   protected String entityClause( String entity ) {
     293           return ' EXISTS( FROM %1$s %2$s WHERE %2$s IN (:%3$s) )'
     294   }
     295   
     296   /**
     297    * Returns true iff the given entity is accessible by the user currently logged in
     298    *
     299    * This method should be overridden in child classes, since the check is different for every type of search
     300    *
     301    * @param entity             Entity to determine accessibility for. The entity is of the type 'this.entity'
     302    * @return                   True iff the user is allowed to access this entity
     303    */
     304   protected boolean isAccessible( def entity ) {
     305           return false
     306   }
     307
     308        /****************************************************
     309         *
     310         * Helper methods for generating HQL statements
     311         *
     312         ****************************************************/
     313       
     314        /**
     315         * Add all conditions for criteria for a specific entity
     316         *
     317         * @param entityName    Name of the entity to search in
     318         * @param entityClass   Class of the entity to search
     319         * @param from                  Name of the HQL collection to search in (e.g. study.subjects)
     320         * @param alias                 Alias of the HQL collection objects (e.g. 'subject')
     321         * @param fullHQL               Original HQL map to be extended (fields 'from', 'where' and 'parameters')
     322         * @param determineParentId     Closure to determine the id of the final entity to search, based on these objects
     323         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
     324         * @return                              True if one ore more entities are found, false otherwise
     325         */
     326        protected boolean addEntityConditions( String entityName, def entityClass, String from, String alias, def fullHQL, def entityCriteria = null ) {
     327                if( entityCriteria == null )
     328                        entityCriteria = getEntityCriteria( entityName )
     329               
     330                // Create HQL for these criteria
     331                def entityHQL = createHQLForEntity( entityName, entityCriteria );
     332               
     333                // If any clauses are generated for these criteria, find entities that match these criteria
     334                def whereClauses = entityHQL.where?.findAll { it && it?.trim() != "" }
     335                if( whereClauses ) {
     336                        // First find all entities that match these criteria
     337                        def hqlQuery = entityHQL.from + " WHERE " + whereClauses.join( searchMode == SearchMode.and ? " AND " : " OR " );                       
     338                        def entities = entityClass.findAll( hqlQuery, entityHQL.parameters )
     339                       
     340                        // If there are entities matching these criteria, put a where clause in the full HQL query
     341                        if( entities ) {
     342                                // Find criteria that match one or more 'complex' fields
     343                                // These criteria must be checked extra, since they are not correctly handled
     344                                // by the HQL criteria. See also Criterion.manyToManyWhereCondition and
     345                                // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     346                                entities = filterForComplexCriteria( entities, entityCriteria );
     347                               
     348                                def paramName = from.replaceAll( /\W/, '' );
     349                                fullHQL.where << sprintf( entityClause( entityName ), from, alias, paramName );
     350                                fullHQL.parameters[ paramName ] = entities
     351                                return true;
     352                        } else {
     353                                results = [];
     354                                return false
     355                        }
     356                }
     357               
     358                return true;
     359        }
     360       
     361        /**
     362         * Add all conditions for a wildcard search (all fields in a given entity)
     363         * @param fullHQL       Original HQL map to be extended (fields 'from', 'where' and 'parameters')
     364         * @return                      True if the addition worked
     365         */
     366        protected boolean addWildcardConditions( def fullHQL, def entities) {
     367                // Append study criteria
     368                def entityCriteria = getEntityCriteria( "*" );
     369               
     370                // If no wildcard criteria are found, return immediately
     371                if( !entityCriteria )
     372                        return true
     373                       
     374                // Wildcards should be checked within each entity
     375                def wildcardHQL = createHQLForEntity( this.entity );
     376               
     377                // Create SQL for other entities, by executing a subquery first, and
     378                // afterwards selecting the study based on the entities found
     379                entities.each { entityToSearch ->
     380                        // Add conditions for all criteria for the given entity. However,
     381                        // the conditions for the 'main' entity (the entity being sought) are already added
     382                        if( entity != entityToSearch ) {
     383                                addEntityConditions(
     384                                        entityToSearch,                                                                                                                 // Name of the entity to search in
     385                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
     386                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
     387                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
     388                                        wildcardHQL,                                                                                                                    // Current HQL statement
     389                                        entityCriteria                                                                                                                  // Only create HQL for these criteria
     390                                )
     391                        }
     392                }
     393               
     394                // Add these clauses to the full HQL statement
     395                def whereClauses = wildcardHQL.where.findAll { it };
     396
     397                if( whereClauses ) {
     398                        fullHQL.from += wildcardHQL.from
     399                        fullHQL.where << whereClauses.join( " OR " )
     400                         
     401                        wildcardHQL[ "parameters" ].each {
     402                                fullHQL.parameters[ it.key ] = it.value
     403                        }
     404                }
     405               
     406                return true;
     407        }
     408       
     409        /**
     410         * Create HQL statement for the given criteria and a specific entity
     411         * @param entityName            Name of the entity
     412         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
     413         * @param includeFrom           (optional) If set to true, the 'FROM entity' is prepended to the from clause. Defaults to true
     414         * @return
     415         */
     416        def createHQLForEntity( String entityName, def entityCriteria = null, includeFrom = true ) {
     417                def fromClause = includeFrom ? "FROM " + entityName + " " + entityName.toLowerCase() : ""
     418                def whereClause = []
     419                def parameters = [:]
     420                def criterionNum = 0;
     421               
     422                // Append study criteria
     423                if( entityCriteria == null )
     424                        entityCriteria = getEntityCriteria( entityName );
     425               
     426                entityCriteria.each {
     427                        def criteriaHQL = it.toHQL( "criterion" +entityName + criterionNum++, entityName.toLowerCase() );
     428                        fromClause += " " + criteriaHQL[ "join" ]
     429                        whereClause << criteriaHQL[ "where" ]
     430                        criteriaHQL[ "parameters" ].each {
     431                                parameters[ it.key ] = it.value
     432                        }
     433                }
     434               
     435                // Add a filter such that only readable studies are returned
     436                if( entityName == "Study" ) {
     437                       
     438                        if( this.user == null ) {
     439                                // Anonymous readers are only given access when published and public
     440                                whereClause << "( study.publicstudy = true AND study.published = true )"
     441                        } else if( !this.user.hasAdminRights() ) {
     442                                // Administrators are allowed to read every study
     443
     444                                // Owners and writers are allowed to read this study
     445                                // Readers are allowed to read this study when it is published
     446                                whereClause << "( study.owner = :sessionUser OR :sessionUser member of study.writers OR ( :sessionUser member of study.readers AND study.published = true ) )"
     447                                parameters[ "sessionUser" ] = this.user
     448                        }
     449                }
     450               
     451                return [ "from": fromClause, "where": whereClause, "parameters": parameters ]
     452        }
     453       
    282454        /*****************************************************
    283455         *
     
    405577                }
    406578        }
    407                
    408         /**
    409          * Filters the given list of studies on the study criteria
    410          * @param studies               Original list of studies
    411          * @param entity                Name of the entity to check the criteria for
    412          * @param valueCallback Callback having a study and criterion as input, returning the value of the field to check on
    413          * @return                              List with all studies that match the Criteria
    414          */
    415         protected List filterOnTemplateEntityCriteria( List studies, String entityName, Closure valueCallback ) {
    416                 def criteria = getEntityCriteria( entityName );
    417 
    418                 def checkCallback = { study, criterion ->
    419                         def value = valueCallback( study, criterion );
    420 
    421                         if( value == null ) {
    422                                 return false
    423                         }
    424 
    425                         if( value instanceof Collection ) {
    426                                 return criterion.matchAny( value )
    427                         } else {
    428                                 return criterion.match( value );
    429                         }
    430                 }
    431 
    432                 return filterEntityList( studies, criteria, checkCallback);
    433         }
    434 
    435         /**
    436          * Filters the given list of studies on the study criteria
    437          * @param studies       Original list of studies
    438          * @return                      List with all studies that match the Study criteria
    439          */
    440         protected List filterOnStudyCriteria( List studies ) {
    441                 def entity = "Study"
    442                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    443         }
    444 
    445         /**
    446          * Filters the given list of studies on the subject criteria
    447          * @param studies       Original list of studies
    448          * @return                      List with all studies that match the Subject-criteria
    449          */
    450         protected List filterOnSubjectCriteria( List studies ) {
    451                 def entity = "Subject"
    452                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    453         }
    454 
    455         /**
    456          * Filters the given list of studies on the sample criteria
    457          * @param studies       Original list of studies
    458          * @return                      List with all studies that match the sample-criteria
    459          */
    460         protected List filterOnSampleCriteria( List studies ) {
    461                 def entity = "Sample"
    462                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    463         }
    464 
    465         /**
    466          * Filters the given list of studies on the event criteria
    467          * @param studies       Original list of studies
    468          * @return                      List with all studies that match the event-criteria
    469          */
    470         protected List filterOnEventCriteria( List studies ) {
    471                 def entity = "Event"
    472                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    473         }
    474 
    475         /**
    476          * Filters the given list of studies on the sampling event criteria
    477          * @param studies       Original list of studies
    478          * @return                      List with all studies that match the event-criteria
    479          */
    480         protected List filterOnSamplingEventCriteria( List studies ) {
    481                 def entity = "SamplingEvent"
    482                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    483         }
    484 
    485         /**
    486          * Filters the given list of studies on the assay criteria
    487          * @param studies       Original list of studies
    488          * @return                      List with all studies that match the assay-criteria
    489          */
    490         protected List filterOnAssayCriteria( List studies ) {
    491                 def entity = "Assay"
    492                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    493         }
    494        
    495        
    496         /**
    497          * Filters the given list of entities on criteria that mention all fields (e.g. search for studies with 'bacteria' in any field)
    498          * @param objects       Original list of entities.
    499          * @return                      List of all entities that match the given criteria
    500          */
    501         protected List filterOnAllFieldsCriteria( List objects ) {
    502                 def criteria = getEntityCriteria( "*" );
    503                
    504                 // Find methods to determine a value for a criterion, based on all entities
    505                 def valueCallbacks = [:];
    506                 def entities = [ "Study", "Subject", "Sample", "Event", "SamplingEvent", "Assay" ];
    507                 entities.each {
    508                         valueCallbacks[ it ] = valueCallback( it );
    509                 }
    510                
    511                 // Create a closure that checks all entities
    512                 def checkCallback = { object, criterion ->
    513                         def value = "";
    514                         for( def entity in entities ) {
    515                                 value = valueCallbacks[ entity ]( object, criterion );
    516                                
     579       
     580        /**
     581         * Filters an entity list manually on complex criteria found in the criteria list.
     582         * This method is needed because hibernate contains a bug in the HQL INDEX() function.
     583         * See also Criterion.manyToManyWhereCondition and
     584         *http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     585         *
     586         * @param entities                      List of entities
     587         * @param entityCriteria        List of criteria that apply to the type of entities given       (e.g. Subject criteria for Subjects)
     588         * @return                                      Filtered entity list
     589         */
     590        protected filterForComplexCriteria( def entities, def entityCriteria ) {
     591                def complexCriteria = entityCriteria.findAll { it.isComplexCriterion() }
     592                if( complexCriteria ) {
     593                        def checkCallback = { entity, criterion ->
     594                                def value = criterion.getFieldValue( entity )
     595
    517596                                if( value == null ) {
    518                                         continue;
    519                                 }
    520        
     597                                        return false
     598                                }
     599
    521600                                if( value instanceof Collection ) {
    522                                         if( criterion.matchAny( value ) )
    523                                                 return true;
     601                                        return criterion.matchAny( value )
    524602                                } else {
    525                                         if( criterion.match( value ) )
    526                                                 return true;
    527                                 }
    528                         }
    529                        
    530                         // If no match is found, return
    531                         return false;
    532                 }
    533 
    534                 return filterEntityList( objects, criteria, checkCallback);
    535         }
    536        
     603                                        return criterion.match( value );
     604                                }
     605                        }
     606
     607                        entities = filterEntityList( entities, complexCriteria, checkCallback );
     608                }
     609               
     610                return entities;
     611        }
     612
    537613        /********************************************************************
    538614         *
     
    541617         ********************************************************************/
    542618
     619        protected boolean hasModuleCriteria() {
     620               
     621                return AssayModule.list().any { module ->
     622                        // Remove 'module' from module name
     623                        def moduleName = module.name.replace( 'module', '' ).trim()
     624                        def moduleCriteria = getEntityCriteria( moduleName );
     625                        return moduleCriteria?.size() > 0
     626                }
     627        }
     628       
    543629        /**
    544630         * Filters the given list of entities on the module criteria
     
    611697                                }
    612698               
    613                                 println this.resultFields;
    614                                
    615699                                return resultingEntities;
    616700                        default:
     
    878962                                        criteria.containsAll( s.criteria ) );
    879963        }
     964       
     965        /**
     966        * Returns the class for the entity being searched
     967        * @return
     968        */
     969        public Class entityClass() {
     970                if( !this.entity )
     971                        return null;
     972                       
     973                try {
     974                        return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity)
     975                } catch( Exception e ) {
     976                        throw new Exception( "Unknown entity for criterion " + this, e );
     977                }
     978        }
     979       
    880980}
  • trunk/src/groovy/dbnp/query/StudySearch.groovy

    r1800 r1820  
    3333
    3434        /**
    35          * Searches for studies based on the given criteria. All criteria have to be satisfied and
    36          * criteria for the different entities are satisfied as follows:
    37          *
    38          *              Study.title = 'abc'             
    39          *                              All returned studies will have title 'abc'
    40          *             
    41          *              Subject.species = 'human'
    42          *                              All returned studies will have one or more subjects with species = 'human' 
    43          *
    44          *              Sample.name = 'sample 1'
    45          *                              All returned studies will have one or more samples with name = 'sample 1'
    46          *
    47          *              Event.startTime = '0s'
    48          *                              All returned studies will have one or more events with start time = '0s' 
    49          *
    50          *              Assay.module = 'metagenomics'
    51          *                              All returned studies will have one or more assays with module = 'metagenomics' 
    52          *
    53          * When searching the system doesn't look at the connections between different entities. This means,
    54          * the system doesn't look for human subjects having a sample with name 'sample 1'. The sample 1 might
    55          * as well belong to a mouse subject and still the study satisfies the criteria.
    56          *
    57          * When searching for more than one criterion per entity, these are taken combined. Searching for
    58          *
    59          *              Subject.species = 'human'
    60          *              Subject.name = 'Jan'
    61          *
    62          *  will result in all studies having a human subject named 'Jan'. Studies with only a mouse subject
    63          *  named 'Jan' or a human subject named 'Kees' won't satisfy the criteria.
    64          *     
    65          */
    66         @Override
    67         protected void executeAnd() {
    68                 def studies = Study.list().findAll { it.canRead( this.user ) };
    69 
    70                 executeAnd( studies );
    71         }
    72 
    73         /**
    74          * Searches for studies based on the given criteria. Only one criteria have to be satisfied and
    75          * criteria for the different entities are satisfied as follows:
    76          *
    77          *              Study.title = 'abc'
    78          *                              The returned study will have title 'abc'
    79          *
    80          *              Subject.species = 'human'
    81          *                              The returned study will have one or more subjects with species = 'human'
    82          *
    83          *              Sample.name = 'sample 1'
    84          *                              The returned study will have one or more samples with name = 'sample 1'
    85          *
    86          *              Event.startTime = '0s'
    87          *                              The returned study will have one or more events with start time = '0s'
    88          *
    89          *              Assay.module = 'metagenomics'
    90          *                              The returned study will have one or more assays with module = 'metagenomics'
    91          *
    92          * When searching the system doesn't look at the connections between different entities. This means,
    93          * the system doesn't look for human subjects having a sample with name 'sample 1'. The sample 1 might
    94          * as well belong to a mouse subject and still the study satisfies the criteria.
    95          *
    96          * When searching for more than one criterion per entity, these are taken separately. Searching for
    97          *
    98          *              Subject.species = 'human'
    99          *              Subject.name = 'Jan'
    100          *
    101          *  will result in all studies having a human subject or a subject named 'Jan'. Studies with only a
    102          *  mouse subject named 'Jan' or a human subject named 'Kees' will satisfy the criteria.
    103          *
    104          */
    105         @Override
    106         protected void executeOr() {
    107                 def allStudies = Study.list().findAll { it.canRead( this.user ) };
    108                 executeOr( allStudies );
    109         }
    110 
    111         /**
    11235        * Returns a closure for the given entitytype that determines the value for a criterion
    11336        * on the given object. The closure receives two parameters: the object and a criterion.
     
    13760                                return { study, criterion -> return study.assays?.collect { criterion.getFieldValue( it ); } }
    13861                        default:
    139                                 return super.valueCallback( entity );
     62                                return null;
    14063                }
    14164        }
    142 
     65       
     66        /**
     67         * Returns the HQL name for the element or collections to be searched in, for the given entity name
     68         * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50.
     69     * But when searching for Sample results, the system must search in sample.parentSubject for age > 50
     70         *
     71     * @param entity    Name of the entity of the criterion
     72         * @return                      HQL name for this element or collection of elements
     73         */
     74        protected String elementName( String entity ) {
     75                switch( entity ) {
     76                        case "Study":                   return "study"
     77                        case "Subject":                 return "study.subjects"
     78                        case "Sample":                  return "study.samples"
     79                        case "Event":                   return "study.events"
     80                        case "SamplingEvent":   return "study.samplingEvents"
     81                        case "Assay":                   return "study.assays"
     82                        default:                                return null;
     83                }
     84        }
     85       
     86        /**
     87         * Returns true iff the given entity is accessible by the user currently logged in
     88         *
     89         * @param entity                Study to determine accessibility for.
     90         * @return                      True iff the user is allowed to access this study
     91         */
     92        protected boolean isAccessible( def entity ) {
     93                return entity?.canRead( this.user );
     94        }
     95       
    14396        /**
    14497         * Returns the saved field data that could be shown on screen. This means, the data
Note: See TracChangeset for help on using the changeset viewer.