Merge branch 'feature/JAL-3686_slivka_client_js_update' into alpha/merge_212_JalviewJ...
[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.WSDiscovererI;
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 WSDiscovererI, Runnable
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       for (JabaWsServerQuery squery : qrys)
244       {
245         if (squery.isRunning())
246         {
247           finished = false;
248         }
249       }
250       if (aborted)
251       {
252         Cache.log.debug(
253                 "Aborting " + qrys.size() + " JABAWS discovery threads.");
254         for (JabaWsServerQuery squery : qrys)
255         {
256           squery.setQuit(true);
257         }
258       }
259     } while (!aborted && !finished);
260     if (!aborted)
261     {
262       // resort services according to order found in jabaws service list
263       // also ensure services for each host are ordered in same way.
264
265       if (services != null && services.size() > 0)
266       {
267         Jws2Instance[] svcs = new Jws2Instance[services.size()];
268         int[] spos = new int[services.size()];
269         int ipos = 0;
270         List<String> svcUrls = getServiceUrls();
271         for (Jws2Instance svc : services)
272         {
273           svcs[ipos] = svc;
274           spos[ipos++] = 1000 * svcUrls.indexOf(svc.getHostURL()) + 1
275                   + svctypes.indexOf(svc.getName());
276         }
277         jalview.util.QuickSort.sort(spos, svcs);
278         services = new Vector<>();
279         for (Jws2Instance svc : svcs)
280         {
281           if (!ignoredServices.contains(svc.getName()))
282           {
283             services.add(svc);
284           }
285         }
286       }
287     }
288     oldthread = null;
289     running = false;
290     changeSupport.firePropertyChange("services", new Vector<Jws2Instance>(),
291             services);
292   }
293
294   /**
295    * record this service endpoint so we can use it
296    * 
297    * @param jwsservers
298    * @param srv
299    * @param service2
300    */
301   synchronized void addService(String jwsservers, Jws2Instance service)
302   {
303     if (services == null)
304     {
305       services = new Vector<>();
306     }
307     System.out.println(
308             "Discovered service: " + jwsservers + " " + service.toString());
309     // Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
310     // service2);
311
312     services.add(service);
313     // retrieve the presets and parameter set and cache now
314     ParamDatastoreI pds = service.getParamStore();
315     if (pds != null)
316     {
317       pds.getPresets();
318     }
319     service.hasParameters();
320     if (validServiceUrls == null)
321     {
322       validServiceUrls = new Vector<>();
323     }
324     validServiceUrls.add(jwsservers);
325   }
326
327   /**
328    * attach all available web services to the appropriate submenu in the given
329    * JMenu
330    */
331   @Override
332   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
333   {
334     if (running || services == null || services.size() == 0)
335     {
336       return;
337     }
338     // dynamically regenerate service list.
339     populateWSMenuEntry(wsmenu, alignFrame, null);
340   }
341
342   private void populateWSMenuEntry(JMenu jws2al,
343           final AlignFrame alignFrame, String typeFilter)
344   {
345     PreferredServiceRegistry.getRegistry().populateWSMenuEntry(
346             getServices(),
347             changeSupport, jws2al,
348             alignFrame, typeFilter);
349   }
350
351   /**
352    * 
353    * @param args
354    * @j2sIgnore
355    */
356   public static void main(String[] args)
357   {
358     if (args.length > 0)
359     {
360       testUrls = new ArrayList<>();
361       for (String url : args)
362       {
363         testUrls.add(url);
364       }
365     }
366     Thread runner = getDiscoverer()
367             .startDiscoverer(new PropertyChangeListener()
368             {
369
370               @Override
371               public void propertyChange(PropertyChangeEvent evt)
372               {
373                 if (getDiscoverer().services != null)
374                 {
375                   System.out.println("Changesupport: There are now "
376                           + getDiscoverer().services.size() + " services");
377                   int i = 1;
378                   for (ServiceWithParameters instance : getDiscoverer().services)
379                   {
380                     System.out.println("Service " + i++ + " "
381                             + instance.getClass() + "@"
382                             + instance.getHostURL()
383                             + ": " + instance.getActionText());
384                   }
385
386                 }
387               }
388             });
389     while (runner.isAlive())
390     {
391       try
392       {
393         Thread.sleep(50);
394       } catch (InterruptedException e)
395       {
396       }
397     }
398     try
399     {
400       Thread.sleep(50);
401     } catch (InterruptedException x)
402     {
403     }
404   }
405
406   /**
407    * Returns the singleton instance of this class.
408    * 
409    * @return
410    */
411   public static Jws2Discoverer getDiscoverer()
412   {
413     if (discoverer == null)
414     {
415       discoverer = new Jws2Discoverer();
416     }
417     return discoverer;
418   }
419
420   @Override
421   public boolean hasServices()
422   {
423     return !running && services != null && services.size() > 0;
424   }
425
426   @Override
427   public boolean isRunning()
428   {
429     return running;
430   }
431
432   @Override
433   public void setServiceUrls(List<String> wsUrls)
434   {
435     if (wsUrls != null && !wsUrls.isEmpty())
436     {
437       StringBuilder urls = new StringBuilder(128);
438       String sep = "";
439       for (String url : wsUrls)
440       {
441         urls.append(sep);
442         urls.append(url);
443         sep = ",";
444       }
445       Cache.setProperty(JWS2HOSTURLS, urls.toString());
446     }
447     else
448     {
449       Cache.removeProperty(JWS2HOSTURLS);
450     }
451   }
452
453   /**
454    * Returns web service URLs, in the order in which they should be tried (or an
455    * empty list).
456    * 
457    * @return
458    */
459   @Override
460   public List<String> getServiceUrls()
461   {
462     if (testUrls != null)
463     {
464       // return test urls, if there are any, instead of touching cache
465       return testUrls;
466     }
467     List<String> urls = new ArrayList<>();
468
469     if (this.preferredUrl != null)
470     {
471       urls.add(preferredUrl);
472     }
473
474     String surls = Cache.getDefault(JWS2HOSTURLS, COMPBIO_JABAWS);
475     try
476     {
477       StringTokenizer st = new StringTokenizer(surls, ",");
478       while (st.hasMoreElements())
479       {
480         String url = null;
481         try
482         {
483           url = st.nextToken();
484           new URL(url);
485           if (!urls.contains(url))
486           {
487             urls.add(url);
488           }
489           else
490           {
491             Cache.log.warn("Ignoring duplicate url " + url + " in "
492                     + JWS2HOSTURLS + " list");
493           }
494         } catch (MalformedURLException ex)
495         {
496           Cache.log.warn("Problem whilst trying to make a URL from '"
497                   + ((url != null) ? url : "<null>") + "'");
498           Cache.log.warn(
499                   "This was probably due to a malformed comma separated list"
500                           + " in the " + JWS2HOSTURLS
501                           + " entry of $(HOME)/.jalview_properties)");
502           Cache.log.debug("Exception was ", ex);
503         }
504       }
505     } catch (Exception ex)
506     {
507       Cache.log.warn("Error parsing comma separated list of urls in "
508               + JWS2HOSTURLS + " preference.", ex);
509     }
510     return urls;
511   }
512
513   @Override
514   public Vector<ServiceWithParameters> getServices()
515   {
516     return (services == null) ? new Vector<>()
517             : new Vector<>(services);
518   }
519
520   /**
521    * test the given URL with the JabaWS test code
522    * 
523    * @param foo
524    * @return
525    */
526   @Override
527   public boolean testServiceUrl(URL foo)
528   {
529     try
530     {
531       compbio.ws.client.WSTester
532               .main(new String[]
533               { "-h=" + foo.toString() });
534     } catch (Exception e)
535     {
536       e.printStackTrace();
537       return false;
538     } catch (OutOfMemoryError e)
539     {
540       e.printStackTrace();
541       return false;
542     } catch (Error e)
543     {
544       e.printStackTrace();
545       return false;
546     }
547
548     return true;
549   }
550
551   public boolean restart()
552   {
553     synchronized (this)
554     {
555       if (running)
556       {
557         aborted = true;
558       }
559       else
560       {
561         running = true;
562       }
563       return aborted;
564     }
565   }
566
567   /**
568    * Start a fresh discovery thread and notify the given object when we're
569    * finished. Any known existing threads will be killed before this one is
570    * started.
571    * 
572    * @param changeSupport2
573    * @return new thread
574    */
575   @Override
576   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
577   {
578     /*    if (restart())
579         {
580           return;
581         }
582         else
583         {
584           Thread thr = new Thread(this);
585           thr.start();
586         }
587        */
588     if (isRunning())
589     {
590       setAborted(true);
591     }
592     addPropertyChangeListener(changeSupport2);
593     Thread thr = new Thread(this);
594     thr.start();
595     return thr;
596   }
597
598   /**
599    * @return the invalidServiceUrls
600    */
601   public Vector<String> getInvalidServiceUrls()
602   {
603     return invalidServiceUrls;
604   }
605
606   /**
607    * @return the urlsWithoutServices
608    */
609   public Vector<String> getUrlsWithoutServices()
610   {
611     return urlsWithoutServices;
612   }
613
614   /**
615    * add an 'empty' JABA server to the list. Only servers not already in the
616    * 'bad URL' list will be added to this list.
617    * 
618    * @param jwsservers
619    */
620   public synchronized void addUrlwithnoservices(String jwsservers)
621   {
622     if (urlsWithoutServices == null)
623     {
624       urlsWithoutServices = new Vector<>();
625     }
626
627     if ((invalidServiceUrls == null
628             || !invalidServiceUrls.contains(jwsservers))
629             && !urlsWithoutServices.contains(jwsservers))
630     {
631       urlsWithoutServices.add(jwsservers);
632     }
633   }
634
635   /**
636    * add a bad URL to the list
637    * 
638    * @param jwsservers
639    */
640   public synchronized void addInvalidServiceUrl(String jwsservers)
641   {
642     if (invalidServiceUrls == null)
643     {
644       invalidServiceUrls = new Vector<>();
645     }
646     if (!invalidServiceUrls.contains(jwsservers))
647     {
648       invalidServiceUrls.add(jwsservers);
649     }
650   }
651
652   /**
653    * 
654    * @return a human readable report of any problems with the service URLs used
655    *         for discovery
656    */
657   @Override
658   public String getErrorMessages()
659   {
660     if (!isRunning() && !isAborted())
661     {
662       StringBuffer ermsg = new StringBuffer();
663       boolean list = false;
664       if (getInvalidServiceUrls() != null
665               && getInvalidServiceUrls().size() > 0)
666       {
667         ermsg.append(MessageManager.getString("warn.urls_not_contacted")
668                 + ": \n");
669         for (String svcurl : getInvalidServiceUrls())
670         {
671           if (list)
672           {
673             ermsg.append(", ");
674           }
675           list = true;
676           ermsg.append(svcurl);
677         }
678         ermsg.append("\n\n");
679       }
680       list = false;
681       if (getUrlsWithoutServices() != null
682               && getUrlsWithoutServices().size() > 0)
683       {
684         ermsg.append(
685                 MessageManager.getString("warn.urls_no_jaba") + ": \n");
686         for (String svcurl : getUrlsWithoutServices())
687         {
688           if (list)
689           {
690             ermsg.append(", ");
691           }
692           list = true;
693           ermsg.append(svcurl);
694         }
695         ermsg.append("\n");
696       }
697       if (ermsg.length() > 1)
698       {
699         return ermsg.toString();
700       }
701
702     }
703     return null;
704   }
705
706   @Override
707   public int getServerStatusFor(String url)
708   {
709     if (validServiceUrls != null && validServiceUrls.contains(url))
710     {
711       return STATUS_OK;
712     }
713     if (urlsWithoutServices != null && urlsWithoutServices.contains(url))
714     {
715       return STATUS_NO_SERVICES;
716     }
717     if (invalidServiceUrls != null && invalidServiceUrls.contains(url))
718     {
719       return STATUS_INVALID;
720     }
721     return STATUS_UNKNOWN;
722   }
723
724   /**
725    * Set a URL to try before any others. For use with command-line parameter to
726    * configure a local Jabaws installation without the need to add to property
727    * files.
728    * 
729    * @param value
730    * @throws MalformedURLException
731    */
732   public void setPreferredUrl(String value) throws MalformedURLException
733   {
734     if (value != null && value.trim().length() > 0)
735     {
736       new URL(value);
737       preferredUrl = value;
738     }
739   }
740 }