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

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

Initial import of basic functionality

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