Merge branch 'alpha/JAL-3362_Jalview_212_alpha' into alpha/merge_212_JalviewJS_2112
[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.WSMenuEntryProviderI;
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 Runnable, WSMenuEntryProviderI
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   public boolean hasServices()
421   {
422     return !running && services != null && services.size() > 0;
423   }
424
425   public boolean isRunning()
426   {
427     return running;
428   }
429
430   public void setServiceUrls(List<String> wsUrls)
431   {
432     if (wsUrls != null && !wsUrls.isEmpty())
433     {
434       StringBuilder urls = new StringBuilder(128);
435       String sep = "";
436       for (String url : wsUrls)
437       {
438         urls.append(sep);
439         urls.append(url);
440         sep = ",";
441       }
442       Cache.setProperty(JWS2HOSTURLS, urls.toString());
443     }
444     else
445     {
446       Cache.removeProperty(JWS2HOSTURLS);
447     }
448   }
449
450   /**
451    * Returns web service URLs, in the order in which they should be tried (or an
452    * empty list).
453    * 
454    * @return
455    */
456   public List<String> getServiceUrls()
457   {
458     if (testUrls != null)
459     {
460       // return test urls, if there are any, instead of touching cache
461       return testUrls;
462     }
463     List<String> urls = new ArrayList<>();
464
465     if (this.preferredUrl != null)
466     {
467       urls.add(preferredUrl);
468     }
469
470     String surls = Cache.getDefault(JWS2HOSTURLS, COMPBIO_JABAWS);
471     try
472     {
473       StringTokenizer st = new StringTokenizer(surls, ",");
474       while (st.hasMoreElements())
475       {
476         String url = null;
477         try
478         {
479           url = st.nextToken();
480           new URL(url);
481           if (!urls.contains(url))
482           {
483             urls.add(url);
484           }
485           else
486           {
487             Cache.log.warn("Ignoring duplicate url " + url + " in "
488                     + JWS2HOSTURLS + " list");
489           }
490         } catch (MalformedURLException ex)
491         {
492           Cache.log.warn("Problem whilst trying to make a URL from '"
493                   + ((url != null) ? url : "<null>") + "'");
494           Cache.log.warn(
495                   "This was probably due to a malformed comma separated list"
496                           + " in the " + JWS2HOSTURLS
497                           + " entry of $(HOME)/.jalview_properties)");
498           Cache.log.debug("Exception was ", ex);
499         }
500       }
501     } catch (Exception ex)
502     {
503       Cache.log.warn("Error parsing comma separated list of urls in "
504               + JWS2HOSTURLS + " preference.", ex);
505     }
506     return urls;
507   }
508
509   public Vector<ServiceWithParameters> getServices()
510   {
511     return (services == null) ? new Vector<>()
512             : new Vector<>(services);
513   }
514
515   /**
516    * test the given URL with the JabaWS test code
517    * 
518    * @param foo
519    * @return
520    */
521   public static boolean testServiceUrl(URL foo)
522   {
523     try
524     {
525       compbio.ws.client.WSTester
526               .main(new String[]
527               { "-h=" + foo.toString() });
528     } catch (Exception e)
529     {
530       e.printStackTrace();
531       return false;
532     } catch (OutOfMemoryError e)
533     {
534       e.printStackTrace();
535       return false;
536     } catch (Error e)
537     {
538       e.printStackTrace();
539       return false;
540     }
541
542     return true;
543   }
544
545   public boolean restart()
546   {
547     synchronized (this)
548     {
549       if (running)
550       {
551         aborted = true;
552       }
553       else
554       {
555         running = true;
556       }
557       return aborted;
558     }
559   }
560
561   /**
562    * Start a fresh discovery thread and notify the given object when we're
563    * finished. Any known existing threads will be killed before this one is
564    * started.
565    * 
566    * @param changeSupport2
567    * @return new thread
568    */
569   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
570   {
571     /*    if (restart())
572         {
573           return;
574         }
575         else
576         {
577           Thread thr = new Thread(this);
578           thr.start();
579         }
580        */
581     if (isRunning())
582     {
583       setAborted(true);
584     }
585     addPropertyChangeListener(changeSupport2);
586     Thread thr = new Thread(this);
587     thr.start();
588     return thr;
589   }
590
591   /**
592    * @return the invalidServiceUrls
593    */
594   public Vector<String> getInvalidServiceUrls()
595   {
596     return invalidServiceUrls;
597   }
598
599   /**
600    * @return the urlsWithoutServices
601    */
602   public Vector<String> getUrlsWithoutServices()
603   {
604     return urlsWithoutServices;
605   }
606
607   /**
608    * add an 'empty' JABA server to the list. Only servers not already in the
609    * 'bad URL' list will be added to this list.
610    * 
611    * @param jwsservers
612    */
613   public synchronized void addUrlwithnoservices(String jwsservers)
614   {
615     if (urlsWithoutServices == null)
616     {
617       urlsWithoutServices = new Vector<>();
618     }
619
620     if ((invalidServiceUrls == null
621             || !invalidServiceUrls.contains(jwsservers))
622             && !urlsWithoutServices.contains(jwsservers))
623     {
624       urlsWithoutServices.add(jwsservers);
625     }
626   }
627
628   /**
629    * add a bad URL to the list
630    * 
631    * @param jwsservers
632    */
633   public synchronized void addInvalidServiceUrl(String jwsservers)
634   {
635     if (invalidServiceUrls == null)
636     {
637       invalidServiceUrls = new Vector<>();
638     }
639     if (!invalidServiceUrls.contains(jwsservers))
640     {
641       invalidServiceUrls.add(jwsservers);
642     }
643   }
644
645   /**
646    * 
647    * @return a human readable report of any problems with the service URLs used
648    *         for discovery
649    */
650   public String getErrorMessages()
651   {
652     if (!isRunning() && !isAborted())
653     {
654       StringBuffer ermsg = new StringBuffer();
655       boolean list = false;
656       if (getInvalidServiceUrls() != null
657               && getInvalidServiceUrls().size() > 0)
658       {
659         ermsg.append(MessageManager.getString("warn.urls_not_contacted")
660                 + ": \n");
661         for (String svcurl : getInvalidServiceUrls())
662         {
663           if (list)
664           {
665             ermsg.append(", ");
666           }
667           list = true;
668           ermsg.append(svcurl);
669         }
670         ermsg.append("\n\n");
671       }
672       list = false;
673       if (getUrlsWithoutServices() != null
674               && getUrlsWithoutServices().size() > 0)
675       {
676         ermsg.append(
677                 MessageManager.getString("warn.urls_no_jaba") + ": \n");
678         for (String svcurl : getUrlsWithoutServices())
679         {
680           if (list)
681           {
682             ermsg.append(", ");
683           }
684           list = true;
685           ermsg.append(svcurl);
686         }
687         ermsg.append("\n");
688       }
689       if (ermsg.length() > 1)
690       {
691         return ermsg.toString();
692       }
693
694     }
695     return null;
696   }
697
698   public int getServerStatusFor(String url)
699   {
700     if (validServiceUrls != null && validServiceUrls.contains(url))
701     {
702       return 1;
703     }
704     if (urlsWithoutServices != null && urlsWithoutServices.contains(url))
705     {
706       return 0;
707     }
708     if (invalidServiceUrls != null && invalidServiceUrls.contains(url))
709     {
710       return -1;
711     }
712     return -2;
713   }
714
715   /**
716    * Set a URL to try before any others. For use with command-line parameter to
717    * configure a local Jabaws installation without the need to add to property
718    * files.
719    * 
720    * @param value
721    * @throws MalformedURLException
722    */
723   public void setPreferredUrl(String value) throws MalformedURLException
724   {
725     if (value != null && value.trim().length() > 0)
726     {
727       new URL(value);
728       preferredUrl = value;
729     }
730   }
731 }