source: trunk/grails-app/services/nl/tno/massSequencing/FastaService.groovy @ 44

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

Removed mass sample editing (to prevent the edit tags screen opening very slowly). Also added the possibility to add an excel file which matches sequence files to samples (when uploading) (#13). Finally added some 'return false' to onClick events, when dialogs were opened, to prevent the browser from scrolling to the top.

File size: 30.6 KB
Line 
1package nl.tno.massSequencing
2
3import java.io.BufferedWriter;
4import java.io.File;
5import java.io.Writer;
6import java.util.ArrayList;
7import java.util.zip.ZipEntry
8import java.util.zip.ZipOutputStream
9import org.codehaus.groovy.grails.commons.ConfigurationHolder
10
11class FastaService {
12        def fileService
13        def fuzzySearchService
14        def sampleExcelService
15        def excelService
16
17        static transactional = true
18
19        /**
20         * Parses uploaded files and checks them for FASTA and QUAL files
21         * @param filenames             List of filenames currently existing in the upload directory
22         * @param onProgress    Closure to execute when progress indicators should be updated.
23         *                                              Has 4 parameters: numFilesProcessed, numBytesProcessed that indicate the number
24         *                                              of files and bytes that have been processed in total. totalFiles, totalBytes indicate the change
25         *                                              in total number of files and bytes (e.g. should take 1 if a new file is added to the list)
26         * @param directory             Directory to move the files to
27         * @return                              Structure with information about the parsed files. The 'success' files are
28         *                                              moved to the given directory
29         *
30         * [
31         *              success: [
32         *                      [filename: 'abc.fasta', type: FASTA, numSequences: 190]
33         *                      [filename: 'cde.fasta', type: FASTA, numSequences: 140]
34         *                      [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
35         *                      [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
36         *              ],
37         *              failure: [
38         *                      [filename: 'testing.xls', type: 'unknown', message: 'Type not recognized']
39         *              ]
40         * ]
41         *
42         */
43        def parseFiles( ArrayList filenames, Closure onProgress, File directory = null ) {
44                if( filenames.size() == 0 ) {
45                        return [ success: [], failure: [] ];
46                }
47
48                if( !directory ) {
49                        directory = fileService.absolutePath( ConfigurationHolder.config.massSequencing.fileDir )
50                }
51
52                def success = [];
53                def failure = [];
54
55                long filesProcessed = 0;
56                long bytesProcessed = 0;
57
58                // Loop through all filenames
59                for( int i = 0; i < filenames.size(); i++ ) {
60                        def filename = filenames[ i ];
61
62                        if( fileService.isZipFile( filename ) ) {
63                                // ZIP files are extracted and appended to the filenames list.
64                                def newfiles = fileService.extractZipFile( filename, { files, bytes, totalFiles, totalBytes ->
65                                        filesProcessed += files;
66                                        bytesProcessed += bytes;
67
68                                        onProgress( filesProcessed, bytesProcessed, totalFiles, totalBytes );
69                                } );
70                                if( newfiles ) {
71                                        newfiles.each {
72                                                filenames.add( it );
73                                        }
74                                }
75                        } else {
76                                def file = fileService.get( filename );
77                                String filetype = fileService.determineFileType( file );
78
79                                if( !fileTypeValid( filetype ) ) {
80                                        // If files are not valid for parsing, delete them and return a message to the user
81                                        fileService.delete(filename);
82                                        failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: 'File type not accepted' ];
83                                } else {
84                                        try {
85                                                def contents = parseFile( file, filetype, { files, bytes ->
86                                                        filesProcessed += files;
87                                                        bytesProcessed += bytes;
88
89                                                        onProgress( filesProcessed, bytesProcessed, 0, 0 );
90                                                } );
91
92                                                contents.filename = file.getName();
93                                                contents.originalfilename = fileService.originalFilename( contents.filename )
94
95                                                if( contents.success ) {
96                                                        success << contents;
97                                                } else {
98                                                        fileService.delete(filename);
99                                                        failure << contents;
100                                                }
101                                        } catch( Exception e ) {
102                                                // If anything fails during parsing, return an error message
103                                                fileService.delete(filename);
104                                                failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: e.getMessage() ];
105                                        }
106                                }
107                        }
108                }
109
110                return [ success: success, failure: failure ];
111        }
112
113        /**
114         * Matches uploaded fasta and qual files and combines them with the samples they probably belong to
115         * @param parsedFiles   Parsed files
116         * [
117         *              [filename: 'abc.fasta', type: FASTA, numSequences: 190]
118         *              [filename: 'cde.fasta', type: FASTA, numSequences: 140]
119         *              [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
120         *              [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
121         *              [filename: 'match.xls', type: EXCEL, matches: [ [ filename: 'abc.fasta', basename: 'abc', sample: 's1' ] ]
122         * ]
123         * @param samples               AssaySample objects to which the files should be matched.
124         * @return                              Structure with information about the matching.
125         * [
126         *              [
127         *                      fasta:  [filename: 'abc.fasta', type: FASTA, numSequences: 190],
128         *                      qual:   [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
129         *                      feasibleQuals: [
130         *                              [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
131         *                              [filename: 'def.qual', type: QUAL, numSequences: 190, avgQuality: 21]
132         *                      ]
133         *                      sample: AssaySample object     
134         */
135        def matchFiles( def parsedFiles, def samples ) {
136                def fastas = parsedFiles.findAll { it.type == "fasta" }
137                def quals = parsedFiles.findAll { it.type == "qual" }
138                def excels = parsedFiles.findAll { it.type == "excel" }
139               
140                samples = samples.toList()
141               
142                // Collect matches from all files
143                def matches = [];
144                excels.each { m ->
145                        if( m.matches ) {
146                                m.matches.each { matches << it }
147                        }
148                }
149
150                def files = [];
151
152                fastas.each { fastaFile ->
153                        // Remove extension
154                        def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.originalfilename.lastIndexOf( '.' ) )
155
156                        // Determine feasible quals (based on number of sequences )
157                        def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences }
158
159                        // Best matching qual file
160                        def qualIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.qual', feasibleQuals.originalfilename );
161
162                        def qual = null
163                        if( qualIdx != null )
164                                qual = feasibleQuals[ qualIdx ];
165
166                        // Best matching sample
167                        def assaySample = null
168                        if( matches ) {
169                                // Match with files from excelsheet
170                               
171                                // First find the best matching filename in the list of matches.
172                                def sampleNameIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, matches*.basename );
173                               
174                                // If one is found, use the sample name associated with it to do the matching with samples
175                                if( sampleNameIdx != null ) {
176                                        matchWith = matches[ sampleNameIdx ].sample;
177                                }
178                        }
179                       
180                        // Match on filenames
181                        def sampleIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, samples.sample.name );
182                        if( sampleIdx != null ) {
183                                assaySample = samples[ sampleIdx ];
184                        }
185
186                        files << [
187                                                fasta: fastaFile,
188                                                feasibleQuals: feasibleQuals,
189                                                qual: qual,
190                                                assaySample: assaySample
191                                        ]
192                }
193
194                return files;
195
196
197        }
198
199        /**
200         * Determines whether a file can be processed.
201         * @param filetype      Filetype of the file
202         * @see determineFileType()
203         * @return
204         */
205        protected boolean fileTypeValid( String filetype ) {
206                switch( filetype ) {
207                        case "fasta":
208                        case "qual":
209                        case "excel":
210                                return true;
211                        default:
212                                return false;
213                }
214        }
215
216        /**
217         * Parses the given file
218         * @param file                  File to parse
219         * @param filetype              Type of the given file
220         * @param onProgress    Closure to execute when progress indicators should be updated.
221         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
222         *                                              of files and bytes that have been processed in this file (so the first parameter should
223         *                                              only be 1 when the file is finished)
224         *
225         * @return                              List structure. Examples:
226         *
227         *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
228         *   [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 36 ]
229         *   [ success: false, filename: 'abc.txt', type: 'txt', message: 'Filetype could not be parsed.' ]
230         */
231        protected def parseFile( File file, String filetype, Closure onProgress ) {
232                switch( filetype ) {
233                        case "fasta":
234                                return parseFasta( file, onProgress );
235                        case "qual":
236                                return parseQual( file, onProgress );
237                        case "excel":
238                                return parseExcelMatch( file, onProgress );
239                        default:
240                                onProgress( 1, file.length() );
241                                return [ success: false, type: filetype, message: 'Filetype could not be parsed.' ]
242                }
243        }
244
245        /**
246         * Parses the given FASTA file
247         * @param file                  File to parse
248         * @param onProgress    Closure to execute when progress indicators should be updated.
249         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
250         *                                              of files and bytes that have been processed in this file (so the first parameter should
251         *                                              only be 1 when the file is finished)
252         * @return                              List structure. Examples:
253         *
254         *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
255         *   [ success: false, filename: 'def.fasta', type: 'fasta', message: 'File is not a valid FASTA file' ]
256         */
257        protected def parseFasta( File file, Closure onProgress ) {
258
259                long startTime = System.nanoTime();
260                log.trace "Start parsing FASTA " + file.getName()
261
262                // Count the number of lines, starting with '>' (and where the following line contains a character other than '>')
263                long numSequences = 0;
264                long bytesProcessed = 0;
265                boolean lookingForSequence = false;
266
267                file.eachLine { line ->
268                        if( line ) {
269                                if( !lookingForSequence && line[0] == '>' ) {
270                                        lookingForSequence = true;
271                                } else if( lookingForSequence ) {
272                                        if( line[0] != '>' ) {
273                                                numSequences++;
274                                                lookingForSequence = false;
275                                        }
276                                }
277
278
279                                // Update progress every time 1MB is processed
280                                bytesProcessed += line.size();
281                                if( bytesProcessed > 1000000 ) {
282                                        onProgress( 0, bytesProcessed );
283                                        bytesProcessed = 0;
284                                }
285                        }
286
287                }
288
289                // Update progress and say we're finished
290                onProgress( 1, bytesProcessed );
291
292                log.trace "Finished parsing FASTA " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
293
294                return [ success: true, type: "fasta", filename: file.getName(), numSequences: numSequences ];
295        }
296
297        /**
298         * Parses the given QUAL file
299         * @param file                  File to parse
300         * @param onProgress    Closure to execute when progress indicators should be updated. 
301         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
302         *                                              of files and bytes that have been processed in this file (so the first parameter should
303         *                                              only be 1 when the file is finished)
304         * @return                              List structure. Examples:
305         *
306         *   [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 31 ]
307         *   [ success: false, filename: 'def.qual', type: 'qual', message: 'File is not a valid QUAL file' ]
308         */
309        protected def parseQual( File file, Closure onProgress ) {
310                long startTime = System.nanoTime();
311                log.trace "Start parsing QUAL " + file.getName()
312
313                // Count the number of lines, starting with '>'. After we've found such a character, we continue looking for
314                // quality scores
315                long numSequences = 0;
316                long bytesProcessed = 0;
317                def quality = [ quality: 0.0, number: 0L ]
318
319                boolean lookingForFirstQualityScores = false;
320                file.eachLine { line ->
321                        if( line ) {
322                                if( !lookingForFirstQualityScores && line[0] == '>' ) {
323                                        lookingForFirstQualityScores = true;
324                                } else if( lookingForFirstQualityScores ) {
325                                        if( line[0] != '>' ) {
326                                                numSequences++;
327                                                lookingForFirstQualityScores = false;
328
329                                                // Don't compute average quality because it takes too much time
330                                                //quality = updateQuality( quality, line );
331                                        }
332                                } else {
333                                        // Don't compute average quality because it takes too much time
334                                        //quality = updateQuality( quality, line );
335                                }
336
337                                // Update progress every time 1MB is processed
338                                bytesProcessed += line.size();
339                                if( bytesProcessed > 1000000 ) {
340                                        onProgress( 0, bytesProcessed );
341                                        bytesProcessed = 0;
342                                }
343                        }
344
345                }
346
347                // Update progress and say we're finished
348                onProgress( 1, bytesProcessed );
349
350                log.trace "Finished parsing QUAL " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
351
352                return [ success: true, type: "qual", filename: file.getName(), numSequences: numSequences, avgQuality: quality.quality ];
353        }
354
355        /**
356         * Parses a given excel file with a match between filenames and samples
357         * @param file                  File to parse
358         * @param onProgress    Closure to execute when progress indicators should be updated.
359         *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
360         *                                              of files and bytes that have been processed in this file (so the first parameter should
361         *                                              only be 1 when the file is finished)
362         * @return                              List structure. The matches array contains an array of matches between filenames and sample(name)s.
363         *                                              The extension for all files are removed in the 'basename' parameter, in order to improve matching.
364         *                                              Examples:
365         *
366         *   [ success: true, filename: 'abc.xls', type: 'excel', matches: [ [ filename: 's1.qual', basename: 's1', sample: 'sample a' ], [ filename: 's9.fna', basename: 's9', sample: 'sample b' ] ]
367         *   [ success: false, filename: 'def.xls', type: 'excel', message: 'File is not a valid XLS file' ]
368         */
369        protected def parseExcelMatch( File file, Closure onProgress ) {
370                long startTime = System.nanoTime();
371                log.trace "Start parsing XLS " + file.getName()
372
373                def matches = []
374
375                // Read excel file
376                def wb;
377                try {
378                        wb = excelService.open( file );
379                } catch( Exception e ) {
380                        // If an exception occurs, the file can't be opened. Return the error message
381                        return [ success: false, type: "excel", filename: file.getName(), message: "Excel file could not be opened or parsed."]
382                }
383
384                // Read all data into an array, and the header in a separate array
385                def header = excelService.readRow( wb, 0, 0 );
386                def data = excelService.readData( wb, 0, 1 );
387
388                // Check whether (at least) 2 columns are present
389                if( header.size() < 2 ) {
390                        return [ success: false, type: "excel", filename: file.getName(), message: "Excel file must contain at least 2 columns, one with filenames and one with sample names"]
391                }
392               
393                // Check the headers to see whether the default columns are switched
394                def filenameColumn = 0;
395                def sampleColumn = 1;
396               
397                header.eachWithIndex { cell, i ->
398                        switch( cell?.toLowerCase() ) {
399                                case "sample":
400                                        sampleColumn = i; break;
401                                case "file":
402                                case "filename":
403                                        filenameColumn = i; break;
404                        }
405                }
406               
407                // If both filenames and samples are found in the same column (happens if only one of the headers is given)
408                // an error is given
409                if( filenameColumn == sampleColumn )
410                        return [ success: false, type: "excel", filename: file.getName(), message: "Excel file must contain 'Sample' and 'Filename' headers."]
411               
412                // Loop through the data and create a match
413                def maxColumn = Math.max( filenameColumn, sampleColumn );
414                data.each { row ->
415                        if( row.size() >= maxColumn ) {
416                                def filename = row[ filenameColumn ];
417                                def sample = row[ sampleColumn ];
418                               
419                                if( sample && sample != "null"  && filename && filename != "null" ) {
420                                        // Remove extension from the filename, but only if it is
421                                        // .fna, .fasta, .qual, .fqa, .xls or .xlsx. Otherwise useful parts of the filename would be removed.
422                                        def basename = ( filename =~ /\.(fna|fasta|qual|fqa|xls|xlsx)$/ ).replaceAll( "" );
423                                       
424                                        matches << [ filename: filename, sample: sample, basename: basename ];
425                                }
426                        }
427                }
428
429                // Update progress and say we're finished
430                onProgress( 1, file.size() );
431
432                log.trace "Finished parsing XLS " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
433
434                return [ success: true, type: "excel", filename: file.getName(), matches: matches ];
435        }
436
437        /**
438         * Parses the given line and updates the average quality based on the scores
439         * @param quality       [quality: 0.0f, number: 0L]
440         * @param line          String of integer quality scores, separated by a whitespace
441         * @return                      [quality: 0.0f, number: 0L]
442         */
443        protected def updateQuality( def quality, String line ) {
444                // Determine current average
445                List tokens = line.tokenize();
446                Long total = 0;
447
448                tokens.each {
449                        total += Integer.parseInt( it );
450                }
451
452                int numTokens = tokens.size();
453
454                // Update the given average
455                if( numTokens > 0 ) {
456                        quality.number += numTokens;
457                        quality.quality = quality.quality + ( ( total / numTokens as double ) - quality.quality ) / quality.number * numTokens;
458                }
459
460                return quality
461        }
462
463        /**
464         * Moves a fasta and qual file to their permanent location, and returns information about these files
465         * 
466         * @param fastaFile                     Filename of the fasta file in the temporary directory
467         * @param qualFile                      Filename of the fasta file in the temporary directory
468         * @param processedFiles        Structure with data about uploaded files and matches
469         * @return      [ fasta: <fasta filename>, qual: <qual filename>, numSequences: <number of sequences>, avgQuality: <average quality> ]
470         */
471        public def savePermanent( String fastaFile, String qualFile, def processedFiles ) {
472                File permanentDirectory = fileService.absolutePath( ConfigurationHolder.config.massSequencing.fileDir );
473                def returnStructure = [:];
474
475                if( fileService.fileExists( fastaFile ) ) {
476                        // Lookup the original filename
477                        def fastaData = processedFiles.parsed.success.find { it.filename == fastaFile };
478                        if( fastaData ) {
479                                returnStructure.fasta = fileService.moveFileToUploadDir( fileService.get( fastaFile ), fastaData.originalfilename, permanentDirectory );
480                                returnStructure.numSequences = fastaData.numSequences;
481                        } else {
482                                throw new Exception( "Fasta file wasn't uploaded the right way. Maybe the session has expired/" );
483                        }
484                } else {
485                        // If the file doesn't exist, we can't save anything to the database.
486                        throw new Exception( "Fasta file to save doesn't exist on disk" );
487                }
488
489                if( qualFile && fileService.fileExists( qualFile ) ) {
490                        // Lookup the original filename
491                        def qualData = processedFiles.parsed.success.find { it.filename == qualFile };
492
493                        if( qualData ) {
494                                returnStructure.qual = fileService.moveFileToUploadDir( fileService.get(qualFile ), qualData.originalfilename, permanentDirectory );
495                                returnStructure.avgQuality = qualData.avgQuality
496                        } else {
497                                // Delete the uploaded fasta file, since this is a serious error
498                                fileService.delete( returnStructure.fasta, permanentDirectory );
499                                throw new Exception( "Qual file wasn't uploaded the right way. Maybe the session has expired" );
500                        }
501                } else {
502                        // If the file doesn't exist, we don't save any quality information
503                        returnStructure.qual = null
504                        returnStructure.avgQuality = 0;
505                }
506
507                return returnStructure;
508        }
509
510        /**
511         * Exports the fasta data of a list of assaysamples
512         * @param assaySamples  Assaysamples to export
513         * @param outStream             Outputstream to send the data to
514         * @return
515         */
516        public def export( List assaySamples, OutputStream outStream, String name = null ) {
517                if( !assaySamples || assaySamples.size() == 0 )
518                        return false;
519
520                // Retrieve the filename from configuration, if none is given
521                if( !name )
522                        name = ConfigurationHolder.config.massSequencing.exportFilename
523
524                // Determine the directory the uploaded files are stored in
525                File permanentDirectory = fileService.absolutePath( ConfigurationHolder.config.massSequencing.fileDir );
526
527                // First check whether qual files should be exported or not
528                // It is only exported if qual scores are available for all sequences
529                def exportQual = ( assaySamples*.numSequences().sum() == assaySamples*.numQualScores().sum() );
530
531                // First create tags for every sample
532                def tags = [];
533
534                // Determine new tag length. Since we can use 4 characters per
535                // tag position, we only have to use 4log( #samples)
536                // The minimum number of characters used is 10, to ensure the correct working of the other
537                // programs.
538                int tagLength = Math.max( 10, Math.ceil( Math.log( assaySamples.size() ) / Math.log( 4 ) ) );
539                int tagNumber = 0;
540
541                assaySamples.each { assaySample ->
542                        if( assaySample.numSequences() > 0 ) {
543                                // Create a new tag for this assaysample
544                                def tag = createTag( tagLength, tagNumber++);
545
546                                // Save the tag for exporting
547                                tags << [       assaySampleId: assaySample.id, sampleName: assaySample.sample.name,
548                                                        assayName: assaySample.assay.name, studyName: assaySample.assay.study.name,
549                                                        forwardPrimer: assaySample.fwPrimerSeq, reversePrimer: assaySample.revPrimerSeq,
550                                                        tag: tag
551                                                ];
552                        }
553                }
554
555                // Now create zip file for fasta and qual files
556                ZipOutputStream zipFile = new ZipOutputStream( new BufferedOutputStream( outStream ) );
557                BufferedWriter zipWriter = new BufferedWriter( new OutputStreamWriter( zipFile ) );
558
559                // We have to loop twice through the sequenceData, since we can't write part of the sequence
560                // file and part of the qual files mixed. We have to write the full sequence file first.
561                try {
562                        zipFile.putNextEntry( new ZipEntry( name + ".fna" ) );
563
564                        assaySamples.each { assaySample ->
565                                if( assaySample.numSequences() > 0 ) {
566                                        def currentTag = tags.find { it.assaySampleId == assaySample.id };
567
568                                        assaySample.sequenceData.each { sequenceData ->
569                                                copyFastaFileForExport( fileService.get( sequenceData.sequenceFile, permanentDirectory ), currentTag.tag, zipWriter)
570                                        }
571                                }
572                        }
573                        zipWriter.flush();
574                        zipFile.closeEntry();
575
576                        if( exportQual ) {
577                                zipFile.putNextEntry( new ZipEntry( name + ".qual" ) );
578
579                                assaySamples.each { assaySample ->
580                                        if( assaySample.numSequences() > 0 ) {
581                                                def currentTag = tags.find { it.assaySampleId == assaySample.id };
582
583                                                assaySample.sequenceData.each { sequenceData ->
584                                                        copyQualFileForExport( fileService.get( sequenceData.qualityFile, permanentDirectory ), currentTag.tag, zipWriter)
585                                                }
586                                        }
587                                }
588
589                                zipWriter.flush();
590                                zipFile.closeEntry();
591                        }
592
593                } catch( Exception e ) {
594                        log.error "Error while writing to fastafile or qualfile: " + e.getMessage();
595                } finally {
596                        // Always close zip entry
597                        try {
598                                zipFile.closeEntry();
599                        } catch( Exception e ) {
600                                log.error "Error while closing zip entry for fasta and qual: " + e.getMessage();
601                        }
602                }
603
604                // Export a tab delimited file with tags
605                zipFile.putNextEntry( new ZipEntry( name + ".tab" ) );
606                exportTabDelimitedSampleTagFile( tags, zipWriter );
607                zipWriter.flush();
608                zipFile.closeEntry();
609
610                // Export a mothur file with tags
611                zipFile.putNextEntry( new ZipEntry( name + ".oligos" ) );
612                exportMothurSampleTagFile( tags, zipWriter );
613                zipWriter.flush();
614                zipFile.closeEntry();
615
616                // Export an excel file with information about the samples
617                zipFile.putNextEntry( new ZipEntry( name + ".xls" ) );
618                sampleExcelService.exportExcelSampleData( assaySamples, tags, zipFile );
619                zipFile.closeEntry();
620
621                zipFile.close();
622        }
623
624        /**
625         * Creates an oligos file for Mothur that represents the connection between samples
626         * and the artificial tags.
627         *
628         * @see http://www.mothur.org/wiki/Trim.seqs#allfiles
629         * @param tags          Map with newly created tags
630         * @param zipWriter     Writer to write the data to
631         */
632        protected void exportMothurSampleTagFile( List tags, Writer zipWriter ) {
633                // Add the forward and reverse primers, as found in the assaysamples
634                // The primers are already cut off, so they are not relevant anymore
635                // For that reason, a '#' is prepended to each line.
636                def fwPrimers = tags.collect { it.forwardPrimer }.findAll { it }.unique();
637                def revPrimers = tags.collect { it.reversePrimer }.findAll { it }.unique();
638
639                fwPrimers.each { zipWriter.write( "#forward\t" + it + "\n" )    }
640                revPrimers.each { zipWriter.write( "#reverse\t" + it + "\n" ) }
641
642                // Check whether the sample names are unique. If they aren't, the assay and study names
643                // are appended to the sample name
644                def sampleNames = tags*.sampleNames;
645                if( sampleNames.unique().size() < sampleNames.size() ) {
646                        tags.each {
647                                it.uniqueName = it.sampleName + " (" + it.assayName + " / " + it.studyName + ")";
648                        }
649                } else {
650                        tags.each {
651                                it.uniqueName = it.sampleName;
652                        }
653                }
654
655                tags.each {
656                        zipWriter.write( "barcode\t" + it.tag + "\t" + it.uniqueName + "\n" );
657                }
658        }
659
660        /**
661         * Creates a tab delimited file with two columns and column headers "Sequence" and "Samplename"
662         * @param tags          Map with newly created tags
663         * @param zipWriter     Writer to write the data to
664         */
665        protected void exportTabDelimitedSampleTagFile( List tags, Writer zipWriter ) {
666                zipWriter.write( "Sequence" + "\t" + "Samplename" + "\n" );
667
668                // Check whether the sample names are unique. If they aren't, the assay and study names
669                // are appended to the sample name
670                def sampleNames = tags*.sampleNames;
671                if( sampleNames.unique().size() < sampleNames.size() ) {
672                        tags.each {
673                                it.uniqueName = it.sampleName + " (" + it.assayName + " / " + it.studyName + ")";
674                        }
675                } else {
676                        tags.each {
677                                it.uniqueName = it.sampleName;
678                        }
679                }
680
681                tags.each {
682                        zipWriter.write( it.tag + "\t" + it.uniqueName + "\n" );
683                }
684        }
685
686
687        /**
688         * Creates a unique tag for the given number
689         * @param length
690         * @param tagNumber
691         * @return
692         */
693        public String createTag( int length, int tagNumber ) {
694                def chars = ["C", "A", "G", "T"];
695                def numChars = chars.size();
696
697                if( tagNumber > numChars ** length )
698                        throw new Exception( "Given tag number (" + tagNumber + ") is too large for the specified length (" + length + ")")
699
700                String tag = "";
701
702                for( def i = 0; i < length; i++ ) {
703                        int currentChar = tagNumber % numChars
704
705                        // Append the new character to the end of the tag, to ensure that the first part of the tag is
706                        // the most volatile. This way it is easy to find the end of the tag and the beginning of the real
707                        // sequence on first sight.
708                        tag = tag + chars[ currentChar ];
709
710                        tagNumber = Math.floor( tagNumber / numChars );
711                }
712
713                return tag;
714        }
715
716        /**
717         * Copies the contents of the given sequence file to the output file and prepends the tag to every sequences
718         * @param inFile        Filename of the file to be read
719         * @param tag           
720         * @param outWriter
721         * @return
722         */
723        protected boolean copyFastaFileForExport( File inFile, String tag, BufferedWriter outWriter ) {
724                // Walk through the lines in the file, starting with '>'
725                // (and where the following line contains a character other than '>')
726
727                try {
728                        BufferedReader inReader = new BufferedReader( new FileReader( inFile ) );
729
730                        String line = null
731                        String newLine = null
732                        String sequence = "";
733
734                        def lengthPattern = ~/length=(\d+)/
735                        def lengthMatches
736                        int length = 0;
737                        int tagLength = tag.size();
738
739                        while( ( line = inReader.readLine()) != null) {
740                                if( line.size() == 0 ) {
741                                        // Print the sequence we collected, before writing the empty line
742                                        printSequence( outWriter, sequence, tag );
743                                        sequence = "";
744
745                                        // Empty line
746                                        outWriter.newLine();
747                                } else if( line[ 0 ] == '>' ) {
748                                        // Print the sequence we collected, before writing the new comments tag
749                                        printSequence( outWriter, sequence, tag );
750                                        sequence = "";
751
752                                        // Comments line: replace length=### with the
753                                        // updated length, and put the line in the
754                                        lengthMatches = ( line =~ lengthPattern );
755                                        if( lengthMatches ) {
756                                                length = Integer.valueOf( lengthMatches[0][1] ) + tagLength;
757                                                newLine = lengthMatches.replaceAll( "length=" + length );
758                                        }
759
760                                        outWriter.write(newLine);
761                                        outWriter.newLine();
762                                } else {
763                                        // This is part of the sequence. We collect the whole sequence and
764                                        // determine in the end how to write it to the file
765                                        sequence += line;
766                                }
767                        }
768
769                        // Print the sequence we collected, before ending the file
770                        printSequence( outWriter, sequence, tag );
771                        sequence = "";
772
773                } catch( Exception e ) {
774                        log.error( "An error occurred while copying contents from " + inFile.getName() + ": " + e.getMessage() );
775                        return false;
776                }
777        }
778
779        /**
780         * Prints a sequence to the output file
781         * @param outWriter
782         * @param sequence
783         * @param tag
784         */
785        private void printSequence( BufferedWriter outWriter, String sequence, String tag, int maxWidth = 60 ) {
786                // If no sequence is given, also don't prepend it with the tag
787                if( sequence.size() == 0 )
788                        return
789
790                // Prepend the tag to the sequence
791                sequence = tag + sequence;
792
793                // Write the sequence with a width of maxWidth characters per line
794                while( sequence ) {
795                        if( sequence.size() > maxWidth ) {
796                                outWriter.write( sequence[0..maxWidth-1] );
797                                sequence = sequence[maxWidth..-1]
798                        } else {
799                                outWriter.write( sequence );
800                                sequence = null;
801                        }
802                        outWriter.newLine();
803                }
804        }
805
806        /**
807         * Copies the contents of the given qual file to the output file and prepends the tag quality score to every sequence.
808         * For every tag character '40' is prepended to the qual scores
809         *
810         * @param inFile        Filename of the file to be read
811         * @param tag
812         * @param outWriter
813         * @return
814         */
815        protected boolean copyQualFileForExport( File inFile, String tag, BufferedWriter outWriter ) {
816                // Walk through the lines in the file, starting with '>'
817                // (and where the following line contains a character other than '>')
818                try {
819                        BufferedReader inReader = new BufferedReader( new FileReader( inFile ) );
820
821                        String line = null
822                        String newLine = null
823                        List<Integer> qualScores = []
824
825                        def lengthPattern = ~/length=(\d+)/
826                        def lengthMatches
827                        int length = 0;
828                        int tagLength = tag.size();
829
830                        while( ( line = inReader.readLine()) != null) {
831                                if( line.size() == 0 ) {
832                                        // Print the quality scores we collected, before writing the empty line
833                                        printQualScore( outWriter, qualScores, tagLength );
834                                        qualScores = [];
835
836                                        // Empty line
837                                        outWriter.newLine();
838                                } else if( line[ 0 ] == '>' ) {
839                                        // Print the quality scores we collected, before writing the empty line
840                                        printQualScore( outWriter, qualScores, tagLength );
841                                        qualScores = [];
842
843                                        // Comments line: replace length=### with the
844                                        // updated length, and put the line in the
845                                        lengthMatches = ( line =~ lengthPattern );
846                                        if( lengthMatches ) {
847                                                length = Integer.valueOf( lengthMatches[0][1] ) + tagLength;
848                                                newLine = lengthMatches.replaceAll( "length=" + length );
849                                        }
850
851                                        outWriter.write(newLine);
852                                        outWriter.newLine();
853                                } else {
854                                        // This is part of the quality score. We collect the whole set of quality
855                                        // scores and determine in the end how to write it to the file
856                                        qualScores += line.split( " " ).collect {
857                                                if( !it.isInteger() )
858                                                        return 0;
859                                                else
860                                                        return Integer.parseInt( it );
861                                        };
862                                }
863                        }
864
865                        // Print the quality scores we collected, before ending the file
866                        printQualScore( outWriter, qualScores, tagLength );
867                        qualScores = [];
868
869                } catch( Exception e ) {
870                        log.error( "An error occurred while copying contents from " + inFile.getName() + ": " + e.getMessage() );
871                        return false;
872                }
873        }
874
875        /**
876         * Prints a sequence to the output file
877         * @param outWriter
878         * @param sequence
879         * @param tag
880         */
881        private void printQualScore( BufferedWriter outWriter, List<Integer> qualScores, int tagLength, int maxWidth = 60 ) {
882                // If no qualScores are given, also don't prepend it with the tag
883                if( qualScores.size() == 0 )
884                        return
885
886                // Prepend the tag to the sequence
887                qualScores = Collections.nCopies( tagLength, 40 ) + qualScores;
888
889                // Write the sequence with a width of maxWidth characters per line
890                while( qualScores ) {
891                        if( qualScores.size() > maxWidth ) {
892                                outWriter.write( qualScores[0..maxWidth-1].join( " " ) );
893                                qualScores = qualScores[maxWidth..-1]
894                        } else {
895                                outWriter.write( qualScores.join( " " ) );
896                                qualScores = null;
897                        }
898                        outWriter.newLine();
899                }
900        }
901
902}
Note: See TracBrowser for help on using the repository browser.