Changeset 1820

Show
Ignore:
Timestamp:
06-05-11 17:41:49 (3 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 modified

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