Ignore:
Timestamp:
May 17, 2011, 1:44:07 PM (8 years ago)
Author:
robert@…
Message:

Implemented importing of classifications

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/services/nl/tno/massSequencing/FastaService.groovy

    r55 r58  
    99import java.util.zip.*
    1010
    11 class FastaService {
     11class FastaService implements nl.tno.massSequencing.imports.Importer {
    1212        def fileService
    1313        def fuzzySearchService
     
    1515        def excelService
    1616
    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                                         // Delete the zip file itself
    76                                         fileService.delete( filename );
    77                                 } else {
    78                                         failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: "zip", message: 'Zip file could not be extracted' ];
    79                                 }
    80                         } else {
    81                                 def file = fileService.get( filename );
    82                                 String filetype = fileService.determineFileType( file );
    83 
    84                                 if( !fileTypeValid( filetype ) ) {
    85                                         // If files are not valid for parsing, delete them and return a message to the user
    86                                         fileService.delete(filename);
    87                                         failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: 'File type not accepted' ];
    88                                 } else {
    89                                         try {
    90                                                 def contents = parseFile( file, filetype, { files, bytes ->
    91                                                         filesProcessed += files;
    92                                                         bytesProcessed += bytes;
    93 
    94                                                         onProgress( filesProcessed, bytesProcessed, 0, 0 );
    95                                                 } );
    96                                                
    97                                                 contents.filename = file.getName();
    98                                                 contents.originalfilename = fileService.originalFilename( contents.filename )
    99 
    100                                                 if( contents.success ) {
    101                                                         success << contents;
    102                                                 } else {
    103                                                         fileService.delete(filename);
    104                                                         failure << contents;
    105                                                 }
    106                                         } catch( Exception e ) {
    107                                                 // If anything fails during parsing, return an error message
    108                                                 fileService.delete(filename);
    109                                                 failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: e.getMessage() ];
    110                                         }
    111                                 }
    112                         }
    113                 }
    114 
    115                 return [ 'success': success, 'failure': failure ];
    116         }
    117 
    118         /**
    119          * Matches uploaded fasta and qual files and combines them with the samples they probably belong to
    120          * @param parsedFiles   Parsed files
    121          * [
    122          *              [filename: 'abc.fasta', type: FASTA, numSequences: 190]
    123          *              [filename: 'cde.fasta', type: FASTA, numSequences: 140]
    124          *              [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
    125          *              [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
    126          *              [filename: 'match.xls', type: EXCEL, matches: [ [ filename: 'abc.fasta', basename: 'abc', sample: 's1' ] ]
    127          * ]
    128          * @param samples               AssaySample objects to which the files should be matched.
    129          * @return                              Structure with information about the matching.
    130          * [
    131          *              [
    132          *                      fasta:  [filename: 'abc.fasta', type: FASTA, numSequences: 190],
    133          *                      qual:   [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
    134          *                      feasibleQuals: [
    135          *                              [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
    136          *                              [filename: 'def.qual', type: QUAL, numSequences: 190, avgQuality: 21]
    137          *                      ]
    138          *                      sample: AssaySample object     
    139          */
    140         def matchFiles( def parsedFiles, def samples ) {
    141                 def fastas = parsedFiles.findAll { it.type == "fasta" }
    142                 def quals = parsedFiles.findAll { it.type == "qual" }
    143                 def excels = parsedFiles.findAll { it.type == "excel" }
    144                
    145                 samples = samples.toList()
    146                
    147                 // Collect matches from all files
    148                 def matches = [];
    149                 excels.each { m ->
    150                         if( m.matches ) {
    151                                 m.matches.each { matches << it }
    152                         }
    153                 }
    154 
    155                 def files = [];
    156 
    157                 fastas.each { fastaFile ->
    158                         // Remove extension
    159                         def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.originalfilename.lastIndexOf( '.' ) )
    160 
    161                         // Determine feasible quals (based on number of sequences )
    162                         def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences }
    163 
    164                         // Best matching qual file
    165                         def qualIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.qual', feasibleQuals.originalfilename );
    166 
    167                         def qual = null
    168                         if( qualIdx != null )
    169                                 qual = feasibleQuals[ qualIdx ];
    170 
    171                         // Best matching sample
    172                         def assaySample = null
    173                         def checked = true;
    174                         if( matches ) {
    175                                 // Match with files from excelsheet
    176                                
    177                                 // First find the best matching filename in the list of matches.
    178                                 def sampleNameIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, matches*.basename, 0.8 );
    179                                
    180                                 // If one is found, use the sample name associated with it to do the matching with samples
    181                                 if( sampleNameIdx != null ) {
    182                                         matchWith = matches[ sampleNameIdx ].sample;
    183                                 } else {
    184                                         // If no match is found in the excel sheet, this sample is unchecked by default
    185                                         checked = false;
    186                                 }
    187                         }
    188                        
    189                         // Match on filenames
    190                         def sampleIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, samples.sample.name );
    191                         if( sampleIdx != null ) {
    192                                 assaySample = samples[ sampleIdx ];
    193                         }
    194 
    195                         files << [
    196                                                 fasta: fastaFile,
    197                                                 feasibleQuals: feasibleQuals,
    198                                                 qual: qual,
    199                                                 assaySample: assaySample,
    200                                                 checked: checked
    201                                         ]
    202                 }
    203 
    204                 return files;
    205 
    206 
    207         }
     17        static transactional = false
    20818
    20919        /**
     
    21323         * @return
    21424         */
    215         protected boolean fileTypeValid( String filetype ) {
     25        public boolean canParseFileType( String filetype ) {
    21626                switch( filetype ) {
    21727                        case "fasta":
    21828                        case "qual":
    219                         case "excel":
    22029                                return true;
    22130                        default:
     
    22534
    22635        /**
    227          * Parses the given file
     36         * Parses the given FASTA or QUAL file
     37         *
    22838         * @param file                  File to parse
    22939         * @param filetype              Type of the given file
    23040         * @param onProgress    Closure to execute when progress indicators should be updated.
    231          *                                              Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number
    232          *                                              of files and bytes that have been processed in this file (so the first parameter should
    233          *                                              only be 1 when the file is finished)
     41         *                                              Has 2 parameters: progress that indicates the number of bytes that have been processed. The second parameter determines
     42         *                                              the number of bytes that has to be processed extra after this action (e.g. when a zip file is extracted, the extracted
     43         *                                              files should be added to the total number of bytes to be processed)
     44         * @return                              Structure with information about the parsed files. The 'success' files are
     45         *                                              moved to the given directory
    23446         *
    235          * @return                              List structure. Examples:
     47         * Examples:
     48         *                      [filename: 'abc.fasta', type: FASTA, numSequences: 190]
     49         *                      [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
    23650         *
    237          *   [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ]
    238          *   [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 36 ]
    239          *   [ success: false, filename: 'abc.txt', type: 'txt', message: 'Filetype could not be parsed.' ]
    240          */
    241         protected def parseFile( File file, String filetype, Closure onProgress ) {
     51         *                      [filename: 'test.zip', type: ZIP, extraFiles: [newfile1.xls, newfile2.xls, newfile3.xls]]
     52         *
     53         *                      [filename: 'testing.xls', type: 'unknown', message: 'Type not recognized']
     54         *
     55         */
     56
     57        public Map parseFile( File file, String filetype, Closure onProgress ) {
    24258                switch( filetype ) {
    24359                        case "fasta":
     
    24561                        case "qual":
    24662                                return parseQual( file, onProgress );
    247                         case "excel":
    248                                 return parseExcelMatch( file, onProgress );
    24963                        default:
    250                                 onProgress( 1, file.length() );
    25164                                return [ success: false, type: filetype, message: 'Filetype could not be parsed.' ]
    25265                }
     
    290103                                bytesProcessed += line.size();
    291104                                if( bytesProcessed > 1000000 ) {
    292                                         onProgress( 0, bytesProcessed );
     105                                        onProgress( bytesProcessed, 0 );
    293106                                        bytesProcessed = 0;
    294107                                }
     
    298111
    299112                // Update progress and say we're finished
    300                 onProgress( 1, bytesProcessed );
     113                onProgress( bytesProcessed, 0 );
    301114
    302115                log.trace "Finished parsing FASTA " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
     
    388201                                bytesProcessed += line.size();
    389202                                if( bytesProcessed > 1000000 ) {
    390                                         onProgress( 0, bytesProcessed );
     203                                        onProgress( bytesProcessed, 0 );
    391204                                        bytesProcessed = 0;
    392205                                }
     
    396209
    397210                // Update progress and say we're finished
    398                 onProgress( 1, bytesProcessed );
     211                onProgress( bytesProcessed, 0 );
    399212
    400213                log.trace "Finished parsing QUAL " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
     
    403216        }
    404217
     218       
     219        /**
     220        * Matches uploaded fasta and qual files and combines them with the samples they probably belong to
     221        * @param parsedFiles    Parsed files
     222        * [
     223        *               [filename: 'abc.fasta', type: FASTA, numSequences: 190]
     224        *               [filename: 'cde.fasta', type: FASTA, numSequences: 140]
     225        *               [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
     226        *               [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
     227        *               [filename: 'match.xls', type: EXCEL, matches: [ [ filename: 'abc.fasta', basename: 'abc', sample: 's1' ] ]
     228        * ]
     229        * @param samples                AssaySample objects to which the files should be matched.
     230        * @return                               Structure with information about the matching.
     231        * [
     232        *               [
     233        *                       fasta:  [filename: 'abc.fasta', type: FASTA, numSequences: 190],
     234        *                       qual:   [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
     235        *                       feasibleQuals: [
     236        *                               [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38],
     237        *                               [filename: 'def.qual', type: QUAL, numSequences: 190, avgQuality: 21]
     238        *                       ]
     239        *                       sample: AssaySample object
     240        */
     241   def matchFiles( def parsedFiles, def samples ) {
     242           def fastas = parsedFiles.findAll { it.type == "fasta" }
     243           def quals = parsedFiles.findAll { it.type == "qual" }
     244           def excels = parsedFiles.findAll { it.type == "excel" }
     245           
     246           samples = samples.toList()
     247           
     248           // Collect (filename) matches from all excel files
     249           def matches = [];
     250           excels.each { m ->
     251                   if( m.matches && m.matches.filenames ) {
     252                           // [ filename: filename, samplename: GSCFSample, basename: basename ]
     253                           m.matches.filenames.each { matches << it }
     254                   }
     255           }
     256
     257           def files = [];
     258
     259           fastas.each { fastaFile ->
     260                   // Remove extension
     261                   def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.originalfilename.lastIndexOf( '.' ) )
     262
     263                   // Determine feasible quals (based on number of sequences )
     264                   def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences }
     265
     266                   // Best matching qual file
     267                   def qualIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.qual', feasibleQuals.originalfilename );
     268
     269                   def qual = null
     270                   if( qualIdx != null )
     271                           qual = feasibleQuals[ qualIdx ];
     272
     273                   // Best matching sample
     274                   def assaySample = null
     275                   def checked = true;
     276                   if( matches ) {
     277                           // Match with files from excelsheet
     278                           
     279                           // First find the best matching filename in the list of matches.
     280                           def sampleNameIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, matches*.basename, 0.8 );
     281                           
     282                           // If one is found, use the sample name associated with it to do the matching with samples
     283                           if( sampleNameIdx != null ) {
     284                                   matchWith = matches[ sampleNameIdx ].samplename;
     285                           } else {
     286                                   // If no match is found in the excel sheet, this sample is unchecked by default
     287                                   checked = false;
     288                           }
     289                   }
     290                   
     291                   // Match on filenames
     292                   def sampleIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, samples.sample.name );
     293                   if( sampleIdx != null ) {
     294                           assaySample = samples[ sampleIdx ];
     295                   }
     296
     297                   files << [
     298                                           fasta: fastaFile,
     299                                           feasibleQuals: feasibleQuals,
     300                                           qual: qual,
     301                                           assaySample: assaySample,
     302                                           checked: checked
     303                                   ]
     304           }
     305
     306           return files;
     307
     308
     309   }
     310
     311       
    405312        /**
    406313         * Parses a given excel file with a match between filenames and samples
     
    417324         *   [ success: false, filename: 'def.xls', type: 'excel', message: 'File is not a valid XLS file' ]
    418325         */
    419         protected def parseExcelMatch( File file, Closure onProgress ) {
    420                 long startTime = System.nanoTime();
    421                 log.trace "Start parsing XLS " + file.getName()
    422 
    423                 def matches = []
    424 
    425                 // Read excel file
    426                 def wb;
    427                 try {
    428                         wb = excelService.open( file );
    429                 } catch( Exception e ) {
    430                         // If an exception occurs, the file can't be opened. Return the error message
    431                         return [ success: false, type: "excel", filename: file.getName(), message: "Excel file could not be opened or parsed."]
    432                 }
    433 
    434                 // Read all data into an array, and the header in a separate array
    435                 def header = excelService.readRow( wb, 0, 0 );
    436                 def data = excelService.readData( wb, 0, 1 );
    437 
    438                 // Check whether (at least) 2 columns are present
    439                 if( header.size() < 2 ) {
    440                         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"]
     326        public def inferExcelMatches( parsedFiles ) {
     327                // Loop through all excel files, and determine which (usable) columns are in the file
     328                for( parsedFile in parsedFiles ) {
     329                        if( parsedFile.type == "excel" ) {
     330                               
     331                                def header = parsedFile.header;
     332                                def data = parsedFile.data
     333                               
     334                                // Check whether (at least) 2 columns are present
     335                                if( header.size() < 2 ) {
     336                                        parsedFile.success = false
     337                                        parsedFile.exceldata = []
     338                                        parsedFile.message = "Excel file must contain at least 2 columns, one with filenames and one with sample names"
     339                                        continue;
     340                                }
     341                                               
     342                                // Check the headers to see whether any columns are found
     343                                def columns = [:]
     344                                def matches = [ "filenames": [], "mothursamples": [] ]
     345                               
     346                                header.eachWithIndex { cell, i ->
     347                                        switch( cell?.toLowerCase() ) {
     348                                                case "gscfsample":
     349                                                case "sample":
     350                                                        columns[ "GSCFSample" ] = i; break;
     351                                                case "file":
     352                                                case "filename":
     353                                                        columns[ "filename" ] = i; break;
     354                                                case "mothursample":
     355                                                        columns[ "MothurSample" ] = i; break;
     356                                        }
     357                                }
     358                                               
     359                                // Loop through the data and find matches
     360                                data.each { row ->
     361                                        def filename = columns[ "filename" ] != null ? row[ columns[ "filename" ] ] : null;
     362                                        def GSCFSample = columns[ "GSCFSample" ] != null ? row[ columns[ "GSCFSample" ] ] : null;
     363                                        def mothurSample = columns[ "MothurSample" ] != null ? row[ columns[ "MothurSample" ] ] : null;
     364                                       
     365                                        // Matches only exist if GSCF sample is entered in this row
     366                                        if( GSCFSample && GSCFSample != "null" ) {
     367                                                if( filename && filename != "null" ) {
     368                                                        // Remove extension from the filename, but only if it is
     369                                                        // .fna, .fasta, .qual, .fqa, .xls or .xlsx. Otherwise useful parts of the filename would be removed.
     370                                                        def basename = ( filename =~ /\.(fna|fasta|qual|fqa|xls|xlsx)$/ ).replaceAll( "" );
     371
     372                                                        matches[ "filenames" ] << [ filename: filename, samplename: GSCFSample, basename: basename ];
     373                                                }
     374                                               
     375                                                if( mothurSample && mothurSample != "null" ) {
     376                                                        matches[ "mothursamples" ] << [ samplename: GSCFSample, mothurname: mothurSample ];
     377                                                }
     378                                        }
     379                                }
     380                               
     381                                parsedFile[ 'columns' ] = columns;
     382                                parsedFile[ 'matches' ] = matches;
     383                        }
    441384                }
    442385               
    443                 // Check the headers to see whether the default columns are switched
    444                 def filenameColumn = 0;
    445                 def sampleColumn = 1;
    446                
    447                 header.eachWithIndex { cell, i ->
    448                         switch( cell?.toLowerCase() ) {
    449                                 case "sample":
    450                                         sampleColumn = i; break;
    451                                 case "file":
    452                                 case "filename":
    453                                         filenameColumn = i; break;
    454                         }
    455                 }
    456                
    457                 // If both filenames and samples are found in the same column (happens if only one of the headers is given)
    458                 // an error is given
    459                 if( filenameColumn == sampleColumn )
    460                         return [ success: false, type: "excel", filename: file.getName(), message: "Excel file must contain 'Sample' and 'Filename' headers."]
    461                
    462                 // Loop through the data and create a match
    463                 def maxColumn = Math.max( filenameColumn, sampleColumn );
    464                 data.each { row ->
    465                         if( row.size() >= maxColumn ) {
    466                                 def filename = row[ filenameColumn ];
    467                                 def sample = row[ sampleColumn ];
    468                                
    469                                 if( sample && sample != "null"  && filename && filename != "null" ) {
    470                                         // Remove extension from the filename, but only if it is
    471                                         // .fna, .fasta, .qual, .fqa, .xls or .xlsx. Otherwise useful parts of the filename would be removed.
    472                                         def basename = ( filename =~ /\.(fna|fasta|qual|fqa|xls|xlsx)$/ ).replaceAll( "" );
    473                                        
    474                                         matches << [ filename: filename, sample: sample, basename: basename ];
    475                                 }
    476                         }
    477                 }
    478 
    479                 // Update progress and say we're finished
    480                 onProgress( 1, file.size() );
    481 
    482                 log.trace "Finished parsing XLS " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L
    483 
    484                 return [ success: true, type: "excel", filename: file.getName(), matches: matches ];
     386                return parsedFiles
    485387        }
    486388
Note: See TracChangeset for help on using the changeset viewer.