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