JAL-2353 primary ref candidates that should be promoted should preserve original...
[jalview.git] / src / jalview / ws / dbsources / Uniprot.java
index 48f8649..c9db7f2 100644 (file)
@@ -1,56 +1,87 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
- * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
+ * 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.
+ * 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/>.
+ * 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.dbsources;
 
-import java.io.File;
-import java.io.FileReader;
-import java.util.Enumeration;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
 import java.util.Vector;
 
-import org.exolab.castor.xml.Unmarshaller;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
 
 import com.stevesoft.pat.Regex;
 
+import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.PDBEntry;
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.UniprotEntry;
-import jalview.datamodel.UniprotFile;
-import jalview.ws.ebi.EBIFetchClient;
-import jalview.ws.seqfetcher.DbSourceProxy;
+import jalview.schemes.ResidueProperties;
+import jalview.util.StringUtils;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
+import jalview.xml.binding.uniprot.DbReferenceType;
+import jalview.xml.binding.uniprot.Entry;
+import jalview.xml.binding.uniprot.FeatureType;
+import jalview.xml.binding.uniprot.LocationType;
+import jalview.xml.binding.uniprot.PositionType;
+import jalview.xml.binding.uniprot.PropertyType;
 
 /**
+ * This class queries the Uniprot database for sequence data, unmarshals the
+ * returned XML, and converts it to Jalview Sequence records (including attached
+ * database references and sequence features)
+ * 
  * @author JimP
  * 
  */
-public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
+public class Uniprot extends DbSourceProxyImpl
 {
+  private static final String DEFAULT_UNIPROT_DOMAIN = "https://www.uniprot.org";
+
+  private static final String BAR_DELIMITER = "|";
+
+  /**
+   * Constructor
+   */
   public Uniprot()
   {
     super();
-    addDbSourceProperty(DBRefSource.SEQDB, DBRefSource.SEQDB);
-    addDbSourceProperty(DBRefSource.PROTSEQDB);
-    // addDbSourceProperty(DBRefSource.MULTIACC, new Integer(50));
+  }
+
+  private String getDomain()
+  {
+    return Cache.getDefault("UNIPROT_DOMAIN", DEFAULT_UNIPROT_DOMAIN);
   }
 
   /*
@@ -58,9 +89,10 @@ public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
    * 
    * @see jalview.ws.DbSourceProxy#getAccessionSeparator()
    */
+  @Override
   public String getAccessionSeparator()
   {
-    return null; // ";";
+    return null;
   }
 
   /*
@@ -68,6 +100,7 @@ public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
    * 
    * @see jalview.ws.DbSourceProxy#getAccessionValidator()
    */
+  @Override
   public Regex getAccessionValidator()
   {
     return new Regex("([A-Z]+[0-9]+[A-Z0-9]+|[A-Z0-9]+_[A-Z0-9]+)");
@@ -78,6 +111,7 @@ public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
    * 
    * @see jalview.ws.DbSourceProxy#getDbSource()
    */
+  @Override
   public String getDbSource()
   {
     return DBRefSource.UNIPROT;
@@ -88,186 +122,353 @@ public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
    * 
    * @see jalview.ws.DbSourceProxy#getDbVersion()
    */
+  @Override
   public String getDbVersion()
   {
     return "0"; // we really don't know what version we're on.
   }
 
-  private EBIFetchClient ebi = null;
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.ws.DbSourceProxy#getSequenceRecords(java.lang.String[])
+   */
+  @Override
+  public AlignmentI getSequenceRecords(String queries) throws Exception
+  {
+    startQuery();
+    try
+    {
+      queries = queries.toUpperCase(Locale.ROOT).replaceAll(
+              "(UNIPROT\\|?|UNIPROT_|UNIREF\\d+_|UNIREF\\d+\\|?)", "");
+      AlignmentI al = null;
 
-  private static org.exolab.castor.mapping.Mapping map;
+      String downloadstring = getDomain() + "/uniprot/" + queries + ".xml";
 
-  public Vector getUniprotEntries(File file)
+      URL url = new URL(downloadstring);
+      HttpURLConnection urlconn = (HttpURLConnection) url.openConnection();
+      // anything other than 200 means we don't have data
+      // TODO: JAL-3882 reuse the EnsemblRestClient's fair
+      // use/backoff logic to retry when the server tells us to go away
+      if (urlconn.getResponseCode() == 200)
+      {
+        InputStream istr = urlconn.getInputStream();
+        List<Entry> entries = getUniprotEntries(istr);
+        if (entries != null)
+        {
+          List<SequenceI> seqs = new ArrayList<>();
+          for (Entry entry : entries)
+          {
+            seqs.add(uniprotEntryToSequence(entry));
+          }
+          al = new Alignment(seqs.toArray(new SequenceI[seqs.size()]));
+        }
+      }
+      stopQuery();
+      return al;
+
+    } catch (Exception e)
+    {
+      throw (e);
+    } finally
+    {
+      stopQuery();
+    }
+  }
+
+  /**
+   * Converts an Entry object (bound from Uniprot XML) to a Jalview Sequence
+   * 
+   * @param entry
+   * @return
+   */
+  SequenceI uniprotEntryToSequence(Entry entry)
   {
-    UniprotFile uni = new UniprotFile();
-    try
+    String id = getUniprotEntryId(entry);
+    /*
+     * Sequence should not include any whitespace, but JAXB leaves these in
+     */
+    String seqString = entry.getSequence().getValue().replaceAll("\\s*",
+            "");
+
+    SequenceI sequence = new Sequence(id, seqString);
+    sequence.setDescription(getUniprotEntryDescription(entry));
+    final String uniprotRecordVersion = "" + entry.getVersion();
+    /*
+     * add a 'self' DBRefEntry for each accession
+     */
+    final String dbVersion = getDbVersion();
+    List<DBRefEntry> dbRefs = new ArrayList<>();
+    boolean canonical = true;
+    for (String accessionId : entry.getAccession())
+    {
+      DBRefEntry dbRef = new DBRefEntry(DBRefSource.UNIPROT,
+              uniprotRecordVersion, accessionId, null, canonical);
+      canonical = false;
+      dbRefs.add(dbRef);
+    }
+
+    /*
+     * add a DBRefEntry for each dbReference element in the XML;
+     * also add a PDBEntry if type="PDB";
+     * also add an EMBLCDS dbref if protein sequence id is given
+     * also add an Ensembl dbref " " " " " "
+     */
+    Vector<PDBEntry> pdbRefs = new Vector<>();
+    for (DbReferenceType dbref : entry.getDbReference())
     {
-      if (map == null)
+      String type = dbref.getType();
+      DBRefEntry dbr = new DBRefEntry(type,
+              DBRefSource.UNIPROT + ":" + dbVersion, dbref.getId());
+      dbRefs.add(dbr);
+      if ("PDB".equals(type))
       {
-        // 1. Load the mapping information from the file
-        map = new org.exolab.castor.mapping.Mapping(uni.getClass()
-                .getClassLoader());
-        java.net.URL url = getClass().getResource("/uniprot_mapping.xml");
-        map.loadMapping(url);
+        pdbRefs.add(new PDBEntry(dbr));
+      }
+      if ("EMBL".equals(type))
+      {
+        /*
+         * e.g. Uniprot accession Q9BXM7 has
+         * <dbReference type="EMBL" id="M19359">
+         *   <property type="protein sequence ID" value="AAA40981.1"/>
+         *   <property type="molecule type" value="Genomic_DNA"/>
+         * </dbReference> 
+         */
+        String cdsId = getProperty(dbref.getProperty(),
+                "protein sequence ID");
+        if (cdsId != null && cdsId.trim().length() > 0)
+        {
+          // remove version
+          String[] vrs = cdsId.split("\\.");
+          String version = vrs.length > 1 ? vrs[1]
+                  : DBRefSource.UNIPROT + ":" + uniprotRecordVersion;
+          dbr = new DBRefEntry(DBRefSource.EMBLCDS, version, vrs[0]);
+          // TODO: process VARIANT features to allow EMBLCDS record's product to
+          // match Uniprot
+          dbr.setCanonical(true);
+          dbRefs.add(dbr);
+        }
+      }
+      if (type != null
+              && type.toLowerCase(Locale.ROOT).startsWith("ensembl"))
+      {
+        // remove version
+        String[] vrs = dbref.getId().split("\\.");
+        String version = vrs.length > 1 ? vrs[1]
+                : DBRefSource.UNIPROT + ":" + uniprotRecordVersion;
+        dbr.setAccessionId(vrs[0]);
+        dbr.setVersion(version);
+        /*
+         * e.g. Uniprot accession Q9BXM7 has
+         * <dbReference type="Ensembl" id="ENST00000321556">
+         *   <molecule id="Q9BXM7-1"/>
+         *   <property type="protein sequence ID" value="ENSP00000364204"/>
+         *   <property type="gene ID" value="ENSG00000158828"/>
+         * </dbReference> 
+         */
+        String cdsId = getProperty(dbref.getProperty(),
+                "protein sequence ID");
+        if (cdsId != null && cdsId.trim().length() > 0)
+        {
+          // remove version
+          String[] cdsVrs = cdsId.split("\\.");
+          String cdsVersion = cdsVrs.length > 1 ? cdsVrs[1]
+                  : DBRefSource.UNIPROT + ":" + uniprotRecordVersion;
+          dbr = new DBRefEntry(DBRefSource.ENSEMBL,
+                  DBRefSource.UNIPROT + ":" + cdsVersion, cdsVrs[0]);
+          dbRefs.add(dbr);
+        }
       }
+    }
 
-      // 2. Unmarshal the data
-      Unmarshaller unmar = new Unmarshaller(uni);
-      unmar.setIgnoreExtraElements(true);
-      unmar.setMapping(map);
-      if (file != null)
+    /*
+     * create features; they have either begin and end, or position, in XML
+     */
+    sequence.setPDBId(pdbRefs);
+    if (entry.getFeature() != null)
+    {
+      for (FeatureType uf : entry.getFeature())
       {
-        uni = (UniprotFile) unmar.unmarshal(new FileReader(file));
+        LocationType location = uf.getLocation();
+        int start = 0;
+        int end = 0;
+        if (location.getPosition() != null)
+        {
+          start = location.getPosition().getPosition().intValue();
+          end = start;
+        }
+        else
+        {
+          start = location.getBegin().getPosition().intValue();
+          end = location.getEnd().getPosition().intValue();
+        }
+        SequenceFeature sf = new SequenceFeature(uf.getType(),
+                getDescription(uf), start, end, "Uniprot");
+        sf.setStatus(uf.getStatus());
+        sequence.addSequenceFeature(sf);
       }
-    } catch (Exception e)
+    }
+    for (DBRefEntry dbr : dbRefs)
     {
-      System.out.println("Error getUniprotEntries() " + e);
+      sequence.addDBRef(dbr);
     }
-
-    return uni.getUniprotEntries();
+    return sequence;
   }
 
-  /*
-   * (non-Javadoc)
+  /**
+   * A helper method that builds a sequence feature description
    * 
-   * @see jalview.ws.DbSourceProxy#getSequenceRecords(java.lang.String[])
+   * @param feature
+   * @return
    */
-  public AlignmentI getSequenceRecords(String queries) throws Exception
+  static String getDescription(FeatureType feature)
   {
-    startQuery();
-    try
+    String orig = feature.getOriginal();
+    List<String> variants = feature.getVariation();
+    StringBuilder sb = new StringBuilder();
+
+    /*
+     * append variant in standard format if present
+     * e.g. p.Arg59Lys
+     * multiple variants are split over lines using <br>
+     */
+    boolean asHtml = false;
+    if (orig != null && !orig.isEmpty() && variants != null
+            && !variants.isEmpty())
     {
-      queries = queries.toUpperCase().replaceAll(
-              "(UNIPROT\\|?|UNIPROT_|UNIREF\\d+_|UNIREF\\d+\\|?)", "");
-      Alignment al = null;
-      ebi = new EBIFetchClient();
-      StringBuffer result = new StringBuffer();
-      // uniprotxml parameter required since december 2007
-      // uniprotkb dbname changed introduced december 2008
-      File file = ebi.fetchDataAsFile("uniprotkb:" + queries, "uniprotxml",
-              null);
-      Vector entries = getUniprotEntries(file);
-
-      if (entries != null)
+      int p = 0;
+      for (String var : variants)
       {
-        // First, make the new sequences
-        Enumeration en = entries.elements();
-        while (en.hasMoreElements())
+        // TODO proper HGVS nomenclature for delins structural variations
+        // http://varnomen.hgvs.org/recommendations/protein/variant/delins/
+        // for now we are pragmatic - any orig/variant sequence longer than
+        // three characters is shown with single-character notation rather than
+        // three-letter notation
+        sb.append("p.");
+        if (orig.length() < 4)
         {
-          UniprotEntry entry = (UniprotEntry) en.nextElement();
-
-          StringBuffer name = new StringBuffer(">UniProt/Swiss-Prot");
-          Enumeration en2 = entry.getAccession().elements();
-          while (en2.hasMoreElements())
-          {
-            name.append("|");
-            name.append(en2.nextElement());
-          }
-          en2 = entry.getName().elements();
-          while (en2.hasMoreElements())
+          for (int c = 0, clen = orig.length(); c < clen; c++)
           {
-            name.append("|");
-            name.append(en2.nextElement());
+            char origchar = orig.charAt(c);
+            String orig3 = ResidueProperties.aa2Triplet.get("" + origchar);
+            sb.append(orig3 == null ? origchar
+                    : StringUtils.toSentenceCase(orig3));
           }
+        }
+        else
+        {
+          sb.append(orig);
+        }
 
-          if (entry.getProtein() != null
-                  && entry.getProtein().getName() != null)
-          {
-            for (int nm = 0, nmSize = entry.getProtein().getName().size(); nm < nmSize; nm++)
-            {
-              name.append(" " + entry.getProtein().getName().elementAt(nm));
-            }
-          }
+        LocationType location = feature.getLocation();
+        PositionType start = location.getPosition() == null
+                ? location.getBegin()
+                : location.getPosition();
+        sb.append(Integer.toString(start.getPosition().intValue()));
 
-          result.append(name + "\n"
-                  + entry.getUniprotSequence().getContent() + "\n");
+        if (var.length() < 4)
+        {
+          for (int c = 0, clen = var.length(); c < clen; c++)
+          {
+            char varchar = var.charAt(c);
+            String var3 = ResidueProperties.aa2Triplet.get("" + varchar);
 
+            sb.append(var3 != null ? StringUtils.toSentenceCase(var3)
+                    : "" + varchar);
+          }
         }
-
-        // Then read in the features and apply them to the dataset
-        al = parseResult(result.toString());
-        if (al != null)
+        else
+        {
+          sb.append(var);
+        }
+        if (++p != variants.size())
         {
-          // Decorate the alignment with database entries.
-          addUniprotXrefs(al, entries);
+          sb.append("<br/>&nbsp;&nbsp;");
+          asHtml = true;
         }
         else
         {
-          results = result;
+          sb.append(" ");
         }
       }
-      stopQuery();
-      return al;
-    } catch (Exception e)
+    }
+    String description = feature.getDescription();
+    if (description != null)
     {
-      stopQuery();
-      throw (e);
+      sb.append(description);
+    }
+    if (asHtml)
+    {
+      sb.insert(0, "<html>");
+      sb.append("</html>");
     }
+
+    return sb.toString();
   }
 
   /**
-   * add an ordered set of UniprotEntry objects to an ordered set of seuqences.
+   * A helper method that searches the list of properties for one with the given
+   * key, and if found returns the property value, else returns null
    * 
-   * @param al
-   *          - a sequence of n sequences
-   * @param entries
-   *          a seuqence of n uniprot entries to be analysed.
+   * @param properties
+   * @param key
+   * @return
    */
-  public void addUniprotXrefs(Alignment al, Vector entries)
+  static String getProperty(List<PropertyType> properties, String key)
   {
-    for (int i = 0; i < entries.size(); i++)
+    String value = null;
+    if (properties != null)
     {
-      UniprotEntry entry = (UniprotEntry) entries.elementAt(i);
-      Enumeration e = entry.getDbReference().elements();
-      Vector onlyPdbEntries = new Vector();
-      Vector dbxrefs = new Vector();
-      while (e.hasMoreElements())
+      for (PropertyType prop : properties)
       {
-        PDBEntry pdb = (PDBEntry) e.nextElement();
-        DBRefEntry dbr = new DBRefEntry();
-        dbr.setSource(pdb.getType());
-        dbr.setAccessionId(pdb.getId());
-        dbr.setVersion(DBRefSource.UNIPROT + ":" + getDbVersion());
-        dbxrefs.addElement(dbr);
-        if (!pdb.getType().equals("PDB"))
+        if (key.equals(prop.getType()))
         {
-          continue;
+          value = prop.getValue();
+          break;
         }
-
-        onlyPdbEntries.addElement(pdb);
-      }
-      SequenceI sq = al.getSequenceAt(i);
-      while (sq.getDatasetSequence() != null)
-      {
-        sq = sq.getDatasetSequence();
       }
+    }
+    return value;
+  }
 
-      Enumeration en2 = entry.getAccession().elements();
-      while (en2.hasMoreElements())
-      {
-        // we always add as uniprot if we retrieved from uniprot or uniprot name
-        sq.addDBRef(new DBRefEntry(DBRefSource.UNIPROT, getDbVersion(), en2
-                .nextElement().toString()));
-      }
-      en2 = dbxrefs.elements();
-      while (en2.hasMoreElements())
-      {
-        // we always add as uniprot if we retrieved from uniprot or uniprot name
-        sq.addDBRef((DBRefEntry) en2.nextElement());
+  /**
+   * Extracts xml element entry/protein/recommendedName/fullName
+   * 
+   * @param entry
+   * @return
+   */
+  static String getUniprotEntryDescription(Entry entry)
+  {
+    String desc = "";
+    if (entry.getProtein() != null
+            && entry.getProtein().getRecommendedName() != null)
+    {
+      // fullName is mandatory if recommendedName is present
+      desc = entry.getProtein().getRecommendedName().getFullName()
+              .getValue();
+    }
+    return desc;
+  }
 
-      }
-      sq.setPDBId(onlyPdbEntries);
-      if (entry.getFeature() != null)
+  /**
+   * Constructs a sequence id by concatenating all entry/name elements with '|'
+   * separator
+   * 
+   * @param entry
+   * @return
+   */
+  static String getUniprotEntryId(Entry entry)
+  {
+    StringBuilder name = new StringBuilder(32);
+    for (String n : entry.getName())
+    {
+      if (name.length() > 0)
       {
-        e = entry.getFeature().elements();
-        while (e.hasMoreElements())
-        {
-          SequenceFeature sf = (SequenceFeature) e.nextElement();
-          sf.setFeatureGroup("Uniprot");
-          sq.addSequenceFeature(sf);
-        }
+        name.append(BAR_DELIMITER);
       }
+      name.append(n);
     }
+    return name.toString();
   }
 
   /*
@@ -275,6 +476,7 @@ public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
    * 
    * @see jalview.ws.DbSourceProxy#isValidReference(java.lang.String)
    */
+  @Override
   public boolean isValidReference(String accession)
   {
     // TODO: make the following a standard validator
@@ -285,13 +487,65 @@ public class Uniprot extends DbSourceProxyImpl implements DbSourceProxy
   /**
    * return LDHA_CHICK uniprot entry
    */
+  @Override
   public String getTestQuery()
   {
     return "P00340";
   }
 
+  @Override
   public String getDbName()
   {
     return "Uniprot"; // getDbSource();
   }
+
+  @Override
+  public int getTier()
+  {
+    return 0;
+  }
+
+  /**
+   * Reads the reply to the EBI Fetch Uniprot data query, unmarshals it to an
+   * Uniprot object, and returns the enclosed Entry objects, or null on any
+   * failure
+   * 
+   * @param is
+   * @return
+   */
+  public List<Entry> getUniprotEntries(InputStream is)
+  {
+    List<Entry> entries = null;
+    try
+    {
+      JAXBContext jc = JAXBContext
+              .newInstance("jalview.xml.binding.uniprot");
+      XMLStreamReader streamReader = XMLInputFactory.newInstance()
+              .createXMLStreamReader(is);
+      javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
+      JAXBElement<jalview.xml.binding.uniprot.Uniprot> uniprotElement = um
+              .unmarshal(streamReader,
+                      jalview.xml.binding.uniprot.Uniprot.class);
+      jalview.xml.binding.uniprot.Uniprot uniprot = uniprotElement
+              .getValue();
+
+      if (uniprot != null && !uniprot.getEntry().isEmpty())
+      {
+        entries = uniprot.getEntry();
+      }
+    } catch (JAXBException | XMLStreamException
+            | FactoryConfigurationError e)
+    {
+      if (e instanceof javax.xml.bind.UnmarshalException
+              && e.getCause() != null
+              && e.getCause() instanceof XMLStreamException
+              && e.getCause().getMessage().contains("[row,col]:[1,1]"))
+      {
+        // trying to parse an empty stream
+        return null;
+      }
+      e.printStackTrace();
+    }
+    return entries;
+  }
 }