root/grails-app/controllers/importer/ImporterController.groovy @ 149

Revision 149, 33.0 KB (checked in by adem.bilican@…, 4 years ago)

error messages for the flows and Error page when GSCF is down

Line 
1package importer
2
3import data.SimpleAssay
4import data.SimpleAssayMeasurement
5import data.SimpleAssayMeasurementType
6import data.SimpleAssaySample
7
8/**
9 * @author Adem and Jahn
10 * @since 20100602
11 *
12 *
13 * This Controller provides controler functionality for three views.
14 *
15 * (1) A workflow for creating new Assays from a string of text given as XLS sheet or as pasted, tab-delimited plain text.
16 *
17 * (2) A simple preview for all Assay data within a Study.
18 *
19 * (3) A simple editor for editing Assay measurements.
20 *
21 */
22
23
24
25import org.codehaus.groovy.grails.web.json.*
26import org.apache.poi.hssf.usermodel.*
27import org.apache.poi.poifs.filesystem.POIFSFileSystem
28import org.apache.poi.ss.usermodel.DataFormatter
29import grails.converters.*
30import org.apache.poi.hssf.usermodel.HSSFWorkbook
31import dbnp.rest.common.CommunicationManager
32import org.codehaus.groovy.grails.commons.GrailsApplication
33import org.apache.commons.codec.*
34
35
36class ImporterController {
37
38    def ImporterService
39    def searchableService
40    def fileService
41
42    def index = {
43        String.metaClass.mostSimilarTo = { mostSimilar(delegate, it) }
44        redirect(action: 'pages')
45    }
46   
47    /**
48     *   This flow is provides the user with a means for importing Assay data into the Simple Assay Module (SAM).
49     *
50     *   @param string externalStudyID          The code member of a Study domain object in GSCF.
51     *   @param string externalAssayID          The externalAssayID of member of a Assay domain object in GSCF.
52     *
53     *
54     *
55     *   The flow performs the following view actions.
56     *
57     *   (1) The user pastes a tab-delimted string into a text area containing a table to import.
58     *
59     *   (2) The user selects the columns of the table that he/she wants to import.
60     *
61     *   (3) The user matches the subjects in the table to those in the system.
62     *
63     *   (4) The data is validated at the GSCF and saved in SAM.
64     *
65     *   (5) An overvie of the imported data is shown to the user.
66     *
67     *
68     *
69     *   Variables stored in flow scope:
70     *
71     *   flow.selectedAssayID   The externalAssayID of a GSCF assay.
72     *   flow.studies                   list of all studies. each element e is a map with two entires. e['name'] gives
73     *                                                  the name of the study, e['id'] gives the corresponding id.
74     *
75     *   flow.selectedStudy             name of the selected study as string
76     *
77     *   flow.selectedStudyID   external ID of the selected study
78     *
79     *   flow.selectedAsasyID   external ID of the selected assay
80     *
81     *   flow.data                              2-dimensional map containing information to be stored as read in from the user
82     *                          The format of this map is as follows.
83     *                          The first level map has GSCF-subject names as keys. The values are level-2 maps.
84     *                          Level-2 maps have GSCF start time as keys which are mapped to string values.
85     *                          These string values are at some point stored as SimpleAssayMeasurements.
86     *
87     *   flow.samples                   list of SimpleAssaySamples that belong to this import's assay
88     *   
89     *   flow.tabDelimitedString  tab-delimited string representing a table. given by user.
90     */
91
92    def test = { render params }
93
94    def startError = {
95        render (view:'/importer/pages/_startError')
96    }
97
98    def pagesFlow = {
99
100        String.metaClass.mostSimilarTo = { mostSimilar(delegate, it) }
101
102        onStart {
103            flow.layouts = [ 'Sample Layout','Subject Layout' ]
104
105            try {
106                def list = CommunicationManager.getStudies()
107                def studies = list.collect{ it['studyToken'] }
108                flow.studies = studies
109            }
110            catch (Exception e){
111                redirect(action: 'startError')
112            }
113            flow.warnings = []
114
115            flow.page = 0
116            flow.pages = [
117                [title: 'Select Study'],
118                [title: 'Select Assay'],
119                [title: 'Input Assay Data'],
120                [title: 'Select Layout'],
121                [title: 'Select Data'],
122                [title: 'Show  Data'],
123                [title: 'Done']
124            ]
125        }
126
127        mainPage {
128            render(view: '/importer/importMain')
129            onRender {
130                flow.page = 0
131                next()
132            }
133            on("next") {
134                success()
135            }.to "selectStudy"
136        }
137
138        selectStudy {
139            render(view: '_selectStudy')
140            onRender {
141                flow.page = 1
142            }
143            on("next") {
144                flow.selectedStudyID = params['studyId']
145                def assays = []
146                def assayNames = []
147                // TODO: there must be a better way, we should get the actual runtime server URL instead of the config one
148                CommunicationManager.getAssays( flow.selectedStudyID, grailsApplication.config.grails.serverURL ).each{
149                    assays.push( ['name':it['name'],'externalAssayId':it['assayToken']] )
150                    assayNames.push( it['name'] )
151                }
152                flow.assays     = assays
153                flow.assayNames = assayNames
154                success()
155            }.to "selectAssay"
156        }
157
158        selectAssay {
159            render( view: '_selectAssay' )
160            onRender {
161                flow.page = 2
162            }
163            on("next") {
164                def selectedName = params['assayName']
165                def data = flow.assays.find{ it['name'] == selectedName }
166                flow.selectedAssayID = data['externalAssayId']
167                flow.selectedAssayName = selectedName
168                // Save the local cache of the selected assay
169                flow.assay = saveAssay(flow.selectedAssayID, flow.selectedStudyID, selectedName )
170                success()
171            }.to "selectLayout"
172            on('previous').to('selectStudy')
173        }
174
175
176        // get table data from user
177        getTable {                                                      // user gives text input as file or by copy-and-paste
178            render(view: '_getTable')
179            onRender {
180                flow.page = 3
181            }
182            on("next"){
183                def tabbedTable = params['assayData']
184                if( (tabbedTable.replaceAll("^\\s+", "").size() > 0) && (params['file'] == "null" ) ) {
185                    flow.tabDelimitedString = tabbedTable
186                    flow.file = params['file']
187                    success()
188                }
189                // there is a file
190                else if ((tabbedTable.replaceAll("^\\s+", "").size() == 0) && (params['file'] != "null" )) {
191                    flow.tabDelimitedString = fileService.get(params['file'])
192                    flow.file = params['file']
193                    success()
194                }
195                // catch error if there is nothing to do
196                else if ((tabbedTable.replaceAll("^\\s+", "").size() == 0) && (params['file'] == "null" ))
197                {
198                    flash.errors = 'No user input : Please paste your data or select a file'
199                    error()
200                }
201                //catch error if there is too much information
202                else if ((tabbedTable.replaceAll("^\\s+", "").size() > 0) && (params['file'] != "null" ))
203                {
204                    flash.errors = 'Too much information : Please paste your data OR select a file'
205                    error()
206                }
207               
208            }.to "buildTableModel"
209            on("previous").to "selectAssay"
210        }
211
212
213        selectLayout{
214            render( view: '_selectLayout' )
215            onRender {
216                flow.page = 4
217            }
218            on("next") {
219                def layout = params['layout']
220                flow.hasSubjectLayout =  (layout==flow.layouts[1]) ? true : false ;
221            }.to "getTable"
222            on('previous').to('selectAssay')
223        }
224
225
226        // methods for parsing the file
227        // only for Subject Layout
228        buildTableModel{
229            action {
230                // define the list of samples into the study
231                flow.samples     = getSamples( flow.selectedAssayID )
232                flow.sampleTimes = flow.samples.collect{ it.getStartTime() }.unique()
233                buildModel( flow )   // add all table data to the flow
234                if( flow.hasSubjectLayout ) { return subjectLayout() }
235                else                                        { return sampleLayout() }
236            }
237            on("subjectLayout"){}.to "selectTypesForSubjectLayout"
238            on( "sampleLayout"){}.to "selectTypesForSampleLayout"
239        }
240
241
242        // only for sample layout
243        selectTypesForSampleLayout{                                     // match columns to assays and samples (i.e., date-subject pair)
244            render(view: '_selectTypesForSampleLayout')
245            onRender {
246                flow.page = 5
247            }
248            on('refresh'){
249               
250                println( ' refreshing ')
251                def measurementTypes = []
252
253                for(i in data.SimpleAssayMeasurementType.list()){
254                    measurementTypes.add(i.name)
255                    i.discard()
256                }
257                flow.measurementTypes = measurementTypes
258
259            }.to('selectTypesForSampleLayout')
260            on("next"){
261                println('PARAMS '+ params)
262                // return the list of subjects and assays
263                addMeasurementsToFlow(flow, params)  // set up-to-date measurements in flow.measurements
264            }.to "showDataForSampleLayout"
265            on("previous").to('selectLayout')
266        }
267
268        // only for subject layout
269        selectTypesForSubjectLayout {           // match columns to assays and samples (i.e., date-subject pair)
270            render(view: '_selectTypesForSubjectLayout' )
271            onRender {
272                flow.page = 5
273            }
274            on("next"){
275                // return the list of subjects and assays
276                getAttributes(flow, params)
277            }.to "fetchSubjectsForSubjectLayout"
278            on("previous").to('selectLayout')
279        }
280
281
282        // return the list of the subjects into the study from GSCF
283        // only for subject layout
284        fetchSubjectsForSubjectLayout {
285            action {
286                def studySubjects = []
287                for (subject in CommunicationManager.getSubjects(flow.selectedStudyID.toString())){
288                    studySubjects.add(subject)
289                }
290
291                if (studySubjects.size() < flow.importedSubjects.size() ) {
292                    flash.errors = 'Error : wrong number of subjects'
293                    error()
294                }
295
296                flow.studySubjects = studySubjects
297            }
298            on("success").to("validateSubjects")
299        }
300
301
302        // only for subject layout
303        validateSubjects {
304            render(view: '_validateSubjects')                           // probably this requires more than one state
305            onRender {
306                flow.page = 5
307            }
308            on("next"){
309                // get the list of matching subjects and the data
310                getSubjects(flow, params)
311            }.to "showDataForSubjectLayout"
312            on("previous").to "selectTypesForSubjectLayout"
313        }
314
315
316        // for sample layout
317        showDataForSampleLayout {                                                       // show summary of assay to user
318            render(view:'_showSampleLayout')
319            onRender {
320                flow.page = 6
321            }
322            on("next").to "saveData"
323            on('previous').to('selectTypesForSampleLayout')
324        }
325       
326
327       
328
329        // for subject layout and sample layout
330        showDataForSubjectLayout {                                                      // show summary of assay to user
331            render(view:'_showSubjectLayout')
332            onRender {
333                flow.page = 6
334            }
335            on("next").to "saveData"
336            on('previous').to('validateSubject')
337        }
338       
339
340
341        // try to save data
342        // for subject layout and sample layout
343        saveData {                                                     
344            action{
345                if( flow.hasSubjectLayout ) {                                   // save subject layout
346                    flow.warnings = saveSAMforSubjectLayout( flow.selectedStudyID, flow.selectedAssayID,
347                        flow.selectedAssayName, flow.subjects, flow.times, flow.data )
348                }
349                else {                                                                                  // save sample
350                    flow.measurements.each {
351                        it.save(flush:true)
352                    }
353                }
354                fileService.delete(flow.file)
355                return success()
356            }
357            on("success").to "done"
358        }
359
360
361        done {                                                          // display success or error message,
362            render(view: '_done')                       // go back to first step or exit
363            onRender {
364                flow.page = 7
365            }
366            on("next").to "done"
367        }
368
369    }
370
371    /* what is this function */
372    private getAttributes(flow, params) {
373        // matchingAssays contains the ID for each assays into the file and into the study
374        def matchingAssays = []
375        // importedSubjects contains the list of subjects into the file
376        def importedSubjects = []
377        // assayValuesMap contains the list of data for each assay
378        def assayValuesMap = []
379        def typeList = []
380
381        for (entity in 0..params['entity'].size()-1){
382            if( params['entity'].getAt(entity).equals("Subjects")){
383                for (line in flow.datamatrix){
384                    importedSubjects.add(line.getAt(entity))
385                }
386            }
387            else if (params['entity'].getAt(entity).equals("Not Used")){}
388
389            else {
390                def assayID = []
391
392                assayID.add(flow.headersList.getAt(entity))
393                assayID.add(params['entity'].getAt(entity))
394                matchingAssays.add(assayID)
395
396                def assayValues = []
397                for (line in flow.datamatrix){
398                    assayValues.add(line.getAt(entity))
399                }
400                def assayMap = [:]
401                assayMap[params['entity'].getAt(entity)]= assayValues
402                assayValuesMap.add assayMap
403                typeList.add(params['type'].getAt(entity))
404            }
405        }
406        flow.importedSubjects = importedSubjects
407        flow.assayValuesMap   = assayValuesMap
408        flow.typeList = typeList
409    }
410
411
412    private getSubjects(flow, params){
413
414        def rankedImportedSubjects = []
415        def rankedStudySubjects = []
416        for (importedSubjects in params['importedSubject']){
417            rankedImportedSubjects.add(importedSubjects)
418        }
419        for (studySubjects in params['studySubject']){
420            rankedStudySubjects.add(studySubjects)
421        }
422        flow.rankedImportedSubjects = rankedImportedSubjects
423        flow.rankedStudySubjects = rankedStudySubjects  //simpleAssayMeasurementAssayType/create
424
425
426        // the list of the subjects [importedID, studyID]
427        def matchingSubjects = []
428        def mapToSave = [:]
429
430        // creation of a map to save into the SAM database
431        // exemple data = [subject1 : [time1 : (type1 , 1.2), time2 : 2.3], subject2 : [time1 : 3.4, time2 : 7.4]]
432        for (i in 0..flow.rankedStudySubjects.size()-1){
433            def matchingSubjectstmp = []
434            matchingSubjectstmp.add(flow.rankedStudySubjects.getAt(i))
435            matchingSubjectstmp.add(flow.rankedImportedSubjects.getAt(i))
436            matchingSubjects.add(matchingSubjectstmp)
437
438            def subjectAssaysList = []
439            int pos = 0
440            for (j in flow.assayValuesMap) {
441                def studyAssayMap = [:]
442                j.each { key,value ->
443                    def valuesList = []
444                    valuesList.add(flow.typeList.getAt(pos))
445                    valuesList.add(value.getAt(i))
446                    studyAssayMap[key]= valuesList
447                    pos = pos + 1
448                }
449                subjectAssaysList.add(studyAssayMap)
450            }
451            mapToSave[flow.rankedStudySubjects.getAt(i)]= subjectAssaysList
452        }
453        flow.subjects = matchingSubjects
454        flow.times    = flow.assayValuesMap
455        flow.data     = mapToSave
456    }
457
458
459    /**
460     * This method handles a file being uploaded and storing it in a temporary directory
461     * and returning a workbook
462     *
463     * @param formfilename name used for the file field in the form
464     * @return workbook object reference
465     */
466    private HSSFWorkbook handleUpload() {
467        def tempfile = null // new File("/home/lupin/projects/eclipse/sam/testTjeerdCode.xls")
468        //tempfile.leftShift(pastedData)
469        //def tempfile = new File("/home/ademcan/Desktop/testSAM.xls")
470        POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(tempfile))
471        HSSFWorkbook    wb = new HSSFWorkbook(fs);
472        return wb
473        //return ImporterService.getWorkbook(new FileInputStream(tempfile))
474    }
475
476
477
478
479
480
481    /* Create a Horrible Style Sheet Framework Workbook from tab-delimited string.
482     *
483     * @param  tabbed:  A tab-delimited string representing a table as by copy and paste from 
484     *                  an Excell sheet.
485     * @return HSSFWorkbook containing the same table.
486     *       
487     */
488    private HSSFWorkbook getWorkBook( tabbed ) {
489
490        //tabbed=tabbed.trim()
491        //def wb = new HSSFWorkbook()
492
493        def wb
494        if (tabbed instanceof File){
495            def fs = new POIFSFileSystem(new FileInputStream(tabbed))
496            wb = new HSSFWorkbook(fs)
497        }
498        else {
499            wb = new HSSFWorkbook()
500        }
501
502        def createHelper = wb.getCreationHelper()
503        def sheet = wb.createSheet("sheet")
504
505        def rowIndex = 0
506        tabbed.splitEachLine("\t") { line ->
507            def row = sheet.createRow((short)rowIndex++)
508            def colIndex = 0
509            line.each{ entry ->
510                def cell = row.createCell(colIndex++).setCellValue(entry)
511            }
512        }
513
514        return wb
515    }
516
517
518
519    /* Convenience method for isConsistentWithGSCF()
520     * see there for documentation.
521     *
522     * @param  subject: a GSCF subject.name string
523     * @param  time:    a GSCF time string for the event that belongs to a sample in GSCF
524     * @param  samples: a list of sample related strings as given by the CommunicationManager
525     * @return true, if the pair (subject, time) exists as a sample in GSCF (for the current
526     *         assay)
527     */
528    private boolean sampleExists( subject, time, samples )
529    {
530        for( sample in samples ) {
531            def sampleSubject = sample['subject'].toString()
532            def sampleTime    = sample['startTime'].toString()
533            if( subject.toString()==sampleSubject && time.toString()==sampleTime ) {
534                return true
535            }
536        }
537
538        return false
539    }
540
541
542
543    /**
544     * This method checks whether the data imported and matched by the user is
545     * still consistent with the subject names and sample times found in GSCF.
546     *
547     * We veryfy that:
548     * (1) All subject names (i.e. names in GSCF) with non-null measurement occur in GSCF.
549     * (2) All pairs (time, subject name) selected by the user correspond to a sample existing in GSCF.
550     *
551     * @param  externalAssayID Id of an assay to be tested
552     * @param  subjects:  Map of subjects, mapping GSCF subject names to local names given by the user as column names
553     * @param  ttimes:    Map of times, mapping GSCF times names to local times given by the user as column names
554     * @param  data:      Big data map containing all meassurements. GSCF subject names are mapped to a second-level
555     *                    map M2. M2 maps GSCF times to list-pairs of measurement type and value. Example:
556     *
557     *                    [ subject1 : [ time1 : [type1, 1.2], time2 : [type2, 2.3] ],
558     *                      subject2 : [ time1 : [type1, 3.4], time2 : [type2, 7.4] ]  ]
559     *
560     *                    Here, we ignore the actual values pairs and only look at data like this:
561     *
562     *                    [ subject1 : [ time1 : XXX, time2 : XXX ],
563     *                      subject2 : [ time1 : XXX, time2 : XXX ]  ]
564     *
565     *                    where XXX is ignored.
566     *
567     * @return true if data is still in sync with GSCF
568     */
569    public boolean isConsistentWithGSCF( externalAssayID, subjects, ttimes, data, samples ) {
570 
571        // Loop through all pairs ( GSCF subject names,  Time) selected by the user.
572        // Check whether the corresponding sample exists in GSCF.
573
574        def returnValue = true
575        data.each{ subject, list ->       
576            if( returnValue )  list.each{ pair ->
577                if( returnValue )  pair.keySet().each { time ->
578                    if( !sampleExists( subject, time, samples) ) {
579                        returnValue = false
580                    }
581                }
582            }
583        }                                                      // remark: calling break in a closure does not work
584
585        return returnValue
586    }
587
588
589
590    /** Save user-imported data on SAM. Before saving, check if subjects and times
591     *  given by the user are still consistent with the GSCF. Then save the data.
592     *  Then check for inconsistencies again. If inconsistencies arise at any point
593     *  don't save changes, or even roll back changes made, and return false.
594     *
595     *
596     * @param  externalAssayID Id of an assay to be tested
597     * @param  subjects:  Map of subjects, mapping GSCF subject names to local names given by the user as column names
598     * @param  ttimes:    Map of times, mapping GSCF times names to local times given by the user as column names
599     * @param  data:      Big data map containing all meassurements. GSCF subject names are mapped to a second-level
600     *                    map M2. M2 maps GSCF times to list-pairs of measurement type and value. Example:
601     *
602     *                    [ subject1 : [ time1 : [type1, 1.2], time2 : [type2, 2.3] ],
603     *                      subject2 : [ time1 : [type1, 3.4], time2 : [type2, 7.4] ]  ]
604     *
605     * @return true if data is still in sync with GSCF
606     */
607    private Object saveSAMforSubjectLayout( selectedStudyID, externalAssayID, assayName, subjects, ttimes, data ) {
608        def warnings = saveMeasurements( selectedStudyID, externalAssayID, subjects, ttimes, data )
609        saveAssay( externalAssayID, selectedStudyID, assayName )
610        return warnings
611    }
612
613
614
615    /* convenience method for saveMeasurements()
616     *
617     * There should be error handling added here.
618     *
619     * This method saves measurement data to the database by
620     * creating new instances of domain objects classes.
621     */
622    private String saveMeasurement( assaySamples, subject, time, list, type, value, assay ) {
623
624        def measurementType =
625        SimpleAssayMeasurementType.find( "from SimpleAssayMeasurementType as s where s.name = '${type}'" )
626
627        // TODO: check here whether a measurementType was actually found (and handle '-' )
628        if (!measurementType) {
629            return "Could not find measurement type ${type}"
630        }
631
632        def sample =  assaySamples.find{ it.startTime==time && it.subject==subject && it.assay==assay }
633        if( !sample ) {                 // this is only a work-around; if a sample cannot be found, this is actually an error; gscf should supply it.
634            return    "Could not save Measurement. Sample not found in GSCF for time ${time}, subject ${subject}"
635        }
636
637        // possible error: type mismatch
638        // possible error: sample not found
639
640        def measurementQuery = "from SimpleAssayMeasurement as m where m.assay= ${assay.id} " +
641                                "AND m.type= ${measurementType.id} AND m.sample = ${sample.id} "
642        def measurement = SimpleAssayMeasurementType.find( measurementQuery )
643
644        if(measurement) {
645            measurement.value  = value.toDouble()
646            measurement.sample = sample
647            measurement.type   = measurementType
648            measurement.value  = value.toDouble()
649        }
650        else {
651            measurement = new SimpleAssayMeasurement(
652                startTime:time, value:value, type:measurementType, assay:assay, sample:sample )
653        }
654
655        // save(flush:true) and discard() are neccessary here.
656        // Without, grails tries to serialise the object (measurement) to
657        // store it in the 'persistenceContext' of the flow object.
658        // This in turn leads to an error, as persistent domain objects are
659        // usually not serializable. Grails strange behaviour is documented.
660        // discard() tells hibernate to 'forget' a domain object.
661        measurement.type.discard()
662        measurement.save(flush:true)
663        sample.discard()           // needed at all?
664        sample.save(flush:true)    // needed at all?
665        return null
666    }
667
668
669
670
671    /* convenience method for saveSAM()
672     *
673     * @return  list of SimpleAssayMeasurements that have been saved in SAM. The list is mixed:
674     *          it contains measurements that are new and also measurements that have been modified.
675     *          The modified measurements contain their old values in the transient member oldValue.
676     */
677    private List saveMeasurements( externalStudyID, externalAssayID, subjects, ttimes, data ) {
678
679        def warnings = []
680        def assaySamples = getSamples( externalAssayID )
681        def assay = SimpleAssay.find("from SimpleAssay s where s.externalAssayID = '${externalAssayID}'")
682
683        data.each{ subject, list ->
684            list.each{ pair ->
685                pair.each { time, typeValuePair ->
686                    def type  = typeValuePair[0]
687                    def value = typeValuePair[1]
688                    def warning = saveMeasurement( assaySamples, subject, time, list, type, value, assay )
689                    if(warning) { warnings.add warning }
690                }
691            }
692        }
693
694        return warnings
695    }
696
697
698
699
700    /* convenience method for saveSAM() */
701    private SimpleAssay saveAssay( externalAssayID, externalStudyID, assayName ) {
702        def assay = SimpleAssayMeasurement.find( "from SimpleAssay as s where s.externalAssayID='${externalAssayID}'" )
703        if(!assay) { assay = new SimpleAssay(
704                externalAssayID:externalAssayID,
705                externalStudyID:externalStudyID,
706                isNew:true,
707                name:assayName ) }
708        else       { assay.isNew = false }
709        assay.save(flush:true)
710        return assay
711    }
712
713
714
715    /* convenience method for importer flow)
716    in future, use samples from cache!    */
717    private getSamples( externalAssayID )
718    {
719        def assay = SimpleAssay.find( "from SimpleAssay s where s.externalAssayID = '${externalAssayID}'" )
720        def samples = []
721        for ( i in CommunicationManager.getSamples(externalAssayID) ) {
722            samples.add( getSample(assay,i) )
723        }
724        assay.save(flush:true)
725        return samples.unique()
726    }
727
728
729
730    /** convenience method for get samples
731     *  Get all Samples belonging to map.
732     *  If Sample does no occure in SAM yet, then create a new Sample object in SAM
733     *  based on Sample domain object from GSCF. */
734    private getSample(assay,map)
735    {
736        def subject  = map[  'subject']
737        def time     = map['startTime']
738        def material = map[ 'material']
739        def name     = map[     'event']
740        def externalSampleId = map['sampleToken']
741        def query  = "from SimpleAssaySample as s where s.assay= '${assay.id}' and s.subject = '${subject}' AND s.startTime = '${time}' "
742        def sample = SimpleAssaySample.find(query)
743        if(!sample)
744        {
745            sample = new SimpleAssaySample( assay:assay, startTime:time, name:name, material:material,
746                subject:subject, externalSampleId:externalSampleId )
747            sample.save(flush:true)
748        }
749        assay.addToSamples(sample)
750        return sample
751    }
752
753
754
755    /* add to flow scope all data need for building the table view */
756    private buildModel(flow)
757    {
758   
759        def datamatrix = []         // the values containes into the lines
760        def headersList =  []       // the list of all headers in the file to import
761        //def wb = (flow.tabDelimitedString==null) ? handleUpload() : getWorkBook(flow.tabDelimitedString)
762
763        def wb = getWorkBook(flow.tabDelimitedString)
764
765        // getHeader contains information about headers
766        for (header in ImporterService.getHeader(wb, 0)){
767            headersList.add(header.name)
768        }
769        // getDatamatrix contains the data for each line
770        for (line in importerService.getDatamatrix(wb,0,5,1)){
771            def lines = []
772            for (box in line ){
773                lines.add(box.toString())
774            }
775            datamatrix.add(lines)
776        }
777
778        def typeList = data.SimpleAssayMeasurementType.list()
779        def measurementTypes = []
780
781        for(i in typeList){
782            measurementTypes.add(i.name)
783            i.discard()
784        }
785
786        flow.headersList      = headersList
787        flow.datamatrix       = datamatrix
788        flow.measurementTypes = measurementTypes
789    }
790
791
792
793    /**   Make a list of updated and new SimpleAssayMeasurements containing
794     *   the values provided by the user. Store this list in flow.measurements
795     *
796     *   This method fills three flow variables:
797     *
798     *   (1) flow.measurements     list of measurements changed
799     *   (2) flow.usedSamples     samples effected
800     *   (3) flow.usedTypes       types effected
801     *
802     */
803    private addMeasurementsToFlow(flow, params) {
804        def samples = params['samples'] // get data from form
805        def types   = params['types']
806        def cells   = params['cells']
807        def typeObjects = SimpleAssayMeasurementType.findAll()
808        def sampleObjects = flow.samples
809        def measurements = []
810        def assay = SimpleAssay.find("from SimpleAssay as a where a.externalAssayID = '${flow.selectedAssayID}'")
811        flow.usedSamples = []
812        flow.usedTypes   = []
813
814        samples.each{ row,sampleName ->
815            types.each{ col,typeName ->
816                if( !sampleName.equals('') && !typeName.equals('-') ) {
817                    def ck = col+'.' + row
818                    def value = cells[ck].replace(",",".").toDouble()
819                    measurements.add setMeasurement(flow,sampleObjects,typeObjects,sampleName,typeName,value,assay)
820                }
821            }
822        }
823
824        flow.measurements = measurements
825    }
826
827    /**   Convenience method for fillSamples()
828     *   Make a list of updated and new SimpleAssayMeasurements containing
829     *   the values provided by the user.
830     *
831     *   @return updated or new SimpleAssayMeasurements which is transient.
832     *           The values of the new measurment are set according to the arguments given.
833     */
834    private setMeasurement( flow,samples,types,sampleName,typeName,value,assay ) {
835        def sample = samples.find{ it.externalSampleId==sampleName }
836        def type   = types.find{ it.name==typeName}
837
838        if( !flow.usedSamples.contains(sample) ) { flow.usedSamples.add(sample) }
839        if( !flow.usedTypes.contains(type)     ) { flow.usedTypes.add(type)     }
840
841        def query = "from SimpleAssayMeasurement as m where m.type= '${type.id}' AND m.sample= '${sample.id}'"
842        def measurement =
843        SimpleAssayMeasurement.find(query) ?: new SimpleAssayMeasurement()
844        measurement.assay  = assay
845        measurement.sample = sample
846        measurement.type   = type
847        measurement.value  = value
848        return measurement
849    }
850
851    // Quickly add subAssay method
852    // Used for adding subAssays form the assay importer webflow
853    private addSubAssays(params,flow){
854        for (i in 0..params['name'].size()-1){
855            def subAssay = new SimpleAssayMeasurementType(
856                    name: params['name'].getAt(i),
857                    unit: params['unit'].getAt(i),
858                    detectableLimit: params['limit'].getAt(i).toDouble()
859                )
860                subAssay.save(flush:true)
861                flow.measurementTypes.add(subAssay.name)
862        }
863    }
864
865
866    // classes for fuzzy string matching
867    // <FUZZY MATCHING>
868    static def similarity(l_seq, r_seq, degree=2) {
869        def l_histo = countNgramFrequency(l_seq, degree)
870        def r_histo = countNgramFrequency(r_seq, degree)
871
872        dotProduct(l_histo, r_histo) /
873        Math.sqrt(dotProduct(l_histo, l_histo) *
874            dotProduct(r_histo, r_histo))
875    }
876
877    static def countNgramFrequency(sequence, degree) {
878        def histo = [:]
879        def items = sequence.size()
880
881        for (int i = 0; i + degree <= items; i++)
882        {
883            def gram = sequence[i..<(i + degree)]
884            histo[gram] = 1 + histo.get(gram, 0)
885        }
886        histo
887    }
888
889    static def dotProduct(l_histo, r_histo) {
890        def sum = 0
891        l_histo.each { key, value ->
892            sum = sum + l_histo[key] * r_histo.get(key, 0)
893        }
894        sum
895    }
896
897    static def stringSimilarity (l_str, r_str, degree=2) {
898        similarity(l_str.toLowerCase().toCharArray(),
899            r_str.toLowerCase().toCharArray(),
900            degree)
901    }
902
903    static def mostSimilar(pattern, candidates, threshold=0) {
904        def topScore = 0
905        def bestFit = null
906
907        candidates.each { candidate ->
908            def score = stringSimilarity(pattern, candidate)
909            if (score > topScore) {
910                topScore = score
911                bestFit = candidate
912            }
913        }
914
915        if (topScore < threshold)
916        bestFit = null
917
918        bestFit
919    }
920    // </FUZZY MATCHING>
921
922}
Note: See TracBrowser for help on using the browser.