JAL-3690 refactoring web-services discovery
[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.CopyOnWriteArraySet;
45 import java.util.concurrent.ExecutionException;
46 import java.util.concurrent.Future;
47 import java.util.concurrent.FutureTask;
48
49 import javax.swing.JMenu;
50
51 import compbio.ws.client.Services;
52
53 /**
54  * discoverer for jws2 services. Follows the lightweight service discoverer
55  * pattern (archetyped by EnfinEnvision2OneWay)
56  * 
57  * @author JimP
58  * 
59  */
60 public class Jws2Discoverer implements WSDiscovererI, Runnable
61 {
62   public static final String COMPBIO_JABAWS = "http://www.compbio.dundee.ac.uk/jabaws";
63
64   /*
65    * the .jalview_properties entry for JWS2 URLS
66    */
67   private final static String JWS2HOSTURLS = "JWS2HOSTURLS";
68
69   /*
70    * Singleton instance
71    */
72   private static Jws2Discoverer discoverer;
73
74   /*
75    * Override for testing only
76    */
77   private static List<String> testUrls = null;
78
79   // preferred url has precedence over others
80   private String preferredUrl;
81   
82   private Set<ServiceChangeListener> serviceListeners = new CopyOnWriteArraySet<>();
83
84   private Vector<String> invalidServiceUrls = null;
85
86   private Vector<String> urlsWithoutServices = null;
87
88   private Vector<String> validServiceUrls = null;
89
90   private volatile boolean running = false;
91
92   private volatile boolean aborted = false;
93   
94   private volatile Thread oldthread = null;
95
96   /**
97    * holds list of services.
98    */
99   protected Vector<Jws2Instance> services;
100
101   /**
102    * Private constructor enforces use of singleton via getDiscoverer()
103    */
104   private Jws2Discoverer()
105   {
106   }
107
108
109   @Override
110   public void addServiceChangeListener(ServiceChangeListener listener)
111   {
112     serviceListeners.add(listener);
113   }
114
115   @Override
116   public void removeServiceChangeListener(ServiceChangeListener listener)
117   {
118     serviceListeners.remove(listener);
119   }
120
121   private void notifyServiceListeners(List<? extends ServiceWithParameters> services) 
122   {
123     if (services == null) services = this.services;
124     for (var listener : serviceListeners) {
125       listener.servicesChanged(this, services);
126     }
127   }
128
129   /**
130    * @return the aborted
131    */
132   public boolean isAborted()
133   {
134     return aborted;
135   }
136
137   /**
138    * @param aborted
139    *          the aborted to set
140    */
141   public void setAborted(boolean aborted)
142   {
143     this.aborted = aborted;
144   }
145
146   @Override
147   public void run()
148   {
149
150     if (running && oldthread != null && oldthread.isAlive())
151     {
152       if (!aborted)
153       {
154         return;
155       }
156       while (running)
157       {
158         try
159         {
160           Cache.log.debug(
161                   "Waiting around for old discovery thread to finish.");
162           // wait around until old discoverer dies
163           Thread.sleep(100);
164         } catch (Exception e)
165         {
166         }
167       }
168       aborted = false;
169       Cache.log.debug("Old discovery thread has finished.");
170     }
171     running = true;
172
173     // first set up exclusion list if needed
174     final Set<String> ignoredServices = new HashSet<>();
175     for (String ignored : Cache
176             .getDefault("IGNORED_JABAWS_SERVICETYPES", "").split("\\|"))
177     {
178       ignoredServices.add(ignored);
179     }
180
181     notifyServiceListeners(Collections.emptyList());
182     oldthread = Thread.currentThread();
183     try
184     {
185       getClass().getClassLoader().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     notifyServiceListeners(services);
291   }
292
293   /**
294    * record this service endpoint so we can use it
295    * 
296    * @param jwsservers
297    * @param srv
298    * @param service2
299    */
300   synchronized void addService(String jwsservers, Jws2Instance service)
301   {
302     if (services == null)
303     {
304       services = new Vector<>();
305     }
306     System.out.println(
307             "Discovered service: " + jwsservers + " " + service.toString());
308     // Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
309     // service2);
310
311     services.add(service);
312     // retrieve the presets and parameter set and cache now
313     ParamDatastoreI pds = service.getParamStore();
314     if (pds != null)
315     {
316       pds.getPresets();
317     }
318     service.hasParameters();
319     if (validServiceUrls == null)
320     {
321       validServiceUrls = new Vector<>();
322     }
323     validServiceUrls.add(jwsservers);
324   }
325
326   /**
327    * 
328    * @param args
329    * @j2sIgnore
330    */
331   public static void main(String[] args)
332   {
333     if (args.length > 0)
334     {
335       testUrls = new ArrayList<>();
336       for (String url : args)
337       {
338         testUrls.add(url);
339       }
340     }
341     var discoverer = getDiscoverer();
342     discoverer.addServiceChangeListener((_discoverer, _services) -> {
343       if (discoverer.services != null)
344       {
345         System.out.println("Changesupport: There are now "
346                 + discoverer.services.size() + " services");
347         int i = 1;
348         for (ServiceWithParameters instance : discoverer.services)
349         {
350           System.out.println(
351                   "Service " + i++ + " " + instance.getClass()
352                           + "@" + instance.getHostURL() + ": "
353                           + instance.getActionText());
354         }
355
356       }
357     });
358     try
359     {
360       discoverer.startDiscoverer().get();
361     } catch (InterruptedException | ExecutionException e)
362     {
363     }
364     try
365     {
366       Thread.sleep(50);
367     } catch (InterruptedException x)
368     {
369     }
370   }
371
372   /**
373    * Returns the singleton instance of this class.
374    * 
375    * @return
376    */
377   public static Jws2Discoverer getDiscoverer()
378   {
379     if (discoverer == null)
380     {
381       discoverer = new Jws2Discoverer();
382     }
383     return discoverer;
384   }
385
386   @Override
387   public boolean hasServices()
388   {
389     return !running && services != null && services.size() > 0;
390   }
391
392   @Override
393   public boolean isRunning()
394   {
395     return running;
396   }
397
398   @Override
399   public void setServiceUrls(List<String> wsUrls)
400   {
401     if (wsUrls != null && !wsUrls.isEmpty())
402     {
403       StringBuilder urls = new StringBuilder(128);
404       String sep = "";
405       for (String url : wsUrls)
406       {
407         urls.append(sep);
408         urls.append(url);
409         sep = ",";
410       }
411       Cache.setProperty(JWS2HOSTURLS, urls.toString());
412     }
413     else
414     {
415       Cache.removeProperty(JWS2HOSTURLS);
416     }
417   }
418
419   /**
420    * Returns web service URLs, in the order in which they should be tried (or an
421    * empty list).
422    * 
423    * @return
424    */
425   @Override
426   public List<String> getServiceUrls()
427   {
428     if (testUrls != null)
429     {
430       // return test urls, if there are any, instead of touching cache
431       return testUrls;
432     }
433     List<String> urls = new ArrayList<>();
434
435     if (this.preferredUrl != null)
436     {
437       urls.add(preferredUrl);
438     }
439
440     String surls = Cache.getDefault(JWS2HOSTURLS, COMPBIO_JABAWS);
441     try
442     {
443       StringTokenizer st = new StringTokenizer(surls, ",");
444       while (st.hasMoreElements())
445       {
446         String url = null;
447         try
448         {
449           url = st.nextToken();
450           new URL(url);
451           if (!urls.contains(url))
452           {
453             urls.add(url);
454           }
455           else
456           {
457             Cache.log.warn("Ignoring duplicate url " + url + " in "
458                     + JWS2HOSTURLS + " list");
459           }
460         } catch (MalformedURLException ex)
461         {
462           Cache.log.warn("Problem whilst trying to make a URL from '"
463                   + ((url != null) ? url : "<null>") + "'");
464           Cache.log.warn(
465                   "This was probably due to a malformed comma separated list"
466                           + " in the " + JWS2HOSTURLS
467                           + " entry of $(HOME)/.jalview_properties)");
468           Cache.log.debug("Exception was ", ex);
469         }
470       }
471     } catch (Exception ex)
472     {
473       Cache.log.warn("Error parsing comma separated list of urls in "
474               + JWS2HOSTURLS + " preference.", ex);
475     }
476     return urls;
477   }
478
479   @Override
480   public Vector<ServiceWithParameters> getServices()
481   {
482     return (services == null) ? new Vector<>() : new Vector<>(services);
483   }
484
485   /**
486    * test the given URL with the JabaWS test code
487    * 
488    * @param foo
489    * @return
490    */
491   @Override
492   public boolean testServiceUrl(URL foo)
493   {
494     try
495     {
496       compbio.ws.client.WSTester
497               .main(new String[]
498               { "-h=" + foo.toString() });
499     } catch (Exception e)
500     {
501       e.printStackTrace();
502       return false;
503     } catch (OutOfMemoryError e)
504     {
505       e.printStackTrace();
506       return false;
507     } catch (Error e)
508     {
509       e.printStackTrace();
510       return false;
511     }
512
513     return true;
514   }
515
516   public boolean restart()
517   {
518     synchronized (this)
519     {
520       if (running)
521       {
522         aborted = true;
523       }
524       else
525       {
526         running = true;
527       }
528       return aborted;
529     }
530   }
531
532   /**
533    * Start a fresh discovery thread and notify the given object when we're
534    * finished. Any known existing threads will be killed before this one is
535    * started.
536    * 
537    * @param changeSupport2
538    * @return new thread
539    */
540   @Override
541   public FutureTask<WSDiscovererI> startDiscoverer()
542   {
543     /*    if (restart())
544         {
545           return;
546         }
547         else
548         {
549           Thread thr = new Thread(this);
550           thr.start();
551         }
552        */
553     if (isRunning())
554     {
555       setAborted(true);
556     }
557     FutureTask<WSDiscovererI> task = new FutureTask<>(this, this);
558     new Thread(task).start();
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 }