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

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

Initial import of basic functionality

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