JAL-1432 updated copyright notices
[jalview.git] / src / jalview / ws / jws2 / Jws2Discoverer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.0b1)
3  * Copyright (C) 2014 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 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  * The Jalview Authors are detailed in the 'AUTHORS' file.
18  */
19 package jalview.ws.jws2;
20
21 import jalview.bin.Cache;
22 import jalview.gui.AlignFrame;
23 import jalview.gui.Desktop;
24 import jalview.gui.JvSwingUtils;
25 import jalview.ws.WSMenuEntryProviderI;
26 import jalview.ws.jws2.jabaws2.Jws2Instance;
27 import jalview.ws.params.ParamDatastoreI;
28
29 import java.awt.Color;
30 import java.awt.event.ActionEvent;
31 import java.awt.event.ActionListener;
32 import java.beans.PropertyChangeEvent;
33 import java.beans.PropertyChangeListener;
34 import java.net.URL;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.Hashtable;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.StringTokenizer;
43 import java.util.Vector;
44
45 import javax.swing.JMenu;
46 import javax.swing.JMenuItem;
47
48 import compbio.ws.client.Services;
49
50 /**
51  * discoverer for jws2 services. Follows the lightweight service discoverer
52  * pattern (archetyped by EnfinEnvision2OneWay)
53  * 
54  * @author JimP
55  * 
56  */
57 public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
58 {
59   private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
60           this);
61
62   /**
63    * change listeners are notified of "services" property changes
64    * 
65    * @param listener
66    *          to be added that consumes new services Hashtable object.
67    */
68   public void addPropertyChangeListener(
69           java.beans.PropertyChangeListener listener)
70   {
71     changeSupport.addPropertyChangeListener(listener);
72   }
73
74   /**
75    * 
76    * 
77    * @param listener
78    *          to be removed
79    */
80   public void removePropertyChangeListener(
81           java.beans.PropertyChangeListener listener)
82   {
83     changeSupport.removePropertyChangeListener(listener);
84   }
85
86   boolean running = false, aborted = false;
87
88   /**
89    * @return the aborted
90    */
91   public boolean isAborted()
92   {
93     return aborted;
94   }
95
96   /**
97    * @param aborted
98    *          the aborted to set
99    */
100   public void setAborted(boolean aborted)
101   {
102     this.aborted = aborted;
103   }
104
105   Thread oldthread = null;
106
107   public void run()
108   {
109     if (running && oldthread != null && oldthread.isAlive())
110     {
111       if (!aborted)
112       {
113         return;
114       }
115       while (running)
116       {
117         try
118         {
119           Cache.log
120                   .debug("Waiting around for old discovery thread to finish.");
121           // wait around until old discoverer dies
122           Thread.sleep(100);
123         } catch (Exception e)
124         {
125         }
126       }
127       Cache.log.debug("Old discovery thread has finished.");
128     }
129     running = true;
130     changeSupport.firePropertyChange("services", services, new Vector());
131     oldthread = Thread.currentThread();
132     try
133     {
134       Class foo = getClass().getClassLoader().loadClass(
135               "compbio.ws.client.Jws2Client");
136     } catch (ClassNotFoundException e)
137     {
138       System.err
139               .println("Not enabling JABA Webservices : client jar is not available."
140                       + "\nPlease check that your webstart JNLP file is up to date!");
141       running = false;
142       return;
143     }
144     // reinitialise records of good and bad service URLs
145     if (services != null)
146     {
147       services.removeAllElements();
148     }
149     if (urlsWithoutServices != null)
150     {
151       urlsWithoutServices.removeAllElements();
152     }
153     if (invalidServiceUrls != null)
154     {
155       invalidServiceUrls.removeAllElements();
156     }
157     if (validServiceUrls != null)
158     {
159       validServiceUrls.removeAllElements();
160     }
161     ArrayList<String> svctypes = new ArrayList<String>();
162
163     List<JabaWsServerQuery> qrys = new ArrayList<JabaWsServerQuery>();
164     for (final String jwsservers : getServiceUrls())
165     {
166       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsservers);
167       if (svctypes.size() == 0)
168       {
169         // TODO: remove this ugly hack to get Canonical JABA service ordering
170         // for all possible services
171         for (Services sv : squery.JABAWS2SERVERS)
172         {
173           svctypes.add(sv.toString());
174         }
175
176       }
177       qrys.add(squery);
178       new Thread(squery).start();
179     }
180     boolean finished = true;
181     do
182     {
183       finished = true;
184       try
185       {
186         Thread.sleep(100);
187       } catch (Exception e)
188       {
189       }
190       ;
191       for (JabaWsServerQuery squery : qrys)
192       {
193         finished = finished && !squery.isRunning();
194       }
195       if (aborted)
196       {
197         Cache.log.debug("Aborting " + qrys.size()
198                 + " JABAWS discovery threads.");
199         for (JabaWsServerQuery squery : qrys)
200         {
201           squery.setQuit(true);
202         }
203       }
204     } while (!aborted && !finished);
205     if (!aborted)
206     {
207       // resort services according to order found in jabaws service list
208       // also ensure servics for each host are ordered in same way.
209
210       if (services != null && services.size() > 0)
211       {
212         Jws2Instance[] svcs = new Jws2Instance[services.size()];
213         int[] spos = new int[services.size()];
214         int ipos = 0;
215         Vector svcUrls = getServiceUrls();
216         for (Jws2Instance svc : services)
217         {
218           svcs[ipos] = svc;
219           spos[ipos++] = 1000 * svcUrls.indexOf(svc.getHost()) + 1
220                   + svctypes.indexOf(svc.serviceType);
221         }
222         jalview.util.QuickSort.sort(spos, svcs);
223         services = new Vector<Jws2Instance>();
224         for (Jws2Instance svc : svcs)
225         {
226           services.add(svc);
227         }
228       }
229     }
230     oldthread = null;
231     running = false;
232     changeSupport.firePropertyChange("services", new Vector(), services);
233   }
234
235   /**
236    * record this service endpoint so we can use it
237    * 
238    * @param jwsservers
239    * @param srv
240    * @param service2
241    */
242   synchronized void addService(String jwsservers, Jws2Instance service)
243   {
244     if (services == null)
245     {
246       services = new Vector<Jws2Instance>();
247     }
248     System.out.println("Discovered service: " + jwsservers + " "
249             + service.toString());
250     // Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
251     // service2);
252
253     services.add(service);
254     // retrieve the presets and parameter set and cache now
255     ParamDatastoreI pds = service.getParamStore();
256     if (pds != null)
257     {
258       pds.getPresets();
259     }
260     service.hasParameters();
261     if (validServiceUrls == null)
262     {
263       validServiceUrls = new Vector();
264     }
265     validServiceUrls.add(jwsservers);
266   }
267
268   /**
269    * holds list of services.
270    */
271   protected Vector<Jws2Instance> services;
272
273   /**
274    * attach all available web services to the appropriate submenu in the given
275    * JMenu
276    */
277   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
278   {
279     // dynamically regenerate service list.
280     populateWSMenuEntry(wsmenu, alignFrame, null);
281   }
282
283   private boolean isRecalculable(String action)
284   {
285     return (action != null && action.equalsIgnoreCase("conservation"));
286   }
287
288   private void populateWSMenuEntry(JMenu jws2al,
289           final AlignFrame alignFrame, String typeFilter)
290   {
291     if (running || services == null || services.size() == 0)
292     {
293       return;
294     }
295     boolean byhost = Cache.getDefault("WSMENU_BYHOST", false), bytype = Cache
296             .getDefault("WSMENU_BYTYPE", false);
297     /**
298      * eventually, JWS2 services will appear under the same align/etc submenus.
299      * for moment we keep them separate.
300      */
301     JMenu atpoint;
302     List<Jws2Instance> enumerableServices = new ArrayList<Jws2Instance>();
303     // jws2al.removeAll();
304     Map<String, Jws2Instance> preferredHosts = new HashMap<String, Jws2Instance>();
305     Map<String, List<Jws2Instance>> alternates = new HashMap<String, List<Jws2Instance>>();
306     for (Jws2Instance service : services.toArray(new Jws2Instance[0]))
307     {
308       if (!isRecalculable(service.action))
309       {
310         // add 'one shot' services to be displayed using the classic menu
311         // structure
312         enumerableServices.add(service);
313       }
314       else
315       {
316         if (!preferredHosts.containsKey(service.serviceType))
317         {
318           Jws2Instance preferredInstance = getPreferredServiceFor(
319                   alignFrame, service.serviceType);
320           if (preferredInstance != null)
321           {
322             preferredHosts.put(service.serviceType, preferredInstance);
323           }
324           else
325           {
326             preferredHosts.put(service.serviceType, service);
327           }
328         }
329         List<Jws2Instance> ph = alternates.get(service.serviceType);
330         if (preferredHosts.get(service.serviceType) != service)
331         {
332           if (ph == null)
333           {
334             ph = new ArrayList<Jws2Instance>();
335           }
336           ph.add(service);
337           alternates.put(service.serviceType, ph);
338         }
339       }
340
341     }
342
343     // create GUI element for classic services
344     addEnumeratedServices(jws2al, alignFrame, enumerableServices);
345     // and the instantaneous services
346     for (final Jws2Instance service : preferredHosts.values())
347     {
348       atpoint = JvSwingUtils.findOrCreateMenu(jws2al, service.action);
349       JMenuItem hitm;
350       if (atpoint.getItemCount() > 1)
351       {
352         // previous service of this type already present
353         atpoint.addSeparator();
354       }
355       atpoint.add(hitm = new JMenuItem(service.getHost()));
356       hitm.setForeground(Color.blue);
357       hitm.addActionListener(new ActionListener()
358       {
359
360         @Override
361         public void actionPerformed(ActionEvent e)
362         {
363           Desktop.showUrl(service.getHost());
364         }
365       });
366       hitm.setToolTipText(JvSwingUtils
367               .wrapTooltip("Opens the JABAWS server's homepage in web browser"));
368
369       service.attachWSMenuEntry(atpoint, alignFrame);
370       if (alternates.containsKey(service.serviceType))
371       {
372         atpoint.add(hitm = new JMenu("Switch server"));
373         hitm.setToolTipText(JvSwingUtils
374                 .wrapTooltip("Choose a server for running this service"));
375         for (final Jws2Instance sv : alternates.get(service.serviceType))
376         {
377           JMenuItem itm;
378           hitm.add(itm = new JMenuItem(sv.getHost()));
379           itm.setForeground(Color.blue);
380           itm.addActionListener(new ActionListener()
381           {
382
383             @Override
384             public void actionPerformed(ActionEvent arg0)
385             {
386               new Thread(new Runnable()
387               {
388                 public void run()
389                 {
390                   setPreferredServiceFor(alignFrame, sv.serviceType,
391                           sv.action, sv);
392                   changeSupport.firePropertyChange("services",
393                           new Vector(), services);
394                 };
395               }).start();
396
397             }
398           });
399         }
400         /*
401          * hitm.addActionListener(new ActionListener() {
402          * 
403          * @Override public void actionPerformed(ActionEvent arg0) { new
404          * Thread(new Runnable() {
405          * 
406          * @Override public void run() { new SetPreferredServer(alignFrame,
407          * service.serviceType, service.action); } }).start(); } });
408          */
409       }
410     }
411   }
412
413   /**
414    * add services using the Java 2.5/2.6/2.7 system which optionally creates
415    * submenus to index by host and service program type
416    */
417   private void addEnumeratedServices(final JMenu jws2al,
418           final AlignFrame alignFrame, List<Jws2Instance> enumerableServices)
419   {
420     boolean byhost = Cache.getDefault("WSMENU_BYHOST", false), bytype = Cache
421             .getDefault("WSMENU_BYTYPE", false);
422     /**
423      * eventually, JWS2 services will appear under the same align/etc submenus.
424      * for moment we keep them separate.
425      */
426     JMenu atpoint;
427     MsaWSClient msacl = new MsaWSClient();
428     List<String> hostLabels = new ArrayList<String>();
429     Hashtable<String, String> lasthostFor = new Hashtable<String, String>();
430     Hashtable<String, ArrayList<Jws2Instance>> hosts = new Hashtable<String, ArrayList<Jws2Instance>>();
431     ArrayList<String> hostlist = new ArrayList<String>();
432     for (Jws2Instance service : enumerableServices)
433     {
434       ArrayList<Jws2Instance> hostservices = hosts.get(service.getHost());
435       if (hostservices == null)
436       {
437         hosts.put(service.getHost(),
438                 hostservices = new ArrayList<Jws2Instance>());
439         hostlist.add(service.getHost());
440       }
441       hostservices.add(service);
442     }
443     // now add hosts in order of the given array
444     for (String host : hostlist)
445     {
446       Jws2Instance orderedsvcs[] = hosts.get(host).toArray(
447               new Jws2Instance[1]);
448       String sortbytype[] = new String[orderedsvcs.length];
449       for (int i = 0; i < sortbytype.length; i++)
450       {
451         sortbytype[i] = orderedsvcs[i].serviceType;
452       }
453       jalview.util.QuickSort.sort(sortbytype, orderedsvcs);
454       for (final Jws2Instance service : orderedsvcs)
455       {
456         atpoint = JvSwingUtils.findOrCreateMenu(jws2al, service.action);
457         String type = service.serviceType;
458         if (byhost)
459         {
460           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, host);
461           if (atpoint.getToolTipText() == null)
462           {
463             atpoint.setToolTipText("Services at " + host);
464           }
465         }
466         if (bytype)
467         {
468           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, type);
469           if (atpoint.getToolTipText() == null)
470           {
471             atpoint.setToolTipText(service.getActionText());
472           }
473         }
474         if (!byhost
475                 && !hostLabels.contains(host + service.serviceType
476                         + service.getActionText()))
477         // !hostLabels.contains(host + (bytype ?
478         // service.serviceType+service.getActionText() : "")))
479         {
480           // add a marker indicating where this service is hosted
481           // relies on services from the same host being listed in a
482           // contiguous
483           // group
484           JMenuItem hitm;
485           if (hostLabels.contains(host))
486           {
487             atpoint.addSeparator();
488           }
489           else
490           {
491             hostLabels.add(host);
492           }
493           if (lasthostFor.get(service.action) == null
494                   || !lasthostFor.get(service.action).equals(host))
495           {
496             atpoint.add(hitm = new JMenuItem(host));
497             hitm.setForeground(Color.blue);
498             hitm.addActionListener(new ActionListener()
499             {
500
501               @Override
502               public void actionPerformed(ActionEvent e)
503               {
504                 Desktop.showUrl(service.getHost());
505               }
506             });
507             hitm.setToolTipText(JvSwingUtils
508                     .wrapTooltip("Opens the JABAWS server's homepage in web browser"));
509             lasthostFor.put(service.action, host);
510           }
511           hostLabels.add(host + service.serviceType
512                   + service.getActionText());
513         }
514
515         service.attachWSMenuEntry(atpoint, alignFrame);
516       }
517     }
518   }
519
520   public static void main(String[] args)
521   {
522     if (args.length > 0)
523     {
524       testUrls = new Vector<String>();
525       for (String url : args)
526       {
527         testUrls.add(url);
528       }
529       ;
530     }
531     Thread runner = getDiscoverer().startDiscoverer(
532             new PropertyChangeListener()
533             {
534
535               public void propertyChange(PropertyChangeEvent evt)
536               {
537                 if (getDiscoverer().services != null)
538                 {
539                   System.out.println("Changesupport: There are now "
540                           + getDiscoverer().services.size() + " services");
541                   int i = 1;
542                   for (Jws2Instance instance : getDiscoverer().services)
543                   {
544                     System.out.println("Service " + i++ + " "
545                             + instance.getClass() + "@"
546                             + instance.getHost() + ": "
547                             + instance.getActionText());
548                   }
549
550                 }
551               }
552             });
553     while (runner.isAlive())
554     {
555       try
556       {
557         Thread.sleep(50);
558       } catch (InterruptedException e)
559       {
560       }
561       ;
562     }
563     try
564     {
565       Thread.sleep(50);
566     } catch (InterruptedException x)
567     {
568     }
569   }
570
571   private static Jws2Discoverer discoverer;
572
573   public static Jws2Discoverer getDiscoverer()
574   {
575     if (discoverer == null)
576     {
577       discoverer = new Jws2Discoverer();
578     }
579     return discoverer;
580   }
581
582   public boolean hasServices()
583   {
584     // TODO Auto-generated method stub
585     return !running && services != null && services.size() > 0;
586   }
587
588   public boolean isRunning()
589   {
590     return running;
591   }
592
593   /**
594    * the jalview .properties entry for JWS2 URLS
595    */
596   final static String JWS2HOSTURLS = "JWS2HOSTURLS";
597
598   public static void setServiceUrls(Vector<String> urls)
599   {
600     if (urls != null)
601     {
602       StringBuffer urlbuffer = new StringBuffer();
603       String sep = "";
604       for (String url : urls)
605       {
606         urlbuffer.append(sep);
607         urlbuffer.append(url);
608         sep = ",";
609       }
610       Cache.setProperty(JWS2HOSTURLS, urlbuffer.toString());
611     }
612     else
613     {
614       Cache.removeProperty(JWS2HOSTURLS);
615     }
616   }
617
618   private static Vector<String> testUrls = null;
619
620   public static Vector<String> getServiceUrls()
621   {
622     if (testUrls != null)
623     {
624       // return test urls, if there are any, instead of touching cache
625       return testUrls;
626     }
627     String surls = Cache.getDefault(JWS2HOSTURLS,
628             "http://www.compbio.dundee.ac.uk/jabaws");
629     Vector<String> urls = new Vector<String>();
630     try
631     {
632       StringTokenizer st = new StringTokenizer(surls, ",");
633       while (st.hasMoreElements())
634       {
635         String url = null;
636         try
637         {
638           java.net.URL u = new java.net.URL(url = st.nextToken());
639           if (!urls.contains(url))
640           {
641             urls.add(url);
642           }
643           else
644           {
645             jalview.bin.Cache.log.info("Ignoring duplicate url in "
646                     + JWS2HOSTURLS + " list");
647           }
648         } catch (Exception ex)
649         {
650           jalview.bin.Cache.log
651                   .warn("Problem whilst trying to make a URL from '"
652                           + ((url != null) ? url : "<null>") + "'");
653           jalview.bin.Cache.log
654                   .warn("This was probably due to a malformed comma separated list"
655                           + " in the "
656                           + JWS2HOSTURLS
657                           + " entry of $(HOME)/.jalview_properties)");
658           jalview.bin.Cache.log.debug("Exception was ", ex);
659         }
660       }
661     } catch (Exception ex)
662     {
663       jalview.bin.Cache.log.warn(
664               "Error parsing comma separated list of urls in "
665                       + JWS2HOSTURLS + " preference.", ex);
666     }
667     if (urls.size() >= 0)
668     {
669       return urls;
670     }
671     return null;
672   }
673
674   public Vector<Jws2Instance> getServices()
675   {
676     return (services == null) ? new Vector<Jws2Instance>()
677             : new Vector<Jws2Instance>(services);
678   }
679
680   /**
681    * test the given URL with the JabaWS test code
682    * 
683    * @param foo
684    * @return
685    */
686   public static boolean testServiceUrl(URL foo)
687   {
688     try
689     {
690       compbio.ws.client.WSTester.main(new String[]
691       { "-h=" + foo.toString() });
692     } catch (Exception e)
693     {
694       e.printStackTrace();
695       return false;
696     } catch (OutOfMemoryError e)
697     {
698       e.printStackTrace();
699       return false;
700     } catch (Error e)
701     {
702       e.printStackTrace();
703       return false;
704     }
705
706     return true;
707   }
708
709   /**
710    * Start a fresh discovery thread and notify the given object when we're
711    * finished. Any known existing threads will be killed before this one is
712    * started.
713    * 
714    * @param changeSupport2
715    * @return new thread
716    */
717   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
718   {
719     if (isRunning())
720     {
721       setAborted(true);
722     }
723     addPropertyChangeListener(changeSupport2);
724     Thread thr = new Thread(this);
725     thr.start();
726     return thr;
727   }
728
729   Vector<String> invalidServiceUrls = null, urlsWithoutServices = null,
730           validServiceUrls = null;
731
732   /**
733    * @return the invalidServiceUrls
734    */
735   public Vector<String> getInvalidServiceUrls()
736   {
737     return invalidServiceUrls;
738   }
739
740   /**
741    * @return the urlsWithoutServices
742    */
743   public Vector<String> getUrlsWithoutServices()
744   {
745     return urlsWithoutServices;
746   }
747
748   /**
749    * add an 'empty' JABA server to the list. Only servers not already in the
750    * 'bad URL' list will be added to this list.
751    * 
752    * @param jwsservers
753    */
754   public synchronized void addUrlwithnoservices(String jwsservers)
755   {
756     if (urlsWithoutServices == null)
757     {
758       urlsWithoutServices = new Vector<String>();
759     }
760
761     if ((invalidServiceUrls == null || !invalidServiceUrls
762             .contains(jwsservers))
763             && !urlsWithoutServices.contains(jwsservers))
764     {
765       urlsWithoutServices.add(jwsservers);
766     }
767   }
768
769   /**
770    * add a bad URL to the list
771    * 
772    * @param jwsservers
773    */
774   public synchronized void addInvalidServiceUrl(String jwsservers)
775   {
776     if (invalidServiceUrls == null)
777     {
778       invalidServiceUrls = new Vector<String>();
779     }
780     if (!invalidServiceUrls.contains(jwsservers))
781     {
782       invalidServiceUrls.add(jwsservers);
783     }
784   }
785
786   /**
787    * 
788    * @return a human readable report of any problems with the service URLs used
789    *         for discovery
790    */
791   public String getErrorMessages()
792   {
793     if (!isRunning() && !isAborted())
794     {
795       StringBuffer ermsg = new StringBuffer();
796       boolean list = false;
797       if (getInvalidServiceUrls() != null
798               && getInvalidServiceUrls().size() > 0)
799       {
800         ermsg.append("URLs that could not be contacted: \n");
801         for (String svcurl : getInvalidServiceUrls())
802         {
803           if (list)
804           {
805             ermsg.append(", ");
806           }
807           list = true;
808           ermsg.append(svcurl);
809         }
810         ermsg.append("\n\n");
811       }
812       list = false;
813       if (getUrlsWithoutServices() != null
814               && getUrlsWithoutServices().size() > 0)
815       {
816         ermsg.append("URLs without any JABA Services : \n");
817         for (String svcurl : getUrlsWithoutServices())
818         {
819           if (list)
820           {
821             ermsg.append(", ");
822           }
823           list = true;
824           ermsg.append(svcurl);
825         }
826         ermsg.append("\n");
827       }
828       if (ermsg.length() > 1)
829       {
830         return ermsg.toString();
831       }
832
833     }
834     return null;
835   }
836
837   public int getServerStatusFor(String url)
838   {
839     if (validServiceUrls != null && validServiceUrls.contains(url))
840     {
841       return 1;
842     }
843     if (urlsWithoutServices != null && urlsWithoutServices.contains(url))
844       return 0;
845     if (invalidServiceUrls != null && invalidServiceUrls.contains(url))
846     {
847       return -1;
848     }
849     return -2;
850   }
851
852   /**
853    * pick the user's preferred service based on a set of URLs (jaba server
854    * locations) and service URIs (specifying version and service interface
855    * class)
856    * 
857    * @param serviceURL
858    * @return null or best match for given uri/ls.
859    */
860   public Jws2Instance getPreferredServiceFor(String[] serviceURLs)
861   {
862     HashSet<String> urls = new HashSet<String>();
863     urls.addAll(Arrays.asList(serviceURLs));
864     Jws2Instance match = null;
865     if (services != null)
866     {
867       for (Jws2Instance svc : services)
868       {
869         if (urls.contains(svc.getServiceTypeURI()))
870         {
871           if (match == null)
872           {
873             // for moment we always pick service from server ordered first in
874             // user's preferences
875             match = svc;
876           }
877           if (urls.contains(svc.getUri()))
878           {
879             // stop and return - we've matched type URI and URI for service
880             // endpoint
881             return svc;
882           }
883         }
884       }
885     }
886     return match;
887   }
888
889   Map<String, Map<String, String>> preferredServiceMap = new HashMap<String, Map<String, String>>();;
890
891   /**
892    * get current preferred service of the given type, or global default
893    * 
894    * @param af
895    *          null or a specific alignFrame
896    * @param serviceType
897    *          Jws2Instance.serviceType for service
898    * @return null if no service of this type is available, the preferred service
899    *         for the serviceType and af if specified and if defined.
900    */
901   public Jws2Instance getPreferredServiceFor(AlignFrame af,
902           String serviceType)
903   {
904     String serviceurl = null;
905     synchronized (preferredServiceMap)
906     {
907       String afid = (af == null) ? "" : af.getViewport().getSequenceSetId();
908       Map<String, String> prefmap = preferredServiceMap.get(afid);
909       if (afid.length() > 0 && prefmap == null)
910       {
911         // recover global setting, if any
912         prefmap = preferredServiceMap.get("");
913       }
914       if (prefmap != null)
915       {
916         serviceurl = prefmap.get(serviceType);
917       }
918
919     }
920     Jws2Instance response = null;
921     for (Jws2Instance svc : services)
922     {
923       if (svc.serviceType.equals(serviceType))
924       {
925         if (serviceurl == null || serviceurl.equals(svc.getHost()))
926         {
927           response = svc;
928           break;
929         }
930       }
931     }
932     return response;
933   }
934
935   public void setPreferredServiceFor(AlignFrame af, String serviceType,
936           String serviceAction, Jws2Instance selectedServer)
937   {
938     String afid = (af == null) ? "" : af.getViewport().getSequenceSetId();
939     if (preferredServiceMap == null)
940     {
941       preferredServiceMap = new HashMap<String, Map<String, String>>();
942     }
943     Map<String, String> prefmap = preferredServiceMap.get(afid);
944     if (prefmap == null)
945     {
946       prefmap = new HashMap<String, String>();
947       preferredServiceMap.put(afid, prefmap);
948     }
949     prefmap.put(serviceType, selectedServer.getHost());
950     prefmap.put(serviceAction, selectedServer.getHost());
951   }
952
953   public void setPreferredServiceFor(String serviceType,
954           String serviceAction, Jws2Instance selectedServer)
955   {
956     setPreferredServiceFor(null, serviceType, serviceAction, selectedServer);
957   }
958 }