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

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

Implemented trash in order to prevent deletion of data

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