JAL-976 don't clear menu out before adding jabaws services so JPred service still...
[jalview.git] / src / jalview / ws / jws2 / Jws2Discoverer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.ws.jws2;
19
20 import jalview.bin.Cache;
21 import jalview.gui.AlignFrame;
22 import jalview.gui.Desktop;
23 import jalview.gui.JvSwingUtils;
24 import jalview.ws.WSMenuEntryProviderI;
25 import jalview.ws.jws2.jabaws2.Jws2Instance;
26
27 import java.awt.Color;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.beans.PropertyChangeEvent;
31 import java.beans.PropertyChangeListener;
32 import java.net.URL;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.HashSet;
36 import java.util.Hashtable;
37 import java.util.List;
38 import java.util.StringTokenizer;
39 import java.util.Vector;
40
41 import javax.swing.JMenu;
42 import javax.swing.JMenuItem;
43
44 import compbio.ws.client.Services;
45
46 /**
47  * discoverer for jws2 services. Follows the lightweight service discoverer
48  * pattern (archetyped by EnfinEnvision2OneWay)
49  * 
50  * @author JimP
51  * 
52  */
53 public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
54 {
55   private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
56           this);
57
58   /**
59    * change listeners are notified of "services" property changes
60    * 
61    * @param listener
62    *          to be added that consumes new services Hashtable object.
63    */
64   public void addPropertyChangeListener(
65           java.beans.PropertyChangeListener listener)
66   {
67     changeSupport.addPropertyChangeListener(listener);
68   }
69
70   /**
71    * 
72    * 
73    * @param listener
74    *          to be removed
75    */
76   public void removePropertyChangeListener(
77           java.beans.PropertyChangeListener listener)
78   {
79     changeSupport.removePropertyChangeListener(listener);
80   }
81
82   boolean running = false, aborted = false;
83
84   /**
85    * @return the aborted
86    */
87   public boolean isAborted()
88   {
89     return aborted;
90   }
91
92   /**
93    * @param aborted
94    *          the aborted to set
95    */
96   public void setAborted(boolean aborted)
97   {
98     this.aborted = aborted;
99   }
100
101   Thread oldthread = null;
102
103   public void run()
104   {
105     if (running && oldthread != null && oldthread.isAlive())
106     {
107       if (!aborted)
108       {
109         return;
110       }
111       while (running)
112       {
113         try
114         {
115           Cache.log
116                   .debug("Waiting around for old discovery thread to finish.");
117           // wait around until old discoverer dies
118           Thread.sleep(100);
119         } catch (Exception e)
120         {
121         }
122       }
123       Cache.log.debug("Old discovery thread has finished.");
124     }
125     running = true;
126     changeSupport.firePropertyChange("services", services, new Vector());
127     oldthread = Thread.currentThread();
128     try
129     {
130       Class foo = getClass().getClassLoader().loadClass(
131               "compbio.ws.client.Jws2Client");
132     } catch (ClassNotFoundException e)
133     {
134       System.err
135               .println("Not enabling JABA Webservices : client jar is not available."
136                       + "\nPlease check that your webstart JNLP file is up to date!");
137       running = false;
138       return;
139     }
140     // reinitialise records of good and bad service URLs
141     if (services != null)
142     {
143       services.removeAllElements();
144     }
145     if (urlsWithoutServices != null)
146     {
147       urlsWithoutServices.removeAllElements();
148     }
149     if (invalidServiceUrls != null)
150     {
151       invalidServiceUrls.removeAllElements();
152     }
153     if (validServiceUrls != null)
154     {
155       validServiceUrls.removeAllElements();
156     }
157     ArrayList<String> svctypes=new ArrayList<String>();
158
159     List<JabaWsServerQuery> qrys = new ArrayList<JabaWsServerQuery>();
160     for (final String jwsservers : getServiceUrls())
161     {
162       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsservers);
163       if (svctypes.size()==0)
164       {
165         // TODO: remove this ugly hack to get Canonical JABA service ordering for all possible services 
166         for (Services sv:squery.JABAWS2SERVERS)
167         {
168           svctypes.add(sv.toString());
169         }
170
171       }
172       qrys.add(squery);
173       new Thread(squery).start();
174     }
175     boolean finished = true;
176     do
177     {
178       finished=true;
179       try
180       {
181         Thread.sleep(100);
182       } catch (Exception e)
183       {
184       }
185       ;
186       for (JabaWsServerQuery squery : qrys)
187       {
188         finished = finished && !squery.isRunning();
189       }
190       if (aborted)
191       {
192         Cache.log.debug("Aborting " + qrys.size()
193                 + " JABAWS discovery threads.");
194         for (JabaWsServerQuery squery : qrys)
195         {
196           squery.setQuit(true);
197         }
198       }
199     } while (!aborted && !finished);
200     if (!aborted)
201     {
202       // resort services according to order found in jabaws service list
203       // also ensure servics for each host are ordered in same way.
204       
205       if (services!=null && services.size()>0)
206       {
207         Jws2Instance[] svcs=new Jws2Instance[services.size()];
208         int[] spos=new int[services.size()];
209         int ipos=0;
210         Vector svcUrls = getServiceUrls();
211         for (Jws2Instance svc:services)
212         {
213           svcs[ipos]=svc;
214           spos[ipos++]=1000*svcUrls.indexOf(svc.getHost()) + 1+svctypes.indexOf(svc.serviceType);
215         }
216         jalview.util.QuickSort.sort(spos, svcs);
217         services=new Vector<Jws2Instance>();
218         for (Jws2Instance svc:svcs) {
219           services.add(svc);
220         }
221       }
222     }
223     oldthread = null;
224     running = false;
225     changeSupport.firePropertyChange("services", new Vector(), services);
226   }
227
228   /**
229    * record this service endpoint so we can use it
230    * 
231    * @param jwsservers
232    * @param srv
233    * @param service2
234    */
235   synchronized void addService(String jwsservers, Jws2Instance service)
236   {
237     if (services == null)
238     {
239       services = new Vector<Jws2Instance>();
240     }
241     System.out.println("Discovered service: " + jwsservers + " "
242             + service.toString());
243 //    Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
244 //            service2);
245
246     services.add(service);
247     // retrieve the presets and parameter set and cache now
248     service.getParamStore().getPresets();
249     service.hasParameters();
250     if (validServiceUrls==null)
251     {
252       validServiceUrls=new Vector();
253     }
254     validServiceUrls.add(jwsservers);
255   }
256
257   /**
258    * holds list of services.
259    */
260   protected Vector<Jws2Instance> services;
261   /**
262    * attach all available web services to the appropriate submenu in the given JMenu
263    */
264   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
265   {
266     // dynamically regenerate service list.
267     populateWSMenuEntry(wsmenu, alignFrame, null);
268   }
269
270   private void populateWSMenuEntry(JMenu jws2al, final AlignFrame alignFrame, String typeFilter)
271   {
272     if (running || services == null || services.size() == 0)
273     {
274       return;
275     }
276     boolean byhost = Cache.getDefault("WSMENU_BYHOST", false), bytype = Cache
277             .getDefault("WSMENU_BYTYPE", false);
278     /**
279      * eventually, JWS2 services will appear under the same align/etc submenus.
280      * for moment we keep them separate.
281      */
282     JMenu atpoint;
283     MsaWSClient msacl = new MsaWSClient();
284     Vector hostLabels = new Vector();
285     //jws2al.removeAll();
286     Hashtable<String,String> lasthostFor = new Hashtable<String,String>();
287     Hashtable<String, ArrayList<Jws2Instance>> hosts = new Hashtable<String, ArrayList<Jws2Instance>>();
288     ArrayList<String> hostlist=new ArrayList<String>();
289     for (Jws2Instance service : services)
290     {
291       ArrayList<Jws2Instance> hostservices = hosts.get(service.getHost());
292       if (hostservices == null)
293       {
294         hosts.put(service.getHost(),
295                 hostservices = new ArrayList<Jws2Instance>());
296         hostlist.add(service.getHost());
297       }
298       hostservices.add(service);
299     }
300     // now add hosts in order of the given array
301     for (String host : hostlist)
302     {
303       Jws2Instance orderedsvcs[] = hosts.get(host).toArray(
304               new Jws2Instance[1]);
305       String sortbytype[] = new String[orderedsvcs.length];
306       for (int i = 0; i < sortbytype.length; i++)
307       {
308         sortbytype[i] = orderedsvcs[i].serviceType;
309       }
310       jalview.util.QuickSort.sort(sortbytype, orderedsvcs);
311       for (final Jws2Instance service : orderedsvcs)
312       {
313         atpoint = JvSwingUtils.findOrCreateMenu(jws2al,service.action);
314         String type = service.serviceType;
315         if (byhost)
316         {
317           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, host);
318           if (atpoint.getToolTipText() == null)
319           {
320             atpoint.setToolTipText("Services at " + host);
321           }
322         }
323         if (bytype)
324         {
325           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, type);
326           if (atpoint.getToolTipText() == null)
327           {
328             atpoint.setToolTipText(service.getActionText());
329           }
330         }
331         if (!byhost
332                 && !hostLabels.contains(host + service.serviceType
333                         + service.getActionText()))
334         // !hostLabels.contains(host + (bytype ?
335         // service.serviceType+service.getActionText() : "")))
336         {
337           // add a marker indicating where this service is hosted
338           // relies on services from the same host being listed in a
339           // contiguous
340           // group
341           JMenuItem hitm;
342           atpoint.addSeparator();
343           if (lasthostFor.get(service.action) == null || !lasthostFor.get(service.action).equals(host))
344           {
345             atpoint.add(hitm = new JMenuItem(host));
346             hitm.setForeground(Color.blue);
347             hitm.addActionListener(new ActionListener()
348             {
349
350               @Override
351               public void actionPerformed(ActionEvent e)
352               {
353                 Desktop.showUrl(service.getHost());
354               }
355             });
356             hitm.setToolTipText(JvSwingUtils
357                     .wrapTooltip("Opens the JABAWS server's homepage in web browser"));
358             lasthostFor.put(service.action,host);
359           }
360           hostLabels.addElement(host + service.serviceType
361                   + service.getActionText());
362           // hostLabels.addElement(host + (bytype ?
363           // service.serviceType+service.getActionText() : ""));
364         }
365         
366         service.attachWSMenuEntry(atpoint, alignFrame);
367         /*
368          * JMenuItem sitem = new JMenuItem(service.serviceType);
369          * sitem.setToolTipText("Hosted at " + service.hosturl);
370          * sitem.addActionListener(new ActionListener() {
371          * 
372          * @Override public void actionPerformed(ActionEvent e) { AlignmentView
373          * msa = alignFrame.gatherSequencesForAlignment(); MsaWSClient client =
374          * new MsaWSClient(service, "JWS2 Alignment of " +
375          * alignFrame.getTitle(), msa, false, true,
376          * alignFrame.getViewport().getAlignment().getDataset(), alignFrame); }
377          * });
378          */
379       }
380     }
381
382   }
383
384   public static void main(String[] args)
385   {
386     if (args.length>0)
387     {
388       testUrls = new Vector<String>();
389       for (String url:args)
390       {
391         testUrls.add(url);
392       };
393     }
394     Thread runner = getDiscoverer().startDiscoverer(
395             new PropertyChangeListener()
396             {
397
398               public void propertyChange(PropertyChangeEvent evt)
399               {
400                 if (getDiscoverer().services != null)
401                 {
402                   System.out.println("Changesupport: There are now "
403                           + getDiscoverer().services.size() + " services");
404                   int i=1;
405                   for (Jws2Instance instance:getDiscoverer().services)
406                   {
407                     System.out.println("Service "+i+++" "+instance.getClass()+"@"+instance.getHost()+": "+instance.getActionText());
408                   }
409
410                 }
411               }
412             });
413     while (runner.isAlive())
414     {
415       try
416       {
417         Thread.sleep(50);
418       } catch (InterruptedException e)
419       {
420       }
421       ;
422     }
423     try {
424       Thread.sleep(50);
425     } catch (InterruptedException x) {}
426   }
427
428   private static Jws2Discoverer discoverer;
429
430   public static Jws2Discoverer getDiscoverer()
431   {
432     if (discoverer == null)
433     {
434       discoverer = new Jws2Discoverer();
435     }
436     return discoverer;
437   }
438
439   public boolean hasServices()
440   {
441     // TODO Auto-generated method stub
442     return !running && services != null && services.size() > 0;
443   }
444
445   public boolean isRunning()
446   {
447     return running;
448   }
449
450   /**
451    * the jalview .properties entry for JWS2 URLS
452    */
453   final static String JWS2HOSTURLS = "JWS2HOSTURLS";
454
455   public static void setServiceUrls(Vector<String> urls)
456   {
457     if (urls != null)
458     {
459       StringBuffer urlbuffer = new StringBuffer();
460       String sep = "";
461       for (String url : urls)
462       {
463         urlbuffer.append(sep);
464         urlbuffer.append(url);
465         sep = ",";
466       }
467       Cache.setProperty(JWS2HOSTURLS, urlbuffer.toString());
468     }
469     else
470     {
471       Cache.removeProperty(JWS2HOSTURLS);
472     }
473   }
474
475   private static Vector<String> testUrls=null;
476   public static Vector<String> getServiceUrls()
477   {
478     if (testUrls!=null)
479     {
480       // return test urls, if there are any, instead of touching cache
481       return testUrls;
482     }
483     String surls = Cache.getDefault(JWS2HOSTURLS,
484             "http://www.compbio.dundee.ac.uk/jabaws");
485     Vector<String> urls = new Vector<String>();
486     try
487     {
488       StringTokenizer st = new StringTokenizer(surls, ",");
489       while (st.hasMoreElements())
490       {
491         String url = null;
492         try
493         {
494           java.net.URL u = new java.net.URL(url = st.nextToken());
495           if (!urls.contains(url))
496           {
497             urls.add(url);
498           }
499           else
500           {
501             jalview.bin.Cache.log.info("Ignoring duplicate url in "
502                     + JWS2HOSTURLS + " list");
503           }
504         } catch (Exception ex)
505         {
506           jalview.bin.Cache.log
507                   .warn("Problem whilst trying to make a URL from '"
508                           + ((url != null) ? url : "<null>") + "'");
509           jalview.bin.Cache.log
510                   .warn("This was probably due to a malformed comma separated list"
511                           + " in the "
512                           + JWS2HOSTURLS
513                           + " entry of $(HOME)/.jalview_properties)");
514           jalview.bin.Cache.log.debug("Exception was ", ex);
515         }
516       }
517     } catch (Exception ex)
518     {
519       jalview.bin.Cache.log.warn(
520               "Error parsing comma separated list of urls in "
521                       + JWS2HOSTURLS + " preference.", ex);
522     }
523     if (urls.size() >= 0)
524     {
525       return urls;
526     }
527     return null;
528   }
529
530   public Vector<Jws2Instance> getServices()
531   {
532     return (services == null) ? new Vector<Jws2Instance>()
533             : new Vector<Jws2Instance>(services);
534   }
535
536   /**
537    * test the given URL with the JabaWS test code
538    * 
539    * @param foo
540    * @return
541    */
542   public static boolean testServiceUrl(URL foo)
543   {
544     try
545     {
546       compbio.ws.client.WSTester.main(new String[]
547       { "-h=" + foo.toString() });
548     } catch (Exception e)
549     {
550       e.printStackTrace();
551       return false;
552     } catch (OutOfMemoryError e)
553     {
554       e.printStackTrace();
555       return false;
556     } catch (Error e)
557     {
558       e.printStackTrace();
559       return false;
560     }
561
562     return true;
563   }
564
565   /**
566    * Start a fresh discovery thread and notify the given object when we're
567    * finished. Any known existing threads will be killed before this one is
568    * started.
569    * 
570    * @param changeSupport2
571    * @return new thread
572    */
573   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
574   {
575     if (isRunning())
576     {
577       setAborted(true);
578     }
579     addPropertyChangeListener(changeSupport2);
580     Thread thr = new Thread(this);
581     thr.start();
582     return thr;
583   }
584
585   Vector<String> invalidServiceUrls = null, urlsWithoutServices = null, validServiceUrls=null;
586
587   /**
588    * @return the invalidServiceUrls
589    */
590   public Vector<String> getInvalidServiceUrls()
591   {
592     return invalidServiceUrls;
593   }
594
595   /**
596    * @return the urlsWithoutServices
597    */
598   public Vector<String> getUrlsWithoutServices()
599   {
600     return urlsWithoutServices;
601   }
602
603   /**
604    * add an 'empty' JABA server to the list. Only servers not already in the
605    * 'bad URL' list will be added to this list.
606    * 
607    * @param jwsservers
608    */
609   public synchronized void addUrlwithnoservices(String jwsservers)
610   {
611     if (urlsWithoutServices == null)
612     {
613       urlsWithoutServices = new Vector<String>();
614     }
615
616     if ((invalidServiceUrls == null || !invalidServiceUrls
617             .contains(jwsservers))
618             && !urlsWithoutServices.contains(jwsservers))
619     {
620       urlsWithoutServices.add(jwsservers);
621     }
622   }
623
624   /**
625    * add a bad URL to the list
626    * 
627    * @param jwsservers
628    */
629   public synchronized void addInvalidServiceUrl(String jwsservers)
630   {
631     if (invalidServiceUrls == null)
632     {
633       invalidServiceUrls = new Vector<String>();
634     }
635     if (!invalidServiceUrls.contains(jwsservers))
636     {
637       invalidServiceUrls.add(jwsservers);
638     }
639   }
640
641   /**
642    * 
643    * @return a human readable report of any problems with the service URLs used
644    *         for discovery
645    */
646   public String getErrorMessages()
647   {
648     if (!isRunning() && !isAborted())
649     {
650       StringBuffer ermsg = new StringBuffer();
651       boolean list = false;
652       if (getInvalidServiceUrls() != null
653               && getInvalidServiceUrls().size() > 0)
654       {
655         ermsg.append("URLs that could not be contacted: \n");
656         for (String svcurl : getInvalidServiceUrls())
657         {
658           if (list)
659           {
660             ermsg.append(", ");
661           }
662           list = true;
663           ermsg.append(svcurl);
664         }
665         ermsg.append("\n\n");
666       }
667       list = false;
668       if (getUrlsWithoutServices() != null
669               && getUrlsWithoutServices().size() > 0)
670       {
671         ermsg.append("URLs without any JABA Services : \n");
672         for (String svcurl : getUrlsWithoutServices())
673         {
674           if (list)
675           {
676             ermsg.append(", ");
677           }
678           list = true;
679           ermsg.append(svcurl);
680         }
681         ermsg.append("\n");
682       }
683       if (ermsg.length() > 1)
684       {
685         return ermsg.toString();
686       }
687
688     }
689     return null;
690   }
691
692   public int getServerStatusFor(String url)
693   {
694     if (validServiceUrls!=null && validServiceUrls.contains(url))
695     {
696       return 1;
697     }
698     if (urlsWithoutServices!=null && urlsWithoutServices.contains(url))
699     return 0;
700     if (invalidServiceUrls!=null && invalidServiceUrls.contains(url))
701     {
702       return -1;
703     }
704     return -2;
705   }
706
707   /**
708    * pick the user's preferred service based on a set of URLs (jaba server locations) and service URIs (specifying version and service interface class)
709    * 
710    * @param serviceURL
711    * @return null or best match for given uri/ls.
712    */
713   public Jws2Instance getPreferredServiceFor(String[] serviceURLs)
714   {
715     HashSet<String> urls=new HashSet<String>();
716     urls.addAll(Arrays.asList(serviceURLs));
717     Jws2Instance match=null;
718     for (Jws2Instance svc:services)
719     {
720       if (urls.contains(svc.getServiceTypeURI()))
721       {
722         if (match==null) {
723           // for moment we always pick service from server ordered first in user's preferences
724           match=svc;
725         }
726         if (urls.contains(svc.getUri()))
727         {
728           // stop and return - we've matched type URI and URI for service endpoint
729           return svc;
730         }
731       }
732     }
733     return match;
734   }
735 }