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