source: trunk/grails-app/services/nl/tno/metagenomics/FastaService.groovy @ 5

Last change on this file since 5 was 5, checked in by robert@…, 9 years ago

Implemented uploading and parsing of zip files

File size: 13.7 KB
Line 
1package nl.tno.metagenomics
2
3import java.io.File;
4import java.util.ArrayList;
5import org.codehaus.groovy.grails.commons.ConfigurationHolder
6
7class FastaService {
8        def fileService
9        def fuzzySearchService
10
11        static transactional = true
12
13        /**
14         * Parses uploaded files and checks them for FASTA and QUAL files
15         * @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 4 parameters: numFilesProcessed, numBytesProcessed that indicate the number
18         *                                              of files and bytes that have been processed in total. totalFiles, totalBytes indicate the change
19         *                                              in total number of files and bytes (e.g. should take 1 if a new file is added to the list)
20         * @param directory             Directory to move the files to
21         * @return                              Structure with information about the parsed files. The 'success' files are
22         *                                              moved to the given directory
23         *
24         * [
25         *              success: [
26         *                      [filename: 'abc.fasta', type: FASTA, numSequences: 190]
27         *                      [filename: 'cde.fasta', type: FASTA, numSequences: 140]
28         *                      [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
29         *                      [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
30         *              ],
31         *              failure: [
32         *                      [filename: 'testing.xls', type: 'unknown', message: 'Type not recognized']
33         *              ]
34         * ]
35         *
36         */
37        def parseFiles( ArrayList filenames, Closure onProgress, File directory = null ) {
38                if( filenames.size() == 0 ) {
39                        return [ success: [], failure: [] ];
40                }
41
42                if( !directory ) {
43                        directory = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir )
44                }
45
46                def success = [];
47                def failure = [];
48
49                long filesProcessed = 0;
50                long bytesProcessed = 0;
51
52                // Loop through all filenames
53                for( int i = 0; i < filenames.size(); i++ ) {
54                        def filename = filenames[ i ];
55                       
56                        if( fileService.isZipFile( filename ) ) {
57                                // ZIP files are extracted and appended to the filenames list.
58                                def newfiles = fileService.extractZipFile( filename, { files, bytes, totalFiles, totalBytes ->
59                                                        filesProcessed += files;
60                                                        bytesProcessed += bytes;
61       
62                                                        onProgress( filesProcessed, bytesProcessed, totalFiles, totalBytes );
63                                } );
64                                if( newfiles ) {
65                                        newfiles.each {
66                                                filenames.add( it );
67                                        }
68                                }
69                        } else {
70                                def file = fileService.get( filename );
71                                String filetype = fileService.determineFileType( file );
72       
73                                if( !fileTypeValid( filetype ) ) {
74                                        // If files are not valid for parsing, delete them and return a message to the user
75                                        fileService.delete(filename);
76                                        failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: 'File type not accepted' ];
77                                } else {
78                                        try {
79                                                def contents = parseFile( file, filetype, { files, bytes ->
80                                                        filesProcessed += files;
81                                                        bytesProcessed += bytes;
82       
83                                                        onProgress( filesProcessed, bytesProcessed, 0, 0 );
84                                                } );
85       
86                                                contents.filename = file.getName();
87                                                contents.originalfilename = fileService.originalFilename( contents.filename )
88       
89                                                if( contents.success ) {
90                                                        success << contents;
91                                                } else {
92                                                        fileService.delete(filename);
93                                                        failure << contents;
94                                                }
95                                        } catch( Exception e ) {
96                                                // If anything fails during parsing, return an error message
97                                                fileService.delete(filename);
98                                                failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: e.getMessage() ];
99                                        }
100                                }
101                        }
102                }
103
104                return [ success: success, failure: failure ];
105        }
106
107        /**
108         * Matches uploaded fasta and qual files and combines them with the samples they probably belong to
109         * @param parsedFiles   Parsed files
110         * [
111         *              [filename: 'abc.fasta', type: FASTA, numSequences: 190]
112         *              [filename: 'cde.fasta', type: FASTA, numSequences: 140]
113         *              [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
114         *              [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
115         * ]
116         * @param samples               AssaySample objects to which the files should be matched.
117         * @return                              Structure with information about the matching.
118         * [
119         *              [
120         *                      fasta:  [filename: 'abc.fasta', type: FASTA, numSequences: 190],
121         *                      qual:   [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
122         *                      feasibleQuals: [
123         *                              [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
124         *                              [filename: 'def.qual', type: QUAL, numSequences: 190, avgQuality: 21]
125         *                      ]
126         *                      sample: AssaySample object     
127         */
128        def matchFiles( def parsedFiles, def samples ) {
129                def fastas = parsedFiles.findAll { it.type == "fasta" }
130                def quals = parsedFiles.findAll { it.type == "qual" }
131                samples = samples.toList()
132
133                def files = [];
134
135                fastas.each { fastaFile ->
136                        // Remove extension
137                        def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.originalfilename.lastIndexOf( '.' ) )
138
139                        // Determine feasible quals (based on number of sequences )
140                        def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences }
141
142                        // Best matching qual file
143                        def qualIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.qual', feasibleQuals.originalfilename );
144
145                        def qual = null
146                        if( qualIdx != null )
147                                qual = feasibleQuals[ qualIdx ];
148
149                        // Best matching sample
150                        // TODO: Implement method to search for sample matches in a provided excel sheet
151                        def sampleIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, samples.sample.name );
152                        def assaySample = null
153                        if( sampleIdx != null ) {
154                                assaySample = samples[ sampleIdx ];
155                        }
156
157                        files << [
158                                                fasta: fastaFile,
159                                                feasibleQuals: feasibleQuals,
160                                                qual: qual,
161                                                assaySample: assaySample
162                                        ]
163                }
164
165                return files;
166
167
168        }
169
170        /**
171         * Determines whether a file can be processed.
172         * @param filetype      Filetype of the file
173         * @see determineFileType()
174         * @return
175         */
176        protected boolean fileTypeValid( String filetype ) {
177                switch( filetype ) {
178                        case "fasta":
179                        case "qual":
180                                return true;
181                        default:
182                                return false;
183                }
184        }
185
186        /**
187         * Parses the given file
188         * @param file                  File to parse
189         * @param filetype              Type of the given file
190         * @param onProgress    Closure to execute when progress indicators should be updated.
191         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
192         *                                              of files and bytes that have been processed in this file (so the first parameter should
193         *                                              only be 1 when the file is finished)
194         *
195         * @return                              List structure. Examples:
196         *
197         *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
198         *   [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 36 ]
199         *   [ success: false, filename: 'abc.txt', type: 'txt', message: 'Filetype could not be parsed.' ]
200         */
201        protected def parseFile( File file, String filetype, Closure onProgress ) {
202                switch( filetype ) {
203                        case "fasta":
204                                return parseFasta( file, onProgress );
205                        case "qual":
206                                return parseQual( file, onProgress );
207                        default:
208                                onProgress( 1, file.length() );
209                                return [ success: false, type: filetype, message: 'Filetype could not be parsed.' ]
210                }
211        }
212
213        /**
214         * Parses the given FASTA file
215         * @param file                  File to parse
216         * @param onProgress    Closure to execute when progress indicators should be updated.
217         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
218         *                                              of files and bytes that have been processed in this file (so the first parameter should
219         *                                              only be 1 when the file is finished)
220         * @return                              List structure. Examples:
221         *
222         *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
223         *   [ success: false, filename: 'def.fasta', type: 'fasta', message: 'File is not a valid FASTA file' ]
224         */
225        protected def parseFasta( File file, Closure onProgress ) {
226
227                long startTime = System.nanoTime();
228                log.trace "Start parsing FASTA " + file.getName()
229
230                // Count the number of lines, starting with '>' (and where the following line contains a character other than '>')
231                long numSequences = 0;
232                long bytesProcessed = 0;
233                boolean lookingForSequence = false;
234
235                file.eachLine { line ->
236                        if( line ) {
237                                if( !lookingForSequence && line[0] == '>' ) {
238                                        lookingForSequence = true;
239                                } else if( lookingForSequence ) {
240                                        if( line[0] != '>' ) {
241                                                numSequences++;
242                                                lookingForSequence = false;
243                                        }
244                                }
245
246
247                                // Update progress every time 1MB is processed
248                                bytesProcessed += line.size();
249                                if( bytesProcessed > 1000000 ) {
250                                        onProgress( 0, bytesProcessed );
251                                        bytesProcessed = 0;
252                                }
253                        }
254
255
256
257                }
258
259                // Update progress and say we're finished
260                onProgress( 1, bytesProcessed );
261
262                log.trace "Finished parsing FASTA " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
263
264                return [ success: true, type: "fasta", numSequences: numSequences ];
265        }
266
267        /**
268         * Parses the given QUAL file
269         * @param file                  File to parse
270         * @param onProgress    Closure to execute when progress indicators should be updated. 
271         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
272         *                                              of files and bytes that have been processed in this file (so the first parameter should
273         *                                              only be 1 when the file is finished)
274         * @return                              List structure. Examples:
275         *
276         *   [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 31 ]
277         *   [ success: false, filename: 'def.qual', type: 'qual', message: 'File is not a valid QUAL file' ]
278         */
279        protected def parseQual( File file, Closure onProgress ) {
280                long startTime = System.nanoTime();
281                log.trace "Start parsing QUAL " + file.getName()
282
283                // Count the number of lines, starting with '>'. After we've found such a character, we continue looking for
284                // quality scores
285                long numSequences = 0;
286                long bytesProcessed = 0;
287                def quality = [ quality: 0.0, number: 0L ]
288
289                boolean lookingForFirstQualityScores = false;
290                file.eachLine { line ->
291                        if( line ) {
292                                if( !lookingForFirstQualityScores && line[0] == '>' ) {
293                                        lookingForFirstQualityScores = true;
294                                } else if( lookingForFirstQualityScores ) {
295                                        if( line[0] != '>' ) {
296                                                numSequences++;
297                                                lookingForFirstQualityScores = false;
298
299                                                // Don't compute average quality because it takes too much time
300                                                //quality = updateQuality( quality, line );
301                                        }
302                                } else {
303                                        // Don't compute average quality because it takes too much time
304                                        //quality = updateQuality( quality, line );
305                                }
306
307                                // Update progress every time 1MB is processed
308                                bytesProcessed += line.size();
309                                if( bytesProcessed > 1000000 ) {
310                                        onProgress( 0, bytesProcessed );
311                                        bytesProcessed = 0;
312                                }
313                        }
314
315                }
316
317                // Update progress and say we're finished
318                onProgress( 1, bytesProcessed );
319
320                log.trace "Finished parsing QUAL " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
321
322                return [ success: true, type: "qual", numSequences: numSequences, avgQuality: quality.quality ];
323        }
324
325        /**
326         * Parses the given line and updates the average quality based on the scores
327         * @param quality       [quality: 0.0f, number: 0L]
328         * @param line          String of integer quality scores, separated by a whitespace
329         * @return                      [quality: 0.0f, number: 0L]
330         */
331        protected def updateQuality( def quality, String line ) {
332                // Determine current average
333                List tokens = line.tokenize();
334                Long total = 0;
335
336                tokens.each {
337                        total += Integer.parseInt( it );
338                }
339
340                int numTokens = tokens.size();
341
342                // Update the given average
343                if( numTokens > 0 ) {
344                        quality.number += numTokens;
345                        quality.quality = quality.quality + ( ( total / numTokens as double ) - quality.quality ) / quality.number * numTokens;
346                }
347
348                return quality
349        }
350
351        /**
352         * Moves a fasta and qual file to their permanent location, and returns information about these files
353         *
354         * @param fastaFile                     Filename of the fasta file in the temporary directory
355         * @param qualFile                      Filename of the fasta file in the temporary directory
356         * @param processedFiles        Structure with data about uploaded files and matches
357         * @return      [ fasta: <fasta filename>, qual: <qual filename>, numSequences: <number of sequences>, avgQuality: <average quality> ]
358         */
359        public def savePermanent( String fastaFile, String qualFile, def processedFiles ) {
360                File permanentDirectory = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir );
361                def returnStructure = [:];
362
363                if( fileService.fileExists( fastaFile ) ) {
364                        // Lookup the original filename
365                        def fastaData = processedFiles.parsed.success.find { it.filename == fastaFile };
366                        if( fastaData ) {
367                                returnStructure.fasta = fileService.moveFileToUploadDir( fileService.get( fastaFile ), fastaData.originalfilename, permanentDirectory );
368                                returnStructure.numSequences = fastaData.numSequences;
369                        } else {
370                                throw new Exception( "Fasta file wasn't uploaded the right way. Maybe the session has expired/" );
371                        }
372                } else {
373                        // If the file doesn't exist, we can't save anything to the database.
374                        throw new Exception( "Fasta file to save doesn't exist on disk" );
375                }
376
377                if( qualFile && fileService.fileExists( qualFile ) ) {
378                        // Lookup the original filename
379                        def qualData = processedFiles.parsed.success.find { it.filename == qualFile };
380
381                        if( qualData ) {
382                                returnStructure.qual = fileService.moveFileToUploadDir( fileService.get(qualFile ), qualData.originalfilename, permanentDirectory );
383                                returnStructure.avgQuality = qualData.avgQuality
384                        } else {
385                                // Delete the uploaded fasta file, since this is a serious error
386                                fileService.delete( returnStructure.fasta, permanentDirectory );
387                                throw new Exception( "Qual file wasn't uploaded the right way. Maybe the session has expired" );
388                        }
389                } else {
390                        // If the file doesn't exist, we don't save any quality information
391                        returnStructure.qual = null
392                        returnStructure.avgQuality = 0;
393                }
394
395                return returnStructure;
396        }
397
398
399
400}
Note: See TracBrowser for help on using the repository browser.