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