links associated with non-positional features do not need a 0_0 appended to their...
[jalview.git] / src / jalview / ws / DasSequenceFeatureFetcher.java
1 /*\r
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.4)\r
3  * Copyright (C) 2008 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle\r
4  * \r
5  * This program is free software; you can redistribute it and/or\r
6  * modify it under the terms of the GNU General Public License\r
7  * as published by the Free Software Foundation; either version 2\r
8  * of the License, or (at your option) any later version.\r
9  * \r
10  * This program is distributed in the hope that it will be useful,\r
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
13  * GNU General Public License for more details.\r
14  * \r
15  * You should have received a copy of the GNU General Public License\r
16  * along with this program; if not, write to the Free Software\r
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA\r
18  */\r
19 package jalview.ws;\r
20 \r
21 import java.net.*;\r
22 import java.util.*;\r
23 \r
24 import javax.swing.*;\r
25 \r
26 import org.biojava.dasobert.das.*;\r
27 import org.biojava.dasobert.das2.*;\r
28 import org.biojava.dasobert.das2.io.*;\r
29 import org.biojava.dasobert.dasregistry.*;\r
30 import org.biojava.dasobert.eventmodel.*;\r
31 import jalview.bin.Cache;\r
32 import jalview.datamodel.*;\r
33 import jalview.gui.*;\r
34 \r
35 /**\r
36  * DOCUMENT ME!\r
37  * \r
38  * @author $author$\r
39  * @version $Revision$\r
40  */\r
41 public class DasSequenceFeatureFetcher\r
42 {\r
43   SequenceI[] sequences;\r
44 \r
45   AlignFrame af;\r
46 \r
47   FeatureSettings fsettings;\r
48 \r
49   StringBuffer sbuffer = new StringBuffer();\r
50 \r
51   Vector selectedSources;\r
52 \r
53   boolean cancelled = false;\r
54 \r
55   private void debug(String mesg)\r
56   {\r
57     debug(mesg, null);\r
58   }\r
59 \r
60   private void debug(String mesg, Exception e)\r
61   {\r
62     if (Cache.log != null)\r
63     {\r
64       Cache.log.debug(mesg, e);\r
65     }\r
66     else\r
67     {\r
68       System.err.println(mesg);\r
69       if (e != null)\r
70       {\r
71         e.printStackTrace();\r
72       }\r
73     }\r
74   }\r
75 \r
76   long startTime;\r
77 \r
78   /**\r
79    * Creates a new SequenceFeatureFetcher object. Uses default\r
80    * \r
81    * @param align\r
82    *                DOCUMENT ME!\r
83    * @param ap\r
84    *                DOCUMENT ME!\r
85    */\r
86   public DasSequenceFeatureFetcher(SequenceI[] sequences,\r
87           FeatureSettings fsettings, Vector selectedSources)\r
88   {\r
89     this(sequences, fsettings, selectedSources, true, true);\r
90   }\r
91 \r
92   public DasSequenceFeatureFetcher(SequenceI[] sequences,\r
93           FeatureSettings fsettings, Vector selectedSources,\r
94           boolean checkDbrefs, boolean promptFetchDbrefs)\r
95   {\r
96     this.selectedSources = selectedSources;\r
97     this.sequences = sequences;\r
98     if (fsettings != null)\r
99     {\r
100       this.fsettings = fsettings;\r
101       this.af = fsettings.af;\r
102       af.getViewport().setShowSequenceFeatures(true);\r
103     }\r
104     int uniprotCount = 0;\r
105     for (int i = 0; i < selectedSources.size(); i++)\r
106     {\r
107       DasSource source = (DasSource) selectedSources.elementAt(i);\r
108       DasCoordinateSystem[] coords = source.getCoordinateSystem();\r
109       for (int c = 0; c < coords.length; c++)\r
110       {\r
111         // TODO: match UniProt coord system canonically (?) - does\r
112         // UniProt==uniprot==UNIPROT ?\r
113         if (coords[c].getName().indexOf("UniProt") > -1)\r
114         {\r
115           uniprotCount++;\r
116           break;\r
117         }\r
118       }\r
119     }\r
120 \r
121     int refCount = 0;\r
122     for (int i = 0; i < sequences.length; i++)\r
123     {\r
124       DBRefEntry[] dbref = sequences[i].getDBRef();\r
125       if (dbref != null)\r
126       {\r
127         for (int j = 0; j < dbref.length; j++)\r
128         {\r
129           if (dbref[j].getSource().equals(\r
130                   jalview.datamodel.DBRefSource.UNIPROT))\r
131           {\r
132             refCount++;\r
133             break;\r
134           }\r
135         }\r
136       }\r
137     }\r
138 \r
139     if (checkDbrefs && refCount < sequences.length && uniprotCount > 0)\r
140     {\r
141 \r
142       int reply = JOptionPane.YES_OPTION;\r
143       if (promptFetchDbrefs)\r
144       {\r
145         reply = JOptionPane\r
146                 .showInternalConfirmDialog(\r
147                         Desktop.desktop,\r
148                         "Do you want Jalview to find\n"\r
149                                 + "Uniprot Accession ids for given sequence names?",\r
150                         "Find Uniprot Accession Ids",\r
151                         JOptionPane.YES_NO_OPTION,\r
152                         JOptionPane.QUESTION_MESSAGE);\r
153       }\r
154 \r
155       if (reply == JOptionPane.YES_OPTION)\r
156       {\r
157         Thread thread = new Thread(new FetchDBRefs());\r
158         thread.start();\r
159       }\r
160       else\r
161       {\r
162         startFetching();\r
163       }\r
164     }\r
165     else\r
166     {\r
167       startFetching();\r
168     }\r
169 \r
170   }\r
171 \r
172   class FetchDBRefs implements Runnable\r
173   {\r
174     public void run()\r
175     {\r
176       new DBRefFetcher(sequences, af).fetchDBRefs(true);\r
177       startFetching();\r
178     }\r
179   }\r
180 \r
181   /**\r
182    * Spawns a number of dasobert Fetcher threads to add features to sequences in\r
183    * the dataset\r
184    */\r
185   void startFetching()\r
186   {\r
187     cancelled = false;\r
188     startTime = System.currentTimeMillis();\r
189     if (af != null)\r
190     {\r
191       af.setProgressBar("Fetching DAS Sequence Features", startTime);\r
192     }\r
193 \r
194     DasSource[] sources = new jalview.gui.DasSourceBrowser().getDASSource();\r
195 \r
196     if (selectedSources == null || selectedSources.size() == 0)\r
197     {\r
198       String active = jalview.bin.Cache.getDefault("DAS_ACTIVE_SOURCE",\r
199               "uniprot");\r
200       StringTokenizer st = new StringTokenizer(active, "\t");\r
201       Vector selectedSources = new Vector();\r
202       String token;\r
203       while (st.hasMoreTokens())\r
204       {\r
205         token = st.nextToken();\r
206         for (int i = 0; i < sources.length; i++)\r
207         {\r
208           if (sources[i].getNickname().equals(token))\r
209           {\r
210             selectedSources.addElement(sources[i]);\r
211             break;\r
212           }\r
213         }\r
214       }\r
215     }\r
216 \r
217     if (selectedSources == null || selectedSources.size() == 0)\r
218     {\r
219       System.out.println("No DAS Sources active");\r
220       cancelled = true;\r
221       setGuiNoDassourceActive();\r
222       return;\r
223     }\r
224 \r
225     sourcesRemaining = selectedSources.size();\r
226     // Now sending requests one at a time to each server\r
227     for (int sourceIndex = 0; sourceIndex < selectedSources.size()\r
228             && !cancelled; sourceIndex++)\r
229     {\r
230       DasSource dasSource = (DasSource) selectedSources\r
231               .elementAt(sourceIndex);\r
232 \r
233       nextSequence(dasSource, sequences[0]);\r
234     }\r
235   }\r
236 \r
237   private void setGuiNoDassourceActive()\r
238   {\r
239 \r
240     if (af != null)\r
241     {\r
242       af.setProgressBar("No DAS Sources Active", startTime);\r
243     }\r
244     if (getFeatSettings() != null)\r
245     {\r
246       fsettings.noDasSourceActive();\r
247     }\r
248   }\r
249 \r
250   /**\r
251    * Update our fsettings dialog reference if we didn't have one when we were\r
252    * first initialised.\r
253    * \r
254    * @return fsettings\r
255    */\r
256   private FeatureSettings getFeatSettings()\r
257   {\r
258     if (fsettings == null)\r
259     {\r
260       if (af != null)\r
261       {\r
262         fsettings = af.featureSettings;\r
263       }\r
264     }\r
265     return fsettings;\r
266   }\r
267 \r
268   public void cancel()\r
269   {\r
270     if (af != null)\r
271     {\r
272       af.setProgressBar("DAS Feature Fetching Cancelled", startTime);\r
273     }\r
274     cancelled = true;\r
275   }\r
276 \r
277   int sourcesRemaining = 0;\r
278 \r
279   void responseComplete(DasSource dasSource, SequenceI seq)\r
280   {\r
281     if (seq != null)\r
282     {\r
283       for (int seqIndex = 0; seqIndex < sequences.length - 1 && !cancelled; seqIndex++)\r
284       {\r
285         if (sequences[seqIndex] == seq)\r
286         {\r
287           nextSequence(dasSource, sequences[++seqIndex]);\r
288           return;\r
289         }\r
290       }\r
291     }\r
292 \r
293     sourcesRemaining--;\r
294 \r
295     if (sourcesRemaining == 0)\r
296     {\r
297       System.err.println("Fetching Complete.");\r
298       setGuiFetchComplete();\r
299     }\r
300 \r
301   }\r
302 \r
303   private void setGuiFetchComplete()\r
304   {\r
305 \r
306     if (af != null)\r
307     {\r
308       af.setProgressBar("DAS Feature Fetching Complete", startTime);\r
309     }\r
310 \r
311     if (af != null && af.featureSettings != null)\r
312     {\r
313       af.featureSettings.setTableData();\r
314     }\r
315 \r
316     if (getFeatSettings() != null)\r
317     {\r
318       fsettings.complete();\r
319     }\r
320   }\r
321 \r
322   void featuresAdded(SequenceI seq)\r
323   {\r
324     if (af == null)\r
325     {\r
326       // no gui to update with features.\r
327       return;\r
328     }\r
329     af.getFeatureRenderer().featuresAdded();\r
330 \r
331     int start = af.getViewport().getStartSeq();\r
332     int end = af.getViewport().getEndSeq();\r
333     int index;\r
334     for (index = start; index < end; index++)\r
335     {\r
336       if (seq == af.getViewport().getAlignment().getSequenceAt(index)\r
337               .getDatasetSequence())\r
338       {\r
339         af.alignPanel.paintAlignment(true);\r
340         break;\r
341       }\r
342     }\r
343   }\r
344 \r
345   void nextSequence(DasSource dasSource, SequenceI seq)\r
346   {\r
347     if (cancelled)\r
348       return;\r
349     DBRefEntry[] uprefs = jalview.util.DBRefUtils.selectRefs(\r
350             seq.getDBRef(), new String[]\r
351             {\r
352             // jalview.datamodel.DBRefSource.PDB,\r
353             jalview.datamodel.DBRefSource.UNIPROT,\r
354             // jalview.datamodel.DBRefSource.EMBL - not tested on any EMBL coord\r
355             // sys sources\r
356             });\r
357     // TODO: minimal list of DAS queries to make by querying with untyped ID if\r
358     // distinct from any typed IDs\r
359 \r
360     boolean dasCoordSysFound = false;\r
361 \r
362     if (uprefs != null)\r
363     {\r
364       // do any of these ids match the source's coordinate system ?\r
365       for (int j = 0; !dasCoordSysFound && j < uprefs.length; j++)\r
366       {\r
367         DasCoordinateSystem cs[] = dasSource.getCoordinateSystem();\r
368 \r
369         for (int csIndex = 0; csIndex < cs.length && !dasCoordSysFound; csIndex++)\r
370         {\r
371           if (cs.length > 0\r
372                   && jalview.util.DBRefUtils.isDasCoordinateSystem(\r
373                           cs[csIndex].getName(), uprefs[j]))\r
374           {\r
375             debug("Launched fetcher for coordinate system "\r
376                     + cs[0].getName());\r
377             // Will have to pass any mapping information to the fetcher\r
378             // - the start/end for the DBRefEntry may not be the same as the\r
379             // sequence's start/end\r
380 \r
381             System.out.println(seq.getName() + " "\r
382                     + (seq.getDatasetSequence() == null) + " "\r
383                     + dasSource.getUrl());\r
384 \r
385             dasCoordSysFound = true; // break's out of the loop\r
386             createFeatureFetcher(seq, dasSource, uprefs[j]);\r
387           }\r
388           else\r
389             System.out.println("IGNORE " + cs[csIndex].getName());\r
390         }\r
391       }\r
392     }\r
393 \r
394     if (!dasCoordSysFound)\r
395     {\r
396       String id = null;\r
397       // try and use the name as the sequence id\r
398       if (seq.getName().indexOf("|") > -1)\r
399       {\r
400         id = seq.getName().substring(seq.getName().lastIndexOf("|") + 1);\r
401         if (id.trim().length() < 4)\r
402         {\r
403           // hack - we regard a significant ID as being at least 4\r
404           // non-whitespace characters\r
405           id = seq.getName().substring(0, seq.getName().lastIndexOf("|"));\r
406           if (id.indexOf("|") > -1)\r
407           {\r
408             id = id.substring(id.lastIndexOf("|") + 1);\r
409           }\r
410         }\r
411       }\r
412       else\r
413       {\r
414         id = seq.getName();\r
415       }\r
416       if (id != null)\r
417       {\r
418         // Should try to call a general feature fetcher that\r
419         // queries many sources with name to discover applicable ID references\r
420         createFeatureFetcher(seq, dasSource, id);\r
421       }\r
422     }\r
423 \r
424   }\r
425 \r
426   /**\r
427    * fetch and add das features to a sequence using the given source URL and\r
428    * compatible DbRef id. new features are mapped using the DbRef mapping to the\r
429    * local coordinate system.\r
430    * \r
431    * @param seq\r
432    * @param SourceUrl\r
433    * @param dbref\r
434    */\r
435   protected void createFeatureFetcher(final SequenceI seq,\r
436           final DasSource dasSource, final DBRefEntry dbref)\r
437   {\r
438 \r
439     // ////////////\r
440     // / fetch DAS features\r
441     final Das1Source source = new Das1Source();\r
442     source.setUrl(dasSource.getUrl());\r
443     source.setNickname(dasSource.getNickname());\r
444     if (dbref == null || dbref.getAccessionId() == null\r
445             || dbref.getAccessionId().length() < 1)\r
446     {\r
447       responseComplete(dasSource, seq); // reduce thread count anyhow\r
448       return;\r
449     }\r
450     debug("new Das Feature Fetcher for " + dbref.getSource() + ":"\r
451             + dbref.getAccessionId() + " querying " + dasSource.getUrl());\r
452     FeatureThread fetcher = new FeatureThread(dbref.getAccessionId()\r
453     // + ":" + start + "," + end,\r
454             , source);\r
455 \r
456     fetcher.addFeatureListener(new FeatureListener()\r
457     {\r
458       public void comeBackLater(FeatureEvent e)\r
459       {\r
460         responseComplete(dasSource, seq);\r
461         debug("das source " + e.getSource().getNickname()\r
462                 + " asked us to come back in " + e.getComeBackLater()\r
463                 + " secs.");\r
464       }\r
465 \r
466       public void newFeatures(FeatureEvent e)\r
467       {\r
468 \r
469         Das1Source ds = e.getSource();\r
470 \r
471         Map[] features = e.getFeatures();\r
472         // add features to sequence\r
473         debug("das source " + ds.getUrl() + " returned " + features.length\r
474                 + " features");\r
475 \r
476         if (features.length > 0)\r
477         {\r
478           for (int i = 0; i < features.length; i++)\r
479           {\r
480             SequenceFeature f = newSequenceFeature(features[i], source\r
481                     .getNickname());\r
482             if (dbref.getMap() != null && f.getBegin() > 0\r
483                     && f.getEnd() > 0)\r
484             {\r
485               debug("mapping from " + f.getBegin() + " - " + f.getEnd());\r
486               SequenceFeature vf[] = null;\r
487 \r
488               try\r
489               {\r
490                 vf = dbref.getMap().locateFeature(f);\r
491               } catch (Exception ex)\r
492               {\r
493                 Cache.log\r
494                         .info("Error in 'experimental' mapping of features. Please try to reproduce and then report info to help@jalview.org.");\r
495                 Cache.log.info("Mapping feature from " + f.getBegin()\r
496                         + " to " + f.getEnd() + " in dbref "\r
497                         + dbref.getAccessionId() + " in "\r
498                         + dbref.getSource());\r
499                 Cache.log.info("using das Source " + ds.getUrl());\r
500                 Cache.log.info("Exception", ex);\r
501               }\r
502 \r
503               if (vf != null)\r
504               {\r
505                 for (int v = 0; v < vf.length; v++)\r
506                 {\r
507                   debug("mapping to " + v + ": " + vf[v].getBegin() + " - "\r
508                           + vf[v].getEnd());\r
509                   seq.addSequenceFeature(vf[v]);\r
510                 }\r
511               }\r
512             }\r
513             else\r
514             {\r
515               seq.addSequenceFeature(f);\r
516             }\r
517           }\r
518 \r
519           featuresAdded(seq);\r
520         }\r
521         else\r
522         {\r
523           // System.out.println("No features found for " + seq.getName()\r
524           // + " from: " + e.getDasSource().getNickname());\r
525         }\r
526         responseComplete(dasSource, seq);\r
527 \r
528       }\r
529     }\r
530 \r
531     );\r
532 \r
533     fetcher.start();\r
534   }\r
535 \r
536   protected void createFeatureFetcher(final SequenceI seq,\r
537           final DasSource dasSource, String id)\r
538   {\r
539     // ////////////\r
540     // / fetch DAS features\r
541     final Das1Source source = new Das1Source();\r
542     source.setUrl(dasSource.getUrl());\r
543     source.setNickname(dasSource.getNickname());\r
544 \r
545     if (id != null)\r
546     {\r
547       id = id.trim();\r
548     }\r
549     if (id != null && id.length() > 0)\r
550     {\r
551       debug("new Das Feature Fetcher for " + id + " querying "\r
552               + dasSource.getUrl());\r
553       FeatureThread fetcher = new FeatureThread(id\r
554       // + ":" + start + "," + end,\r
555               , source);\r
556 \r
557       fetcher.addFeatureListener(new FeatureListener()\r
558       {\r
559         public void comeBackLater(FeatureEvent e)\r
560         {\r
561           responseComplete(dasSource, seq);\r
562           debug("das source " + e.getSource().getNickname()\r
563                   + " asked us to come back in " + e.getComeBackLater()\r
564                   + " secs.");\r
565         }\r
566 \r
567         public void newFeatures(FeatureEvent e)\r
568         {\r
569 \r
570           Das1Source ds = e.getSource();\r
571 \r
572           Map[] features = e.getFeatures();\r
573           // add features to sequence\r
574           debug("das source " + ds.getUrl() + " returned "\r
575                   + features.length + " features");\r
576 \r
577           if (features.length > 0)\r
578           {\r
579             for (int i = 0; i < features.length; i++)\r
580             {\r
581               SequenceFeature f = newSequenceFeature(features[i], source\r
582                       .getNickname());\r
583 \r
584               seq.addSequenceFeature(f);\r
585             }\r
586 \r
587             featuresAdded(seq);\r
588           }\r
589           else\r
590           {\r
591             // System.out.println("No features found for " + seq.getName()\r
592             // + " from: " + e.getDasSource().getNickname());\r
593           }\r
594           responseComplete(dasSource, seq);\r
595 \r
596         }\r
597       }\r
598 \r
599       );\r
600 \r
601       fetcher.start();\r
602     }\r
603     else\r
604     {\r
605       // invalid fetch - indicate it is finished.\r
606       debug("Skipping empty ID for querying " + dasSource.getUrl());\r
607       responseComplete(dasSource, seq);\r
608     }\r
609   }\r
610 \r
611   /**\r
612    * creates a jalview sequence feature from a das feature document\r
613    * \r
614    * @param dasfeature\r
615    * @return sequence feature object created using dasfeature information\r
616    */\r
617   SequenceFeature newSequenceFeature(Map dasfeature, String nickname)\r
618   {\r
619     if (dasfeature == null)\r
620     {\r
621       return null;\r
622     }\r
623     try\r
624     {\r
625       /**\r
626        * Different qNames for a DAS Feature - are string keys to the HashMaps in\r
627        * features "METHOD") || qName.equals("TYPE") || qName.equals("START") ||\r
628        * qName.equals("END") || qName.equals("NOTE") || qName.equals("LINK") ||\r
629        * qName.equals("SCORE")\r
630        */\r
631       String desc = new String();\r
632       if (dasfeature.containsKey("NOTE"))\r
633       {\r
634         desc += (String) dasfeature.get("NOTE");\r
635       }\r
636 \r
637       int start = 0, end = 0;\r
638       float score = 0f;\r
639 \r
640       try\r
641       {\r
642         start = Integer.parseInt(dasfeature.get("START").toString());\r
643       } catch (Exception ex)\r
644       {\r
645       }\r
646       try\r
647       {\r
648         end = Integer.parseInt(dasfeature.get("END").toString());\r
649       } catch (Exception ex)\r
650       {\r
651       }\r
652       try\r
653       {\r
654         score = Integer.parseInt(dasfeature.get("SCORE").toString());\r
655       } catch (Exception ex)\r
656       {\r
657       }\r
658 \r
659       SequenceFeature f = new SequenceFeature((String) dasfeature\r
660               .get("TYPE"), desc, start, end, score, nickname);\r
661 \r
662       if (dasfeature.containsKey("LINK"))\r
663       {\r
664         // Do not put feature extent in link text for non-positional features\r
665         if (f.begin==0 && f.end==0)\r
666         {        f.addLink(f.getType()+"|"\r
667                 + dasfeature.get("LINK"));\r
668         } else {\r
669           f.addLink(f.getType() + " " + f.begin + "_" + f.end + "|"\r
670                         + dasfeature.get("LINK"));\r
671         }\r
672       }\r
673 \r
674       return f;\r
675     } catch (Exception e)\r
676     {\r
677       System.out.println("ERRR " + e);\r
678       e.printStackTrace();\r
679       System.out.println("############");\r
680       debug("Failed to parse " + dasfeature.toString(), e);\r
681       return null;\r
682     }\r
683   }\r
684 \r
685   /**\r
686    * query the default DAS Source Registry for sources. Uses value of jalview\r
687    * property DAS_REGISTRY_URL and the DasSourceBrowser.DEFAULT_REGISTRY if that\r
688    * doesn't exist.\r
689    * \r
690    * @return list of sources\r
691    */\r
692   public static DasSource[] getDASSources()\r
693   {\r
694 \r
695     String registryURL = jalview.bin.Cache.getDefault("DAS_REGISTRY_URL",\r
696             DasSourceBrowser.DEFAULT_REGISTRY);\r
697     return getDASSources(registryURL);\r
698   }\r
699 \r
700   /**\r
701    * query the given URL for DasSources.\r
702    * \r
703    * @param registryURL\r
704    *                return sources from registryURL\r
705    */\r
706   public static DasSource[] getDASSources(String registryURL)\r
707   {\r
708     DasSourceReaderImpl reader = new DasSourceReaderImpl();\r
709 \r
710     try\r
711     {\r
712       URL url = new URL(registryURL);\r
713 \r
714       DasSource[] sources = reader.readDasSource(url);\r
715 \r
716       List das1sources = new ArrayList();\r
717       for (int i = 0; i < sources.length; i++)\r
718       {\r
719         DasSource ds = sources[i];\r
720         if (ds instanceof Das2Source)\r
721         {\r
722           Das2Source d2s = (Das2Source) ds;\r
723           if (d2s.hasDas1Capabilities())\r
724           {\r
725             Das1Source d1s = DasSourceConverter.toDas1Source(d2s);\r
726             das1sources.add(d1s);\r
727           }\r
728 \r
729         }\r
730         else if (ds instanceof Das1Source)\r
731         {\r
732           das1sources.add((Das1Source) ds);\r
733         }\r
734       }\r
735 \r
736       return (Das1Source[]) das1sources.toArray(new Das1Source[das1sources\r
737               .size()]);\r
738     } catch (Exception ex)\r
739     {\r
740       ex.printStackTrace();\r
741       return null;\r
742     }\r
743   }\r
744 \r
745 }\r