Ignore:
Timestamp:
Jan 12, 2011, 9:45:08 PM (9 years ago)
Author:
robert@…
Message:

Externalized configuration; improved assay view (detail views of runs and samples); implemented uploading and parsing of FASTA and QUAL files

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk

    • Property svn:ignore
      •  

        old new  
        44.classpath
        55.project
         6fileuploads
  • trunk/grails-app/services/nl/tno/metagenomics/FastaService.groovy

    r2 r3  
    11package nl.tno.metagenomics
    2 
    32
    43import java.io.File;
     
    98        def fileService
    109        def fuzzySearchService
    11        
    12     static transactional = true
     10
     11        static transactional = true
    1312
    1413        /**
    1514         * Parses uploaded files and checks them for FASTA and QUAL files
    1615         * @param filenames             List of filenames currently existing in the upload directory
     16         * @param onProgress    Closure to execute when progress indicators should be updated.
     17         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
     18         *                                              of files and bytes that have been processed in total
    1719         * @param directory             Directory to move the files to
    1820         * @return                              Structure with information about the parsed files. The 'success' files are
     
    3234         *
    3335         */
    34         def parseFiles( ArrayList filenames, File directory = null ) {
     36        def parseFiles( ArrayList filenames, Closure onProgress, File directory = null ) {
    3537                if( filenames.size() == 0 ) {
    3638                        return [ success: [], failure: [] ];
     
    4345                def success = [];
    4446                def failure = [];
     47
     48                long filesProcessed = 0;
     49                long bytesProcessed = 0;
    4550
    4651                // Loop through all filenames
     
    5560                        } else {
    5661                                try {
    57                                         def contents = parseFile( file, filetype );
    58                                        
     62                                        def contents = parseFile( file, filetype, { files, bytes ->
     63                                                filesProcessed += files;
     64                                                bytesProcessed += bytes;
     65
     66                                                onProgress( filesProcessed, bytesProcessed );
     67                                        } );
     68
    5969                                        contents.filename = file.getName();
    60                                         contents.originalfilename = fileService.originalFilename( contents.filename ) 
    61                                        
     70                                        contents.originalfilename = fileService.originalFilename( contents.filename )
     71
    6272                                        if( contents.success ) {
    6373                                                success << contents;
     
    7181                                        failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: e.getMessage() ];
    7282                                }
    73        
     83
    7484                        }
    7585                }
     
    103113                def quals = parsedFiles.findAll { it.type == "qual" }
    104114                samples = samples.toList()
    105                
     115
    106116                def files = [];
    107                
     117
    108118                fastas.each { fastaFile ->
    109119                        // Remove extension
    110                         def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.filename.lastIndexOf( '.' ) )
    111                        
     120                        def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.originalfilename.lastIndexOf( '.' ) )
     121
    112122                        // Determine feasible quals (based on number of sequences )
    113123                        def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences }
    114                        
     124
    115125                        // Best matching qual file
    116126                        def qualIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.qual', feasibleQuals.originalfilename );
     
    119129                        if( qualIdx != null )
    120130                                qual = feasibleQuals[ qualIdx ];
    121                        
     131
    122132                        // Best matching sample
    123133                        def sampleIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, samples.sample.name );
    124134                        def assaySample = null
    125                         if( sampleIdx != null ) { 
     135                        if( sampleIdx != null ) {
    126136                                assaySample = samples[ sampleIdx ];
    127137                        }
    128138
    129139                        files << [
    130                                 fasta: fastaFile,
    131                                 feasibleQuals: feasibleQuals,
    132                                 qual: qual,
    133                                 assaySample: assaySample
    134                         ]
    135                 }
    136                
     140                                                fasta: fastaFile,
     141                                                feasibleQuals: feasibleQuals,
     142                                                qual: qual,
     143                                                assaySample: assaySample
     144                                        ]
     145                }
     146
    137147                return files;
    138                
    139                
    140         }
    141        
    142        
     148
     149
     150        }
     151
     152
    143153        /**
    144154         * Determines the file type of a given file, based on the extension.
     
    195205        /**
    196206         * Parses the given file
    197          * @param file          File to parse
    198          * @param filetype      Type of the given file
    199          * @return                      List structure. Examples:
     207         * @param file                  File to parse
     208         * @param filetype              Type of the given file
     209         * @param onProgress    Closure to execute when progress indicators should be updated.
     210         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
     211         *                                              of files and bytes that have been processed in this file (so the first parameter should
     212         *                                              only be 1 when the file is finished)
     213         *
     214         * @return                              List structure. Examples:
    200215         *
    201216         *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
     
    203218         *   [ success: false, filename: 'abc.txt', type: 'txt', message: 'Filetype could not be parsed.' ]
    204219         */
    205         protected def parseFile( File file, String filetype ) {
     220        protected def parseFile( File file, String filetype, Closure onProgress ) {
    206221                switch( filetype ) {
    207222                        case "fasta":
    208                                 return parseFasta( file );
     223                                return parseFasta( file, onProgress );
    209224                        case "qual":
    210                                 return parseQual( file );
     225                                return parseQual( file, onProgress );
    211226                        default:
     227                                onProgress( 1, file.length() );
    212228                                return [ success: false, type: filetype, message: 'Filetype could not be parsed.' ]
    213229                }
     
    216232        /**
    217233         * Parses the given FASTA file
    218          * @param file          File to parse
    219          * @return                      List structure. Examples:
     234         * @param file                  File to parse
     235         * @param onProgress    Closure to execute when progress indicators should be updated.
     236         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
     237         *                                              of files and bytes that have been processed in this file (so the first parameter should
     238         *                                              only be 1 when the file is finished)
     239         * @return                              List structure. Examples:
    220240         *
    221241         *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
    222242         *   [ success: false, filename: 'def.fasta', type: 'fasta', message: 'File is not a valid FASTA file' ]
    223243         */
    224         protected def parseFasta( File file ) {
    225                
     244        protected def parseFasta( File file, Closure onProgress ) {
     245
    226246                long startTime = System.nanoTime();
    227                 log.trace "Start parsing FASTA " + file.getName() 
    228                
     247                log.trace "Start parsing FASTA " + file.getName()
     248
    229249                // Count the number of lines, starting with '>' (and where the following line contains a character other than '>')
    230250                long numSequences = 0;
     251                long bytesProcessed = 0;
    231252                boolean lookingForSequence = false;
    232253
     
    241262                                        }
    242263                                }
    243                         }
    244                 }
     264
     265
     266                                // Update progress every time 1MB is processed
     267                                bytesProcessed += line.size();
     268                                if( bytesProcessed > 1000000 ) {
     269                                        onProgress( 0, bytesProcessed );
     270                                        bytesProcessed = 0;
     271                                }
     272                        }
     273
     274
     275
     276                }
     277
     278                // Update progress and say we're finished
     279                onProgress( 1, bytesProcessed );
    245280
    246281                log.trace "Finished parsing FASTA " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
     
    251286        /**
    252287         * Parses the given QUAL file
    253          * @param file          File to parse
    254          * @return                      List structure. Examples:
     288         * @param file                  File to parse
     289         * @param onProgress    Closure to execute when progress indicators should be updated.
     290         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
     291         *                                              of files and bytes that have been processed in this file (so the first parameter should
     292         *                                              only be 1 when the file is finished)
     293         * @return                              List structure. Examples:
    255294         *
    256295         *   [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 31 ]
    257296         *   [ success: false, filename: 'def.qual', type: 'qual', message: 'File is not a valid QUAL file' ]
    258297         */
    259         protected def parseQual( File file ) {
     298        protected def parseQual( File file, Closure onProgress ) {
    260299                long startTime = System.nanoTime();
    261300                log.trace "Start parsing QUAL " + file.getName()
    262301
    263                 // Count the number of lines, starting with '>'. After we've found such a character, we continue looking for 
     302                // Count the number of lines, starting with '>'. After we've found such a character, we continue looking for
    264303                // quality scores
    265304                long numSequences = 0;
     305                long bytesProcessed = 0;
    266306                def quality = [ quality: 0.0, number: 0L ]
    267                
     307
    268308                boolean lookingForFirstQualityScores = false;
    269309                file.eachLine { line ->
     
    275315                                                numSequences++;
    276316                                                lookingForFirstQualityScores = false;
    277                                                
     317
    278318                                                quality = updateQuality( quality, line );
    279319                                        }
     
    281321                                        quality = updateQuality( quality, line );
    282322                                }
    283                         }
    284                 }
     323                               
     324                                // Update progress every time 1MB is processed
     325                                bytesProcessed += line.size();
     326                                if( bytesProcessed > 1000000 ) {
     327                                        onProgress( 0, bytesProcessed );
     328                                        bytesProcessed = 0;
     329                                }
     330                        }
     331
     332                }
     333
     334                // Update progress and say we're finished
     335                onProgress( 1, bytesProcessed );
    285336
    286337                log.trace "Finished parsing QUAL " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
     
    288339                return [ success: true, type: "qual", numSequences: numSequences, avgQuality: quality.quality ];
    289340        }
    290        
     341
    291342        /**
    292343         * Parses the given line and updates the average quality based on the scores
     
    298349                // Determine current average
    299350                List tokens = line.tokenize();
    300                 Long total = 0; 
    301                
     351                Long total = 0;
     352
    302353                tokens.each {
    303354                        total += Integer.parseInt( it );
    304355                }
    305                
     356
    306357                int numTokens = tokens.size();
    307                
     358
    308359                // Update the given average
    309360                if( numTokens > 0 ) {
     
    311362                        quality.quality = quality.quality + ( ( total / numTokens as double ) - quality.quality ) / quality.number * numTokens;
    312363                }
    313                
     364
    314365                return quality
    315366        }
    316        
     367
    317368        /**
    318369         * Moves a fasta and qual file to their permanent location, and returns information about these files
     
    326377                File permanentDirectory = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir );
    327378                def returnStructure = [:];
    328                
     379
    329380                if( fileService.fileExists( fastaFile ) ) {
    330381                        // Lookup the original filename
     
    340391                        throw new Exception( "Fasta file to save doesn't exist on disk" );
    341392                }
    342                
     393
    343394                if( qualFile && fileService.fileExists( qualFile ) ) {
    344395                        // Lookup the original filename
    345396                        def qualData = processedFiles.parsed.success.find { it.filename == qualFile };
    346                        
     397
    347398                        if( qualData ) {
    348399                                returnStructure.qual = fileService.moveFileToUploadDir( fileService.get(qualFile ), qualData.originalfilename, permanentDirectory );
     
    358409                        returnStructure.avgQuality = 0;
    359410                }
    360                
     411
    361412                return returnStructure;
    362413        }
  • trunk/grails-app/services/nl/tno/metagenomics/integration/GscfService.groovy

    r2 r3  
    2727         */
    2828        public String urlAuthRemote( def params, def token ) {
    29                 def redirectURL = "${config.gscf.baseURL}/login/auth_remote?moduleURL=${this.moduleURL()}&consumer=${this.consumerID()}&token=${token}&returnUrl=${config.metagenomics.baseURL}"
     29                def redirectURL = "${config.gscf.baseURL}/login/auth_remote?moduleURL=${this.moduleURL()}&consumer=${this.consumerID()}&token=${token}&returnUrl=${config.grails.serverURL}"
    3030
    3131                if (params.controller != null){
     
    330330         */
    331331        private String moduleURL() {
    332                 return config.metagenomics.baseURL
     332                return config.grails.serverURL
    333333        }
    334334
  • trunk/grails-app/services/nl/tno/metagenomics/integration/SynchronizationService.groovy

    r2 r3  
    77class SynchronizationService {
    88        def gscfService
    9        
     9
    1010        String sessionToken = ""        // Session token to use for communication
    1111        User user = null                        // Currently logged in user. Must be set when synchronizing authorization
    1212        boolean eager = false           // When set to true, this method fetches data about all studies from GSCF. Otherwise, it will only look at the
    13                                                                 // studies marked as dirty in the database. Defaults to false.
     13        // studies marked as dirty in the database. Defaults to false.
     14
     15        // Keeps track of the last time this module performed a full synchronization.
     16        static Date lastFullSynchronization = null;
    1417
    1518        static transactional = true
    16        
     19
    1720        /**
    1821         * Determines whether the synchronization should be performed or not. This can be entered
     
    2225        protected performSynchronization() {
    2326                def conf = ConfigurationHolder.config.metagenomics.synchronization;
    24                
     27
    2528                // If nothing is entered in configuration, return true (default value)
    2629                if( conf == null )
    2730                        return true
     31
     32                return conf
     33        }
     34       
     35        /**
     36         * Returns true iff a full synchronization should be performed
     37         * @return
     38         */
     39        public boolean timeForFullSynchronization() {
     40                if( SynchronizationService.lastFullSynchronization == null )
     41                        return true
    2842                       
    29                 return conf
     43                // Compute the time since the last full synchronization in  milliseconds
     44                Date today = new Date();
     45                long difference = SynchronizationService.lastFullSynchronization.getTime() - today.getTime()
     46               
     47                if( difference / 1000 > ConfigurationHolder.config.metagenomics.fullSynchronization )
     48                        return true
     49                else
     50                        return false
     51        }
     52
     53        /**
     54         * Redirects to a temporary page to give the user a 'waiting' page while synchronizing
     55         * @return
     56         */
     57        public String urlForFullSynchronization( def params ) {
     58                def returnUrl = ConfigurationHolder.config.grails.serverURL
     59                if (params.controller != null){
     60                        returnUrl += "/${params.controller}"
     61                        if (params.action != null){
     62                                returnUrl += "/${params.action}"
     63                                if (params.id != null){
     64                                        returnUrl += "/${params.id}"
     65                                }
     66                        }
     67                }
     68                if( timeForFullSynchronization() ) {
     69                        return ConfigurationHolder.config.grails.serverURL + "/synchronize/full?redirect=" + returnUrl.encodeAsURL()
     70                } else {
     71                        return returnUrl
     72                }
     73        }
     74
     75        /**
     76         * Performs a full synchronization in order to retrieve all studies
     77         * @return
     78         */
     79        public void fullSynchronization() {
     80                if( !timeForFullSynchronization() )
     81                        return
     82
     83                def previousEager = eager
     84                eager = true
     85                synchronizeStudies();
     86                eager = previousEager
     87               
     88                SynchronizationService.lastFullSynchronization = new Date();
    3089        }
    3190
     
    3796                if( !performSynchronization() )
    3897                        return Study.findAll()
    39                                
     98
    4099                // When eager fetching is enabled, ask for all studies, otherwise only ask for studies marked dirty
    41100                // Synchronization is performed on all studies, not only the studies the user has access to. Otherwise
     
    47106                        studies = Study.findAllWhere( [isDirty: true] );
    48107                }
    49                
     108
    50109                // Perform no synchronization if no studies have to be synchronized
    51110                // Perform synchronization on only one study directly, because otherwise
     
    57116                        println "Study: " + studies[ 0]
    58117                        def newStudy = synchronizeStudy( studies[0] );
    59                         if( newStudy ) 
     118                        if( newStudy )
    60119                                return [ newStudy ];
    61                         else 
     120                        else
    62121                                return []
    63122                }
    64                
     123
    65124                // Fetch all studies from GSCF
    66125                def newStudies
     
    68127                        if( !eager ) {
    69128                                def studyTokens = studies.studyToken;
    70                                
     129
    71130                                if( studyTokens instanceof String ) {
    72131                                        studyTokens = [studyTokens];
     
    86145                synchronizeStudies( newStudies );
    87146                studies = handleDeletedStudies( studies, newStudies );
    88                
     147
    89148                log.trace( "Returning " + studies.size() + " studies after synchronization" )
    90                
     149
    91150                return studies
    92151        }
    93        
     152
    94153        /**
    95154         * Synchronizes all studies given by 'newStudies' with existing studies in the database, and adds them
     
    107166
    108167                                Study studyFound = Study.findByStudyToken( gscfStudy.studyToken as String )
    109                                
     168
    110169                                if(studyFound) {
    111170                                        log.trace( "Study found with name " + studyFound.name )
    112                                        
     171
    113172                                        // Synchronize the study itself with the data retrieved
    114173                                        synchronizeStudy( studyFound, gscfStudy );
     
    123182                                        synchronizeAuthorization(studyFound);
    124183                                        synchronizeStudyAssays(studyFound);
    125                                        
     184
    126185                                        // Mark the study as clean
    127186                                        studyFound.isDirty = false
     
    131190                }
    132191        }
    133        
     192
    134193        /**
    135194         * Removes studies from the database that are expected but not found in the list from GSCF
     
    151210                                log.trace( "Study " + existingStudy.studyToken + " not found. Check whether it is removed or the user just can't see it." )
    152211
    153                                 // Study was not given to us by GSCF. This might be because the study is removed, or because the study is not visible (anymore) 
     212                                // Study was not given to us by GSCF. This might be because the study is removed, or because the study is not visible (anymore)
    154213                                // to the current user.
    155214                                // Synchronize authorization and see what is the case (it returns null if the study has been deleted)
     
    160219                        }
    161220                }
    162                
     221
    163222                return studies
    164223        }
    165        
     224
    166225        /**
    167226         * Synchronizes the given study with the data from GSCF
     
    172231                if( !performSynchronization() )
    173232                        return study
    174                        
     233
    175234                if( study == null )
    176235                        return null
     
    179238                if( !eager && !study.isDirty )
    180239                        return study;
    181                        
     240
    182241                // Retrieve the study from GSCF
    183242                def newStudy
     
    218277                if( !performSynchronization() )
    219278                        return study
    220                        
     279
    221280                if( study == null || newStudy == null)
    222281                        return null
     
    224283                // If the study hasn't changed, don't update anything
    225284                if( !eager && !study.isDirty ) {
    226                         return study;                   
     285                        return study;
    227286                }
    228287
     
    231290                        return null;
    232291                }
    233                
     292
    234293                // Mark study dirty to enable synchronization
    235294                study.isDirty = true;
    236295                synchronizeAuthorization( study );
    237296                synchronizeStudyAssays( study );
    238                
     297
    239298                // Update properties and mark as clean
    240299                study.name = newStudy.title
     
    254313                        return study.assays.toList()
    255314
    256                 if( !eager && !study.isDirty ) 
    257                         return study.assays as List     
    258                
     315                if( !eager && !study.isDirty )
     316                        return study.assays as List
     317
    259318                // Also update all assays, belonging to this study
    260319                // Retrieve the assays from GSCF
     
    287346                return handleDeletedAssays( study, newAssays );
    288347        }
    289        
     348
    290349        /**
    291350         * Synchronizes the assays of a study with the given data from GSCF
     
    323382                }
    324383        }
    325        
     384
    326385        /**
    327386         * Removes assays from the system that have been deleted from GSCF
     
    354413                        }
    355414                }
    356                
     415
    357416                return study.assays.toList()
    358417        }
    359        
     418
    360419        /**
    361420         * Retrieves the authorization for the currently logged in user
     
    374433                        throw new Exception( "Property user of SynchronizationService must be set to the currently logged in user" );
    375434                }
    376                
     435
    377436                // Only perform synchronization if needed
    378437                if( !eager && !study.isDirty )
    379438                        return Auth.findByUserAndStudy( user, study )
    380                        
     439
    381440                if( !performSynchronization() )
    382441                        return Auth.findByUserAndStudy( user, study )
    383                
     442
    384443                def gscfAuthorization
    385444                try {
     
    389448                        // TODO: handle deletion
    390449                        log.trace( "Study " + study.studyToken + " has been deleted. Remove all authorization on that study")
    391                        
     450
    392451                        Auth.findAllByStudy( study ).each {
    393452                                it.delete()
    394453                        }
    395                        
    396                         return null
    397                 }
    398                
     454
     455                        return null
     456                }
     457
    399458                // Update the authorization object, or create a new one
    400459                Auth a = Auth.authorization( study, user )
    401                
     460
    402461                if( !a ) {
    403462                        log.trace( "Authorization not found for " + study.studyToken + " and " + user.username + ". Creating a new object" );
    404                        
     463
    405464                        a = Auth.createAuth( study, user );
    406465                }
    407                
     466
    408467                // Copy properties from gscf object
    409468                a.canRead = gscfAuthorization.canRead
    410469                a.canWrite = gscfAuthorization.canWrite
    411470                a.isOwner = gscfAuthorization.isOwner
    412                
     471
    413472                a.save()
    414                
     473
    415474                return a
    416475        }
     
    431490                if( !eager && !assay.study.isDirty )
    432491                        return assay
    433                        
     492
    434493                // Retrieve the assay from GSCF
    435494                def newAssay
     
    477536                if( !eager && !assay.study.isDirty )
    478537                        return assay
    479                        
     538
    480539                // If new assay is empty, something went wrong
    481540                if( newAssay.size() == 0 ) {
     
    535594                }
    536595
    537                
     596
    538597                synchronizeAssaySamples( assay, newSamples );
    539598                return handleDeletedSamples( assay, newSamples );
    540599        }
    541        
     600
    542601        /**
    543602         * Synchronize all samples for a given assay with the data from GSCF
     
    597656                }
    598657        }
    599        
     658
    600659        /**
    601660         * Removes samples from the system that have been removed from an assay in GSCF
     
    612671                        for( int i = numAssaySamples - 1; i >= 0; i-- ) {
    613672                                def existingSample = assaySamples[i];
    614        
     673
    615674                                AssaySample sampleFound = newSamples.find { it.name == existingSample.sample.sampleToken }
    616        
     675
    617676                                if( !sampleFound ) {
    618677                                        log.trace( "Sample " + existingSample.sample.sampleToken + " not found. Removing it." )
    619        
     678
    620679                                        // The sample has been removed
    621680                                        // TODO: What to do with the data associated with this AssaySample (e.g. sequences)? See also GSCF ticket #255
Note: See TracChangeset for help on using the changeset viewer.