source: trunk/grails-app/services/nl/tno/metagenomics/integration/SynchronizationService.groovy @ 13

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

Improved user interface and implemented basic export functionality

File size: 23.7 KB
Line 
1package nl.tno.metagenomics.integration
2
3import nl.tno.metagenomics.*
4import nl.tno.metagenomics.auth.*
5import org.codehaus.groovy.grails.commons.ConfigurationHolder
6
7class SynchronizationService {
8        def gscfService
9        def trashService
10
11        String sessionToken = ""        // Session token to use for communication
12        User user = null                        // Currently logged in user. Must be set when synchronizing authorization
13        boolean eager = false           // When set to true, this method fetches data about all studies from GSCF. Otherwise, it will only look at the
14        // studies marked as dirty in the database. Defaults to false.
15
16        // Keeps track of the last time this module performed a full synchronization.
17        static Date lastFullSynchronization = null;
18
19        static transactional = true
20
21        /**
22         * Determines whether the synchronization should be performed or not. This can be entered
23         * in configuration, to avoid synchronization when developing.
24         * @return
25         */
26        protected performSynchronization() {
27                def conf = ConfigurationHolder.config.metagenomics.synchronization;
28
29                // If nothing is entered in configuration, return true (default value)
30                if( conf == null )
31                        return true
32
33                return conf
34        }
35       
36        /**
37         * Returns true iff a full synchronization should be performed
38         * @return
39         */
40        public boolean timeForFullSynchronization() {
41                if( SynchronizationService.lastFullSynchronization == null )
42                        return true
43                       
44                // Compute the time since the last full synchronization in  milliseconds
45                Date today = new Date();
46                long difference = SynchronizationService.lastFullSynchronization.getTime() - today.getTime()
47               
48                if( difference / 1000 > ConfigurationHolder.config.metagenomics.fullSynchronization )
49                        return true
50                else
51                        return false
52        }
53
54        /**
55         * Redirects to a temporary page to give the user a 'waiting' page while synchronizing
56         * @return
57         */
58        public String urlForFullSynchronization( def params ) {
59                def returnUrl = ConfigurationHolder.config.grails.serverURL
60                if (params.controller != null){
61                        returnUrl += "/${params.controller}"
62                        if (params.action != null){
63                                returnUrl += "/${params.action}"
64                                if (params.id != null){
65                                        returnUrl += "/${params.id}"
66                                }
67                        }
68                }
69                if( timeForFullSynchronization() ) {
70                        return ConfigurationHolder.config.grails.serverURL + "/synchronize/full?redirect=" + returnUrl.encodeAsURL()
71                } else {
72                        return returnUrl
73                }
74        }
75
76        /**
77         * Performs a full synchronization in order to retrieve all studies
78         * @return
79         */
80        public void fullSynchronization() {
81                def previousEager = this.eager
82                this.eager = true
83                this.synchronizeStudies();
84                this.eager = previousEager
85               
86                SynchronizationService.lastFullSynchronization = new Date();
87        }
88
89        /**
90         * Synchronizes all studies with the data from GSCF.
91         * @return      ArrayList       List of studies or null if the synchronization has failed
92         */
93        public ArrayList<Study> synchronizeStudies() throws NotAuthenticatedException, Exception {
94                if( !performSynchronization() )
95                        return Study.findAllWhereTrashcan(false)
96
97                // When eager fetching is enabled, ask for all studies, otherwise only ask for studies marked dirty
98                // Synchronization is performed on all studies, not only the studies the user has access to. Otherwise
99                // we would never notice that a user was given read-access to a study.
100                def studies
101                if( eager ) {
102                        studies = []
103                        log.trace "Eager synchronization";
104                } else {
105                        studies = Study.findAllWhere( [trashcan: false, isDirty: true] );
106                        log.trace "Default synchronization: " + studies.size()
107
108                        // Perform no synchronization if no studies have to be synchronized
109                        if( studies.size() == 0 )
110                                return []
111                }
112               
113                // Perform synchronization on only one study directly, because otherwise
114                // the getStudies method could throw a ResourceNotFoundException or NotAuthorizedException
115                // that can better be handled by synchronizeStudy
116                if( studies.size() == 1 ) {
117                        def newStudy = synchronizeStudy( studies[0] );
118                        if( newStudy )
119                                return [ newStudy ];
120                        else
121                                return []
122                }
123
124                // Fetch all studies from GSCF
125                def newStudies
126                try {
127                        if( !eager ) {
128                                def studyTokens = studies.studyToken;
129
130                                if( studyTokens instanceof String ) {
131                                        studyTokens = [studyTokens];
132                                }
133                                println "Only updating studies with tokens " + studyTokens.join( ',' );
134                                newStudies = gscfService.getStudies(sessionToken, studyTokens)
135                        } else {
136                                newStudies = gscfService.getStudies(sessionToken)
137                        }
138                } catch( Exception e ) { // All exceptions are thrown.
139                        // Can't retrieve data. Maybe sessionToken has expired or invalid. Anyway, stop
140                        // synchronizing and return null
141                        log.error( "Exception occurred when fetching studies: " + e.getMessage() )
142                        throw new Exception( "Error while fetching studies", e)
143                }
144
145                synchronizeStudies( newStudies );
146                studies = handleDeletedStudies( studies, newStudies );
147
148                log.trace( "Returning " + studies.size() + " studies after synchronization" )
149
150                return studies
151        }
152
153        /**
154         * Synchronizes all studies given by 'newStudies' with existing studies in the database, and adds them
155         * if they don't exist
156         *
157         * @param newStudies            JSON object with studies as returned by GSCF
158         * @return
159         */
160        protected synchronizeStudies( def newStudies ) {
161                // Synchronize all studies that are returned. Studies that are not returned by GSCF might be removed
162                // but could also be invisible for the current user.
163                newStudies.each { gscfStudy ->
164                        if( gscfStudy.studyToken ) {
165                                log.trace( "Processing GSCF study " + gscfStudy.studyToken + ": " + gscfStudy )
166
167                                Study studyFound = Study.findByStudyToken( gscfStudy.studyToken as String )
168
169                                if(studyFound) {
170                                        log.trace( "Study found with name " + studyFound.name )
171
172                                        // Synchronize the study itself with the data retrieved
173                                        synchronizeStudy( studyFound, gscfStudy );
174                                } else {
175                                        log.trace( "Study not found. Creating a new one" )
176
177                                        // If it doesn't exist, create a new object
178                                        studyFound = new Study( studyToken: gscfStudy.studyToken, name: gscfStudy.title, isDirty: true );
179                                        studyFound.save();
180
181                                        // Synchronize authorization and study assays (since the study itself is already synchronized)
182                                        synchronizeAuthorization(studyFound);
183                                        if( studyFound.canRead( user ) )
184                                                synchronizeStudyAssays(studyFound);
185
186                                        // Mark the study as clean
187                                        studyFound.isDirty = false
188                                        studyFound.save();
189                                }
190                        }
191                }
192        }
193
194        /**
195         * Removes studies from the database that are expected but not found in the list from GSCF
196         * @param studies               List with existing studies in the database that were expected in the output of GSCF
197         * @param newStudies    JSON object with studies as returned by GSCF
198         * @return                              List of remaining studies
199         */
200        protected ArrayList<Study> handleDeletedStudies( def studies, def newStudies ) {
201                // If might also be that studies have been removed from the system. In that case, the studies
202                // should be deleted from this module as well. Looping backwards in order to avoid conflicts
203                // when removing elements from the list
204               
205                println "Handle deleted studies: " + studies.size() + " -> " + newStudies.size();
206                def numStudies = studies.size();
207                for( int i = numStudies - 1; i >= 0; i-- ) {
208                        def existingStudy = studies[i];
209
210                        def studyFound = newStudies.find { it.studyToken == existingStudy.studyToken }
211
212                        if( !studyFound ) {
213                                log.trace( "Study " + existingStudy.studyToken + " not found. Check whether it is removed or the user just can't see it." )
214
215                                // Study was not given to us by GSCF. This might be because the study is removed, or because the study is not visible (anymore)
216                                // to the current user.
217                                // Synchronize authorization and see what is the case (it returns null if the study has been deleted)
218                                if( synchronizeAuthorization( existingStudy ) == null ) {
219                                        // Update studies variable to keep track of all existing studies
220                                        studies.remove( existingStudy )
221                                }
222                        }
223                }
224
225                return studies
226        }
227
228        /**
229         * Synchronizes the given study with the data from GSCF
230         * @param       study   Study to synchronize
231         * @return      Study   Synchronized study or null if the synchronization has failed
232         */
233        public Study synchronizeStudy(Study study ) {
234                if( !performSynchronization() )
235                        return study
236
237                if( study == null )
238                        return null
239
240                // Trashcan should never be synchronized
241                if( study.trashcan )
242                        return study
243                       
244                // If the study hasn't changed, don't update anything
245                if( !eager && !study.isDirty )
246                        return study;
247
248                // Retrieve the study from GSCF
249                def newStudy
250                try {
251                        newStudy = gscfService.getStudy(sessionToken, study.studyToken)
252                } catch( NotAuthorizedException e ) {
253                        // User is not authorized to access this study. Update the authorization within the module and return
254                        synchronizeAuthorization( study );
255                        return null
256                } catch( ResourceNotFoundException e ) {
257                        // Study can't be found within GSCF.
258                        trashService.moveToTrash( study );
259                        return null
260                } catch( Exception e ) { // All other exceptions
261                        // Can't retrieve data. Maybe sessionToken has expired or invalid. Anyway, stop
262                        // synchronizing and return null
263                        e.printStackTrace()
264                        log.error( "Exception occurred when fetching study " + study.studyToken + ": " + e.getMessage() )
265                        throw new Exception( "Error while fetching study " + study.studyToken, e)
266                }
267
268                // If no study is returned, something went wrong.
269                if( newStudy.size() == 0 ) {
270                        throw new Exception( "No data returned for study " + study.studyToken + " but no error has occurred either. Please contact your system administrator" );
271                        return null;
272                }
273
274                return synchronizeStudy(study, newStudy);
275        }
276
277        /**
278         * Synchronizes the given study with the data from GSCF
279         * @param       study           Study to synchronize
280         * @param       newStudy        Data to synchronize the study with
281         * @return      Study           Synchronized study or null if the synchronization has failed
282         */
283        protected Study synchronizeStudy(Study study, def newStudy) {
284                if( !performSynchronization() )
285                        return study
286
287                if( study == null || newStudy == null)
288                        return null
289
290                // If the study hasn't changed, don't update anything
291                if( !eager && !study.isDirty ) {
292                        return study;
293                }
294
295                // If no study is returned, something went wrong.
296                if( newStudy.size() == 0 ) {
297                        return null;
298                }
299
300                // Mark study dirty to enable synchronization
301                study.isDirty = true;
302                synchronizeAuthorization( study );
303                if( study.canRead( user ) )
304                        synchronizeStudyAssays( study );
305
306                // Update properties and mark as clean
307                study.name = newStudy.title
308                study.isDirty = false;
309                study.save(flush:true)
310
311                return study
312        }
313
314        /**
315         * Synchronizes the assays of the given study with the data from GSCF
316         * @param study         Study of which the assays should be synchronized
317         * @return      ArrayList       List of assays or null if the synchronization has failed
318         */
319        protected ArrayList<Assay> synchronizeStudyAssays( Study study ) {
320                if( !performSynchronization() )
321                        return study.assays.toList()
322
323                if( !eager && !study.isDirty )
324                        return study.assays as List
325
326                // Also update all assays, belonging to this study
327                // Retrieve the assays from GSCF
328                def newAssays
329                try {
330                        newAssays = gscfService.getAssays(sessionToken, study.studyToken)
331                } catch( Exception e ) { // All exceptions are thrown. If we get a NotAuthorized or NotFound Exception, something has changed in between the two requests. This will result in an error
332                        // Can't retrieve data. Maybe sessionToken has expired or invalid. Anyway, stop
333                        // synchronizing and return null
334                        log.error( "Exception occurred when fetching assays for study " + study.studyToken + ": " + e.getMessage() )
335                        throw new Exception( "Error while fetching samples for assay " + study.studyToken, e)
336                }
337
338                // If no assay is returned, we remove all assays
339                // from this study and return an empty list
340                if( newAssays.size() == 0 && study.assays != null ) {
341                        def studyAssays = study.assays.toArray();
342                        def numStudyAssays = study.assays.size();
343                        for( int i = numStudyAssays - 1; i >= 0; i-- ) {
344                                def existingAssay = studyAssays[i];
345                               
346                                // Move data to trash
347                                trashService.moveToTrash( existingAssay );
348                        }
349
350                        return []
351                }
352
353                synchronizeStudyAssays( study, newAssays );
354                return handleDeletedAssays( study, newAssays );
355        }
356
357        /**
358         * Synchronizes the assays of a study with the given data from GSCF
359         * @param study         Study to synchronize
360         * @param newAssays     JSON object given by GSCF to synchronize the assays with
361         */
362        protected void synchronizeStudyAssays( Study study, def newAssays ) {
363                // Otherwise, we search for all assays in the new list, if they
364                // already exist in the list of assays
365                newAssays.each { gscfAssay ->
366                        if( gscfAssay.assayToken ) {
367                                log.trace( "Processing GSCF assay " + gscfAssay.assayToken + ": " + gscfAssay )
368
369                                Assay assayFound = study.assays.find { it.assayToken == gscfAssay.assayToken }
370
371                                if(assayFound) {
372                                        log.trace( "Assay found with name " + assayFound.name )
373
374                                        // Synchronize the assay itself with the data retrieved
375                                        synchronizeAssay( assayFound, gscfAssay );
376                                } else {
377                                        log.trace( "Assay not found in study. Creating a new one" )
378
379                                        // If it doesn't exist, create a new object
380                                        assayFound = new Assay( assayToken: gscfAssay.assayToken, name: gscfAssay.name, study: study );
381
382                                        log.trace( "Connecting assay to study" )
383                                        study.addToAssays( assayFound );
384                                        assayFound.save()
385
386                                        // Synchronize assay samples (since the assay itself is already synchronized)
387                                        synchronizeAssaySamples(assayFound)
388                                }
389                        }
390                }
391        }
392
393        /**
394         * Removes assays from the system that have been deleted from GSCF
395         * @param study         Study to synchronize
396         * @param newAssays     JSON object given by GSCF to synchronize the assays with
397         * @return      List with all assays from the study
398         */
399        protected ArrayList<Assay> handleDeletedAssays( Study study, def newAssays ) {
400                if( study.assays == null ) {
401                        return []
402                }
403
404                // If might also be that assays have been removed from this study. In that case, the removed assays
405                // should be deleted from this study in the module as well. Looping backwards in order to avoid conflicts
406                // when removing elements from the list
407                def assays = study.assays.toArray();
408                def numAssays = assays.size();
409                for( int i = numAssays - 1; i >= 0; i-- ) {
410                        def existingAssay = assays[i];
411
412                        Assay assayFound = newAssays.find { it.assayToken == existingAssay.assayToken }
413
414                        if( !assayFound ) {
415                                log.trace( "Assay " + existingAssay.assayToken + " not found. Removing it." )
416
417                                // The assay has been removed
418                                trashService.moveToTrash( existingAssay );
419                        }
420                }
421
422                return study.assays.toList()
423        }
424
425        /**
426         * Retrieves the authorization for the currently logged in user
427         * Since GSCF only provides authorization information about the currently
428         * logged in user, we can not guarantee that the authorization information
429         * is synchronized for all users.
430         *
431         * Make sure synchronizationService.user is set beforehand
432         * 
433         * @param study Study to synchronize authorization for
434         * @return      Auth object for the given study and user
435         */
436        public Auth synchronizeAuthorization(Study study) {
437                // If the user is not set, we can't save anything to the database.
438                if( user == null ) {
439                        throw new Exception( "Property user of SynchronizationService must be set to the currently logged in user" );
440                }
441
442                // Only perform synchronization if needed
443                if( !eager && !study.isDirty )
444                        return Auth.findByUserAndStudy( user, study )
445
446                if( !performSynchronization() )
447                        return Auth.findByUserAndStudy( user, study )
448
449                def gscfAuthorization
450                try {
451                        gscfAuthorization = gscfService.getAuthorizationLevel( sessionToken, study.studyToken )
452                } catch( ResourceNotFoundException e ) {
453                        // Study has been deleted, remove all authorization on that study
454                        log.trace( "Study " + study.studyToken + " has been deleted. Remove all authorization on that study")
455                        trashService.moveToTrash( study );
456
457                        return null
458                }
459
460                // Update the authorization object, or create a new one
461                Auth a = Auth.authorization( study, user )
462
463                if( !a ) {
464                        log.trace( "Authorization not found for " + study.studyToken + " and " + user.username + ". Creating a new object" );
465
466                        a = Auth.createAuth( study, user );
467                }
468
469                // Copy properties from gscf object
470                if( gscfAuthorization.canRead instanceof Boolean  )
471                        a.canRead = gscfAuthorization.canRead.booleanValue()
472               
473                if( gscfAuthorization.canWrite instanceof Boolean )
474                        a.canWrite = gscfAuthorization.canWrite.booleanValue()
475               
476                if( gscfAuthorization.isOwner instanceof Boolean )
477                        a.isOwner = gscfAuthorization.isOwner.booleanValue()
478                       
479                a.save()
480
481                return a
482        }
483
484        /**
485         * Synchronizes the given assay with the data from GSCF
486         * @param assay         Assay to synchronize
487         * @return      Assay   Synchronized assay or null if the synchronization has failed
488         */
489        public Assay synchronizeAssay(Assay assay) {
490                if( !performSynchronization() )
491                        return assay
492
493                if( assay == null )
494                        return null
495
496                // Only perform synchronization if needed
497                if( !eager && !assay.study.isDirty )
498                        return assay
499
500                // Retrieve the assay from GSCF
501                def newAssay
502                try {
503                        newAssay = gscfService.getAssay(sessionToken, assay.study.studyToken, assay.assayToken)
504                } catch( NotAuthorizedException e ) {
505                        // User is not authorized to access this study. Update the authorization within the module and return
506                        synchronizeAuthorization( assay.study );
507                        return null
508                } catch( ResourceNotFoundException e ) {
509                        // Assay can't be found within GSCF.
510                        trashService.moveToTrash( assay );
511                        return null
512                } catch( Exception e ) { // All other exceptions are thrown
513                        // Can't retrieve data. Maybe sessionToken has expired or invalid. Anyway, stop
514                        // synchronizing and return null
515                        log.error( "Exception occurred when fetching assay " + assay.assayToken + ": " + e.getMessage() )
516                        throw new Exception( "Error while fetching assay " + assay.assayToken, e)
517                }
518
519                // If new assay is empty, this means that the assay does exist, but now belongs to another module. Remove it from our system
520                if( newAssay.size() == 0 ) {
521                        log.info( "No data is returned by GSCF for assay  " + assay.assayToken + "; probably the assay is connected to another module." )
522                        trashService.moveToTrash( assay );
523                        return null;
524                }
525
526                return synchronizeAssay( assay, newAssay );
527        }
528
529        /**
530         * Synchronizes the given assay with the data given
531         * @param assay         Assay to synchronize
532         * @param newAssay      New data for the assay, retrieved from GSCF
533         * @return      Assay   Synchronized assay or null if the synchronization has failed
534         */
535        protected Assay synchronizeAssay(Assay assay, def newAssay) {
536                if( !performSynchronization() )
537                        return assay
538
539                if( assay == null || newAssay == null )
540                        return null
541
542                // Only perform synchronization if needed
543                if( !eager && !assay.study.isDirty )
544                        return assay
545
546                // If new assay is empty, something went wrong
547                if( newAssay.size() == 0 ) {
548                        return null;
549                }
550
551                log.trace( "Assay is found in GSCF: " + assay.name + " / " + newAssay )
552                if( newAssay?.name ) {
553                        assay.name = newAssay.name
554                        assay.save()
555                }
556
557                // Synchronize samples
558                synchronizeAssaySamples(assay);
559
560                return assay
561        }
562
563        /**
564         * Synchronizes the samples of a given assay with the data from GSCF
565         * @param       assay   Assay to synchronize
566         * @return      Sample  List of samples or null if the synchronization failed
567         */
568        protected ArrayList<AssaySample> synchronizeAssaySamples(Assay assay) {
569                if( !performSynchronization() )
570                        return assay.assaySamples.toList()
571
572                // If no assay is given, return null
573                if( assay == null )
574                        return null
575
576                // Retrieve the assay from GSCF
577                def newSamples
578                try {
579                        newSamples = gscfService.getSamples(sessionToken, assay.assayToken)
580                } catch( NotAuthorizedException e ) {
581                        // User is not authorized to access this study. Update the authorization within the module and return
582                        synchronizeAuthorization( assay.study );
583                        return null
584                } catch( ResourceNotFoundException e ) {
585                        // Assay can't be found within GSCF. Samples will be removed
586                        trashService.moveToTrash( assay );
587
588                        return null
589                } catch( Exception e ) {
590                        // Can't retrieve data. Maybe sessionToken has expired or invalid. Anyway, stop
591                        // synchronizing and return null
592                        log.error( "Exception occurred when fetching samples for assay " + assay.assayToken + ": " + e.getMessage() )
593                        throw new Exception( "Error while fetching samples for assay " + assay.assayToken, e)
594                }
595
596                // If no sample is returned, we remove all samples from the list
597                if( newSamples.size() == 0 ) {
598                        assay.removeAssaySamples();
599                        return []
600                }
601
602                synchronizeAssaySamples( assay, newSamples );
603                return handleDeletedSamples( assay, newSamples );
604        }
605
606        /**
607         * Synchronize all samples for a given assay with the data from GSCF
608         * @param assay                 Assay to synchronize samples for
609         * @param newSamples    New samples in JSON object, as given by GSCF
610         */
611        protected void synchronizeAssaySamples( Assay assay, def newSamples ) {
612                // Otherwise, we search for all samples in the new list, if they
613                // already exist in the list of samples
614                newSamples.each { gscfSample ->
615                        log.trace( "Processing GSCF sample " + gscfSample.sampleToken + ": " + gscfSample )
616                        if( gscfSample.name ) {
617
618                                AssaySample assaySampleFound = assay.assaySamples.find { it.sample.sampleToken == gscfSample.sampleToken }
619                                Sample sampleFound
620
621                                if(assaySampleFound) {
622                                        sampleFound = assaySampleFound.sample
623                                        log.trace( "AssaySample found with sample name " + sampleFound.name )
624
625                                        // Update the sample object if necessary
626                                        sampleFound.name = gscfSample.name
627                                        sampleFound.subject = gscfSample.subject
628                                        sampleFound.event = gscfSample.event + ( gscfSample.startTime ? " (" + gscfSample.startTime + ")" : "" )
629                                        sampleFound.save();
630                                } else {
631                                        log.trace( "AssaySample not found in assay" )
632
633                                        // Check if the sample already exists in the database.
634                                        sampleFound = Sample.findBySampleTokenAndStudy( gscfSample.sampleToken as String, assay.study )
635
636                                        if( sampleFound ){
637                                                log.trace( "Sample " + gscfSample.sampleToken + " is found in database. Updating if necessary" )
638
639                                                // Update the sample object if necessary
640                                                sampleFound.name = gscfSample.name
641                                                sampleFound.subject = gscfSample.subject
642                                                sampleFound.event = gscfSample.event + ( gscfSample.startTime ? " (" + gscfSample.startTime + ")" : "" )
643                                                sampleFound.save();
644                                        } else {
645                                                log.trace( "Sample " + gscfSample.sampleToken + " not found in database. Creating a new object." )
646
647                                                // If it doesn't exist, create a new object
648                                                sampleFound = new Sample( sampleToken: gscfSample.sampleToken, name: gscfSample.name, study: assay.study );
649                                                sampleFound.subject = gscfSample.subject
650                                                sampleFound.event = gscfSample.event + ( gscfSample.startTime ? " (" + gscfSample.startTime + ")" : "" )
651                                                assay.study.addToSamples( sampleFound );
652                                                sampleFound.save();
653                                        }
654
655                                        // Create a new assay-sample combination
656                                        log.trace( "Connecting sample to assay" )
657                                        assaySampleFound = new AssaySample();
658
659                                        assay.addToAssaySamples( assaySampleFound );
660                                        sampleFound.addToAssaySamples( assaySampleFound );
661
662                                        assaySampleFound.save()
663                                }
664                        }
665                }
666        }
667
668        /**
669         * Removes samples from the system that have been removed from an assay in GSCF
670         * @param assay                 Assay to remove samples for
671         * @param newSamples    JSON object with all samples for this assay as given by GSCF
672         */
673        protected ArrayList<AssaySample> handleDeletedSamples( Assay assay, def newSamples ) {
674                // If might also be that samples have been removed from this assay. In that case, the removed samples
675                // should be deleted from this assay. Looping backwards in order to avoid conflicts when removing elements
676                // from the list
677                if( assay.assaySamples != null ) {
678                        def assaySamples = assay.assaySamples.toArray();
679                        def numAssaySamples = assay.assaySamples.size();
680                        for( int i = numAssaySamples - 1; i >= 0; i-- ) {
681                                def existingSample = assaySamples[i];
682
683                                AssaySample sampleFound = newSamples.find { it.sampleToken == existingSample.sample.sampleToken }
684
685                                if( !sampleFound ) {
686                                        log.trace( "Sample " + existingSample.sample.sampleToken + " not found. Removing it." )
687
688                                        // The sample has been removed
689                                        trashService.moveToTrash( existingSample );
690                                }
691                        }
692                }
693
694                // Create a list of samples to return
695                if( assay.assaySamples )
696                        return assay.assaySamples.toList()
697                else
698                        return []
699        }
700}
Note: See TracBrowser for help on using the repository browser.