update author list in license for (JAL-826)
[jalview.git] / src / jalview / ws / jws2 / Jws2Discoverer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.ws.jws2;
19
20 import java.awt.Color;
21 import java.awt.event.ActionEvent;
22 import java.awt.event.ActionListener;
23 import java.beans.PropertyChangeEvent;
24 import java.beans.PropertyChangeListener;
25 import java.io.Closeable;
26 import java.net.ConnectException;
27 import java.net.URL;
28 import java.util.ArrayList;
29 import java.util.HashSet;
30 import java.util.Hashtable;
31 import java.util.List;
32 import java.util.StringTokenizer;
33 import java.util.Vector;
34
35 import javax.swing.JMenu;
36 import javax.swing.JMenuItem;
37 import javax.swing.event.MenuEvent;
38 import javax.swing.event.MenuListener;
39
40 import org.apache.log4j.Level;
41
42 import jalview.bin.Cache;
43 import jalview.datamodel.AlignmentView;
44 import jalview.gui.AlignFrame;
45 import jalview.gui.Desktop;
46 import jalview.gui.JalviewChangeSupport;
47 import jalview.gui.JvSwingUtils;
48 import jalview.ws.WSMenuEntryProviderI;
49 import jalview.ws.jws2.Jws2Discoverer.Jws2Instance;
50 import jalview.ws.params.ParamDatastoreI;
51 import compbio.data.msa.MsaWS;
52 import compbio.metadata.Option;
53 import compbio.metadata.Preset;
54 import compbio.metadata.PresetManager;
55 import compbio.metadata.RunnerConfig;
56 import compbio.ws.client.Jws2Client;
57 import compbio.ws.client.Services;
58
59 /**
60  * discoverer for jws2 services. Follows the lightweight service discoverer
61  * pattern (archetyped by EnfinEnvision2OneWay)
62  * 
63  * @author JimP
64  * 
65  */
66 public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
67 {
68   private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
69           this);
70
71   /**
72    * change listeners are notified of "services" property changes
73    * 
74    * @param listener
75    *          to be added that consumes new services Hashtable object.
76    */
77   public void addPropertyChangeListener(
78           java.beans.PropertyChangeListener listener)
79   {
80     changeSupport.addPropertyChangeListener(listener);
81   }
82
83   /**
84    * 
85    * 
86    * @param listener
87    *          to be removed
88    */
89   public void removePropertyChangeListener(
90           java.beans.PropertyChangeListener listener)
91   {
92     changeSupport.removePropertyChangeListener(listener);
93   }
94
95   boolean running = false, aborted = false;
96
97   /**
98    * @return the aborted
99    */
100   public boolean isAborted()
101   {
102     return aborted;
103   }
104
105   /**
106    * @param aborted
107    *          the aborted to set
108    */
109   public void setAborted(boolean aborted)
110   {
111     this.aborted = aborted;
112   }
113
114   Thread oldthread = null;
115
116   public void run()
117   {
118     if (running && oldthread != null && oldthread.isAlive())
119     {
120       if (!aborted)
121       {
122         return;
123       }
124       while (running)
125       {
126         try
127         {
128           Cache.log
129                   .debug("Waiting around for old discovery thread to finish.");
130           // wait around until old discoverer dies
131           Thread.sleep(100);
132         } catch (Exception e)
133         {
134         }
135       }
136       Cache.log.debug("Old discovery thread has finished.");
137     }
138     running = true;
139     oldthread = Thread.currentThread();
140     try
141     {
142       Class foo = getClass().getClassLoader().loadClass(
143               "compbio.ws.client.Jws2Client");
144     } catch (ClassNotFoundException e)
145     {
146       System.err
147               .println("Not enabling JABA Webservices : client jar is not available."
148                       + "\nPlease check that your webstart JNLP file is up to date!");
149       running = false;
150       return;
151     }
152     // reinitialise records of good and bad service URLs
153     if (services != null)
154     {
155       services.removeAllElements();
156     }
157     if (urlsWithoutServices != null)
158     {
159       urlsWithoutServices.removeAllElements();
160     }
161     if (invalidServiceUrls != null)
162     {
163       invalidServiceUrls.removeAllElements();
164     }
165
166     List<JabaWsServerQuery> qrys = new ArrayList<JabaWsServerQuery>();
167     for (final String jwsservers : getServiceUrls())
168     {
169       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsservers);
170       qrys.add(squery);
171       new Thread(squery).start();
172     }
173     boolean finished = false;
174     do
175     {
176       try
177       {
178         Thread.sleep(100);
179       } catch (Exception e)
180       {
181       }
182       ;
183       for (JabaWsServerQuery squery : qrys)
184       {
185         finished |= !squery.isRunning();
186       }
187       if (aborted)
188       {
189         Cache.log.debug("Aborting " + qrys.size()
190                 + " JABAWS discovery threads.");
191         for (JabaWsServerQuery squery : qrys)
192         {
193           squery.setQuit(true);
194         }
195       }
196     } while (!aborted && !finished);
197     oldthread = null;
198     running = false;
199     if (!aborted)
200     {
201       changeSupport.firePropertyChange("services", new Vector(), services);
202     }
203   }
204
205   /**
206    * record this service endpoint so we can use it
207    * 
208    * @param jwsservers
209    * @param srv
210    * @param service2
211    */
212   synchronized void addService(String jwsservers, Services srv,
213           MsaWS service2)
214   {
215     if (services == null)
216     {
217       services = new Vector<Jws2Instance>();
218     }
219     System.out.println("Discovered service: " + jwsservers + " "
220             + srv.toString());
221     Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
222             service2);
223
224     services.add(service);
225     // retrieve the presets and parameter set and cache now
226     service.getParamStore().getPresets();
227     service.hasParameters();
228   }
229
230   public class Jws2Instance
231   {
232     public String hosturl;
233
234     public String serviceType;
235
236     public MsaWS service;
237
238     public Jws2Instance(String hosturl, String serviceType, MsaWS service)
239     {
240       super();
241       this.hosturl = hosturl;
242       this.serviceType = serviceType;
243       this.service = service;
244     }
245
246     PresetManager presets = null;
247
248     public JabaParamStore paramStore = null;
249
250     /**
251      * non thread safe - gets the presets for this service (blocks whilst it
252      * calls the service to get the preset set)
253      * 
254      * @return service presets or null if exceptions were raised.
255      */
256     public PresetManager getPresets()
257     {
258       if (presets == null)
259       {
260         try
261         {
262           presets = service.getPresets();
263         } catch (Exception ex)
264         {
265           System.err
266                   .println("Exception when retrieving presets for service "
267                           + serviceType + " at " + hosturl);
268         }
269       }
270       return presets;
271     }
272
273     public String getHost()
274     {
275       return hosturl;
276       /*
277        * try { URL serviceurl = new URL(hosturl); if (serviceurl.getPort()!=80)
278        * { return serviceurl.getHost()+":"+serviceurl.getPort(); } return
279        * serviceurl.getHost(); } catch (Exception e) {
280        * System.err.println("Failed to parse service URL '" + hosturl +
281        * "' as a valid URL!"); } return null;
282        */
283     }
284
285     /**
286      * @return short description of what the service will do
287      */
288     public String getActionText()
289     {
290       return "Align with " + serviceType;
291     }
292
293     /**
294      * non-thread safe - blocks whilst accessing service to get complete set of
295      * available options and parameters
296      * 
297      * @return
298      */
299     public RunnerConfig getRunnerConfig()
300     {
301       return service.getRunnerOptions();
302     }
303
304     @Override
305     protected void finalize() throws Throwable
306     {
307       if (service != null)
308       {
309         try
310         {
311           Closeable svc = (Closeable) service;
312           service = null;
313           svc.close();
314         } catch (Exception e)
315         {
316         }
317         ;
318       }
319       super.finalize();
320     }
321
322     public ParamDatastoreI getParamStore()
323     {
324       if (paramStore == null)
325       {
326         try
327         {
328           paramStore = new JabaParamStore(this,
329                   (Desktop.instance != null ? Desktop
330                           .getUserParameterStore() : null));
331         } catch (Exception ex)
332         {
333         }
334
335       }
336       return paramStore;
337     }
338
339     public String getUri()
340     {
341       // this is only valid for Jaba 1.0 - this formula might have to change!
342       return hosturl
343               + (hosturl.lastIndexOf("/") == (hosturl.length() - 1) ? ""
344                       : "/") + serviceType;
345     }
346
347     private boolean hasParams = false, lookedForParams = false;
348
349     public boolean hasParameters()
350     {
351       if (!lookedForParams)
352       {
353         lookedForParams = true;
354         try
355         {
356           hasParams = (getRunnerConfig().getArguments().size() > 0);
357         } catch (Exception e)
358         {
359
360         }
361       }
362       return hasParams;
363     }
364   };
365
366   /**
367    * holds list of services.
368    */
369   protected Vector<Jws2Instance> services;
370
371   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
372   {
373     // dynamically regenerate service list.
374     final JMenu jws2al = wsmenu; // new JMenu("JABAWS Alignment");
375     jws2al.addMenuListener(new MenuListener()
376     {
377       // TODO: future: add menu listener to parent menu - so submenus are
378       // populated *before* they are selected.
379       @Override
380       public void menuSelected(MenuEvent e)
381       {
382         populateWSMenuEntry(jws2al, alignFrame);
383       }
384
385       @Override
386       public void menuDeselected(MenuEvent e)
387       {
388         // TODO Auto-generated method stub
389
390       }
391
392       @Override
393       public void menuCanceled(MenuEvent e)
394       {
395         // TODO Auto-generated method stub
396
397       }
398
399     });
400     wsmenu.add(jws2al);
401
402   }
403
404   private void populateWSMenuEntry(JMenu jws2al, final AlignFrame alignFrame)
405   {
406     if (running || services == null || services.size() == 0)
407     {
408       return;
409     }
410     boolean byhost = Cache.getDefault("WSMENU_BYHOST", false), bytype = Cache
411             .getDefault("WSMENU_BYTYPE", false);
412     /**
413      * eventually, JWS2 services will appear under the same align/etc submenus.
414      * for moment we keep them separate.
415      */
416     JMenu atpoint;
417     MsaWSClient msacl = new MsaWSClient();
418     Vector hostLabels = new Vector();
419     jws2al.removeAll();
420     String lasthost = null;
421     Hashtable<String, ArrayList<Jws2Instance>> hosts = new Hashtable<String, ArrayList<Jws2Instance>>();
422     String[] sorton;
423     for (Jws2Instance service : services)
424     {
425       ArrayList<Jws2Instance> hostservices = hosts.get(service.getHost());
426       if (hostservices == null)
427       {
428         hosts.put(service.getHost(),
429                 hostservices = new ArrayList<Jws2Instance>());
430       }
431       hostservices.add(service);
432     }
433     sorton = hosts.keySet().toArray(new String[1]);
434     String hostlist[] = sorton.clone();
435     jalview.util.QuickSort.sort(sorton, hostlist);
436     for (String host : hostlist)
437     {
438       Jws2Instance orderedsvcs[] = hosts.get(host).toArray(
439               new Jws2Instance[1]);
440       String sortbytype[] = new String[orderedsvcs.length];
441       for (int i = 0; i < sortbytype.length; i++)
442       {
443         sortbytype[i] = orderedsvcs[i].serviceType;
444       }
445       jalview.util.QuickSort.sort(sortbytype, orderedsvcs);
446       for (final Jws2Instance service : orderedsvcs)
447       {
448         atpoint = jws2al;
449         String type = service.serviceType;
450         if (byhost)
451         {
452           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, host);
453           if (atpoint.getToolTipText() == null)
454           {
455             atpoint.setToolTipText("Services at " + host);
456           }
457         }
458         if (bytype)
459         {
460           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, type);
461           if (atpoint.getToolTipText() == null)
462           {
463             atpoint.setToolTipText(service.getActionText());
464           }
465         }
466         if (!byhost
467                 && !hostLabels.contains(host + service.serviceType
468                         + service.getActionText()))
469         // !hostLabels.contains(host + (bytype ?
470         // service.serviceType+service.getActionText() : "")))
471         {
472           // add a marker indicating where this service is hosted
473           // relies on services from the same host being listed in a
474           // contiguous
475           // group
476           JMenuItem hitm;
477           atpoint.addSeparator();
478           if (lasthost == null || !lasthost.equals(host))
479           {
480             atpoint.add(hitm = new JMenuItem(host));
481             hitm.setForeground(Color.blue);
482             hitm.addActionListener(new ActionListener()
483             {
484
485               @Override
486               public void actionPerformed(ActionEvent e)
487               {
488                 Desktop.showUrl(service.getHost());
489               }
490             });
491             hitm.setToolTipText(JvSwingUtils
492                     .wrapTooltip("Opens the JABAWS server's homepage in web browser"));
493             lasthost = host;
494           }
495           hostLabels.addElement(host + service.serviceType
496                   + service.getActionText());
497           // hostLabels.addElement(host + (bytype ?
498           // service.serviceType+service.getActionText() : ""));
499         }
500         msacl.attachWSMenuEntry(atpoint, service, alignFrame);
501         /*
502          * JMenuItem sitem = new JMenuItem(service.serviceType);
503          * sitem.setToolTipText("Hosted at " + service.hosturl);
504          * sitem.addActionListener(new ActionListener() {
505          * 
506          * @Override public void actionPerformed(ActionEvent e) { AlignmentView
507          * msa = alignFrame.gatherSequencesForAlignment(); MsaWSClient client =
508          * new MsaWSClient(service, "JWS2 Alignment of " +
509          * alignFrame.getTitle(), msa, false, true,
510          * alignFrame.getViewport().getAlignment().getDataset(), alignFrame); }
511          * });
512          */
513       }
514     }
515
516   }
517
518   public static void main(String[] args)
519   {
520     Thread runner = getDiscoverer().startDiscoverer(
521             new PropertyChangeListener()
522             {
523
524               public void propertyChange(PropertyChangeEvent evt)
525               {
526                 if (getDiscoverer().services != null)
527                 {
528                   System.out.println("Changesupport: There are now "
529                           + getDiscoverer().services.size() + " services");
530
531                 }
532               }
533             });
534     while (runner.isAlive())
535     {
536       try
537       {
538         Thread.sleep(50);
539       } catch (InterruptedException e)
540       {
541       }
542       ;
543     }
544   }
545
546   private static Jws2Discoverer discoverer;
547
548   public static Jws2Discoverer getDiscoverer()
549   {
550     if (discoverer == null)
551     {
552       discoverer = new Jws2Discoverer();
553     }
554     return discoverer;
555   }
556
557   public boolean hasServices()
558   {
559     // TODO Auto-generated method stub
560     return !running && services != null && services.size() > 0;
561   }
562
563   public boolean isRunning()
564   {
565     return running;
566   }
567
568   /**
569    * the jalview .properties entry for JWS2 URLS
570    */
571   final static String JWS2HOSTURLS = "JWS2HOSTURLS";
572
573   public static void setServiceUrls(Vector<String> urls)
574   {
575     if (urls != null)
576     {
577       StringBuffer urlbuffer = new StringBuffer();
578       String sep = "";
579       for (String url : urls)
580       {
581         urlbuffer.append(sep);
582         urlbuffer.append(url);
583         sep = ",";
584       }
585       Cache.setProperty(JWS2HOSTURLS, urlbuffer.toString());
586     }
587     else
588     {
589       Cache.removeProperty(JWS2HOSTURLS);
590     }
591   }
592
593   public static Vector<String> getServiceUrls()
594   {
595     String surls = Cache.getDefault(JWS2HOSTURLS,
596             "http://www.compbio.dundee.ac.uk/jabaws");
597     Vector<String> urls = new Vector<String>();
598     try
599     {
600       StringTokenizer st = new StringTokenizer(surls, ",");
601       while (st.hasMoreElements())
602       {
603         String url = null;
604         try
605         {
606           java.net.URL u = new java.net.URL(url = st.nextToken());
607           if (!urls.contains(url))
608           {
609             urls.add(url);
610           }
611           else
612           {
613             jalview.bin.Cache.log.info("Ignoring duplicate url in "
614                     + JWS2HOSTURLS + " list");
615           }
616         } catch (Exception ex)
617         {
618           jalview.bin.Cache.log
619                   .warn("Problem whilst trying to make a URL from '"
620                           + ((url != null) ? url : "<null>") + "'");
621           jalview.bin.Cache.log
622                   .warn("This was probably due to a malformed comma separated list"
623                           + " in the "
624                           + JWS2HOSTURLS
625                           + " entry of $(HOME)/.jalview_properties)");
626           jalview.bin.Cache.log.debug("Exception was ", ex);
627         }
628       }
629     } catch (Exception ex)
630     {
631       jalview.bin.Cache.log.warn(
632               "Error parsing comma separated list of urls in "
633                       + JWS2HOSTURLS + " preference.", ex);
634     }
635     if (urls.size() >= 0)
636     {
637       return urls;
638     }
639     return null;
640   }
641
642   public Vector<Jws2Instance> getServices()
643   {
644     return (services == null) ? new Vector<Jws2Instance>()
645             : new Vector<Jws2Instance>(services);
646   }
647
648   /**
649    * test the given URL with the JabaWS test code
650    * 
651    * @param foo
652    * @return
653    */
654   public static boolean testServiceUrl(URL foo)
655   {
656     try
657     {
658       compbio.ws.client.WSTester.main(new String[]
659       { "-h=" + foo.toString() });
660     } catch (Exception e)
661     {
662       e.printStackTrace();
663       return false;
664     } catch (OutOfMemoryError e)
665     {
666       e.printStackTrace();
667       return false;
668     } catch (Error e)
669     {
670       e.printStackTrace();
671       return false;
672     }
673
674     return true;
675   }
676
677   /**
678    * Start a fresh discovery thread and notify the given object when we're
679    * finished. Any known existing threads will be killed before this one is
680    * started.
681    * 
682    * @param changeSupport2
683    * @return new thread
684    */
685   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
686   {
687     if (isRunning())
688     {
689       setAborted(true);
690     }
691     addPropertyChangeListener(changeSupport2);
692     Thread thr = new Thread(this);
693     thr.start();
694     return thr;
695   }
696
697   Vector<String> invalidServiceUrls = null, urlsWithoutServices = null;
698
699   /**
700    * @return the invalidServiceUrls
701    */
702   public Vector<String> getInvalidServiceUrls()
703   {
704     return invalidServiceUrls;
705   }
706
707   /**
708    * @return the urlsWithoutServices
709    */
710   public Vector<String> getUrlsWithoutServices()
711   {
712     return urlsWithoutServices;
713   }
714
715   /**
716    * add an 'empty' JABA server to the list. Only servers not already in the
717    * 'bad URL' list will be added to this list.
718    * 
719    * @param jwsservers
720    */
721   public synchronized void addUrlwithnoservices(String jwsservers)
722   {
723     if (urlsWithoutServices == null)
724     {
725       urlsWithoutServices = new Vector<String>();
726     }
727
728     if ((invalidServiceUrls == null || !invalidServiceUrls
729             .contains(jwsservers))
730             && !urlsWithoutServices.contains(jwsservers))
731     {
732       urlsWithoutServices.add(jwsservers);
733     }
734   }
735
736   /**
737    * add a bad URL to the list
738    * 
739    * @param jwsservers
740    */
741   public synchronized void addInvalidServiceUrl(String jwsservers)
742   {
743     if (invalidServiceUrls == null)
744     {
745       invalidServiceUrls = new Vector<String>();
746     }
747     if (!invalidServiceUrls.contains(jwsservers))
748     {
749       invalidServiceUrls.add(jwsservers);
750     }
751   }
752
753   /**
754    * 
755    * @return a human readable report of any problems with the service URLs used
756    *         for discovery
757    */
758   public String getErrorMessages()
759   {
760     if (!isRunning() && !isAborted())
761     {
762       StringBuffer ermsg = new StringBuffer();
763       boolean list = false;
764       if (getInvalidServiceUrls() != null
765               && getInvalidServiceUrls().size() > 0)
766       {
767         ermsg.append("URLs that could not be contacted: \n");
768         for (String svcurl : getInvalidServiceUrls())
769         {
770           if (list)
771           {
772             ermsg.append(", ");
773           }
774           list = true;
775           ermsg.append(svcurl);
776         }
777         ermsg.append("\n\n");
778       }
779       list = false;
780       if (getUrlsWithoutServices() != null
781               && getUrlsWithoutServices().size() > 0)
782       {
783         ermsg.append("URLs without any JABA Services : \n");
784         for (String svcurl : getUrlsWithoutServices())
785         {
786           if (list)
787           {
788             ermsg.append(", ");
789           }
790           list = true;
791           ermsg.append(svcurl);
792         }
793         ermsg.append("\n");
794       }
795       if (ermsg.length() > 1)
796       {
797         return ermsg.toString();
798       }
799
800     }
801     return null;
802   }
803
804 }