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