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