Merge branch 'develop' into features/JAL-2316
authorkiramt <k.mourao@dundee.ac.uk>
Tue, 13 Dec 2016 11:10:05 +0000 (11:10 +0000)
committerkiramt <k.mourao@dundee.ac.uk>
Tue, 13 Dec 2016 11:10:05 +0000 (11:10 +0000)
Conflicts (resolved):
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/gui/Preferences.java

34 files changed:
help/html/webServices/urllinks.html
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/appletgui/IdPanel.java
src/jalview/bin/Cache.java
src/jalview/gui/DasSourceBrowser.java
src/jalview/gui/Desktop.java
src/jalview/gui/IdPanel.java
src/jalview/gui/Preferences.java
src/jalview/gui/WsPreferences.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GSequenceLink.java
src/jalview/urls/CustomUrlProvider.java [new file with mode: 0644]
src/jalview/urls/IdOrgSettings.java [new file with mode: 0644]
src/jalview/urls/IdentifiersUrlProvider.java [new file with mode: 0644]
src/jalview/urls/UrlLinkDisplay.java [new file with mode: 0644]
src/jalview/urls/UrlLinkTableModel.java [new file with mode: 0644]
src/jalview/urls/UrlProvider.java [new file with mode: 0644]
src/jalview/urls/UrlProviderImpl.java [new file with mode: 0644]
src/jalview/urls/api/UrlProviderFactoryI.java [new file with mode: 0644]
src/jalview/urls/api/UrlProviderI.java [new file with mode: 0644]
src/jalview/urls/applet/AppletUrlProviderFactory.java [new file with mode: 0644]
src/jalview/urls/desktop/DesktopUrlProviderFactory.java [new file with mode: 0644]
src/jalview/util/UrlConstants.java
src/jalview/util/UrlLink.java
src/jalview/ws/UrlDownloadClient.java [new file with mode: 0644]
test/jalview/urls/AppletUrlProviderFactoryTest.java [new file with mode: 0644]
test/jalview/urls/CustomUrlProviderTest.java [new file with mode: 0644]
test/jalview/urls/DesktopUrlProviderFactoryTest.java [new file with mode: 0644]
test/jalview/urls/IdentifiersUrlProviderTest.java [new file with mode: 0644]
test/jalview/urls/UrlLinkDisplayTest.java [new file with mode: 0644]
test/jalview/urls/UrlLinkTableModelTest.java [new file with mode: 0644]
test/jalview/urls/UrlProviderTest.java [new file with mode: 0644]
test/jalview/ws/UrlDownloadClientTest.java [new file with mode: 0644]

index 088a539..d99633a 100644 (file)
@@ -27,7 +27,9 @@
     <strong>Opening URLs from Jalview</strong><br> Both the applet
     and the desktop application are able to open URLs as 'popups' in
     your web browser. <br> Double-clicking on the ID of a sequence
-    will open the first URL that can be generated from its sequence ID.
+    will open the URL designated for 'popups' in the &quot;Connections&quot; tab of the <a
+    href="../features/preferences.html">Jalview desktop
+    preferences</a>.
     This is by default the EMBL-EBI site, but you can easily configure your own <a
       href="#urllinks">sequence URL links</a>.
   </p>
     href="../features/preferences.html">Jalview desktop
     preferences</a>, or specified as <a
     href="http://www.jalview.org/examples/appletParameters.html#parameters">applet
-    parameters</a>. <br> By default the item &quot;EMBL-EBI Search&quot; is added
-    to this link menu. This link will show a web page in your default
-    browser with the selected sequence id as part of the URL.<br>
-    In the preferences dialog box, click <strong>new</strong> to add a
+    parameters</a>.</p>
+    <p> 
+    By default, the list of available links in the preferences dialog box 
+    contains the item &quot;EMBL-EBI Search&quot;, 
+    which is set as the URL which opens on double-clicking on a sequence ID, and as a 
+    menu item in the Links menu. This link will show a web page in your default
+    browser with the selected sequence id as part of the URL.
+    <br>
+    Also by default, the list of available links contains persistent URLs for many common 
+    bioinformatics databases. These links are downloaded by Jalview from
+    the <em>identifiers.org</em> website, and the names and URLs are not user editable.
+    <br>
+    The list of links is sortable, by clicking on the headers of the table. The list
+    can be filtered using the free text search box below the table, or the 
+    &quot;Custom Only&quot; button, which displays only user-defined links.
+    </p> 
+    <p>
+    In the preferences dialog box, the links which appear in the Links menu 
+    can be configured by selecting or deselecting links in the &quot;In Menu&quot; 
+    column. The names of selected links will be displayed
+    on new menu items under the &quot;Link&quot; menu when you right
+    click on a sequence id.<br> 
+    You can configure which link is used when double-clicking on a sequence
+    by selecting or deselecting links in the &quot;On Click&quot; column. Exactly one
+    link must be configured for double-clicking. Since the link uses the sequence id
+    to construct the URL to open, the selected link must contain the 
+    &quot;$SEQUENCE_ID$&quot; token (see below for details of the &quot;$SEQUENCE_ID$&quot;
+    and other tokens).</p>
+    <p>
+    Additionally you can click <strong>new</strong> to add a
     new link, and <strong>edit</strong> to modify an existing link, or <strong>delete</strong>
-    to remove it.<br> You can name the link, this will be displayed
-    on a new menu item under the &quot;Link&quot; menu when you right
-    click on a sequence id. <br> The URL string must contain a
+    to remove it. Only URLs entered by the user (or the default EMBL-EBI link) may
+    be edited or deleted. When adding or editing a link, the URL string must contain a
     token that can be replaced with a sequence ID or DB accession ID. The simplest token is
     &quot;$SEQUENCE_ID$&quot;, which will be replaced by the chosen
     sequence id when you click on it. 
index 6360dc7..7833903 100644 (file)
@@ -139,7 +139,8 @@ action.view_flanking_regions = Show flanking regions
 label.view_flanking_regions = Show sequence data either side of the subsequences involved in this alignment
 label.structures_manager = Structures Manager
 label.nickname = Nickname:
-label.url = URL:
+label.url = URL
+label.url\: = URL:
 label.input_file_url = Enter URL or Input File
 label.select_feature = Select feature
 label.name = Name
@@ -411,7 +412,6 @@ label.couldnt_import_as_vamsas_session = Couldn't import {0} as a new vamsas ses
 label.vamsas_document_import_failed = Vamsas Document Import Failed
 label.couldnt_locate = Couldn't locate {0}
 label.url_not_found = URL not found
-label.no_link_selected = No link selected
 label.new_sequence_url_link = New sequence URL link
 label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
 label.wrapped_view_no_edit = Wrapped view - no edit
@@ -1274,4 +1274,19 @@ label.SEQUENCE_ID_no_longer_used = $SEQUENCE_ID$ is no longer used for DB access
 label.SEQUENCE_ID_for_DB_ACCESSION1 = Please review your URL links in the 'Connections' tab of the Preferences window:
 label.SEQUENCE_ID_for_DB_ACCESSION2 = URL links using '$SEQUENCE_ID$' for DB accessions now use '$DB_ACCESSION$'.
 label.do_not_display_again = Do not display this message again
-label.output_seq_details = Output Sequence Details to list all database references
+exception.url_cannot_have_miriam_id = {0} is a MIRIAM id and cannot be used as a custom url name
+exception.url_cannot_have_duplicate_id = {0} cannot be used as a label for more than one line
+label.filter = Filter text:
+action.customfilter = Custom only
+action.showall = Show All
+label.insert = Insert:
+action.seq_id = $SEQUENCE_ID$
+action.db_acc = $DB_ACCESSION$
+label.default = On Click
+label.inmenu = In Menu
+label.id = ID
+label.urltooltip = Only one url, which must use a sequence id, can be selected for the 'On Click' option
+label.edit_sequence_url_link = Edit sequence URL link
+warn.name_cannot_be_duplicate = URL names must be unique and cannot be MIRIAM ids
+label.invalid_name = Invalid Name !
+label.output_seq_details = Output Sequence Details to list all database references
\ No newline at end of file
index e5b5e27..88fb0a4 100644 (file)
@@ -136,7 +136,8 @@ action.view_flanking_regions = Mostrar flancos
 label.view_flanking_regions = Mostrar los datos de la secuencia a ambos lados de las subsecuencias implicadas en este alineamiento
 label.structures_manager = Administrar estructuras
 label.nickname = Sobrenombre:
-label.url = URL: 
+label.url\: = URL:
+label.url = URL 
 label.input_file_url = Introducir URL en el fichero de entrada
 label.select_feature = Seleccionar característica
 label.name = Nombre
@@ -379,7 +380,6 @@ label.couldnt_import_as_vamsas_session = No se pudo importar {0} como una nueva
 label.vamsas_document_import_failed =  Fallo en la importación del documento Vamsas
 label.couldnt_locate = No se pudo localizar {0}
 label.url_not_found = URL no encontrada
-label.no_link_selected = Enlace no seleccionado
 label.new_sequence_url_link = Enlace a una nueva secuencia URL
 label.cannot_edit_annotations_in_wrapped_view = No se pueden editar anotaciones en vista envolvente
 label.wrapped_view_no_edit = Vista envolvente - no editar
@@ -1275,4 +1275,19 @@ label.SEQUENCE_ID_no_longer_used = $SEQUENCE_ID$ no se utiliza m
 label.SEQUENCE_ID_for_DB_ACCESSION1 = Por favor, revise sus URLs en la pestaña 'Conexiones' de la ventana de Preferencias:
 label.SEQUENCE_ID_for_DB_ACCESSION2 = URL enlaza usando '$SEQUENCE_ID$' para accesiones DB ahora usar '$DB_ACCESSION$'.
 label.do_not_display_again = No mostrar este mensaje de nuevo
-label.output_seq_details = Seleccionar Detalles de la secuencia para ver todas
+exception.url_cannot_have_miriam_id = {0} is a MIRIAM id and cannot be used as a custom url name
+exception.url_cannot_have_duplicate_id = {0} cannot be used as a label for more than one link
+label.filter = Filter text:
+action.customfilter = Custom only
+action.showall = Show All
+label.insert = Insert:
+action.seq_id = $SEQUENCE_ID$
+action.db_acc = $DB_ACCESSION$
+label.default = On Click
+label.inmenu = In Menu
+label.id = ID
+label.urltooltip = Only one url, which must use a sequence id, can be selected for the 'On Click' option
+label.edit_sequence_url_link = Edit sequence URL link
+warn.name_cannot_be_duplicate = URL names must be unique and cannot be MIRIAM ids
+label.invalid_name = Invalid Name !
+label.output_seq_details = Seleccionar Detalles de la secuencia para ver todas
\ No newline at end of file
index 182f20e..4e7c9cd 100755 (executable)
  */
 package jalview.appletgui;
 
-import static jalview.util.UrlConstants.EMBLEBI_STRING;
-import static jalview.util.UrlConstants.SRS_STRING;
-
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.util.UrlLink;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.applet.AppletUrlProviderFactory;
 import jalview.viewmodel.AlignmentViewport;
 
 import java.awt.BorderLayout;
@@ -36,6 +35,7 @@ import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Vector;
 
@@ -55,7 +55,7 @@ public class IdPanel extends Panel implements MouseListener,
 
   boolean mouseDragging = false;
 
-  java.util.Vector links = new java.util.Vector();
+  UrlProviderI urlProvider = null;
 
   public IdPanel(AlignViewport av, AlignmentPanel parent)
   {
@@ -69,6 +69,9 @@ public class IdPanel extends Panel implements MouseListener,
 
     String label, url;
     // TODO: add in group link parameter
+
+    // make a list of label,url pairs
+    HashMap<String, String> urlList = new HashMap<String, String>();
     if (av.applet != null)
     {
       for (int i = 1; i < 10; i++)
@@ -76,26 +79,18 @@ public class IdPanel extends Panel implements MouseListener,
         label = av.applet.getParameter("linkLabel_" + i);
         url = av.applet.getParameter("linkURL_" + i);
 
-        if (label != null && url != null)
-        {
-          links.addElement(label + "|" + url);
-        }
-
+        urlList.put(label, url);
       }
-    }
-    {
-      // upgrade old SRS link
-      int srsPos = links.indexOf(SRS_STRING);
-      if (srsPos > -1)
+
+      if (!urlList.isEmpty())
       {
-        links.setElementAt(EMBLEBI_STRING, srsPos);
+        // set default as first entry in list
+        String defaultUrl = av.applet.getParameter("linkLabel_1");
+        UrlProviderFactoryI factory = new AppletUrlProviderFactory(
+                defaultUrl, urlList);
+        urlProvider = factory.createUrlProvider();
       }
     }
-    if (links.size() < 1)
-    {
-      links = new java.util.Vector();
-      links.addElement(EMBLEBI_STRING);
-    }
   }
 
   Tooltip tooltip;
@@ -217,7 +212,7 @@ public class IdPanel extends Panel implements MouseListener,
       return;
     }
 
-    // DEFAULT LINK IS FIRST IN THE LINK LIST
+    // get the sequence details
     int seq = alignPanel.seqPanel.findSeq(e);
     SequenceI sq = av.getAlignment().getSequenceAt(seq);
     if (sq == null)
@@ -226,53 +221,11 @@ public class IdPanel extends Panel implements MouseListener,
     }
     String id = sq.getName();
 
-    String target = null;
-    String url = null;
-    int i = 0;
-    while (url == null && i < links.size())
-    {
-      // DEFAULT LINK IS FIRST IN THE LINK LIST
-      // BUT IF ITS A REGEX AND DOES NOT MATCH THE NEXT ONE WILL BE TRIED
-      url = links.elementAt(i++).toString();
-      jalview.util.UrlLink urlLink = null;
-      try
-      {
-        urlLink = new UrlLink(url);
-        target = urlLink.getTarget();
-      } catch (Exception foo)
-      {
-        System.err.println("Exception for URLLink '" + url + "'");
-        foo.printStackTrace();
-        url = null;
-        continue;
-      }
-
-      if (urlLink.usesDBAccession())
-      {
-        // this URL requires an accession id, not the name of a sequence
-        url = null;
-        continue;
-      }
-
-      if (!urlLink.isValid())
-      {
-        System.err.println(urlLink.getInvalidMessage());
-        url = null;
-        continue;
-      }
-
-      String urls[] = urlLink.makeUrls(id, true);
-      if (urls == null || urls[0] == null || urls[0].length() < 1)
-      {
-        url = null;
-        continue;
-      }
-      // just take first URL made from regex
-      url = urls[1];
-    }
+    // get the default url with the sequence details filled in
+    String url = urlProvider.getDefaultUrl(id);
+    String target = urlProvider.getDefaultTarget(id);
     try
     {
-
       alignPanel.alignFrame.showURL(url, target);
     } catch (Exception ex)
     {
@@ -331,11 +284,8 @@ public class IdPanel extends Panel implements MouseListener,
 
       // build a new links menu based on the current links + any non-positional
       // features
-      Vector nlinks = new Vector();
-      for (int l = 0, lSize = links.size(); l < lSize; l++)
-      {
-        nlinks.addElement(links.elementAt(l));
-      }
+      Vector<String> nlinks = urlProvider.getLinksForMenu();
+
       SequenceFeature sf[] = sq == null ? null : sq.getSequenceFeatures();
       for (int sl = 0; sf != null && sl < sf.length; sl++)
       {
index 8412dab..6764430 100755 (executable)
@@ -22,6 +22,7 @@ package jalview.bin;
 
 import jalview.datamodel.PDBEntry;
 import jalview.structure.StructureImportSettings;
+import jalview.urls.IdOrgSettings;
 import jalview.ws.dbsources.das.api.DasSourceRegistryI;
 import jalview.ws.dbsources.das.datamodel.DasSourceRegistry;
 import jalview.ws.sifts.SiftsSettings;
@@ -122,6 +123,10 @@ import org.apache.log4j.SimpleLayout;
  * <li>SORT_ALIGNMENT (No sort|Id|Pairwise Identity)</li>
  * <li>SEQUENCE_LINKS list of name|URL pairs for opening a url with
  * $SEQUENCE_ID$</li>
+ * <li>STORED_LINKS list of name|url pairs which user has entered but are not
+ * currently used
+ * <li>DEFAULT_LINK name of single url to be used when user double clicks a
+ * sequence id (must be in SEQUENCE_LINKS or STORED_LINKS)
  * <li>GROUP_LINKS list of name|URL[|&lt;separator&gt;] tuples - see
  * jalview.utils.GroupURLLink for more info</li>
  * <li>DAS_REGISTRY_URL the registry to query</li>
@@ -220,6 +225,9 @@ public class Cache
 
   public static final String DAS_ACTIVE_SOURCE = "DAS_ACTIVE_SOURCE";
 
+  /**
+   * Sifts settings
+   */
   public static final String DEFAULT_SIFTS_DOWNLOAD_DIR = System
           .getProperty("user.home")
           + File.separatorChar
@@ -230,6 +238,14 @@ public class Cache
   private final static String DEFAULT_FAIL_SAFE_PID_THRESHOLD = "30";
 
   /**
+   * Identifiers.org download settings
+   */
+  private static final String ID_ORG_FILE = System.getProperty("user.home")
+          + File.separatorChar + ".jalview_identifiers";
+
+  private static final String ID_ORG_URL = "http://identifiers.org/rest/collections/";
+
+  /**
    * Allowed values are PDB or mmCIF
    */
   private final static String PDB_DOWNLOAD_FORMAT = PDBEntry.Type.MMCIF
@@ -440,6 +456,9 @@ public class Cache
             "sifts_cache_threshold_in_days",
             DEFAULT_CACHE_THRESHOLD_IN_DAYS));
 
+    IdOrgSettings.setUrl(ID_ORG_URL);
+    IdOrgSettings.setDownloadLocation(ID_ORG_FILE);
+
     System.out
             .println("Jalview Version: " + codeVersion + codeInstallation);
 
index 8c8f228..c5ec067 100644 (file)
@@ -453,7 +453,7 @@ public class DasSourceBrowser extends GDasSourceBrowser implements
     pane12.add(nametf, BorderLayout.EAST);
     panel.add(pane12, BorderLayout.NORTH);
     pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.url")),
+    pane12.add(new JLabel(MessageManager.getString("label.url:")),
             BorderLayout.NORTH);
     pane12.add(seqs, BorderLayout.SOUTH);
     pane12.add(urltf, BorderLayout.EAST);
index ac957d8..b4b41a3 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.gui;
 
-import static jalview.util.UrlConstants.EMBLEBI_STRING;
 import static jalview.util.UrlConstants.SEQUENCE_ID;
 
 import jalview.api.AlignViewportI;
@@ -38,10 +37,13 @@ import jalview.io.JalviewFileView;
 import jalview.jbgui.GSplitFrame;
 import jalview.jbgui.GStructureViewer;
 import jalview.structure.StructureSelectionManager;
+import jalview.urls.IdOrgSettings;
 import jalview.util.ImageMaker;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.UrlConstants;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.UrlDownloadClient;
 import jalview.ws.params.ParamManager;
 
 import java.awt.BorderLayout;
@@ -77,6 +79,7 @@ import java.beans.PropertyChangeListener;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Hashtable;
@@ -391,6 +394,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
 
     showNews.setVisible(false);
 
+    getIdentifiersOrgData();
+
     checkURLLinks();
 
     this.addWindowListener(new WindowAdapter()
@@ -524,6 +529,29 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     });
   }
 
+  public void getIdentifiersOrgData()
+  {
+    // Thread off the identifiers fetcher
+    addDialogThread(new Runnable()
+    {
+      @Override
+      public void run()
+      {
+        Cache.log.debug("Downloading data from identifiers.org");
+        UrlDownloadClient client = new UrlDownloadClient();
+        try
+        {
+          client.download(IdOrgSettings.getUrl(),
+                  IdOrgSettings.getDownloadLocation());
+        } catch (IOException e)
+        {
+          Cache.log.debug("Exception downloading identifiers.org data"
+                  + e.getMessage());
+        }
+      }
+    });
+  }
+
   @Override
   protected void showNews_actionPerformed(ActionEvent e)
   {
@@ -2288,7 +2316,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
         {
           // check what the actual links are - if it's just the default don't
           // bother with the warning
-          Vector<String> links = Preferences.sequenceURLLinks;
+          Vector<String> links = Preferences.sequenceUrlLinks
+                  .getLinksForMenu();
 
           // only need to check links if there is one with a
           // SEQUENCE_ID which is not the default EMBL_EBI link
@@ -2298,7 +2327,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
           while (li.hasNext())
           {
             String link = li.next();
-            if (link.contains(SEQUENCE_ID) && !link.equals(EMBLEBI_STRING))
+            if (link.contains(SEQUENCE_ID)
+                    && !link.equals(UrlConstants.DEFAULT_STRING))
             {
               check = true;
               int barPos = link.indexOf("|");
index 59d12d9..e326a7a 100755 (executable)
@@ -27,7 +27,6 @@ import jalview.datamodel.SequenceI;
 import jalview.io.SequenceAnnotationReport;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.util.UrlLink;
 import jalview.viewmodel.AlignmentViewport;
 
 import java.awt.BorderLayout;
@@ -199,56 +198,10 @@ public class IdPanel extends JPanel implements MouseListener,
       return;
     }
 
-    Vector links = Preferences.sequenceURLLinks;
-    if (links == null || links.size() < 1)
-    {
-      return;
-    }
-
     int seq = alignPanel.getSeqPanel().findSeq(e);
-    String url = null;
-    int i = 0;
     String id = av.getAlignment().getSequenceAt(seq).getName();
-    while (url == null && i < links.size())
-    {
-      // DEFAULT LINK IS FIRST IN THE LINK LIST
-      // BUT IF ITS A REGEX AND DOES NOT MATCH THE NEXT ONE WILL BE TRIED
-      url = links.elementAt(i++).toString();
-      jalview.util.UrlLink urlLink = null;
-      try
-      {
-        urlLink = new UrlLink(url);
-      } catch (Exception foo)
-      {
-        jalview.bin.Cache.log.error("Exception for URLLink '" + url + "'",
-                foo);
-        url = null;
-        continue;
-      }
+    String url = Preferences.sequenceUrlLinks.getDefaultUrl(id);
 
-      if (urlLink.usesDBAccession())
-      {
-        // this URL requires an accession id, not the name of a sequence
-        url = null;
-        continue;
-      }
-
-      if (!urlLink.isValid())
-      {
-        jalview.bin.Cache.log.error(urlLink.getInvalidMessage());
-        url = null;
-        continue;
-      }
-
-      String urls[] = urlLink.makeUrls(id, true);
-      if (urls == null || urls[0] == null || urls[0].length() < 4)
-      {
-        url = null;
-        continue;
-      }
-      // just take first URL made from regex
-      url = urls[1];
-    }
     try
     {
       jalview.util.BrowserLauncher.openURL(url);
@@ -375,7 +328,7 @@ public class IdPanel extends JPanel implements MouseListener,
     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
     // build a new links menu based on the current links + any non-positional
     // features
-    Vector<String> nlinks = new Vector<String>(Preferences.sequenceURLLinks);
+    Vector<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
     SequenceFeature sfs[] = sq == null ? null : sq.getSequenceFeatures();
     if (sfs != null)
     {
index 0b65c1b..00b2a38 100755 (executable)
  */
 package jalview.gui;
 
-import static jalview.util.UrlConstants.DB_ACCESSION;
-import static jalview.util.UrlConstants.EMBLEBI_STRING;
-import static jalview.util.UrlConstants.SEQUENCE_ID;
-import static jalview.util.UrlConstants.SRS_STRING;
-
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.bin.Cache;
 import jalview.gui.Help.HelpId;
@@ -35,12 +30,18 @@ import jalview.io.JalviewFileView;
 import jalview.jbgui.GPreferences;
 import jalview.jbgui.GSequenceLink;
 import jalview.schemes.ColourSchemeProperty;
+import jalview.urls.UrlLinkTableModel;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.UrlConstants;
 import jalview.ws.sifts.SiftsSettings;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.event.ActionEvent;
@@ -49,14 +50,24 @@ import java.awt.event.MouseEvent;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.StringTokenizer;
-import java.util.Vector;
 
 import javax.help.HelpSetException;
 import javax.swing.JColorChooser;
 import javax.swing.JFileChooser;
 import javax.swing.JInternalFrame;
 import javax.swing.JPanel;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.RowSorter;
+import javax.swing.SortOrder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
 
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
 
@@ -102,7 +113,9 @@ public class Preferences extends GPreferences
    * Holds name and link separated with | character. Sequence ID must be
    * $SEQUENCE_ID$ or $SEQUENCE_ID=/.possible | chars ./=$
    */
-  public static Vector<String> sequenceURLLinks;
+  public static UrlProviderI sequenceUrlLinks;
+
+  public static UrlLinkTableModel dataModel;
 
   /**
    * Holds name and link separated with | character. Sequence IDS and Sequences
@@ -115,40 +128,23 @@ public class Preferences extends GPreferences
   public static List<String> groupURLLinks;
   static
   {
-    String string = Cache.getDefault("SEQUENCE_LINKS", EMBLEBI_STRING);
-    sequenceURLLinks = new Vector<String>();
-
-    try
+    // get links selected to be in the menu (SEQUENCE_LINKS)
+    // and links entered by the user but not selected (STORED_LINKS)
+    String inMenuString = Cache.getDefault("SEQUENCE_LINKS", "");
+    String notInMenuString = Cache.getDefault("STORED_LINKS", "");
+    String defaultUrl = Cache.getDefault("DEFAULT_URL",
+            UrlConstants.DEFAULT_LABEL);
+
+    // if both links lists are empty, add the DEFAULT_URL link
+    // otherwise we assume the default link is in one of the lists
+    if (inMenuString.isEmpty() && notInMenuString.isEmpty())
     {
-      StringTokenizer st = new StringTokenizer(string, "|");
-      while (st.hasMoreElements())
-      {
-        String name = st.nextToken();
-        String url = st.nextToken();
-        // check for '|' within a regex
-        int rxstart = url.indexOf("$" + DB_ACCESSION + "$");
-        if (rxstart == -1)
-        {
-          rxstart = url.indexOf("$" + SEQUENCE_ID + "$");
-        }
-        while (rxstart == -1 && url.indexOf("/=$") == -1)
-        {
-          url = url + "|" + st.nextToken();
-        }
-        sequenceURLLinks.addElement(name + "|" + url);
-      }
-    } catch (Exception ex)
-    {
-      System.out.println(ex + "\nError parsing sequence links");
-    }
-    {
-      // upgrade old SRS link
-      int srsPos = sequenceURLLinks.indexOf(SRS_STRING);
-      if (srsPos > -1)
-      {
-        sequenceURLLinks.setElementAt(EMBLEBI_STRING, srsPos);
-      }
+      inMenuString = UrlConstants.DEFAULT_STRING;
     }
+    UrlProviderFactoryI factory = new DesktopUrlProviderFactory(defaultUrl,
+            inMenuString, notInMenuString);
+    sequenceUrlLinks = factory.createUrlProvider();
+    dataModel = new UrlLinkTableModel(sequenceUrlLinks);
 
     /**
      * TODO: reformulate groupURL encoding so two or more can be stored in the
@@ -158,8 +154,6 @@ public class Preferences extends GPreferences
     groupURLLinks = new ArrayList<String>();
   }
 
-  Vector<String> nameLinks, urlLinks;
-
   JInternalFrame frame;
 
   DasSourceBrowser dasSource;
@@ -343,18 +337,128 @@ public class Preferences extends GPreferences
     /*
      * Set Connections tab defaults
      */
-    nameLinks = new Vector<String>();
-    urlLinks = new Vector<String>();
-    for (int i = 0; i < sequenceURLLinks.size(); i++)
+
+    // set up sorting
+    linkUrlTable.setModel(dataModel);
+    final TableRowSorter<TableModel> sorter = new TableRowSorter<>(
+            linkUrlTable.getModel());
+    linkUrlTable.setRowSorter(sorter);
+    List<RowSorter.SortKey> sortKeys = new ArrayList<>();
+
+    UrlLinkTableModel m = (UrlLinkTableModel) linkUrlTable.getModel();
+    sortKeys.add(new RowSorter.SortKey(m.getDefaultColumn(),
+            SortOrder.DESCENDING));
+    sortKeys.add(new RowSorter.SortKey(m.getSelectedColumn(),
+            SortOrder.DESCENDING));
+    sortKeys.add(new RowSorter.SortKey(m.getNameColumn(),
+            SortOrder.ASCENDING));
+
+    sorter.setSortKeys(sortKeys);
+    sorter.sort();
+    
+    // set up filtering
+    ActionListener onReset;
+    onReset = new ActionListener()
     {
-      String link = sequenceURLLinks.elementAt(i).toString();
-      nameLinks.addElement(link.substring(0, link.indexOf("|")));
-      urlLinks.addElement(link.substring(link.indexOf("|") + 1));
-    }
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filterTB.setText("");
+        sorter.setRowFilter(RowFilter.regexFilter(""));
+      }
+
+    };
+    doReset.addActionListener(onReset);
+
+    // filter to display only custom urls
+    final RowFilter<TableModel, Object> customUrlFilter = new RowFilter<TableModel, Object>()
+    {
+      @Override
+      public boolean include(
+              Entry<? extends TableModel, ? extends Object> entry)
+      {
+        return ((UrlLinkTableModel) entry.getModel()).isUserEntry(entry);
+      }
+    };
+
+    final TableRowSorter<TableModel> customSorter = new TableRowSorter<>(
+            linkUrlTable.getModel());
+    customSorter.setRowFilter(customUrlFilter);
+
+    ActionListener onCustomOnly;
+    onCustomOnly = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filterTB.setText("");
+        sorter.setRowFilter(customUrlFilter);
+      }
+    };
+    userOnly.addActionListener(onCustomOnly);
+
+    filterTB.getDocument().addDocumentListener(new DocumentListener()
+    {
+      String caseInsensitiveFlag = "(?i)";
 
-    updateLinkData();
+      @Override
+      public void changedUpdate(DocumentEvent e)
+      {
+        sorter.setRowFilter(RowFilter.regexFilter(caseInsensitiveFlag
+                + filterTB.getText()));
+      }
+
+      @Override
+      public void removeUpdate(DocumentEvent e)
+      {
+        sorter.setRowFilter(RowFilter.regexFilter(caseInsensitiveFlag
+                + filterTB.getText()));
+      }
+
+      @Override
+      public void insertUpdate(DocumentEvent e)
+      {
+        sorter.setRowFilter(RowFilter.regexFilter(caseInsensitiveFlag
+                + filterTB.getText()));
+      }
+    });
+
+    // set up list selection functionality
+    linkUrlTable.getSelectionModel().addListSelectionListener(
+            new UrlListSelectionHandler());
+
+    // set up radio buttons
+    int onClickCol = ((UrlLinkTableModel) linkUrlTable.getModel())
+            .getDefaultColumn();
+    String onClickName = linkUrlTable.getColumnName(onClickCol);
+    linkUrlTable.getColumn(onClickName).setCellRenderer(
+               new RadioButtonRenderer());
+    linkUrlTable.getColumn(onClickName)
+            .setCellEditor(new RadioButtonEditor());
+
+    // get boolean columns and resize those to min possible
+    for (int column = 0; column < linkUrlTable.getColumnCount(); column++)
+    {
+      if (linkUrlTable.getModel().getColumnClass(column)
+              .equals(Boolean.class))
+      {
+        TableColumn tableColumn = linkUrlTable.getColumnModel().getColumn(
+                column);
+        int preferredWidth = tableColumn.getMinWidth();
+
+        TableCellRenderer cellRenderer = linkUrlTable.getCellRenderer(0,
+                column);
+        Component c = linkUrlTable.prepareRenderer(cellRenderer, 0, column);
+        int cwidth = c.getPreferredSize().width
+                + linkUrlTable.getIntercellSpacing().width;
+        preferredWidth = Math.max(preferredWidth, cwidth);
+
+        tableColumn.setPreferredWidth(preferredWidth);
+      }
+    }
 
     useProxy.setSelected(Cache.getDefault("USE_PROXY", false));
+    useProxy_actionPerformed(); // make sure useProxy is correctly initialised
     proxyServerTB.setEnabled(useProxy.isSelected());
     proxyPortTB.setEnabled(useProxy.isSelected());
     proxyServerTB.setText(Cache.getDefault("PROXY_SERVER", ""));
@@ -548,28 +652,32 @@ public class Preferences extends GPreferences
 
     jalview.util.BrowserLauncher.resetBrowser();
 
-    if (nameLinks.size() > 0)
+    // save user-defined and selected links
+    String menuLinks = sequenceUrlLinks.writeUrlsAsString(true);
+    if (menuLinks.isEmpty())
+    {
+      Cache.applicationProperties.remove("SEQUENCE_LINKS");
+    }
+    else
     {
-      StringBuffer links = new StringBuffer();
-      sequenceURLLinks = new Vector<String>();
-      for (int i = 0; i < nameLinks.size(); i++)
-      {
-        sequenceURLLinks.addElement(nameLinks.elementAt(i) + "|"
-                + urlLinks.elementAt(i));
-        links.append(sequenceURLLinks.elementAt(i).toString());
-        links.append("|");
-      }
-      // remove last "|"
-      links.setLength(links.length() - 1);
       Cache.applicationProperties.setProperty("SEQUENCE_LINKS",
-              links.toString());
+              menuLinks.toString());
+    }
+
+    String nonMenuLinks = sequenceUrlLinks.writeUrlsAsString(false);
+    if (nonMenuLinks.isEmpty())
+    {
+      Cache.applicationProperties.remove("STORED_LINKS");
     }
     else
     {
-      Cache.applicationProperties.remove("SEQUENCE_LINKS");
-      sequenceURLLinks.clear();
+      Cache.applicationProperties.setProperty("STORED_LINKS",
+              nonMenuLinks.toString());
     }
 
+    Cache.applicationProperties.setProperty("DEFAULT_URL",
+            sequenceUrlLinks.getDefaultUrlId());
+
     Cache.applicationProperties.setProperty("USE_PROXY",
             Boolean.toString(useProxy.isSelected()));
 
@@ -750,7 +858,6 @@ public class Preferences extends GPreferences
   @Override
   public void newLink_actionPerformed(ActionEvent e)
   {
-
     GSequenceLink link = new GSequenceLink();
     boolean valid = false;
     while (!valid)
@@ -761,10 +868,18 @@ public class Preferences extends GPreferences
       {
         if (link.checkValid())
         {
-          nameLinks.addElement(link.getName());
-          urlLinks.addElement(link.getURL());
-          updateLinkData();
-          valid = true;
+          if (((UrlLinkTableModel) linkUrlTable.getModel())
+                  .isUniqueName(link.getName()))
+          {
+            ((UrlLinkTableModel) linkUrlTable.getModel()).insertRow(
+                    link.getName(), link.getURL());
+            valid = true;
+          }
+          else
+          {
+            link.notifyDuplicate();
+            continue;
+          }
         }
       }
       else
@@ -779,36 +894,40 @@ public class Preferences extends GPreferences
   {
     GSequenceLink link = new GSequenceLink();
 
-    int index = linkNameList.getSelectedIndex();
+    int index = linkUrlTable.getSelectedRow();
     if (index == -1)
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString("label.no_link_selected"),
-              MessageManager.getString("label.no_link_selected"),
-              JvOptionPane.WARNING_MESSAGE);
+      // button no longer enabled if row is not selected
+      Cache.log.debug("Edit with no row selected in linkUrlTable");
       return;
     }
 
-    link.setName(nameLinks.elementAt(index).toString());
-    link.setURL(urlLinks.elementAt(index).toString());
+    link.setName(linkUrlTable.getValueAt(index, 0).toString());
+    link.setURL(linkUrlTable.getValueAt(index, 1).toString());
 
     boolean valid = false;
     while (!valid)
     {
-
       if (JvOptionPane.showInternalConfirmDialog(Desktop.desktop, link,
-              MessageManager.getString("label.new_sequence_url_link"),
+              MessageManager.getString("label.edit_sequence_url_link"),
               JvOptionPane.OK_CANCEL_OPTION, -1, null) == JvOptionPane.OK_OPTION)
       {
         if (link.checkValid())
         {
-          nameLinks.setElementAt(link.getName(), index);
-          urlLinks.setElementAt(link.getURL(), index);
-          updateLinkData();
-          valid = true;
+          if (((UrlLinkTableModel) linkUrlTable.getModel())
+                  .isUniqueName(link.getName()))
+          {
+            linkUrlTable.setValueAt(link.getName(), index, 0);
+            linkUrlTable.setValueAt(link.getURL(), index, 1);
+            valid = true;
+          }
+          else
+          {
+            link.notifyDuplicate();
+            continue;
+          }
         }
       }
-
       else
       {
         break;
@@ -819,26 +938,24 @@ public class Preferences extends GPreferences
   @Override
   public void deleteLink_actionPerformed(ActionEvent e)
   {
-    int index = linkNameList.getSelectedIndex();
+    int index = linkUrlTable.getSelectedRow();
+    int modelIndex = -1;
     if (index == -1)
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString("label.no_link_selected"),
-              MessageManager.getString("label.no_link_selected"),
-              JvOptionPane.WARNING_MESSAGE);
+      // button no longer enabled if row is not selected
+      Cache.log.debug("Delete with no row selected in linkUrlTable");
       return;
     }
-    nameLinks.removeElementAt(index);
-    urlLinks.removeElementAt(index);
-    updateLinkData();
-  }
+    else
+    {
+      modelIndex = linkUrlTable.convertRowIndexToModel(index);
+    }
 
-  void updateLinkData()
-  {
-    linkNameList.setListData(nameLinks);
-    linkURLList.setListData(urlLinks);
+    // make sure we use the model index to delete, and not the table index
+    ((UrlLinkTableModel) linkUrlTable.getModel()).removeRow(modelIndex);
   }
 
+
   @Override
   public void defaultBrowser_mouseClicked(MouseEvent e)
   {
@@ -1055,4 +1172,45 @@ public class Preferences extends GPreferences
       return name.hashCode() + code.hashCode();
     }
   }
+  
+  private class UrlListSelectionHandler implements ListSelectionListener
+  {
+
+    @Override
+    public void valueChanged(ListSelectionEvent e)
+    {
+      ListSelectionModel lsm = (ListSelectionModel) e.getSource();
+
+      int index = lsm.getMinSelectionIndex();
+      if (index == -1)
+      {
+        // no selection, so disable delete/edit buttons
+        editLink.setEnabled(false);
+        deleteLink.setEnabled(false);
+        return;
+      }
+      int modelIndex = linkUrlTable.convertRowIndexToModel(index);
+
+      // enable/disable edit and delete link buttons
+      if (((UrlLinkTableModel) linkUrlTable.getModel())
+              .isRowDeletable(modelIndex))
+      {
+        deleteLink.setEnabled(true);
+      }
+      else
+      {
+        deleteLink.setEnabled(false);
+      }
+
+      if (((UrlLinkTableModel) linkUrlTable.getModel())
+              .isRowEditable(modelIndex))
+      {
+        editLink.setEnabled(true);
+      }
+      else
+      {
+        editLink.setEnabled(false);
+      }
+    }
+}
 }
index 165e8f2..32671d5 100644 (file)
@@ -454,7 +454,7 @@ public class WsPreferences extends GWsPreferences
     JTextField urltf = new JTextField(url, 40);
     JPanel panel = new JPanel(new BorderLayout());
     JPanel pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.url")),
+    pane12.add(new JLabel(MessageManager.getString("label.url:")),
             BorderLayout.CENTER);
     pane12.add(urltf, BorderLayout.EAST);
     panel.add(pane12, BorderLayout.NORTH);
@@ -574,6 +574,7 @@ public class WsPreferences extends GWsPreferences
     new Thread(new Runnable()
     {
 
+      @Override
       public void run()
       {
         // force a refresh.
@@ -599,6 +600,7 @@ public class WsPreferences extends GWsPreferences
       new Thread(new Runnable()
       {
 
+        @Override
         public void run()
         {
           progressBar.setVisible(true);
@@ -624,6 +626,7 @@ public class WsPreferences extends GWsPreferences
       new Thread(new Runnable()
       {
 
+        @Override
         public void run()
         {
           long ct = System.currentTimeMillis();
@@ -681,6 +684,7 @@ public class WsPreferences extends GWsPreferences
     new Thread(new Runnable()
     {
 
+      @Override
       public void run()
       {
         updateWsMenuConfig(false);
index 90053f5..c1737a1 100755 (executable)
@@ -45,6 +45,7 @@ import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 
+import javax.swing.AbstractCellEditor;
 import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.DefaultListCellRenderer;
@@ -53,11 +54,11 @@ import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JFileChooser;
 import javax.swing.JLabel;
-import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
+import javax.swing.JTable;
 import javax.swing.JTextField;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingConstants;
@@ -67,8 +68,8 @@ import javax.swing.border.EtchedBorder;
 import javax.swing.border.TitledBorder;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
 
 /**
  * Base class for the Preferences panel.
@@ -180,7 +181,21 @@ public class GPreferences extends JPanel
   /*
    * Connections tab components
    */
-  protected JList linkURLList = new JList();
+  protected JTable linkUrlTable = new JTable();
+
+  protected JButton editLink = new JButton();
+
+  protected JButton deleteLink = new JButton();
+
+  protected JTextField filterTB = new JTextField();
+
+  protected JButton doReset = new JButton();
+
+  protected JButton userOnly = new JButton();
+
+  protected JLabel portLabel = new JLabel();
+
+  protected JLabel serverLabel = new JLabel();
 
   protected JTextField proxyServerTB = new JTextField();
 
@@ -188,8 +203,6 @@ public class GPreferences extends JPanel
 
   protected JTextField defaultBrowser = new JTextField();
 
-  protected JList linkNameList = new JList();
-
   protected JCheckBox useProxy = new JCheckBox();
 
   protected JCheckBox usagestats = new JCheckBox();
@@ -483,38 +496,261 @@ public class GPreferences extends JPanel
   {
     JPanel connectTab = new JPanel();
     connectTab.setLayout(new GridBagLayout());
-    JLabel serverLabel = new JLabel();
+
+    // Label for browser text box
+    JLabel browserLabel = new JLabel();
+    browserLabel.setFont(LABEL_FONT);
+    browserLabel.setHorizontalAlignment(SwingConstants.TRAILING);
+    browserLabel.setText(MessageManager
+            .getString("label.default_browser_unix"));
+    defaultBrowser.setFont(LABEL_FONT);
+    defaultBrowser.setText("");
+
+    defaultBrowser.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mouseClicked(MouseEvent e)
+      {
+        if (e.getClickCount() > 1)
+        {
+          defaultBrowser_mouseClicked(e);
+        }
+      }
+    });
+
+    JPanel proxyPanel = initConnTabProxyPanel();
+    JPanel linkPanel = initConnTabUrlLinks();
+    initConnTabCheckboxes();
+
+    // Add URL link panel
+    connectTab.add(linkPanel, new GridBagConstraints(0, 0, 2, 1, 2.0, 2.0,
+            GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(
+                    16, 0, 0, 12), 359, 32));
+
+    // Add default Browser text box
+    connectTab.add(browserLabel, new GridBagConstraints(0, 1, 1, 1, 0.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
+            new Insets(10, 0, 0, 0), 5, 1));
+
+    connectTab.add(defaultBrowser, new GridBagConstraints(1, 1, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(10, 0, 0, 10), 307, 1));
+
+    // Add proxy server panel
+    connectTab.add(proxyPanel, new GridBagConstraints(0, 2, 2, 1, 1.0, 0.0,
+            GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
+            new Insets(10, 0, 0, 12), 4, 10));
+
+    // Add usage stats, version check and questionnaire checkboxes
+    connectTab.add(usagestats, new GridBagConstraints(0, 3, 1, 1, 1.0, 0.0,
+            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 4, 2), 70, 1));
+    connectTab.add(questionnaire, new GridBagConstraints(1, 3, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 4, 10), 70, 1));
+    connectTab.add(versioncheck, new GridBagConstraints(0, 4, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 4, 2), 70, 1));
+    return connectTab;
+  }
+
+  /**
+   * Initialises the proxy server panel in the Connections tab
+   * 
+   * @return the proxy server panel
+   */
+  private JPanel initConnTabProxyPanel()
+  {
+    // Label for server text box
     serverLabel.setText(MessageManager.getString("label.address"));
     serverLabel.setHorizontalAlignment(SwingConstants.RIGHT);
     serverLabel.setFont(LABEL_FONT);
+
+    // Proxy server and port text boxes
     proxyServerTB.setFont(LABEL_FONT);
     proxyPortTB.setFont(LABEL_FONT);
-    JLabel portLabel = new JLabel();
+
+    // Label for Port text box
     portLabel.setFont(LABEL_FONT);
     portLabel.setHorizontalAlignment(SwingConstants.RIGHT);
     portLabel.setText(MessageManager.getString("label.port"));
-    JLabel browserLabel = new JLabel();
-    browserLabel.setFont(new java.awt.Font("SansSerif", 0, 11));
-    browserLabel.setHorizontalAlignment(SwingConstants.TRAILING);
-    browserLabel.setText(MessageManager
-            .getString("label.default_browser_unix"));
-    defaultBrowser.setFont(LABEL_FONT);
-    defaultBrowser.setText("");
+
+    // Use proxy server checkbox
+    useProxy.setFont(LABEL_FONT);
+    useProxy.setHorizontalAlignment(SwingConstants.RIGHT);
+    useProxy.setHorizontalTextPosition(SwingConstants.LEADING);
+    useProxy.setText(MessageManager.getString("label.use_proxy_server"));
+    useProxy.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        useProxy_actionPerformed();
+      }
+    });
+
+    // Make proxy server panel
+    JPanel proxyPanel = new JPanel();
+    TitledBorder titledBorder1 = new TitledBorder(
+            MessageManager.getString("label.proxy_server"));
+    proxyPanel.setBorder(titledBorder1);
+    proxyPanel.setLayout(new GridBagLayout());
+    proxyPanel.add(serverLabel, new GridBagConstraints(0, 1, 1, 1, 0.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
+            new Insets(0, 2, 2, 0), 5, 0));
+    proxyPanel.add(portLabel, new GridBagConstraints(2, 1, 1, 1, 0.0, 0.0,
+            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
+                    0, 2, 0), 11, 0));
+    proxyPanel.add(useProxy, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0,
+            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
+                    2, 5, 185), 2, -4));
+    proxyPanel.add(proxyPortTB, new GridBagConstraints(3, 1, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 2, 2), 54, 1));
+    proxyPanel.add(proxyServerTB, new GridBagConstraints(1, 1, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 2, 0), 263, 1));
+
+    return proxyPanel;
+  }
+
+  /**
+   * Initialises the checkboxes in the Connections tab
+   */
+  private void initConnTabCheckboxes()
+  {
+    // Usage stats checkbox label
     usagestats.setText(MessageManager
             .getString("label.send_usage_statistics"));
     usagestats.setFont(LABEL_FONT);
     usagestats.setHorizontalAlignment(SwingConstants.RIGHT);
     usagestats.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    // Questionnaire checkbox label
     questionnaire.setText(MessageManager
             .getString("label.check_for_questionnaires"));
     questionnaire.setFont(LABEL_FONT);
     questionnaire.setHorizontalAlignment(SwingConstants.RIGHT);
     questionnaire.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    // Check for latest version checkbox label
     versioncheck.setText(MessageManager
             .getString("label.check_for_latest_version"));
     versioncheck.setFont(LABEL_FONT);
     versioncheck.setHorizontalAlignment(SwingConstants.RIGHT);
     versioncheck.setHorizontalTextPosition(SwingConstants.LEADING);
+  }
+
+  /**
+   * Initialises the URL links panel in the Connection tab
+   * 
+   * @return the URL links panel
+   */
+  private JPanel initConnTabUrlLinks()
+  {
+    // Set up table for Url links
+    linkUrlTable.setFillsViewportHeight(true);
+    linkUrlTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
+    linkUrlTable.setAutoCreateRowSorter(true);
+    linkUrlTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+    // adjust row height so radio buttons actually fit
+    // don't do this in the renderer, it causes the awt thread to activate
+    // constantly
+    JRadioButton temp = new JRadioButton();
+    linkUrlTable.setRowHeight(temp.getMinimumSize().height);
+
+    // Table in scrollpane so that the table is given a scrollbar
+    JScrollPane linkScrollPane = new JScrollPane(linkUrlTable);
+    linkScrollPane.setBorder(null);
+
+    // Panel for links functionality
+    JPanel linkPanel = new JPanel(new GridBagLayout());
+    linkPanel.setBorder(new TitledBorder(MessageManager
+            .getString("label.url_linkfrom_sequence_id")));
+
+    // Put the Url links panel together
+
+    // Buttons go at top right, resizing only resizes the blank space vertically
+    JPanel buttonPanel = initConnTabUrlButtons();
+    GridBagConstraints linkConstraints1 = new GridBagConstraints();
+    linkConstraints1.gridx = 1;
+    linkConstraints1.gridy = 0;
+    linkConstraints1.fill = GridBagConstraints.VERTICAL;
+    linkPanel.add(buttonPanel, linkConstraints1);
+
+    // Links table goes at top left, resizing resizes the table
+    GridBagConstraints linkConstraints2 = new GridBagConstraints();
+    linkConstraints2.gridx = 0;
+    linkConstraints2.gridy = 0;
+    linkConstraints2.weightx = 1.0;
+    linkConstraints2.weighty = 1.0;
+    linkConstraints2.fill = GridBagConstraints.BOTH;
+    linkPanel.add(linkScrollPane, linkConstraints2);
+
+    // Filter box and buttons goes at bottom left, resizing resizes the text box
+    JPanel filterPanel = initConnTabFilterPanel();
+    GridBagConstraints linkConstraints3 = new GridBagConstraints();
+    linkConstraints3.gridx = 0;
+    linkConstraints3.gridy = 1;
+    linkConstraints3.weightx = 1.0;
+    linkConstraints3.fill = GridBagConstraints.HORIZONTAL;
+    linkPanel.add(filterPanel, linkConstraints3);
+
+    return linkPanel;
+  }
+
+  private JPanel initConnTabFilterPanel()
+  {
+    // Filter textbox and reset button
+    JLabel filterLabel = new JLabel(
+            MessageManager.getString("label.filter"));
+    filterLabel.setFont(LABEL_FONT);
+    filterLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+    filterLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    filterTB.setFont(LABEL_FONT);
+    filterTB.setText("");
+
+    doReset.setText(MessageManager.getString("action.showall"));
+    userOnly.setText(MessageManager.getString("action.customfilter"));
+
+    // Panel for filter functionality
+    JPanel filterPanel = new JPanel(new GridBagLayout());
+    GridBagConstraints gbc = new GridBagConstraints();
+    gbc.gridx = 0;
+    gbc.gridy = 0;
+    gbc.fill = GridBagConstraints.NONE;
+    gbc.anchor = GridBagConstraints.WEST;
+
+    filterPanel.add(filterLabel, gbc);
+
+    GridBagConstraints gbc1 = new GridBagConstraints();
+    gbc1.gridx = 0;
+    gbc1.gridx = 1;
+    gbc1.fill = GridBagConstraints.HORIZONTAL;
+    gbc1.anchor = GridBagConstraints.WEST;
+    gbc1.weightx = 1.0;
+    filterPanel.add(filterTB, gbc1);
+
+    GridBagConstraints gbc2 = new GridBagConstraints();
+    gbc2.gridx = 2;
+    gbc2.fill = GridBagConstraints.NONE;
+    gbc2.anchor = GridBagConstraints.WEST;
+    filterPanel.add(doReset, gbc2);
+
+    GridBagConstraints gbc3 = new GridBagConstraints();
+    gbc3.gridx = 3;
+    gbc3.fill = GridBagConstraints.NONE;
+    gbc3.anchor = GridBagConstraints.WEST;
+    filterPanel.add(userOnly, gbc3);
+
+    return filterPanel;
+  }
+
+  private JPanel initConnTabUrlButtons()
+  {
+    // Buttons for new / edit / delete Url links
     JButton newLink = new JButton();
     newLink.setText(MessageManager.getString("action.new"));
     newLink.addActionListener(new java.awt.event.ActionListener()
@@ -525,7 +761,7 @@ public class GPreferences extends JPanel
         newLink_actionPerformed(e);
       }
     });
-    JButton editLink = new JButton();
+
     editLink.setText(MessageManager.getString("action.edit"));
     editLink.addActionListener(new java.awt.event.ActionListener()
     {
@@ -535,7 +771,7 @@ public class GPreferences extends JPanel
         editLink_actionPerformed(e);
       }
     });
-    JButton deleteLink = new JButton();
+
     deleteLink.setText(MessageManager.getString("action.delete"));
     deleteLink.addActionListener(new java.awt.event.ActionListener()
     {
@@ -546,117 +782,21 @@ public class GPreferences extends JPanel
       }
     });
 
-    linkURLList.addListSelectionListener(new ListSelectionListener()
-    {
-      @Override
-      public void valueChanged(ListSelectionEvent e)
-      {
-        int index = linkURLList.getSelectedIndex();
-        linkNameList.setSelectedIndex(index);
-      }
-    });
+    // no current selection, so initially disable delete/edit buttons
+    editLink.setEnabled(false);
+    deleteLink.setEnabled(false);
 
-    linkNameList.addListSelectionListener(new ListSelectionListener()
-    {
-      @Override
-      public void valueChanged(ListSelectionEvent e)
-      {
-        int index = linkNameList.getSelectedIndex();
-        linkURLList.setSelectedIndex(index);
-      }
-    });
+    // Panels for new/edit/delete link buttons
+    // buttonContent prevents the buttons from being resized when the window is
+    JPanel buttonContent = new JPanel(new GridLayout(0, 1, 0, 0));
+    JPanel buttonPanel = new JPanel(new BorderLayout());
 
-    JScrollPane linkScrollPane = new JScrollPane();
-    linkScrollPane.setBorder(null);
-    JPanel linkPanel = new JPanel();
-    linkPanel.setBorder(new TitledBorder(MessageManager
-            .getString("label.url_linkfrom_sequence_id")));
-    linkPanel.setLayout(new BorderLayout());
-    GridLayout gridLayout1 = new GridLayout();
-    JPanel editLinkButtons = new JPanel();
-    editLinkButtons.setLayout(gridLayout1);
-    gridLayout1.setRows(3);
-    linkNameList.setFont(LABEL_FONT);
-    linkNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-    BorderLayout borderLayout3 = new BorderLayout();
-    JPanel linkPanel2 = new JPanel();
-    linkPanel2.setLayout(borderLayout3);
-    linkURLList.setFont(LABEL_FONT);
-    linkURLList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+    buttonContent.add(newLink, null);
+    buttonContent.add(editLink, null);
+    buttonContent.add(deleteLink, null);
+    buttonPanel.add(buttonContent, BorderLayout.NORTH);
 
-    defaultBrowser.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mouseClicked(MouseEvent e)
-      {
-        if (e.getClickCount() > 1)
-        {
-          defaultBrowser_mouseClicked(e);
-        }
-      }
-    });
-    useProxy.setFont(LABEL_FONT);
-    useProxy.setHorizontalAlignment(SwingConstants.RIGHT);
-    useProxy.setHorizontalTextPosition(SwingConstants.LEADING);
-    useProxy.setText(MessageManager.getString("label.use_proxy_server"));
-    useProxy.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        useProxy_actionPerformed();
-      }
-    });
-    linkPanel.add(editLinkButtons, BorderLayout.EAST);
-    editLinkButtons.add(newLink, null);
-    editLinkButtons.add(editLink, null);
-    editLinkButtons.add(deleteLink, null);
-    linkPanel.add(linkScrollPane, BorderLayout.CENTER);
-    linkScrollPane.getViewport().add(linkPanel2, null);
-    linkPanel2.add(linkURLList, BorderLayout.CENTER);
-    linkPanel2.add(linkNameList, BorderLayout.WEST);
-    JPanel jPanel1 = new JPanel();
-    TitledBorder titledBorder1 = new TitledBorder(
-            MessageManager.getString("label.proxy_server"));
-    jPanel1.setBorder(titledBorder1);
-    jPanel1.setLayout(new GridBagLayout());
-    jPanel1.add(serverLabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
-                    2, 4, 0), 5, 0));
-    jPanel1.add(portLabel, new GridBagConstraints(2, 1, 1, 1, 0.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
-                    0, 4, 0), 11, 6));
-    connectTab.add(linkPanel, new GridBagConstraints(0, 0, 2, 1, 1.0, 1.0,
-            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
-                    16, 0, 0, 12), 359, -17));
-    connectTab.add(jPanel1, new GridBagConstraints(0, 2, 2, 1, 1.0, 1.0,
-            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
-                    21, 0, 35, 12), 4, 6));
-    connectTab.add(browserLabel, new GridBagConstraints(0, 1, 1, 1, 0.0,
-            0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
-            new Insets(16, 0, 0, 0), 5, 1));
-    jPanel1.add(useProxy, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
-                    2, 5, 185), 2, -4));
-    jPanel1.add(proxyPortTB, new GridBagConstraints(3, 1, 1, 1, 1.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 54, 1));
-    jPanel1.add(proxyServerTB, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 0), 263, 1));
-    connectTab.add(defaultBrowser, new GridBagConstraints(1, 1, 1, 1, 1.0,
-            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(15, 0, 0, 15), 307, 1));
-    connectTab.add(usagestats, new GridBagConstraints(0, 4, 1, 1, 1.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 70, 1));
-    connectTab.add(questionnaire, new GridBagConstraints(1, 4, 1, 1, 1.0,
-            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 70, 1));
-    connectTab.add(versioncheck, new GridBagConstraints(0, 5, 1, 1, 1.0,
-            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 70, 1));
-    return connectTab;
+    return buttonPanel;
   }
 
   /**
@@ -1357,8 +1497,82 @@ public class GPreferences extends JPanel
 
   public void useProxy_actionPerformed()
   {
-    proxyServerTB.setEnabled(useProxy.isSelected());
-    proxyPortTB.setEnabled(useProxy.isSelected());
+    boolean enabled = useProxy.isSelected();
+    portLabel.setEnabled(enabled);
+    serverLabel.setEnabled(enabled);
+    proxyServerTB.setEnabled(enabled);
+    proxyPortTB.setEnabled(enabled);
   }
 
+  /**
+   * Customer renderer for JTable: supports column of radio buttons
+   */
+  public class RadioButtonRenderer extends JRadioButton implements
+          TableCellRenderer
+  {
+    public RadioButtonRenderer()
+    {
+      setHorizontalAlignment(CENTER);
+      setToolTipText(MessageManager.getString("label.urltooltip"));
+    }
+
+    @Override
+    public Component getTableCellRendererComponent(JTable table,
+            Object value, boolean isSelected, boolean hasFocus, int row,
+            int column)
+    {
+      setSelected((boolean) value);
+
+      // set colours to match rest of table
+      if (isSelected)
+       {
+         setBackground(table.getSelectionBackground());
+         setForeground(table.getSelectionForeground());
+       }
+       else
+       {
+         setBackground(table.getBackground());
+         setForeground(table.getForeground());
+      }
+      return this;
+    }
+  }
+
+  /**
+   * Customer cell editor for JTable: supports column of radio buttons in
+   * conjunction with renderer
+   */
+  public class RadioButtonEditor extends AbstractCellEditor implements
+            TableCellEditor
+    {
+      private JRadioButton button = new JRadioButton();
+
+      public RadioButtonEditor()
+      {
+      button.setHorizontalAlignment(SwingConstants.CENTER);
+      this.button.addActionListener(new ActionListener()
+        {
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
+            fireEditingStopped();
+          }
+        });
+      }
+
+      @Override
+      public Component getTableCellEditorComponent(JTable table,
+              Object value, boolean isSelected, int row, int column)
+      {
+      button.setSelected((boolean) value);
+        return button;
+      }
+
+      @Override
+      public Object getCellEditorValue()
+      {
+      return button.isSelected();
+      }
+
+  }
 }
index dbce5f3..ab3ea2c 100755 (executable)
@@ -29,19 +29,48 @@ import java.awt.Font;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
-import java.awt.Panel;
 import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
 
 import javax.swing.BorderFactory;
+import javax.swing.JButton;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
 import javax.swing.SwingConstants;
 
-public class GSequenceLink extends Panel
+public class GSequenceLink extends JPanel
 {
+
+  JTextField nameTB = new JTextField();
+
+  JTextField urlTB = new JTextField();
+
+  JButton insertSeq = new JButton();
+
+  JButton insertDBAcc = new JButton();
+
+  JLabel insert = new JLabel();
+
+  JLabel jLabel1 = new JLabel();
+
+  JLabel jLabel2 = new JLabel();
+
+  JLabel jLabel3 = new JLabel();
+
+  JLabel jLabel4 = new JLabel();
+
+  JLabel jLabel5 = new JLabel();
+
+  JLabel jLabel6 = new JLabel();
+
+  JPanel jPanel1 = new JPanel();
+
+  GridBagLayout gridBagLayout1 = new GridBagLayout();
+
   public GSequenceLink()
   {
     try
@@ -77,23 +106,53 @@ public class GSequenceLink extends Panel
         urlTB_keyTyped(e);
       }
     });
+
+    insertSeq.setLocation(77, 75);
+    insertSeq.setSize(141, 24);
+    insertSeq.setText(MessageManager.getString("action.seq_id"));
+    insertSeq.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        insertSeq_action(e);
+      }
+    });
+
+    insertDBAcc.setLocation(210, 75);
+    insertDBAcc.setSize(141, 24);
+    insertDBAcc.setText(MessageManager.getString("action.db_acc"));
+    insertDBAcc.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        insertDBAcc_action(e);
+      }
+    });
+
+    insert.setText(MessageManager.getString("label.insert"));
+    insert.setFont(JvSwingUtils.getLabelFont());
+    insert.setHorizontalAlignment(SwingConstants.RIGHT);
+    insert.setBounds(17, 78, 58, 16);
+
     jLabel1.setFont(JvSwingUtils.getLabelFont());
     jLabel1.setHorizontalAlignment(SwingConstants.TRAILING);
     jLabel1.setText(MessageManager.getString("label.link_name"));
     jLabel1.setBounds(new Rectangle(4, 10, 71, 24));
     jLabel2.setFont(JvSwingUtils.getLabelFont());
     jLabel2.setHorizontalAlignment(SwingConstants.TRAILING);
-    jLabel2.setText(MessageManager.getString("label.url"));
+    jLabel2.setText(MessageManager.getString("label.url:"));
     jLabel2.setBounds(new Rectangle(17, 37, 54, 27));
     jLabel3.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
     jLabel3.setText(MessageManager.getString("label.use_sequence_id_1"));
-    jLabel3.setBounds(new Rectangle(21, 72, 351, 15));
+    jLabel3.setBounds(new Rectangle(21, 102, 351, 15));
     jLabel4.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
     jLabel4.setText(MessageManager.getString("label.use_sequence_id_2"));
-    jLabel4.setBounds(new Rectangle(21, 88, 351, 15));
+    jLabel4.setBounds(new Rectangle(21, 118, 351, 15));
     jLabel5.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
     jLabel5.setText(MessageManager.getString("label.use_sequence_id_3"));
-    jLabel5.setBounds(new Rectangle(21, 106, 351, 15));
+    jLabel5.setBounds(new Rectangle(21, 136, 351, 15));
 
     String lastLabel = MessageManager.getString("label.use_sequence_id_4");
     if (lastLabel.length() > 0)
@@ -101,7 +160,7 @@ public class GSequenceLink extends Panel
       // e.g. Spanish version has longer text
       jLabel6.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
       jLabel6.setText(lastLabel);
-      jLabel6.setBounds(new Rectangle(21, 122, 351, 15));
+      jLabel6.setBounds(new Rectangle(21, 152, 351, 15));
     }
 
     jPanel1.setBorder(BorderFactory.createEtchedBorder());
@@ -109,16 +168,19 @@ public class GSequenceLink extends Panel
     jPanel1.add(jLabel1);
     jPanel1.add(nameTB);
     jPanel1.add(urlTB);
+    jPanel1.add(insertSeq);
+    jPanel1.add(insertDBAcc);
+    jPanel1.add(insert);
     jPanel1.add(jLabel2);
     jPanel1.add(jLabel3);
     jPanel1.add(jLabel4);
     jPanel1.add(jLabel5);
 
-    int height = 130;
+    int height = 160;
     if (lastLabel.length() > 0)
     {
       jPanel1.add(jLabel6);
-      height = 146;
+      height = 176;
     }
 
     this.add(jPanel1, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
@@ -163,25 +225,13 @@ public class GSequenceLink extends Panel
     return false;
   }
 
-  JTextField nameTB = new JTextField();
-
-  JTextField urlTB = new JTextField();
-
-  JLabel jLabel1 = new JLabel();
-
-  JLabel jLabel2 = new JLabel();
-
-  JLabel jLabel3 = new JLabel();
-
-  JLabel jLabel4 = new JLabel();
-
-  JLabel jLabel5 = new JLabel();
-
-  JLabel jLabel6 = new JLabel();
-
-  JPanel jPanel1 = new JPanel();
-
-  GridBagLayout gridBagLayout1 = new GridBagLayout();
+  public void notifyDuplicate()
+  {
+    JvOptionPane.showInternalMessageDialog(jalview.gui.Desktop.desktop,
+            MessageManager.getString("warn.name_cannot_be_duplicate"),
+            MessageManager.getString("label.invalid_name"),
+            JvOptionPane.WARNING_MESSAGE);
+  }
 
   public void nameTB_keyTyped(KeyEvent e)
   {
@@ -200,4 +250,23 @@ public class GSequenceLink extends Panel
     // }
 
   }
+
+  public void insertSeq_action(ActionEvent e)
+  {
+    insertIntoUrl(insertSeq.getText());
+  }
+
+  public void insertDBAcc_action(ActionEvent e)
+  {
+    insertIntoUrl(insertDBAcc.getText());
+  }
+
+  private void insertIntoUrl(String insertion)
+  {
+    int pos = urlTB.getCaretPosition();
+    String text = urlTB.getText();
+    String newText = text.substring(0, pos) + insertion
+            + text.substring(pos);
+    urlTB.setText(newText);
+  }
 }
diff --git a/src/jalview/urls/CustomUrlProvider.java b/src/jalview/urls/CustomUrlProvider.java
new file mode 100644 (file)
index 0000000..dae9ccd
--- /dev/null
@@ -0,0 +1,337 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+import static jalview.util.UrlConstants.DB_ACCESSION;
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
+import static jalview.util.UrlConstants.SEQUENCE_ID;
+
+import jalview.util.MessageManager;
+import jalview.util.UrlConstants;
+import jalview.util.UrlLink;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+/**
+ * 
+ * Implements the UrlProviderI interface for a UrlProvider object which serves
+ * custom URLs defined by the user
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class CustomUrlProvider extends UrlProviderImpl
+{
+  // Default sequence URL link label for SRS
+  private static final String SRS_LABEL = "SRS";
+
+  // map of string ids to urlLinks (selected)
+  private HashMap<String, UrlLink> selectedUrls;
+
+  // map of string ids to urlLinks (not selected)
+  private HashMap<String, UrlLink> nonselectedUrls;
+
+  /**
+   * Construct UrlProvider for custom (user-entered) URLs
+   * 
+   * @param inMenuUrlList
+   *          list of URLs set to be displayed in menu, in form stored in Cache.
+   *          i.e. SEP delimited string
+   * @param storedUrlList
+   *          list of custom URLs entered by user but not currently displayed in
+   *          menu, in form stored in Cache
+   */
+  public CustomUrlProvider(String inMenuUrlList, String storedUrlList)
+  {
+    try
+    {
+      selectedUrls = parseUrlStrings(inMenuUrlList);
+      nonselectedUrls = parseUrlStrings(storedUrlList);
+    } catch (Exception ex)
+    {
+      System.out
+              .println(ex.getMessage() + "\nError parsing sequence links");
+    }
+  }
+
+  /**
+   * Construct UrlProvider for custom (user-entered) URLs
+   * 
+   * @param urlList
+   *          list of URLs to be displayed in menu, as (label,url) pairs
+   * @param storedUrlList
+   *          list of custom URLs entered by user but not currently displayed in
+   *          menu, as (label,url) pairs
+   */
+  public CustomUrlProvider(Map<String, String> inMenuUrlList,
+          Map<String, String> storedUrlList)
+  {
+    try
+    {
+      selectedUrls = parseUrlList(inMenuUrlList);
+      nonselectedUrls = parseUrlList(storedUrlList);
+    } catch (Exception ex)
+    {
+      System.out
+              .println(ex.getMessage() + "\nError parsing sequence links");
+    }
+  }
+
+  private HashMap<String, UrlLink> parseUrlStrings(String urlStrings)
+  {
+    // cachedUrlList is in form <label>|<url>|<label>|<url>...
+    // parse cachedUrlList into labels (used as id) and url links
+    HashMap<String, UrlLink> urls = new HashMap<String, UrlLink>();
+
+    StringTokenizer st = new StringTokenizer(urlStrings, SEP);
+    while (st.hasMoreElements())
+    {
+      String name = st.nextToken();
+
+      if (!isMiriamId(name))
+      {
+        // this one of our custom urls
+        String url = st.nextToken();
+        // check for '|' within a regex
+        int rxstart = url.indexOf(DELIM + DB_ACCESSION + DELIM);
+        if (rxstart == -1)
+        {
+          rxstart = url.indexOf(DELIM + SEQUENCE_ID + DELIM);
+        }
+        while (rxstart == -1 && url.indexOf("/=" + DELIM) == -1
+                && st.hasMoreTokens())
+        {
+          url = url + SEP + st.nextToken();
+        }
+        urls.put(name, new UrlLink(name + SEP + url));
+      }
+    }
+    upgradeOldLinks(urls);
+    return urls;
+  }
+
+  private HashMap<String, UrlLink> parseUrlList(Map<String, String> urlList)
+  {
+    HashMap<String, UrlLink> urls = new HashMap<String, UrlLink>();
+    if (urlList == null)
+    {
+      return urls;
+    }
+
+    Iterator<Map.Entry<String, String>> it = urlList.entrySet().iterator();
+    while (it.hasNext())
+    {
+      Map.Entry<String, String> pair = it.next();
+      urls.put(pair.getKey(),
+              new UrlLink(pair.getKey() + SEP + pair.getValue()));
+    }
+    upgradeOldLinks(urls);
+    return urls;
+  }
+
+  /*
+   * Upgrade any legacy links which may have been left lying around
+   */
+  private void upgradeOldLinks(HashMap<String, UrlLink> urls)
+  {
+    // upgrade old SRS link
+    if (urls.containsKey(SRS_LABEL))
+    {
+      urls.remove(SRS_LABEL);
+      urls.put(UrlConstants.DEFAULT_LABEL, new UrlLink(UrlConstants.DEFAULT_STRING));
+    }
+  }
+
+  @Override
+  public Vector<String> getLinksForMenu()
+  {
+    Vector<String> links = new Vector<String>();
+    Iterator<Map.Entry<String, UrlLink>> it = selectedUrls.entrySet()
+            .iterator();
+    while (it.hasNext())
+    {
+      Map.Entry<String, UrlLink> pair = it.next();
+      links.add(pair.getValue().toString());
+    }
+    return links;
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    ArrayList<UrlLinkDisplay> displayLinks = new ArrayList<UrlLinkDisplay>();
+    displayLinks = getLinksForTable(selectedUrls, true);
+    displayLinks.addAll(getLinksForTable(nonselectedUrls, false));
+    return displayLinks;
+  }
+
+  private ArrayList<UrlLinkDisplay> getLinksForTable(
+          HashMap<String, UrlLink> urlList, boolean selected)
+  {
+    return super.getLinksForTable(urlList, null, selected);
+  }
+
+  @Override
+  public boolean setDefaultUrl(String id)
+  {
+    if (selectedUrls.containsKey(id))
+    {
+      defaultUrl = id;
+    }
+    else if (nonselectedUrls.containsKey(id))
+    {
+      defaultUrl = id;
+    }
+    else
+    {
+      defaultUrl = null;
+    }
+
+    return (defaultUrl != null);
+  }
+
+  @Override
+  public String writeUrlsAsString(boolean selected)
+  {
+    StringBuffer links = new StringBuffer();
+    HashMap<String, UrlLink> urls;
+    if (selected)
+    {
+      urls = selectedUrls;
+    }
+    else
+    {
+      urls = nonselectedUrls;
+    }
+    if (urls.size() > 0)
+    {
+      for (Entry<String, UrlLink> entry : urls.entrySet())
+      {
+        links.append(entry.getValue().toString());
+        links.append(SEP);
+      }
+
+      // remove last SEP
+      links.setLength(links.length() - 1);
+    }
+    else
+    {
+      urls.clear();
+    }
+    return links.toString();
+  }
+
+  @Override
+  public String getDefaultUrl(String seqid)
+  {
+    String result = super.getDefaultUrl(seqid, selectedUrls);
+    if (result == null)
+    {
+      result = super.getDefaultUrl(seqid, nonselectedUrls);
+    }
+    return result;
+  }
+
+  @Override
+  public String getDefaultUrlId()
+  {
+    return defaultUrl;
+  }
+
+  @Override
+  public String getDefaultTarget(String seqid)
+  {
+    return selectedUrls.get(defaultUrl).getTarget();
+  }
+
+  @Override
+  public void setUrlData(List<UrlLinkDisplay> links)
+  {
+    HashMap<String, UrlLink> unselurls = new HashMap<String, UrlLink>();
+    HashMap<String, UrlLink> selurls = new HashMap<String, UrlLink>();
+
+    Iterator<UrlLinkDisplay> it = links.iterator();
+    while (it.hasNext())
+    {
+      UrlLinkDisplay link = it.next();
+
+      // MIRIAM ids will be handled by a different UrlProvider class
+      if (!isMiriamId(link.getId()))
+      {
+        // don't allow duplicate key names as entries will be overwritten
+        if (unselurls.containsKey(link.getId())
+                || selurls.containsKey(link.getId()))
+        {
+          throw new IllegalArgumentException(MessageManager.formatMessage(
+                  "exception.url_cannot_have_duplicate_id", link.getId()));
+        }
+        if (link.getIsSelected())
+        {
+          selurls.put(link.getId(),
+                  new UrlLink(link.getName() + SEP + link.getUrl()));
+        }
+        else
+        {
+          unselurls.put(link.getId(), new UrlLink(link.getName() + SEP
+                  + link.getUrl()));
+        }
+        // sort out default and selected ids
+        if (link.getIsDefault())
+        {
+          setDefaultUrl(link.getId());
+        }
+      }
+
+    }
+    nonselectedUrls = unselurls;
+    selectedUrls = selurls;
+  }
+
+  @Override
+  public String chooseDefaultUrl()
+  {
+    // unilaterally set the default id to the EMBL_EBI link
+    if ((!nonselectedUrls.containsKey(UrlConstants.DEFAULT_LABEL))
+            && (!selectedUrls.containsKey(UrlConstants.DEFAULT_LABEL)))
+    {
+      selectedUrls.put(UrlConstants.DEFAULT_LABEL, new UrlLink(
+              UrlConstants.DEFAULT_STRING));
+    }
+    defaultUrl = UrlConstants.DEFAULT_LABEL;
+    return UrlConstants.DEFAULT_LABEL;
+  }
+
+  @Override
+  public boolean contains(String id)
+  {
+    return (selectedUrls.containsKey(id) || nonselectedUrls.containsKey(id));
+  }
+
+}
diff --git a/src/jalview/urls/IdOrgSettings.java b/src/jalview/urls/IdOrgSettings.java
new file mode 100644 (file)
index 0000000..7dd1a19
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+/**
+ * Holds settings for identifiers.org e.g. url, download location
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class IdOrgSettings
+{
+  private static String url;
+
+  private static String location;
+
+  public static void setUrl(String seturl)
+  {
+    url = seturl;
+  }
+
+  public static void setDownloadLocation(String setloc)
+  {
+    location = setloc;
+  }
+
+  public static String getUrl()
+  {
+    return url;
+  }
+
+  public static String getDownloadLocation()
+  {
+    return location;
+  }
+}
diff --git a/src/jalview/urls/IdentifiersUrlProvider.java b/src/jalview/urls/IdentifiersUrlProvider.java
new file mode 100644 (file)
index 0000000..6c01f97
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+import static jalview.util.UrlConstants.DB_ACCESSION;
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
+
+import jalview.util.UrlLink;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+/**
+ * 
+ * Implements the UrlProviderI interface for a UrlProvider object which serves
+ * URLs from identifiers.org
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class IdentifiersUrlProvider extends UrlProviderImpl
+{
+
+  // map of string ids to urls
+  private HashMap<String, UrlLink> urls;
+
+  // list of selected urls
+  private ArrayList<String> selectedUrls;
+
+  public IdentifiersUrlProvider(String cachedUrlList)
+  {
+    urls = readIdentifiers(IdOrgSettings.getDownloadLocation());
+    selectedUrls = new ArrayList<String>();
+    checkSelectionMatchesUrls(cachedUrlList);
+  }
+
+  /**
+   * Read data from an identifiers.org download file
+   * 
+   * @param idFileName
+   *          name of identifiers.org download file
+   * @return hashmap of identifiers.org data, keyed by MIRIAM id
+   */
+  private HashMap<String, UrlLink> readIdentifiers(
+          String idFileName)
+  {
+    JSONParser parser = new JSONParser();
+
+    // identifiers.org data
+    HashMap<String, UrlLink> idData = new HashMap<String, UrlLink>();
+
+    try
+    {
+      FileReader reader = new FileReader(idFileName);
+
+      JSONArray jsonarray = (JSONArray) parser.parse(reader);
+
+      // loop over each entry in JSON array and build HashMap entry
+      for (int i = 0; i < jsonarray.size(); i++)
+      {
+        JSONObject item = (JSONObject) jsonarray.get(i);
+
+        String url = (String) item.get("url") + "/" + DELIM + DB_ACCESSION
+                + DELIM;
+        UrlLink link = new UrlLink((String) item.get("name") + SEP + url);
+        idData.put((String) item.get("id"), link);
+      }
+    } catch (FileNotFoundException e)
+    {
+      e.printStackTrace();
+    } catch (IOException e)
+    {
+      e.printStackTrace();
+    } catch (ParseException e)
+    {
+      e.printStackTrace();
+    }
+    return idData;
+  }
+
+  private void checkSelectionMatchesUrls(String cachedUrlList)
+  {
+    StringTokenizer st = new StringTokenizer(cachedUrlList, SEP);
+    while (st.hasMoreElements())
+    {
+      String id = st.nextToken();
+
+      if (isMiriamId(id))
+      {
+        // this is an identifiers.org MIRIAM id
+        if (urls.containsKey(id))
+        {
+          selectedUrls.add(id);
+        }
+      }
+    }
+
+    // reset defaultUrl in case it is no longer selected
+    setDefaultUrl(defaultUrl);
+  }
+
+  @Override
+  public boolean setDefaultUrl(String id)
+  {
+    if (urls.containsKey(id))
+    {
+      defaultUrl = id;
+    }
+    else
+    {
+      defaultUrl = null;
+    }
+
+    return urls.containsKey(id);
+  }
+
+  @Override
+  public String writeUrlsAsString(boolean selected)
+  {
+    if (!selected)
+    {
+      return ""; // we don't cache unselected identifiers.org urls
+    }
+
+    StringBuffer links = new StringBuffer();
+    if (!selectedUrls.isEmpty())
+    {
+      for (String k : selectedUrls)
+      {
+        links.append(k);
+        links.append(SEP);
+      }
+      // remove last SEP
+      links.setLength(links.length() - 1);
+    }
+    return links.toString();
+  }
+
+  @Override
+  public Vector<String> getLinksForMenu()
+  {
+    Vector<String> links = new Vector<String>();
+    for (String key : selectedUrls)
+    {
+      links.add(urls.get(key).toString());
+    }
+    return links;
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    return super.getLinksForTable(urls, selectedUrls, false);
+  }
+
+  @Override
+  public void setUrlData(List<UrlLinkDisplay> links)
+  {
+    selectedUrls = new ArrayList<String>();
+
+    Iterator<UrlLinkDisplay> it = links.iterator();
+    while (it.hasNext())
+    {
+      UrlLinkDisplay link = it.next();
+
+      // Handle links with MIRIAM ids only
+      if (isMiriamId(link.getId()))
+      {
+        // select/deselect links accordingly and set default url
+        if (urls.containsKey(link.getId()))
+        {
+          if (link.getIsSelected())
+          {
+            selectedUrls.add(link.getId());
+          }
+          if (link.getIsDefault())
+          {
+            setDefaultUrl(link.getId());
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public String getDefaultUrl(String seqid)
+  {
+    return super.getDefaultUrl(seqid, urls);
+  }
+
+  @Override
+  public String getDefaultUrlId()
+  {
+    return defaultUrl;
+  }
+
+  @Override
+  public String getDefaultTarget(String seqid)
+  {
+    return null;
+  }
+
+  @Override
+  public String chooseDefaultUrl()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean contains(String id)
+  {
+    return (urls.containsKey(id));
+  }
+}
diff --git a/src/jalview/urls/UrlLinkDisplay.java b/src/jalview/urls/UrlLinkDisplay.java
new file mode 100644 (file)
index 0000000..2582f90
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+import jalview.util.MessageManager;
+import jalview.util.UrlLink;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UrlLink table row definition
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class UrlLinkDisplay
+{
+  private String id; // id is not supplied to display, but used to identify
+                     // entries when saved
+
+  private boolean isDefault;
+
+  private boolean isSelected;
+
+  private UrlLink link;
+
+  // Headers for columns in table
+  private final static List<String> colNames = new ArrayList<String>()
+  {
+    {
+      add(MessageManager.formatMessage("label.name"));
+      add(MessageManager.formatMessage("label.url"));
+      add(MessageManager.formatMessage("label.inmenu"));
+      add(MessageManager.formatMessage("label.default"));
+      add(MessageManager.formatMessage("label.id"));
+    }
+  };
+
+  // column positions
+  public final static int NAME = 0;
+
+  public final static int URL = 1;
+
+  public final static int SELECTED = 2;
+
+  public final static int DEFAULT = 3;
+
+  public final static int ID = 4;
+
+  public UrlLinkDisplay(String rowId, UrlLink rowLink,
+          boolean rowSelected, boolean rowDefault)
+  {
+    id = rowId;
+    isDefault = rowDefault;
+    isSelected = rowSelected;
+
+    link = rowLink;
+  }
+
+  // getters/setters
+  public String getId()
+  {
+    return id;
+  }
+
+  public String getName()
+  {
+    return link.getLabel();
+  }
+
+  public String getUrl()
+  {
+    return link.getUrlWithToken();
+  }
+
+  public boolean getIsDefault()
+  {
+    return isDefault;
+  }
+
+  public boolean getIsSelected()
+  {
+    return isSelected;
+  }
+
+  public void setName(String name)
+  {
+    link.setLabel(name);
+  }
+
+  public void setUrl(String rowUrl)
+  {
+    link = new UrlLink(getName(), rowUrl);
+  }
+
+  public void setIsDefault(boolean rowDefault)
+  {
+    isDefault = rowDefault;
+  }
+
+  public void setIsSelected(boolean rowSelected)
+  {
+    isSelected = rowSelected;
+  }
+
+  public Object getValue(int index)
+  {
+    switch (index)
+    {
+    case ID:
+      return id;
+    case URL:
+      return getUrl();
+    case DEFAULT:
+      return isDefault;
+    case SELECTED:
+      return isSelected;
+    case NAME:
+      return getName();
+    default:
+      return null;
+    }
+  }
+
+  public void setValue(int index, Object value)
+  {
+    switch (index)
+    {
+    case ID:
+      id = (String) value;
+      break;
+    case URL:
+      setUrl((String) value);
+      break;
+    case DEFAULT:
+      isDefault = (boolean) value;
+      break;
+    case SELECTED:
+      isSelected = (boolean) value;
+      break;
+    case NAME:
+      setName((String) value);
+      break;
+    default:
+      // do nothing
+    }
+  }
+
+  /**
+   * Identify editable columns
+   * 
+   * @param index
+   *          index of column
+   * @return whether column can be edited in table
+   */
+  public boolean isEditable(int index)
+  {
+    if (index == DEFAULT)
+    {
+      // default link must not be a $DB_ACCESSION$ link
+      // so only allow editing if it is not
+      return (!link.usesDBAccession());
+    }
+    else if (index == SELECTED)
+    {
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  /**
+   * Get list of column names to display in UI
+   * 
+   * @return column names
+   */
+  public static List<String> getDisplayColumnNames()
+  {
+    // Display names between NAME and ID (excludes ID)
+    return colNames.subList(NAME, ID);
+  }
+}
diff --git a/src/jalview/urls/UrlLinkTableModel.java b/src/jalview/urls/UrlLinkTableModel.java
new file mode 100644 (file)
index 0000000..bda92cc
--- /dev/null
@@ -0,0 +1,272 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+import jalview.bin.Cache;
+import jalview.urls.api.UrlProviderI;
+import jalview.util.UrlLink;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.RowFilter.Entry;
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableModel;
+
+/**
+ * TableModel for UrlLinks table
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class UrlLinkTableModel extends AbstractTableModel
+{
+  // local storage of data
+  private List<UrlLinkDisplay> data;
+
+  // supplier of url data
+  private UrlProviderI dataProvider;
+
+  // list of columns to display in table in correct order
+  private List<String> displayColumns;
+
+  // row in table which is currently the default
+  private int defaultRow;
+
+  /**
+   * UrlLinkTableModel constructor
+   * 
+   * @param baseData
+   *          base data set to be presented in table
+   * @param entryNames
+   *          keys of entries in baseData's nested hashmap. Should match order
+   *          in displayColNames
+   * @param displayColNames
+   *          names of columns to display in order.
+   * @param keyColName
+   *          name of column corresponding to keys in baseData
+   */
+  public UrlLinkTableModel(UrlProviderI baseData)
+  {
+    dataProvider = baseData;
+    data = baseData.getLinksForTable();
+    displayColumns = UrlLinkDisplay.getDisplayColumnNames();
+
+    // find the default row
+    defaultRow = 0;
+    Iterator<UrlLinkDisplay> it = data.iterator();
+    while (it.hasNext())
+    {
+      if (it.next().getIsDefault())
+      {
+        break;
+      }
+      else
+      {
+        defaultRow++;
+      }
+    }
+
+    // set up listener which updates data source when table changes
+    this.addTableModelListener(new TableModelListener()
+    {
+      @Override
+      public void tableChanged(TableModelEvent e)
+      {
+        try
+        {
+          // update the UrlProvider from data list
+          dataProvider.setUrlData(data);
+        } catch (IllegalArgumentException ex)
+        {
+          Cache.log.error(ex.getMessage());
+        }
+      }
+    });
+
+  }
+
+  @Override
+  public int getRowCount()
+  {
+    if (data == null)
+    {
+      return 0;
+    }
+    else
+    {
+      return data.size();
+    }
+  }
+
+  @Override
+  public int getColumnCount()
+  {
+    return displayColumns.size();
+  }
+
+  @Override
+  public Object getValueAt(int rowIndex, int columnIndex)
+  {
+    return data.get(rowIndex).getValue(columnIndex);
+  }
+
+  @Override
+  public boolean isCellEditable(int rowIndex, int columnIndex)
+  {
+    return data.get(rowIndex).isEditable(columnIndex);
+  }
+
+  /**
+   * Determine if a row is editable indirectly (rather than directly in table as
+   * in isCellEditable)
+   * 
+   * @param rowIndex
+   * @return true if row can be edited indirectly
+   */
+  public boolean isRowEditable(int rowIndex)
+  {
+    // to edit, row must be a user entered row
+    return (dataProvider.isUserEntry(data.get(rowIndex).getId()));
+  }
+
+  /**
+   * Determine if a row is deletable
+   * 
+   * @param rowIndex
+   *          the row to be tested
+   * @return true if row can be deleted
+   */
+  public boolean isRowDeletable(int rowIndex)
+  {
+    // to delete, row must be a user entered row, and not the default row
+    return (dataProvider.isUserEntry(data.get(rowIndex).getId()) && !data
+            .get(rowIndex).getIsDefault());
+  }
+
+  @Override
+  public void setValueAt(Object aValue, int rowIndex, int columnIndex)
+  {
+    if (columnIndex == UrlLinkDisplay.DEFAULT)
+    {
+      // Default url column: exactly one row must always be true
+      if (rowIndex != defaultRow)
+      {
+        // selected row is not currently the default
+        // set the current default to false
+        data.get(defaultRow).setValue(columnIndex, false);
+        fireTableRowsUpdated(defaultRow, defaultRow);
+
+        // set the default to be the selected row
+        defaultRow = rowIndex;
+        data.get(rowIndex).setValue(columnIndex, aValue);
+
+        fireTableRowsUpdated(rowIndex, rowIndex);
+      }
+    }
+    else
+    {
+      data.get(rowIndex).setValue(columnIndex, aValue);
+      fireTableRowsUpdated(rowIndex, rowIndex);
+    }
+  }
+
+  @Override
+  public Class<?> getColumnClass(int columnIndex)
+  {
+    return getValueAt(0, columnIndex).getClass();
+  }
+
+  @Override
+  public String getColumnName(int columnIndex)
+  {
+    return displayColumns.get(columnIndex);
+  }
+
+  public void removeRow(int rowIndex)
+  {
+    // remove the row from data
+    data.remove(rowIndex);
+
+    // update default row
+    if (defaultRow > rowIndex)
+    {
+      defaultRow--;
+    }
+
+    // fire update which will update data source
+    fireTableRowsDeleted(rowIndex, rowIndex);
+  }
+
+  public int insertRow(String name, String url)
+  {
+    // add a row to the data
+    UrlLink link = new UrlLink(name, url);
+    UrlLinkDisplay u = new UrlLinkDisplay(name, link, true, false);
+    int index = data.size();
+    data.add(u);
+
+    // fire update which will update data source
+    fireTableRowsInserted(index, index);
+    return index;
+  }
+
+  public int getDefaultColumn()
+  {
+    return UrlLinkDisplay.DEFAULT;
+  }
+
+  public int getNameColumn()
+  {
+    return UrlLinkDisplay.NAME;
+  }
+
+  public int getIdColumn()
+  {
+    return UrlLinkDisplay.ID;
+  }
+
+  public int getUrlColumn()
+  {
+    return UrlLinkDisplay.URL;
+  }
+
+  public int getSelectedColumn()
+  {
+    return UrlLinkDisplay.SELECTED;
+  }
+
+  public boolean isUserEntry(
+          Entry<? extends TableModel, ? extends Object> entry)
+  {
+    return dataProvider
+            .isUserEntry(entry.getStringValue(UrlLinkDisplay.ID));
+  }
+
+  public boolean isUniqueName(String name)
+  {
+    return !dataProvider.contains(name);
+  }
+}
diff --git a/src/jalview/urls/UrlProvider.java b/src/jalview/urls/UrlProvider.java
new file mode 100644 (file)
index 0000000..19fa196
--- /dev/null
@@ -0,0 +1,251 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls;
+
+import static jalview.util.UrlConstants.SEP;
+
+import jalview.urls.api.UrlProviderI;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * 
+ * Implements the UrlProviderI interface for a composite UrlProvider object
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class UrlProvider implements UrlProviderI
+{
+  // List of actual URL link providers
+  private List<UrlProviderI> providers;
+
+  // Specific reference to custom URL link provider
+  private UrlProviderI customProvider;
+
+  /**
+   * Constructor for UrlProvider composite
+   * 
+   * @param defaultUrlString
+   *          id of default url
+   * @param allProviders
+   *          list of UrlProviders this provider gives access to
+   */
+  public UrlProvider(String defaultUrlString,
+          List<UrlProviderI> allProviders)
+  {
+    providers = allProviders;
+
+    customProvider = findCustomProvider();
+
+    // check that the defaultUrl still exists
+    if (!contains(defaultUrlString))
+    {
+      // if the defaultUrl can't be found in any of the providers
+      // set up a custom default url
+      chooseDefaultUrl();
+    }
+    else
+    {
+      setDefaultUrl(defaultUrlString);
+    }
+  }
+
+  /*
+   * Store ref to custom url provider
+   */
+  private UrlProviderI findCustomProvider()
+  {
+    for (UrlProviderI p : providers)
+    {
+      if (p.getClass().equals(CustomUrlProvider.class))
+      {
+        return p;
+      }
+    }
+
+    System.out
+            .println("Error initialising UrlProvider - no custom url provider");
+    return null;
+  }
+  
+  @Override
+  public boolean setDefaultUrl(String id)
+  {
+    boolean outcome = false;
+    for (UrlProviderI p : providers)
+    {
+      if (p.setDefaultUrl(id))
+      {
+        outcome = true;
+      }
+    }
+    if (!outcome)
+    {
+      throw new IllegalArgumentException();
+    }
+    return outcome;
+  }
+
+  @Override
+  public boolean contains(String id)
+  {
+    boolean outcome = false;
+    for (UrlProviderI p : providers)
+    {
+      if (p.contains(id))
+      {
+        outcome = true;
+      }
+    }
+    return outcome;
+  }
+  
+  @Override
+  public String writeUrlsAsString(boolean selected)
+  {
+    String result = "";
+    for (UrlProviderI p : providers)
+    {
+      String next = p.writeUrlsAsString(selected);
+      if (!next.isEmpty())
+      {
+        result += next;
+        result += SEP;
+      }
+    }
+    // remove last sep
+    if (!result.isEmpty())
+    {
+      result = result.substring(0, result.length() - 1);
+    }
+    return result;
+  }
+
+  @Override
+  public Vector<String> getLinksForMenu()
+  {
+    Vector<String> fullLinks = new Vector<String>();
+    for (UrlProviderI p : providers)
+    {
+      List<String> links = p.getLinksForMenu();
+      if (links != null)
+      {
+        // will obliterate links with same keys from different providers
+        // must have checks in place to prevent user from duplicating ids
+        fullLinks.addAll(links);
+      }
+    }
+    return fullLinks;
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    ArrayList<UrlLinkDisplay> displayLinks = new ArrayList<UrlLinkDisplay>();
+    for (UrlProviderI p : providers)
+    {
+      displayLinks.addAll(p.getLinksForTable());
+    }
+    return displayLinks;
+  }
+
+  @Override
+  public void setUrlData(List<UrlLinkDisplay> links)
+  {
+    for (UrlProviderI p : providers)
+    {
+      p.setUrlData(links);
+    }
+  }
+
+  @Override
+  public String getDefaultUrl(String seqid)
+  {
+    String link = null;
+    for (UrlProviderI p : providers)
+    {
+      if (p.getDefaultUrl(seqid) == null)
+      {
+        continue;
+      }
+      else
+      {
+        link = p.getDefaultUrl(seqid);
+        break;
+      }
+    }
+    return link;
+  }
+
+  @Override
+  public String getDefaultUrlId()
+  {
+    String id = null;
+    for (UrlProviderI p : providers)
+    {
+      if (p.getDefaultUrlId() == null)
+      {
+        continue;
+      }
+      else
+      {
+        id = p.getDefaultUrlId();
+        break;
+      }
+    }
+    return id;
+  }
+
+  @Override
+  public String getDefaultTarget(String seqid)
+  {
+    String target = null;
+    for (UrlProviderI p : providers)
+    {
+      if (p.getDefaultTarget(seqid) == null)
+      {
+        continue;
+      }
+      else
+      {
+        target = p.getDefaultTarget(seqid);
+        break;
+      }
+    }
+    return target;
+  }
+  
+  @Override
+  public String chooseDefaultUrl()
+  {
+    // choose a custom url default
+    return customProvider.chooseDefaultUrl();
+  }
+
+  @Override
+  public boolean isUserEntry(String id)
+  {
+    return customProvider.isUserEntry(id);
+  }
+}
diff --git a/src/jalview/urls/UrlProviderImpl.java b/src/jalview/urls/UrlProviderImpl.java
new file mode 100644 (file)
index 0000000..87e40c6
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.util.UrlLink;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+/**
+ * Leaf node of UrlProvider composite
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public abstract class UrlProviderImpl implements UrlProviderI
+{
+  // minimum length of substitution in url link string
+  protected static final int MIN_SUBST_LENGTH = 4;
+
+  private static final Pattern MIRIAM_PATTERN = Pattern
+          .compile("^MIR:\\d{8}$");
+
+  protected String defaultUrl;
+
+  protected String getDefaultUrl(String seqid, HashMap<String, UrlLink> urls)
+  {
+    if (seqid.length() < MIN_SUBST_LENGTH)
+    {
+      return null;
+    }
+    else if (defaultUrl == null)
+    {
+      return null;
+    }
+    else if (!urls.containsKey(defaultUrl))
+    {
+      return null;
+    }
+    else
+    {
+      String url = null;
+      UrlLink urlLink = urls.get(defaultUrl);
+      String[] defaultUrls = urlLink.makeUrls(seqid, true);
+      if (defaultUrls == null || defaultUrls[0] == null
+              || defaultUrls[0].length() < MIN_SUBST_LENGTH)
+      {
+        url = null;
+      }
+      else
+      {
+        // just take first URL made from regex
+        url = defaultUrls[1];
+      }
+      return url;
+    }
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    return null;
+  }
+
+  protected ArrayList<UrlLinkDisplay> getLinksForTable(
+          HashMap<String, UrlLink> urls, ArrayList<String> selectedUrls,
+          boolean selected)
+  {
+    ArrayList<UrlLinkDisplay> displayLinks = new ArrayList<UrlLinkDisplay>();
+    for (Entry<String, UrlLink> entry : urls.entrySet())
+    {
+      String key = entry.getKey();
+      boolean isDefault = (key.equals(defaultUrl));
+      boolean isSelected;
+      if (selectedUrls != null)
+      {
+        isSelected = selectedUrls.contains(key);
+      }
+      else
+      {
+        isSelected = selected;
+      }
+      displayLinks.add(new UrlLinkDisplay(key, entry.getValue(),
+              isSelected, isDefault));
+    }
+    return displayLinks;
+  }
+
+  protected boolean isMiriamId(String id)
+  {
+    return MIRIAM_PATTERN.matcher(id).matches();
+  }
+
+  @Override
+  public boolean isUserEntry(String id)
+  {
+    return !isMiriamId(id);
+  }
+}
+
diff --git a/src/jalview/urls/api/UrlProviderFactoryI.java b/src/jalview/urls/api/UrlProviderFactoryI.java
new file mode 100644 (file)
index 0000000..b96113e
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls.api;
+
+
+/**
+ * Interface to UrlProvider factories
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public interface UrlProviderFactoryI
+{
+  public UrlProviderI createUrlProvider();
+
+}
diff --git a/src/jalview/urls/api/UrlProviderI.java b/src/jalview/urls/api/UrlProviderI.java
new file mode 100644 (file)
index 0000000..50e2b78
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls.api;
+
+import jalview.urls.UrlLinkDisplay;
+
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * Methods for providing consistent access to up-to-date URLs
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public interface UrlProviderI
+{
+
+  /**
+   * Get names and urls in the UrlProvider as strings for display
+   * 
+   */
+  Vector<String> getLinksForMenu();
+
+  /**
+   * Get names and urls as strings for display
+   * 
+   */
+  List<UrlLinkDisplay> getLinksForTable();
+
+  /**
+   * Set names and urls from display settings
+   */
+  void setUrlData(List<UrlLinkDisplay> links);
+
+  /**
+   * Get the link for the default URL
+   * 
+   * @seqid sequence id for which to build link
+   * @return link for the default URL
+   */
+  String getDefaultUrl(String seqid);
+
+  /**
+   * Get the default URL id
+   * 
+   * @return id for default URL
+   */
+  String getDefaultUrlId();
+
+  /**
+   * Get the target of the link for the default URL
+   * 
+   * @seqid sequence id for which to build link
+   * @return target of link for the default URL
+   */
+  String getDefaultTarget(String seqid);
+
+  /**
+   * Set the default URL
+   * 
+   * @param id
+   *          the id of the URL to set as default
+   * @return true if setting is successful
+   * @throws IllegalArgumentException
+   *           if id does not exist as a url in the UrlProvider
+   */
+  boolean setDefaultUrl(String id) throws IllegalArgumentException;
+
+  /**
+   * Test if UrlProvider contains a url
+   * 
+   * @param id
+   *          to test for
+   * @return true of UrlProvider contains this id, false otherwise
+   */
+  boolean contains(String id);
+
+  /**
+   * Write out all URLs as a string suitable for serialising
+   * 
+   * @return string representation of available URLs
+   */
+  String writeUrlsAsString(boolean selected);
+
+  /**
+   * Choose the default URL in the event of the selected default being
+   * unavailable
+   * 
+   * @return id of chosen default url
+   */
+  String chooseDefaultUrl();
+
+  /**
+   * Determine if id is for a user-defined URL
+   */
+  boolean isUserEntry(String id);
+}
diff --git a/src/jalview/urls/applet/AppletUrlProviderFactory.java b/src/jalview/urls/applet/AppletUrlProviderFactory.java
new file mode 100644 (file)
index 0000000..fcedaa4
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls.applet;
+
+import jalview.urls.CustomUrlProvider;
+import jalview.urls.UrlProvider;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * UrlProvider factory for applet code
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class AppletUrlProviderFactory implements UrlProviderFactoryI
+{
+  private String provDefaultUrl;
+
+  private Map<String, String> provUrlList;
+
+  public AppletUrlProviderFactory(String defaultUrlString,
+          Map<String, String> urlList)
+  {
+    provDefaultUrl = defaultUrlString;
+    provUrlList = urlList;
+  }
+
+  @Override
+  public UrlProviderI createUrlProvider()
+  {
+    // create all the UrlProviders we need
+    List<UrlProviderI> providers = new ArrayList<UrlProviderI>();
+    UrlProviderI customProvider = new CustomUrlProvider(provUrlList, null);
+    providers.add(customProvider);
+
+    UrlProviderI prov = new UrlProvider(provDefaultUrl, providers);
+    return prov;
+  }
+
+}
diff --git a/src/jalview/urls/desktop/DesktopUrlProviderFactory.java b/src/jalview/urls/desktop/DesktopUrlProviderFactory.java
new file mode 100644 (file)
index 0000000..ce933c2
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls.desktop;
+
+import jalview.urls.CustomUrlProvider;
+import jalview.urls.IdentifiersUrlProvider;
+import jalview.urls.UrlProvider;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UrlProvider factory for desktop code
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class DesktopUrlProviderFactory implements UrlProviderFactoryI
+{
+
+  private String provDefaultUrl;
+
+  private String menuUrlList;
+
+  private String nonMenuUrlList;
+
+  public DesktopUrlProviderFactory(String defaultUrlString,
+          String cachedUrlList, String userUrlList)
+  {
+    provDefaultUrl = defaultUrlString;
+    menuUrlList = cachedUrlList;
+    nonMenuUrlList = userUrlList;
+  }
+
+  @Override
+  public UrlProviderI createUrlProvider()
+  {
+    // create all the UrlProviders we need
+    List<UrlProviderI> providers = new ArrayList<UrlProviderI>();
+
+    UrlProviderI idProvider = new IdentifiersUrlProvider(menuUrlList);
+    UrlProviderI customProvider = new CustomUrlProvider(menuUrlList,
+            nonMenuUrlList);
+    providers.add(idProvider);
+    providers.add(customProvider);
+
+    return new UrlProvider(provDefaultUrl, providers);
+  }
+
+}
index 1910bff..3347cc7 100644 (file)
@@ -37,14 +37,25 @@ public class UrlConstants
   public static final String SEQUENCE_ID = "SEQUENCE_ID";
 
   /*
-   * Default sequence URL link string for EMBL-EBI search
+   * Separator character used in Url links
+   */
+  public static final String SEP = "|";
+
+  /*
+   * Delimiter character used in Url links
    */
-  public static final String EMBLEBI_STRING = "EMBL-EBI Search|http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
+  public static final String DELIM = "$";
 
   /*
-   * Default sequence URL link string for SRS 
+   * Default sequence URL link label for EMBL-EBI search
+   */
+  public static final String DEFAULT_LABEL = "EMBL-EBI Search";
+
+  /*
+   * Default sequence URL link string for EMBL-EBI search
    */
-  public static final String SRS_STRING = "SRS|http://srs.ebi.ac.uk/srsbin/cgi-bin/wgetz?-newId+(([uniprot-all:$SEQUENCE_ID$]))+-view+SwissEntry";
+  public static final String DEFAULT_STRING = DEFAULT_LABEL
+          + "|http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
 
   /*
    * not instantiable
index 3ee6432..186b682 100644 (file)
@@ -21,6 +21,8 @@
 package jalview.util;
 
 import static jalview.util.UrlConstants.DB_ACCESSION;
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
 import static jalview.util.UrlConstants.SEQUENCE_ID;
 
 import jalview.datamodel.DBRefEntry;
@@ -43,11 +45,6 @@ public class UrlLink
    * documentation todo.
    */
 
-  // Internal constants
-  private static final String SEP = "|";
-
-  private static final String DELIM = "$";
-
   private String urlSuffix;
 
   private String urlPrefix;
@@ -107,6 +104,17 @@ public class UrlLink
   }
 
   /**
+   * Alternative constructor for separate name and link
+   * 
+   * @param name
+   * @param url
+   */
+  public UrlLink(String name, String url)
+  {
+    this(name + SEP + url);
+  }
+
+  /**
    * @return the url_suffix
    */
   public String getUrl_suffix()
@@ -138,6 +146,16 @@ public class UrlLink
     return label;
   }
 
+  public String getUrlWithToken()
+  {
+    String var = (usesDBaccession ? DB_ACCESSION : SEQUENCE_ID);
+
+    return urlPrefix
+            + (dynamic ? (DELIM + var + ((regexReplace != null) ? "="
+                    + regexReplace + "=" + DELIM : DELIM)) : "")
+            + ((urlSuffix == null) ? "" : urlSuffix);
+  }
+
   /**
    * @return the regexReplace
    */
@@ -307,14 +325,7 @@ public class UrlLink
   @Override
   public String toString()
   {
-    String var = (usesDBaccession ? DB_ACCESSION : SEQUENCE_ID);
-
-    return label
-            + SEP
-            + urlPrefix
-            + (dynamic ? (DELIM + var + ((regexReplace != null) ? "="
-                    + regexReplace + "=" + DELIM : DELIM)) : "")
-            + ((urlSuffix == null) ? "" : urlSuffix);
+    return label + SEP + getUrlWithToken();
   }
 
   /**
diff --git a/src/jalview/ws/UrlDownloadClient.java b/src/jalview/ws/UrlDownloadClient.java
new file mode 100644 (file)
index 0000000..4787c26
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.ws;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+public class UrlDownloadClient
+{
+  public UrlDownloadClient()
+  {
+
+  }
+
+  /**
+   * Download persistent database URLs from identifiers.org
+   * 
+   * @param urlstring
+   *          url to download from, as string
+   * @param outfile
+   *          the name of file to save the URLs to
+   * @throws IOException
+   */
+  public void download(String urlstring, String outfile) throws IOException
+  {
+    FileOutputStream fos = null;
+    ReadableByteChannel rbc = null;
+    Path temp = null;
+    try
+    {
+      temp = Files.createTempFile(".jalview_", ".tmp");
+
+      URL url = new URL(urlstring);
+      rbc = Channels.newChannel(url.openStream());
+      fos = new FileOutputStream(temp.toString());
+      fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+
+      // copy tempfile to outfile once our download completes
+      // incase something goes wrong
+      Files.copy(temp, Paths.get(outfile),
+              StandardCopyOption.REPLACE_EXISTING);
+    } catch (IOException e)
+    {
+      throw e;
+    } finally
+    {
+      try
+      {
+        if (fos != null)
+        {
+          fos.close();
+        }
+      } catch (IOException e)
+      {
+        System.out
+                .println("Exception while closing download file output stream: "
+                        + e.getMessage());
+      }
+      try
+      {
+        if (rbc != null)
+        {
+          rbc.close();
+        }
+      } catch (IOException e)
+      {
+        System.out.println("Exception while closing download channel: "
+                        + e.getMessage());
+      }
+      try
+      {
+        if (temp != null)
+        {
+          Files.deleteIfExists(temp);
+        }
+      } catch (IOException e)
+      {
+        System.out.println("Exception while deleting download temp file: "
+                        + e.getMessage());
+      }
+    }
+  }
+}
diff --git a/test/jalview/urls/AppletUrlProviderFactoryTest.java b/test/jalview/urls/AppletUrlProviderFactoryTest.java
new file mode 100644 (file)
index 0000000..f679490
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls;
+
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.applet.AppletUrlProviderFactory;
+import jalview.util.UrlConstants;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class AppletUrlProviderFactoryTest {
+
+  @Test(groups = { "Functional" })
+  public void testCreateUrlProvider()
+  {
+    final String defaultUrl = UrlConstants.DEFAULT_STRING.substring(
+            UrlConstants.DEFAULT_STRING.indexOf(UrlConstants.SEP) + 1,
+            UrlConstants.DEFAULT_STRING.length());
+    Map<String, String> urlList = new HashMap<String, String>()
+    {
+      {
+        put("Test1", "http://identifiers.org/uniprot/$DB_ACCESSION$");
+        put("Test2", defaultUrl);
+      }
+    };
+
+    UrlProviderFactoryI factory = new AppletUrlProviderFactory("Test2",
+            urlList);
+    UrlProviderI prov = factory.createUrlProvider();
+
+    // default url correctly set
+    Assert.assertEquals(prov.getDefaultUrlId(), "Test2");
+    Assert.assertEquals(prov.getDefaultUrl("FER_CAPAN"),
+            defaultUrl.replace("$SEQUENCE_ID$",
+                    "FER_CAPAN"));
+
+    List<UrlLinkDisplay> allLinks = prov.getLinksForTable();
+
+    // 2 links in provider
+    Assert.assertEquals(allLinks.size(), 2);
+
+    // first link set correctly
+    Assert.assertEquals(allLinks.get(0).getId(), "Test1");
+    Assert.assertEquals(allLinks.get(0).getName(), "Test1");
+    Assert.assertEquals(allLinks.get(0).getUrl(),
+            "http://identifiers.org/uniprot/$DB_ACCESSION$");
+    Assert.assertFalse(allLinks.get(0).getIsDefault());
+    Assert.assertTrue(allLinks.get(0).getIsSelected());
+
+    // second link set correctly
+    Assert.assertEquals(allLinks.get(1).getId(), "Test2");
+    Assert.assertEquals(allLinks.get(1).getName(), "Test2");
+    Assert.assertEquals(allLinks.get(1).getUrl(), defaultUrl);
+    Assert.assertTrue(allLinks.get(1).getIsDefault());
+    Assert.assertTrue(allLinks.get(1).getIsSelected());
+  }
+}
diff --git a/test/jalview/urls/CustomUrlProviderTest.java b/test/jalview/urls/CustomUrlProviderTest.java
new file mode 100644 (file)
index 0000000..853167b
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.util.UrlConstants;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Vector;
+
+import org.testng.annotations.Test;
+
+public class CustomUrlProviderTest
+{
+
+  private static final String cachedList = "TEST|http://someurl.blah/$DB_ACCESSION$|"
+          + "ANOTHER|http://test/t$SEQUENCE_ID$|"
+          + "TEST2|http://address/$SEQUENCE_ID$|SRS|"
+          + "http://theSRSlink/$SEQUENCE_ID$";
+
+  private static final String unselectedList = "NON1|http://x/y/$DB_ACCESSION$|"
+          + "NON2|http://a/b/c/$DB_ACCESSION";
+
+  private static final HashMap<String, String> urlMap = new HashMap<String, String>()
+  {
+    {
+      put("TEST","http://someurl.blah/$DB_ACCESSION$");
+      put("ANOTHER","http://test/t$SEQUENCE_ID$");
+      put("TEST2", "http://address/$SEQUENCE_ID$");
+      put("SRS", "http://theSRSlink/$SEQUENCE_ID$");
+    }
+  };
+  
+  private static final HashMap<String, String> unselUrlMap = new HashMap<String, String>()
+  {
+    {
+      put("NON1", "http://x/y/$DB_ACCESSION$");
+      put("NON2", "http://a/b/c/$DB_ACCESSION");
+    }
+  };
+
+  private static final String[] dlinks = {
+      "TEST|http://someurl.blah/$DB_ACCESSION$",
+      "ANOTHER|http://test/t$SEQUENCE_ID$",
+      "TEST2|http://address/$SEQUENCE_ID$",
+ UrlConstants.DEFAULT_STRING };
+
+  private static final String[] unselDlinks = {
+      "NON1|http://x/y/$DB_ACCESSION$", "NON2|http://a/b/c/$DB_ACCESSION" };
+
+  private static final Vector<String> displayLinks = new Vector<String>(
+          Arrays.asList(dlinks));
+
+  private static final Vector<String> unselDisplayLinks = new Vector<String>(
+          Arrays.asList(unselDlinks));
+
+  private static final String[] dlinks2 = { "a|http://x.y.z/$SEQUENCE_ID$" };
+
+  private static final Vector<String> displayLinks2 = new Vector<String>(
+          Arrays.asList(dlinks2));
+
+  private static final String[] list1 = { "a" };
+
+  private static final String[] list2 = { "http://x.y.z/$SEQUENCE_ID$" };
+
+  private static final Vector<String> names = new Vector<String>(
+          Arrays.asList(list1));
+
+  private static final Vector<String> urls = new Vector<String>(
+          Arrays.asList(list2));
+
+  /*
+   * Test default url is set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testDefaultUrl()
+  {
+    UrlProviderI customProv = new CustomUrlProvider(cachedList,
+            unselectedList);
+    
+    // default url can be set
+    assertTrue(customProv.setDefaultUrl("ANOTHER"));
+
+    // supplied replacement id must be more than 4 chars
+    String result = customProv.getDefaultUrl("123");
+    assertEquals(null, result);
+
+    // default url can be retrieved given a sequence id
+    result = customProv.getDefaultUrl("seqid");
+    assertEquals("http://test/tseqid", result);
+
+    // if there is no default url it sets the default to null
+    assertFalse(customProv.setDefaultUrl("No default"));
+    result = customProv.getDefaultUrl("testid");
+    assertEquals(null, result);
+    
+    // choosing the default picks the DEFAULT_STRING option
+    customProv.chooseDefaultUrl();
+    result = customProv.getDefaultUrl("seqid");
+    assertEquals(
+            UrlConstants.DEFAULT_STRING.split("\\|")[1].split("\\$")[0]
+            + "seqid",
+            result);
+  }
+
+  /*
+   * Test urls are set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testUrlLinks()
+  {
+    // creation from cached url list works + old links upgraded
+    UrlProviderI customProv = new CustomUrlProvider(cachedList,
+            unselectedList);
+    assertTrue(displayLinks.containsAll(customProv.getLinksForMenu()));
+
+    // creation from map works + old links upgraded
+    UrlProviderI customProv2 = new CustomUrlProvider(urlMap, unselUrlMap);
+    assertTrue(displayLinks.containsAll(customProv2.getLinksForMenu()));
+
+    // writing url links as a string works
+    // because UrlProvider does not guarantee order of links, we can't just
+    // compare the output of writeUrlsAsString to a string, hence the hoops here
+    String result = customProv.writeUrlsAsString(true);
+    UrlProviderI up = new CustomUrlProvider(result, "");
+    assertTrue(displayLinks.containsAll(up.getLinksForMenu()));
+
+    result = customProv.writeUrlsAsString(false);
+    up = new CustomUrlProvider("", result);
+    assertTrue(unselDisplayLinks.containsAll(up.getLinksForMenu()));
+
+    result = customProv2.writeUrlsAsString(true);
+    UrlProviderI up2 = new CustomUrlProvider(result, "");
+    assertTrue(displayLinks.containsAll(up2.getLinksForMenu()));
+
+    result = customProv2.writeUrlsAsString(false);
+    up2 = new CustomUrlProvider("", result);
+    assertTrue(displayLinks.containsAll(up2.getLinksForMenu()));
+  }
+}
diff --git a/test/jalview/urls/DesktopUrlProviderFactoryTest.java b/test/jalview/urls/DesktopUrlProviderFactoryTest.java
new file mode 100644 (file)
index 0000000..a114f68
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+import java.util.Vector;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class DesktopUrlProviderFactoryTest
+{
+  // Test identifiers.org download file
+  private static final String testIdOrgString = "[{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+          + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+          + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+          + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+          + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+          + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+          + "\"url\":\"http://identifiers.org/interpro\"},"
+          + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]";
+
+  @BeforeMethod(alwaysRun = true)
+  public void setup()
+  {
+    // make a dummy identifiers.org download file
+    File temp = null;
+
+    try
+    {
+      temp = File.createTempFile("tempfile", ".tmp");
+      temp.deleteOnExit();
+      BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
+      bw.write(testIdOrgString);
+      bw.close();
+    } catch (IOException e)
+    {
+      System.out
+              .println("Error initialising DesktopUrlProviderFactoryTest test: "
+                      + e.getMessage());
+    }
+
+    IdOrgSettings.setDownloadLocation(temp.getPath());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCreateUrlProvider()
+  {
+    String defaultUrlString = "Test1";
+    String defaultUrl = "http://blah.blah/$SEQUENCE_ID$";
+    String cachedUrlList = "MIR:00000005|MIR:00000011|Test1|http://blah.blah/$SEQUENCE_ID$|"
+            + "Test2|http://test2/$DB_ACCESSION$|Test3|http://test3/$SEQUENCE_ID$";
+    String userUrlList = "MIR:00000372|Test4|httpL//another.url/$SEQUENCE_ID$";
+
+    DesktopUrlProviderFactory factory = new DesktopUrlProviderFactory(
+            defaultUrlString, cachedUrlList, userUrlList);
+    UrlProviderI prov = factory.createUrlProvider();
+
+    // default url correctly set
+    Assert.assertEquals(prov.getDefaultUrlId(), "Test1");
+    Assert.assertEquals(prov.getDefaultUrl("FER_CAPAN"),
+            defaultUrl.replace("$SEQUENCE_ID$", "FER_CAPAN"));
+
+    Vector<String> menulinks = prov.getLinksForMenu();
+    List<UrlLinkDisplay> allLinks = prov.getLinksForTable();
+
+    // 8 links in provider - 4 from id file, 4 custom links
+    Assert.assertEquals(allLinks.size(), 8);
+
+    // 5 links in menu (cachedUrlList)
+    Assert.assertEquals(menulinks.size(), 5);
+
+    Assert.assertTrue(menulinks
+            .contains("Test1|http://blah.blah/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("Test2|http://test2/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks
+            .contains("Test3|http://test3/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("UniProt Knowledgebase|http://identifiers.org/uniprot/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks
+            .contains("InterPro|http://identifiers.org/interpro/$DB_ACCESSION$"));
+  }
+}
diff --git a/test/jalview/urls/IdentifiersUrlProviderTest.java b/test/jalview/urls/IdentifiersUrlProviderTest.java
new file mode 100644 (file)
index 0000000..3010ea2
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.urls.api.UrlProviderI;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Vector;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class IdentifiersUrlProviderTest
+{
+  // Test identifiers.org download file
+  private static final String testIdOrgFile = "[{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+          + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+          + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+          + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+          + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+          + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+          + "\"url\":\"http://identifiers.org/interpro\"},"
+          + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]";
+  
+  private static final String[] dlinks = {
+      "UniProt Knowledgebase|http://identifiers.org/uniprot/$DB_ACCESSION$",
+      "InterPro|http://identifiers.org/interpro/$DB_ACCESSION$",
+      "ENA|http://identifiers.org/ena.embl/$DB_ACCESSION$" };
+
+  private static final String[] dlinks1 = {
+      "MIR:00000011|http://identifiers.org/interpro/$DB_ACCESSION$",
+      "MIR:00000372|http://identifiers.org/ena.embl/$DB_ACCESSION$" };
+
+  private static final String[] dlinks2 = {
+      "MIR:00000005|http://identifiers.org/uniprot/$DB_ACCESSION$",
+      "MIR:00000011|http://identifiers.org/interpro/$DB_ACCESSION$" };
+
+  private static final String stringLinks = "MIR:00000005|http://identifiers.org/uniprot/$DB_ACCESSION$"
+          + "MIR:00000011|http://identifiers.org/interpro/$DB_ACCESSION$"
+          + "MIR:00000372|http://identifiers.org/ena.embl/$DB_ACCESSION$";
+
+  private static final String[] unselDlinks = { "ChEBI|http://identifiers.org/chebi/$DB_ACCESSION$" };
+
+  private static final Vector<String> displayLinks = new Vector<String>(
+        Arrays.asList(dlinks));
+  
+  private static final Vector<String> unselDisplayLinks = new Vector<String>(
+          Arrays.asList(unselDlinks));
+
+  private static final Vector<String> displayLinks1 = new Vector<String>(
+          Arrays.asList(dlinks1));
+
+  private static final Vector<String> displayLinks2 = new Vector<String>(
+          Arrays.asList(dlinks2));
+
+  private static final HashMap<String, String> urlMap = new HashMap<String, String>()
+  {
+    {
+      put("MIR:00000005", "http://identifiers.org/uniprot/$DB_ACCESSION$");
+      put("MIR:00000011", "http://identifiers.org/interpro/$DB_ACCESSION$");
+      put("MIR:00000372", "http://identifiers.org/ena.embl/$DB_ACCESSION$");
+    }
+  };
+
+  private String testfile = "";
+
+
+  @BeforeClass(alwaysRun = true)
+  public void setup()
+  {
+    // setup test ids in a file
+    File outFile = null;
+    try
+    {
+      outFile = File.createTempFile("testidsfile", "txt");
+      outFile.deleteOnExit();
+
+      FileWriter fw = new FileWriter(outFile);
+      fw.write(testIdOrgFile);
+      fw.close();
+
+      testfile = outFile.getAbsolutePath();
+
+    } catch (Exception ex)
+    {
+      System.err.println(ex);
+    }
+
+    IdOrgSettings.setDownloadLocation(testfile);
+  }
+
+  /*
+   * Test urls are set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testUrlLinks()
+  {
+    // creation from cached id list
+    String idList = "MIR:00000005|MIR:00000011|MIR:00000372";
+    UrlProviderI idProv = new IdentifiersUrlProvider(idList);
+    
+    assertTrue(displayLinks.containsAll(idProv.getLinksForMenu()));
+
+    // because UrlProvider does not guarantee order of links, we can't just
+    // compare the output of writeUrlsAsString to a string, hence the hoops here
+    String result = idProv.writeUrlsAsString(true);
+    UrlProviderI up = new IdentifiersUrlProvider(result);
+    assertTrue(displayLinks.containsAll(up.getLinksForMenu()));
+
+    result = idProv.writeUrlsAsString(false);
+    up = new IdentifiersUrlProvider(result);
+    assertTrue(unselDisplayLinks.containsAll(up.getLinksForMenu()));
+
+  }
+
+  /*
+   * Test default is set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testDefaultUrl()
+  {
+    // creation from cached id list
+    String idList = "MIR:00000005|MIR:00000011|MIR:00000372";
+    UrlProviderI idProv = new IdentifiersUrlProvider(idList);
+    
+    // initially no default
+    assertEquals(null, idProv.getDefaultUrl("seqid"));
+    
+    // set and then retrieve default
+    assertTrue(idProv.setDefaultUrl("MIR:00000005"));
+    assertEquals("http://identifiers.org/uniprot/seqid",
+            idProv.getDefaultUrl("seqid"));
+
+    // ids less than length 4 return null
+    assertEquals(null,
+            idProv.getDefaultUrl("123"));
+
+    // attempt to set bad default
+    assertFalse(idProv.setDefaultUrl("MIR:00001234"));
+    // default set to null (as default should have been set elsewhere)
+    assertEquals(null, idProv.getDefaultUrl("seqid"));
+
+    // chooseDefaultUrl not implemented for IdentifiersUrlProvider
+    assertEquals(null, idProv.chooseDefaultUrl());
+  }
+}
diff --git a/test/jalview/urls/UrlLinkDisplayTest.java b/test/jalview/urls/UrlLinkDisplayTest.java
new file mode 100644 (file)
index 0000000..5172304
--- /dev/null
@@ -0,0 +1,122 @@
+package jalview.urls;
+
+import jalview.util.UrlLink;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class UrlLinkDisplayTest {
+
+  @Test(groups = { "Functional" })
+  public void testDisplayColumnNames()
+  {
+    // 4 column names returned although 5 names internal to UrlLinkDisplay
+    List<String> names = UrlLinkDisplay.getDisplayColumnNames();
+    Assert.assertEquals(names.size(), 4);
+  }
+
+  @Test(groups = { "Functional" })
+  public void getValue()
+  {
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.DEFAULT));
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.ID), "Test");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.NAME), "Test Url");
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.SELECTED));
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.URL),
+            "http://identifiers.org/$DB_ACCESSION$");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testIsEditable()
+  {
+    // only default and selected columns are editable ever
+    // default only editable if link contains $SEQUENCE_ID$
+
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.DEFAULT));
+    Assert.assertTrue(u.isEditable(UrlLinkDisplay.SELECTED));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.ID));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.URL));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.NAME));
+
+    UrlLink vlink = new UrlLink("Test Sequence ID Url",
+            "http://myurl/$SEQUENCE_ID$");
+    UrlLinkDisplay v = new UrlLinkDisplay("Test", vlink, false, false);
+
+    Assert.assertTrue(v.isEditable(UrlLinkDisplay.DEFAULT));
+    Assert.assertTrue(v.isEditable(UrlLinkDisplay.SELECTED));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.ID));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.URL));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.NAME));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testName()
+  {
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    // Name initially as input in link
+    Assert.assertEquals(u.getName(), "Test Url");
+
+    // Setting updates name
+    u.setName("New Name");
+    Assert.assertEquals(u.getName(), "New Name");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testUrl()
+  {
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    // Url initially as input in link
+    Assert.assertEquals(u.getUrl(), "http://identifiers.org/$DB_ACCESSION$");
+
+    // Setting updates url
+    u.setUrl("http://something.new/$SEQUENCE_ID$");
+    Assert.assertEquals(u.getUrl(), "http://something.new/$SEQUENCE_ID$");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetSetValue()
+  {
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.DEFAULT));
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.SELECTED));
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.NAME), "Test Url");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.ID), "Test");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.URL),
+            "http://identifiers.org/$DB_ACCESSION$");
+
+    u.setValue(UrlLinkDisplay.DEFAULT, true);
+    Assert.assertTrue((boolean) u.getValue(UrlLinkDisplay.DEFAULT));
+
+    u.setValue(UrlLinkDisplay.SELECTED, true);
+    Assert.assertTrue((boolean) u.getValue(UrlLinkDisplay.SELECTED));
+
+    u.setValue(UrlLinkDisplay.NAME, "New Name");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.NAME), "New Name");
+
+    u.setValue(UrlLinkDisplay.ID, "New ID");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.ID), "New ID");
+
+    u.setValue(UrlLinkDisplay.URL, "http://something.new/$SEQUENCE_ID$");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.URL),
+            "http://something.new/$SEQUENCE_ID$");
+  }
+}
diff --git a/test/jalview/urls/UrlLinkTableModelTest.java b/test/jalview/urls/UrlLinkTableModelTest.java
new file mode 100644 (file)
index 0000000..141c8fa
--- /dev/null
@@ -0,0 +1,332 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+
+package jalview.urls;
+
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
+import static jalview.util.UrlConstants.SEQUENCE_ID;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.util.MessageManager;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.TableModelListener;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class UrlLinkTableModelTest {
+
+  private static final String inmenu = "TEST|http://someurl.blah/$DB_ACCESSION$|"
+          + "ANOTHER|http://test/t$SEQUENCE_ID$|"
+          + "TEST2|http://address/$SEQUENCE_ID$|SRS|"
+          + "http://theSRSlink/$SEQUENCE_ID$|"
+          + "MIR:00000005|MIR:00000011|MIR:00000372";
+
+  private static final String notinmenu = "Not1|http://not.in.menu/$DB_ACCESSION$|"
+          + "Not2|http://not.in.menu.either/$DB_ACCESSION$";
+
+  // Test identifiers.org download file
+  private static final String testIdOrgString = "[{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+          + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+          + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+          + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+          + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+          + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+          + "\"url\":\"http://identifiers.org/interpro\"},"
+          + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]";
+
+  private UrlProviderI prov;
+
+  @BeforeMethod(alwaysRun = true)
+  public void setup()
+  {
+    // set up UrlProvider data as the source for the TableModel
+    // the data gets updated by the TableModel, so needs to be reinitialised for
+    // each test
+
+    // make a dummy identifiers.org download file
+    File temp = null;
+    try
+    {
+      temp = File.createTempFile("tempfile", ".tmp");
+      temp.deleteOnExit();
+      BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
+      bw.write(testIdOrgString);
+      bw.close();
+    } catch (IOException e)
+    {
+      System.out.println("Error initialising UrlLinkTableModel test: "
+              + e.getMessage());
+    }
+
+    // set up custom and identifiers.org url providers
+    IdOrgSettings.setDownloadLocation(temp.getPath());
+    IdentifiersUrlProvider idprov = new IdentifiersUrlProvider(inmenu);
+    CustomUrlProvider cprov = new CustomUrlProvider(inmenu, notinmenu);
+    List<UrlProviderI> provlist = new ArrayList<UrlProviderI>();
+    provlist.add(idprov);
+    provlist.add(cprov);
+
+    prov = new UrlProvider("TEST2", provlist);
+  }
+
+  /*
+   * Test that the table model is correctly initialised
+   * Display columns and default row are set; data provider listening event set up
+   */
+  @Test(groups = { "Functional" })
+  public void testInitialisation()
+  {
+    int defaultCol = 3;
+    int nameCol = 0;
+
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // exactly one table model listener
+    TableModelListener[] listeners = m
+            .getListeners(TableModelListener.class);
+    Assert.assertEquals(listeners.length, 1);
+
+    // default row exists, there is exactly 1, and it matches the supplied
+    // default
+    int count = 0;
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      boolean isDefault = (boolean) m.getValueAt(row, defaultCol);
+      if (isDefault)
+      {
+        count++;
+        String defaultName = (String) m.getValueAt(row, nameCol);
+        Assert.assertEquals(defaultName, "TEST2");
+      }
+    }
+    Assert.assertEquals(count, 1);
+  }
+
+  /*
+   * Test row and column counts
+   */
+  @Test(groups = { "Functional" })
+  public void testCounts()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // correct numbers of column and rows
+    Assert.assertEquals(m.getColumnCount(), 4);
+    Assert.assertEquals(m.getRowCount(), 10);
+  }
+
+  /*
+   * Test column access
+   */
+  @Test(groups = { "Functional" })
+  public void testColumns()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // check column names
+    Assert.assertEquals(m.getColumnName(0),
+            MessageManager.formatMessage("label.name"));
+    Assert.assertEquals(m.getColumnName(1),
+            MessageManager.formatMessage("label.url"));
+    Assert.assertEquals(m.getColumnName(2),
+            MessageManager.formatMessage("label.inmenu"));
+    Assert.assertEquals(m.getColumnName(3),
+            MessageManager.formatMessage("label.default"));
+
+    // check column classes
+    Assert.assertEquals(m.getColumnClass(0), String.class);
+    Assert.assertEquals(m.getColumnClass(1), String.class);
+    Assert.assertEquals(m.getColumnClass(2), Boolean.class);
+    Assert.assertEquals(m.getColumnClass(3), Boolean.class);
+  }
+
+  /*
+   * Test row insertion
+   */
+  @Test(groups = { "Functional" })
+  public void testRowInsert()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    m.insertRow("newname", "newurl");
+
+    // check table has new row inserted
+    Assert.assertEquals(m.getValueAt(10, 0), "newname");
+    Assert.assertEquals(m.getValueAt(10, 1), "newurl");
+    Assert.assertEquals(m.getValueAt(10, 2), true);
+    Assert.assertEquals(m.getValueAt(10, 3), false);
+
+    // check data source has new row insrte
+    Assert.assertTrue(prov.getLinksForMenu().contains(
+            "newname" + SEP + "newurl"));
+  }
+
+  /*
+   * Test row deletion
+   */
+  @Test(groups = { "Functional" })
+  public void testRowDelete()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // get name and url at row 0
+    String name = (String) m.getValueAt(0, 0);
+    String url = (String) m.getValueAt(0, 1);
+
+    m.removeRow(0);
+
+    // check table no longer has row 0 elements in it
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      Assert.assertNotEquals(m.getValueAt(row, 0), name);
+    }
+
+    // check data source likewise
+    Assert.assertFalse(prov.getLinksForMenu().contains(name + SEP + url));
+  }
+
+  /*
+   * Test value setting and getting
+   */
+  @Test(groups = { "Functional" })
+  public void testValues()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // get original default
+    int olddefault;
+    boolean isDefault = false;
+    for (olddefault = 0; olddefault < m.getRowCount() && !isDefault; olddefault++)
+    {
+      isDefault = (boolean) m.getValueAt(olddefault, 3);
+    }
+
+    // set new values, one in each row
+    m.setValueAt("namechanged", 6, 0);
+    m.setValueAt("urlchanged", 7, 1);
+    m.setValueAt(false, 8, 2);
+    m.setValueAt(true, 6, 3);
+
+    // check values updated in table
+    Assert.assertEquals(m.getValueAt(6, 0), "namechanged");
+    Assert.assertEquals(m.getValueAt(7, 1), "urlchanged");
+    Assert.assertFalse((boolean) m.getValueAt(8, 2));
+    Assert.assertTrue((boolean) m.getValueAt(6, 3));
+    Assert.assertFalse((boolean) m.getValueAt(olddefault, 3));
+
+    // check default row is exactly one row still
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      isDefault = (boolean) m.getValueAt(row, 3);
+
+      // if isDefault is true, row is 9
+      // if isDefault is false, row is not 9
+      Assert.assertFalse(isDefault && !(row == 6));
+    }
+
+    // check table updated
+    Assert.assertTrue(prov.writeUrlsAsString(true).contains("namechanged" +SEP + m.getValueAt(6, 1)));
+    Assert.assertTrue(prov.writeUrlsAsString(true).contains(m.getValueAt(7,0) + SEP + "urlchanged"));
+    Assert.assertTrue(prov.writeUrlsAsString(false).contains(
+            (String) m.getValueAt(8, 0)));
+    Assert.assertEquals(prov.getDefaultUrl("seqid"), m.getValueAt(6, 1)
+            .toString().replace(DELIM + SEQUENCE_ID + DELIM, "seqid"));
+
+  }
+
+  /*
+   * Test cell editability
+   */
+  @Test(groups = { "Functional" })
+  public void testEditable()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      Assert.assertFalse(m.isCellEditable(row, 0));
+      Assert.assertFalse(m.isCellEditable(row, 1));
+      Assert.assertTrue(m.isCellEditable(row, 2));
+
+      if ((row == 4) || (row == 6) || (row == 7))
+      {
+        Assert.assertTrue(m.isCellEditable(row, 3));
+      }
+      else
+      {
+        Assert.assertFalse(m.isCellEditable(row, 3));
+      }
+    }
+  }
+
+  /*
+   * Test row 'deletability'
+   */
+  @Test(groups = { "Functional" })
+  public void testDeletable()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      if (row > 4)
+      {
+        Assert.assertTrue(m.isRowDeletable(row));
+      }
+      else
+      {
+        Assert.assertFalse(m.isRowDeletable(row));
+      }
+    }
+  }
+
+  /*
+   * Test indirect row editability
+   */
+  @Test(groups = { "Functional" })
+  public void testRowEditable()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      if (row > 3)
+      {
+        Assert.assertTrue(m.isRowEditable(row));
+      }
+      else
+      {
+        Assert.assertFalse(m.isRowEditable(row));
+      }
+    }
+  }
+}
diff --git a/test/jalview/urls/UrlProviderTest.java b/test/jalview/urls/UrlProviderTest.java
new file mode 100644 (file)
index 0000000..3b52c03
--- /dev/null
@@ -0,0 +1,121 @@
+package jalview.urls;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
+import jalview.util.UrlConstants;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+import java.util.Vector;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+
+public class UrlProviderTest {
+  
+  // Test identifiers.org download file
+  private static final String testIdOrgString = "[{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+         + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+         + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+         + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+         + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+         + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+         + "\"url\":\"http://identifiers.org/interpro\"},"
+         + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+         + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]";
+
+  private UrlProviderI prov;
+
+  @BeforeMethod(alwaysRun = true)
+  public void setup()
+  {
+   // make a dummy identifiers.org download file
+   File temp = null;
+
+   try
+   {
+     temp = File.createTempFile("tempfile", ".tmp");
+     temp.deleteOnExit();
+     BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
+     bw.write(testIdOrgString);
+     bw.close();
+   } catch (IOException e)
+   {
+      System.out.println("Error initialising UrlProviderTest test: "
+              + e.getMessage());
+   }
+
+   IdOrgSettings.setDownloadLocation(temp.getPath());
+
+    String defaultUrlString = "No default";
+    String cachedUrlList = "MIR:00000005|MIR:00000011|Test1|http://blah.blah/$SEQUENCE_ID$|"
+            + "Test2|http://test2/$DB_ACCESSION$|Test3|http://test3/$SEQUENCE_ID$";
+    String userUrlList = "MIR:00000372|Test4|httpL//another.url/$SEQUENCE_ID$";
+
+    DesktopUrlProviderFactory factory = new DesktopUrlProviderFactory(
+            defaultUrlString, cachedUrlList, userUrlList);
+    prov = factory.createUrlProvider();
+ }
+
+  @Test(groups = { "Functional" })
+  public void testInitUrlProvider()
+  {
+    String emblUrl = UrlConstants.DEFAULT_STRING.substring(
+            UrlConstants.DEFAULT_STRING.indexOf(UrlConstants.SEP) + 1,
+            UrlConstants.DEFAULT_STRING.length());
+
+    // chooses EMBL url when default Url id does not exist in provided url lists
+    Assert.assertEquals(prov.getDefaultUrlId(), UrlConstants.DEFAULT_LABEL);
+    Assert.assertEquals(prov.getDefaultUrl("FER_CAPAN"),
+            emblUrl.replace("$SEQUENCE_ID$", "FER_CAPAN"));
+
+    Vector<String> menulinks = prov.getLinksForMenu();
+    List<UrlLinkDisplay> allLinks = prov.getLinksForTable();
+
+    // 9 links in provider - 4 from id file, 4 custom links, 1 additional
+    // default
+    Assert.assertEquals(allLinks.size(), 9);
+
+    // 6 links in menu (cachedUrlList) + new default
+    Assert.assertEquals(menulinks.size(), 6);
+
+    Assert.assertTrue(menulinks
+            .contains("Test1|http://blah.blah/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("Test2|http://test2/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks
+            .contains("Test3|http://test3/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("UniProt Knowledgebase|http://identifiers.org/uniprot/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks
+            .contains("InterPro|http://identifiers.org/interpro/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks.contains(UrlConstants.DEFAULT_LABEL
+            + UrlConstants.SEP + emblUrl));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetDefaultUrl()
+  {
+    // set custom url as default
+    Assert.assertTrue(prov.setDefaultUrl("Test1"));
+    Assert.assertEquals(prov.getDefaultUrlId(), "Test1");
+
+    // set identifiers url as default
+    Assert.assertTrue(prov.setDefaultUrl("MIR:00000011"));
+    Assert.assertEquals(prov.getDefaultUrlId(), "MIR:00000011");
+  }
+
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testSetDefaultUrlWrongly()
+  {
+    // don't allow default to be a non-key
+    prov.setDefaultUrl("not-a-key");
+  }
+}
diff --git a/test/jalview/ws/UrlDownloadClientTest.java b/test/jalview/ws/UrlDownloadClientTest.java
new file mode 100644 (file)
index 0000000..90470f5
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.ws;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class UrlDownloadClientTest {
+
+  /**
+   * Test that url is successfully loaded into download file
+   */
+  @Test(groups = { "Network" }, enabled = true)
+  public void UrlDownloadTest()
+  {
+    UrlDownloadClient client = new UrlDownloadClient();
+    String urlstring = "http://identifiers.org/rest/collections/";
+    String outfile = "testfile.tmp";
+
+    try
+    {
+      client.download(urlstring, outfile);
+    } catch (IOException e)
+    {
+      Assert.fail("Exception was thrown from UrlDownloadClient download: "
+              + e.getMessage());
+      File f = new File(outfile);
+      if (f.exists())
+      {
+        f.delete();
+      }
+    }
+
+    // download file exists
+    File f = new File(outfile);
+    Assert.assertTrue(f.exists());
+
+    // download file has a believable size
+    // identifiers.org file typically at least 250K
+    Assert.assertTrue(f.length() > 250000);
+
+    if (f.exists())
+    {
+      f.delete();
+    }
+
+  }
+
+  /**
+   * Test that garbage in results in IOException
+   */
+  @Test(
+    groups = { "Network" },
+    enabled = true,
+    expectedExceptions = { IOException.class })
+  public void DownloadGarbageUrlTest() throws IOException
+  {
+    UrlDownloadClient client = new UrlDownloadClient();
+    String urlstring = "identifiers.org/rest/collections/";
+    String outfile = "testfile.tmp";
+
+    client.download(urlstring, outfile);
+  }
+}