Merge branch 'develop' into alpha/JAL-3362_Jalview_212_alpha
[jalview.git] / src / jalview / ws / jws2 / Jws2Discoverer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
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
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.jws2;
22
23 import jalview.bin.Cache;
24 import jalview.gui.AlignFrame;
25 import jalview.util.MessageManager;
26 import jalview.ws.WSMenuEntryProviderI;
27 import jalview.ws.api.ServiceWithParameters;
28 import jalview.ws.jws2.jabaws2.Jws2Instance;
29 import jalview.ws.params.ParamDatastoreI;
30
31 import java.beans.PropertyChangeEvent;
32 import java.beans.PropertyChangeListener;
33 import java.beans.PropertyChangeSupport;
34 import java.net.MalformedURLException;
35 import java.net.URL;
36 import java.util.ArrayList;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Set;
40 import java.util.StringTokenizer;
41 import java.util.Vector;
42
43 import javax.swing.JMenu;
44
45 import compbio.ws.client.Services;
46
47 /**
48  * discoverer for jws2 services. Follows the lightweight service discoverer
49  * pattern (archetyped by EnfinEnvision2OneWay)
50  * 
51  * @author JimP
52  * 
53  */
54 public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
55 {
56   public static final String COMPBIO_JABAWS = "http://www.compbio.dundee.ac.uk/jabaws";
57
58   /*
59    * the .jalview_properties entry for JWS2 URLS
60    */
61   private final static String JWS2HOSTURLS = "JWS2HOSTURLS";
62
63   /*
64    * Singleton instance
65    */
66   private static Jws2Discoverer discoverer;
67
68   /*
69    * Override for testing only
70    */
71   private static List<String> testUrls = null;
72
73   // preferred url has precedence over others
74   private String preferredUrl;
75
76   private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
77           this);
78
79   private Vector<String> invalidServiceUrls = null;
80
81   private Vector<String> urlsWithoutServices = null;
82
83   private Vector<String> validServiceUrls = null;
84
85   private volatile boolean running = false;
86
87   private volatile boolean aborted = false;
88
89   private Thread oldthread = null;
90
91   /**
92    * holds list of services.
93    */
94   protected Vector<Jws2Instance> services;
95
96   /**
97    * Private constructor enforces use of singleton via getDiscoverer()
98    */
99   private Jws2Discoverer()
100   {
101   }
102
103   /**
104    * change listeners are notified of "services" property changes
105    * 
106    * @param listener
107    *          to be added that consumes new services Hashtable object.
108    */
109   public void addPropertyChangeListener(
110           java.beans.PropertyChangeListener listener)
111   {
112     changeSupport.addPropertyChangeListener(listener);
113   }
114
115   /**
116    * 
117    * 
118    * @param listener
119    *          to be removed
120    */
121   public void removePropertyChangeListener(
122           java.beans.PropertyChangeListener listener)
123   {
124     changeSupport.removePropertyChangeListener(listener);
125   }
126
127   /**
128    * @return the aborted
129    */
130   public boolean isAborted()
131   {
132     return aborted;
133   }
134
135   /**
136    * @param aborted
137    *          the aborted to set
138    */
139   public void setAborted(boolean aborted)
140   {
141     this.aborted = aborted;
142   }
143
144   @Override
145   public void run()
146   {
147
148     if (running && oldthread != null && oldthread.isAlive())
149     {
150       if (!aborted)
151       {
152         return;
153       }
154       while (running)
155       {
156         try
157         {
158           Cache.log.debug(
159                   "Waiting around for old discovery thread to finish.");
160           // wait around until old discoverer dies
161           Thread.sleep(100);
162         } catch (Exception e)
163         {
164         }
165       }
166       aborted = false;
167       Cache.log.debug("Old discovery thread has finished.");
168     }
169     running = true;
170
171     // first set up exclusion list if needed
172     final Set<String> ignoredServices = new HashSet<>();
173     for (String ignored : Cache
174             .getDefault("IGNORED_JABAWS_SERVICETYPES", "").split("\\|"))
175     {
176       ignoredServices.add(ignored);
177     }
178
179     changeSupport.firePropertyChange("services", services,
180             new Vector<Jws2Instance>());
181     oldthread = Thread.currentThread();
182     try
183     {
184       Class foo = getClass().getClassLoader()
185               .loadClass("compbio.ws.client.Jws2Client");
186     } catch (ClassNotFoundException e)
187     {
188       System.err.println(
189               "Not enabling JABA Webservices : client jar is not available."
190                       + "\nPlease check that your webstart JNLP file is up to date!");
191       running = false;
192       return;
193     }
194     // reinitialise records of good and bad service URLs
195     if (services != null)
196     {
197       services.removeAllElements();
198     }
199     if (urlsWithoutServices != null)
200     {
201       urlsWithoutServices.removeAllElements();
202     }
203     if (invalidServiceUrls != null)
204     {
205       invalidServiceUrls.removeAllElements();
206     }
207     if (validServiceUrls != null)
208     {
209       validServiceUrls.removeAllElements();
210     }
211     ArrayList<String> svctypes = new ArrayList<>();
212
213     List<JabaWsServerQuery> qrys = new ArrayList<>();
214     for (final String jwsserver : getServiceUrls())
215     {
216       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsserver);
217       if (svctypes.size() == 0)
218       {
219         // TODO: remove this ugly hack to get Canonical JABA service ordering
220         // for all possible services
221         for (Services sv : squery.JABAWS2SERVERS)
222         {
223           if (!ignoredServices.contains(sv.toString()))
224           {
225             svctypes.add(sv.toString());
226           }
227         }
228
229       }
230       qrys.add(squery);
231       new Thread(squery).start();
232     }
233     boolean finished = true;
234     do
235     {
236       finished = true;
237       try
238       {
239         Thread.sleep(100);
240       } catch (Exception e)
241       {
242       }
243       ;
244       for (JabaWsServerQuery squery : qrys)
245       {
246         if (squery.isRunning())
247         {
248           finished = false;
249         }
250       }
251       if (aborted)
252       {
253         Cache.log.debug(
254                 "Aborting " + qrys.size() + " JABAWS discovery threads.");
255         for (JabaWsServerQuery squery : qrys)
256         {
257           squery.setQuit(true);
258         }
259       }
260     } while (!aborted && !finished);
261     if (!aborted)
262     {
263       // resort services according to order found in jabaws service list
264       // also ensure services for each host are ordered in same way.
265
266       if (services != null && services.size() > 0)
267       {
268         Jws2Instance[] svcs = new Jws2Instance[services.size()];
269         int[] spos = new int[services.size()];
270         int ipos = 0;
271         List<String> svcUrls = getServiceUrls();
272         for (Jws2Instance svc : services)
273         {
274           svcs[ipos] = svc;
275           spos[ipos++] = 1000 * svcUrls.indexOf(svc.getHostURL()) + 1
276                   + svctypes.indexOf(svc.getName());
277         }
278         jalview.util.QuickSort.sort(spos, svcs);
279         services = new Vector<>();
280         for (Jws2Instance svc : svcs)
281         {
282           if (!ignoredServices.contains(svc.getName()))
283           {
284             services.add(svc);
285           }
286         }
287       }
288     }
289     oldthread = null;
290     running = false;
291     changeSupport.firePropertyChange("services", new Vector<Jws2Instance>(),
292             services);
293   }
294
295   /**
296    * record this service endpoint so we can use it
297    * 
298    * @param jwsservers
299    * @param srv
300    * @param service2
301    */
302   synchronized void addService(String jwsservers, Jws2Instance service)
303   {
304     if (services == null)
305     {
306       services = new Vector<>();
307     }
308     System.out.println(
309             "Discovered service: " + jwsservers + " " + service.toString());
310     // Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
311     // service2);
312
313     services.add(service);
314     // retrieve the presets and parameter set and cache now
315     ParamDatastoreI pds = service.getParamStore();
316     if (pds != null)
317     {
318       pds.getPresets();
319     }
320     service.hasParameters();
321     if (validServiceUrls == null)
322     {
323       validServiceUrls = new Vector<>();
324     }
325     validServiceUrls.add(jwsservers);
326   }
327
328   /**
329    * attach all available web services to the appropriate submenu in the given
330    * JMenu
331    */
332   @Override
333   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
334   {
335     if (running || services == null || services.size() == 0)
336     {
337       return;
338     }
339     // dynamically regenerate service list.
340     populateWSMenuEntry(wsmenu, alignFrame, null);
341   }
342
343   private void populateWSMenuEntry(JMenu jws2al,
344           final AlignFrame alignFrame, String typeFilter)
345   {
346     PreferredServiceRegistry.getRegistry().populateWSMenuEntry(
347             getServices(),
348             changeSupport, jws2al,
349             alignFrame, typeFilter);
350   }
351
352   public static void main(String[] args)
353   {
354     if (args.length > 0)
355     {
356       testUrls = new ArrayList<>();
357       for (String url : args)
358       {
359         testUrls.add(url);
360       }
361       ;
362     }
363     Thread runner = getDiscoverer()
364             .startDiscoverer(new PropertyChangeListener()
365             {
366
367               @Override
368               public void propertyChange(PropertyChangeEvent evt)
369               {
370                 if (getDiscoverer().services != null)
371                 {
372                   System.out.println("Changesupport: There are now "
373                           + getDiscoverer().services.size() + " services");
374                   int i = 1;
375                   for (ServiceWithParameters instance : getDiscoverer().services)
376                   {
377                     System.out.println("Service " + i++ + " "
378                             + instance.getClass() + "@"
379                             + instance.getHostURL()
380                             + ": " + instance.getActionText());
381                   }
382
383                 }
384               }
385             });
386     while (runner.isAlive())
387     {
388       try
389       {
390         Thread.sleep(50);
391       } catch (InterruptedException e)
392       {
393       }
394       ;
395     }
396     try
397     {
398       Thread.sleep(50);
399     } catch (InterruptedException x)
400     {
401     }
402   }
403
404   /**
405    * Returns the singleton instance of this class.
406    * 
407    * @return
408    */
409   public static Jws2Discoverer getDiscoverer()
410   {
411     if (discoverer == null)
412     {
413       discoverer = new Jws2Discoverer();
414     }
415     return discoverer;
416   }
417
418   public boolean hasServices()
419   {
420     return !running && services != null && services.size() > 0;
421   }
422
423   public boolean isRunning()
424   {
425     return running;
426   }
427
428   public void setServiceUrls(List<String> wsUrls)
429   {
430     if (wsUrls != null && !wsUrls.isEmpty())
431     {
432       StringBuilder urls = new StringBuilder(128);
433       String sep = "";
434       for (String url : wsUrls)
435       {
436         urls.append(sep);
437         urls.append(url);
438         sep = ",";
439       }
440       Cache.setProperty(JWS2HOSTURLS, urls.toString());
441     }
442     else
443     {
444       Cache.removeProperty(JWS2HOSTURLS);
445     }
446   }
447
448   /**
449    * Returns web service URLs, in the order in which they should be tried (or an
450    * empty list).
451    * 
452    * @return
453    */
454   public List<String> getServiceUrls()
455   {
456     if (testUrls != null)
457     {
458       // return test urls, if there are any, instead of touching cache
459       return testUrls;
460     }
461     List<String> urls = new ArrayList<>();
462
463     if (this.preferredUrl != null)
464     {
465       urls.add(preferredUrl);
466     }
467
468     String surls = Cache.getDefault(JWS2HOSTURLS, COMPBIO_JABAWS);
469     try
470     {
471       StringTokenizer st = new StringTokenizer(surls, ",");
472       while (st.hasMoreElements())
473       {
474         String url = null;
475         try
476         {
477           url = st.nextToken();
478           new URL(url);
479           if (!urls.contains(url))
480           {
481             urls.add(url);
482           }
483           else
484           {
485             Cache.log.warn("Ignoring duplicate url " + url + " in "
486                     + JWS2HOSTURLS + " list");
487           }
488         } catch (MalformedURLException ex)
489         {
490           Cache.log.warn("Problem whilst trying to make a URL from '"
491                   + ((url != null) ? url : "<null>") + "'");
492           Cache.log.warn(
493                   "This was probably due to a malformed comma separated list"
494                           + " in the " + JWS2HOSTURLS
495                           + " entry of $(HOME)/.jalview_properties)");
496           Cache.log.debug("Exception was ", ex);
497         }
498       }
499     } catch (Exception ex)
500     {
501       Cache.log.warn("Error parsing comma separated list of urls in "
502               + JWS2HOSTURLS + " preference.", ex);
503     }
504     return urls;
505   }
506
507   public Vector<ServiceWithParameters> getServices()
508   {
509     return (services == null) ? new Vector<>()
510             : new Vector<>(services);
511   }
512
513   /**
514    * test the given URL with the JabaWS test code
515    * 
516    * @param foo
517    * @return
518    */
519   public static boolean testServiceUrl(URL foo)
520   {
521     try
522     {
523       compbio.ws.client.WSTester
524               .main(new String[]
525               { "-h=" + foo.toString() });
526     } catch (Exception e)
527     {
528       e.printStackTrace();
529       return false;
530     } catch (OutOfMemoryError e)
531     {
532       e.printStackTrace();
533       return false;
534     } catch (Error e)
535     {
536       e.printStackTrace();
537       return false;
538     }
539
540     return true;
541   }
542
543   public boolean restart()
544   {
545     synchronized (this)
546     {
547       if (running)
548       {
549         aborted = true;
550       }
551       else
552       {
553         running = true;
554       }
555       return aborted;
556     }
557   }
558
559   /**
560    * Start a fresh discovery thread and notify the given object when we're
561    * finished. Any known existing threads will be killed before this one is
562    * started.
563    * 
564    * @param changeSupport2
565    * @return new thread
566    */
567   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
568   {
569     /*    if (restart())
570         {
571           return;
572         }
573         else
574         {
575           Thread thr = new Thread(this);
576           thr.start();
577         }
578        */
579     if (isRunning())
580     {
581       setAborted(true);
582     }
583     addPropertyChangeListener(changeSupport2);
584     Thread thr = new Thread(this);
585     thr.start();
586     return thr;
587   }
588
589   /**
590    * @return the invalidServiceUrls
591    */
592   public Vector<String> getInvalidServiceUrls()
593   {
594     return invalidServiceUrls;
595   }
596
597   /**
598    * @return the urlsWithoutServices
599    */
600   public Vector<String> getUrlsWithoutServices()
601   {
602     return urlsWithoutServices;
603   }
604
605   /**
606    * add an 'empty' JABA server to the list. Only servers not already in the
607    * 'bad URL' list will be added to this list.
608    * 
609    * @param jwsservers
610    */
611   public synchronized void addUrlwithnoservices(String jwsservers)
612   {
613     if (urlsWithoutServices == null)
614     {
615       urlsWithoutServices = new Vector<>();
616     }
617
618     if ((invalidServiceUrls == null
619             || !invalidServiceUrls.contains(jwsservers))
620             && !urlsWithoutServices.contains(jwsservers))
621     {
622       urlsWithoutServices.add(jwsservers);
623     }
624   }
625
626   /**
627    * add a bad URL to the list
628    * 
629    * @param jwsservers
630    */
631   public synchronized void addInvalidServiceUrl(String jwsservers)
632   {
633     if (invalidServiceUrls == null)
634     {
635       invalidServiceUrls = new Vector<>();
636     }
637     if (!invalidServiceUrls.contains(jwsservers))
638     {
639       invalidServiceUrls.add(jwsservers);
640     }
641   }
642
643   /**
644    * 
645    * @return a human readable report of any problems with the service URLs used
646    *         for discovery
647    */
648   public String getErrorMessages()
649   {
650     if (!isRunning() && !isAborted())
651     {
652       StringBuffer ermsg = new StringBuffer();
653       boolean list = false;
654       if (getInvalidServiceUrls() != null
655               && getInvalidServiceUrls().size() > 0)
656       {
657         ermsg.append(MessageManager.getString("warn.urls_not_contacted")
658                 + ": \n");
659         for (String svcurl : getInvalidServiceUrls())
660         {
661           if (list)
662           {
663             ermsg.append(", ");
664           }
665           list = true;
666           ermsg.append(svcurl);
667         }
668         ermsg.append("\n\n");
669       }
670       list = false;
671       if (getUrlsWithoutServices() != null
672               && getUrlsWithoutServices().size() > 0)
673       {
674         ermsg.append(
675                 MessageManager.getString("warn.urls_no_jaba") + ": \n");
676         for (String svcurl : getUrlsWithoutServices())
677         {
678           if (list)
679           {
680             ermsg.append(", ");
681           }
682           list = true;
683           ermsg.append(svcurl);
684         }
685         ermsg.append("\n");
686       }
687       if (ermsg.length() > 1)
688       {
689         return ermsg.toString();
690       }
691
692     }
693     return null;
694   }
695
696   public int getServerStatusFor(String url)
697   {
698     if (validServiceUrls != null && validServiceUrls.contains(url))
699     {
700       return 1;
701     }
702     if (urlsWithoutServices != null && urlsWithoutServices.contains(url))
703     {
704       return 0;
705     }
706     if (invalidServiceUrls != null && invalidServiceUrls.contains(url))
707     {
708       return -1;
709     }
710     return -2;
711   }
712
713   /**
714    * Set a URL to try before any others. For use with command-line parameter to
715    * configure a local Jabaws installation without the need to add to property
716    * files.
717    * 
718    * @param value
719    * @throws MalformedURLException
720    */
721   public void setPreferredUrl(String value) throws MalformedURLException
722   {
723     if (value != null && value.trim().length() > 0)
724     {
725       new URL(value);
726       preferredUrl = value;
727     }
728   }
729 }