JAL-4059 Added some more Platform.addJ2SDirectDatabaseCall web service URLs in static...
[jalview.git] / src / jalview / ws / dbsources / Uniprot.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.dbsources;
22
23 import java.io.InputStream;
24 import java.net.HttpURLConnection;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Vector;
30
31 import javax.xml.bind.JAXBContext;
32 import javax.xml.bind.JAXBElement;
33 import javax.xml.bind.JAXBException;
34 import javax.xml.stream.FactoryConfigurationError;
35 import javax.xml.stream.XMLInputFactory;
36 import javax.xml.stream.XMLStreamException;
37 import javax.xml.stream.XMLStreamReader;
38
39 import com.stevesoft.pat.Regex;
40
41 import jalview.bin.Cache;
42 import jalview.bin.Console;
43 import jalview.datamodel.Alignment;
44 import jalview.datamodel.AlignmentI;
45 import jalview.datamodel.DBRefEntry;
46 import jalview.datamodel.DBRefSource;
47 import jalview.datamodel.PDBEntry;
48 import jalview.datamodel.Sequence;
49 import jalview.datamodel.SequenceFeature;
50 import jalview.datamodel.SequenceI;
51 import jalview.schemes.ResidueProperties;
52 import jalview.util.Platform;
53 import jalview.util.StringUtils;
54 import jalview.ws.seqfetcher.DbSourceProxyImpl;
55 import jalview.xml.binding.uniprot.DbReferenceType;
56 import jalview.xml.binding.uniprot.Entry;
57 import jalview.xml.binding.uniprot.FeatureType;
58 import jalview.xml.binding.uniprot.LocationType;
59 import jalview.xml.binding.uniprot.PositionType;
60 import jalview.xml.binding.uniprot.PropertyType;
61
62 /**
63  * This class queries the Uniprot database for sequence data, unmarshals the
64  * returned XML, and converts it to Jalview Sequence records (including attached
65  * database references and sequence features)
66  * 
67  * @author JimP
68  * 
69  */
70 public class Uniprot extends DbSourceProxyImpl
71 {
72   private static final String DEFAULT_UNIPROT_DOMAIN = "https://www.uniprot.org";
73
74   private static final String BAR_DELIMITER = "|";
75
76   static
77   {
78     Platform.addJ2SDirectDatabaseCall(DEFAULT_UNIPROT_DOMAIN);
79   }
80
81   /**
82    * Constructor
83    */
84   public Uniprot()
85   {
86     super();
87   }
88
89   private String getDomain()
90   {
91     return Cache.getDefault("UNIPROT_DOMAIN", DEFAULT_UNIPROT_DOMAIN);
92   }
93
94   /*
95    * (non-Javadoc)
96    * 
97    * @see jalview.ws.DbSourceProxy#getAccessionSeparator()
98    */
99   @Override
100   public String getAccessionSeparator()
101   {
102     return null;
103   }
104
105   /*
106    * (non-Javadoc)
107    * 
108    * @see jalview.ws.DbSourceProxy#getAccessionValidator()
109    */
110   @Override
111   public Regex getAccessionValidator()
112   {
113     return new Regex("([A-Z]+[0-9]+[A-Z0-9]+|[A-Z0-9]+_[A-Z0-9]+)");
114   }
115
116   /*
117    * (non-Javadoc)
118    * 
119    * @see jalview.ws.DbSourceProxy#getDbSource()
120    */
121   @Override
122   public String getDbSource()
123   {
124     return DBRefSource.UNIPROT;
125   }
126
127   /*
128    * (non-Javadoc)
129    * 
130    * @see jalview.ws.DbSourceProxy#getDbVersion()
131    */
132   @Override
133   public String getDbVersion()
134   {
135     return "0"; // we really don't know what version we're on.
136   }
137
138   /*
139    * (non-Javadoc)
140    * 
141    * @see jalview.ws.DbSourceProxy#getSequenceRecords(java.lang.String[])
142    */
143   @Override
144   public AlignmentI getSequenceRecords(String queries) throws Exception
145   {
146     startQuery();
147     try
148     {
149       queries = queries.toUpperCase(Locale.ROOT).replaceAll(
150               "(UNIPROT\\|?|UNIPROT_|UNIREF\\d+_|UNIREF\\d+\\|?)", "");
151       AlignmentI al = null;
152
153       String downloadstring = getDomain() + "/uniprot/" + queries + ".xml";
154
155       URL url = new URL(downloadstring);
156       HttpURLConnection urlconn = (HttpURLConnection) url.openConnection();
157       // anything other than 200 means we don't have data
158       // TODO: JAL-3882 reuse the EnsemblRestClient's fair
159       // use/backoff logic to retry when the server tells us to go away
160       if (urlconn.getResponseCode() == 200)
161       {
162         InputStream istr = urlconn.getInputStream();
163         List<Entry> entries = getUniprotEntries(istr);
164         if (entries != null)
165         {
166           List<SequenceI> seqs = new ArrayList<>();
167           for (Entry entry : entries)
168           {
169             seqs.add(uniprotEntryToSequence(entry));
170           }
171           al = new Alignment(seqs.toArray(new SequenceI[seqs.size()]));
172         }
173       }
174       stopQuery();
175       return al;
176
177     } catch (Exception e)
178     {
179       throw (e);
180     } finally
181     {
182       stopQuery();
183     }
184   }
185
186   /**
187    * Converts an Entry object (bound from Uniprot XML) to a Jalview Sequence
188    * 
189    * @param entry
190    * @return
191    */
192   SequenceI uniprotEntryToSequence(Entry entry)
193   {
194     String id = getUniprotEntryId(entry);
195     /*
196      * Sequence should not include any whitespace, but JAXB leaves these in
197      */
198     String seqString = entry.getSequence().getValue().replaceAll("\\s*",
199             "");
200
201     SequenceI sequence = new Sequence(id, seqString);
202     sequence.setDescription(getUniprotEntryDescription(entry));
203     final String uniprotRecordVersion = "" + entry.getVersion();
204     /*
205      * add a 'self' DBRefEntry for each accession
206      */
207     final String dbVersion = getDbVersion();
208     List<DBRefEntry> dbRefs = new ArrayList<>();
209     boolean canonical = true;
210     for (String accessionId : entry.getAccession())
211     {
212       DBRefEntry dbRef = new DBRefEntry(DBRefSource.UNIPROT,
213               uniprotRecordVersion, accessionId, null, canonical);
214       canonical = false;
215       dbRefs.add(dbRef);
216     }
217
218     /*
219      * add a DBRefEntry for each dbReference element in the XML;
220      * also add a PDBEntry if type="PDB";
221      * also add an EMBLCDS dbref if protein sequence id is given
222      * also add an Ensembl dbref " " " " " "
223      */
224     Vector<PDBEntry> pdbRefs = new Vector<>();
225     for (DbReferenceType dbref : entry.getDbReference())
226     {
227       String type = dbref.getType();
228       DBRefEntry dbr = new DBRefEntry(type,
229               DBRefSource.UNIPROT + ":" + dbVersion, dbref.getId());
230       dbRefs.add(dbr);
231       if ("PDB".equals(type))
232       {
233         pdbRefs.add(new PDBEntry(dbr));
234       }
235       if ("EMBL".equals(type))
236       {
237         /*
238          * e.g. Uniprot accession Q9BXM7 has
239          * <dbReference type="EMBL" id="M19359">
240          *   <property type="protein sequence ID" value="AAA40981.1"/>
241          *   <property type="molecule type" value="Genomic_DNA"/>
242          * </dbReference> 
243          */
244         String cdsId = getProperty(dbref.getProperty(),
245                 "protein sequence ID");
246         if (cdsId != null && cdsId.trim().length() > 0)
247         {
248           // remove version
249           String[] vrs = cdsId.split("\\.");
250           String version = vrs.length > 1 ? vrs[1]
251                   : DBRefSource.UNIPROT + ":" + uniprotRecordVersion;
252           dbr = new DBRefEntry(DBRefSource.EMBLCDS, version, vrs[0]);
253           // TODO: process VARIANT features to allow EMBLCDS record's product to
254           // match Uniprot
255           dbr.setCanonical(true);
256           dbRefs.add(dbr);
257         }
258       }
259       if (type != null
260               && type.toLowerCase(Locale.ROOT).startsWith("ensembl"))
261       {
262         // remove version
263         String[] vrs = dbref.getId().split("\\.");
264         String version = vrs.length > 1 ? vrs[1]
265                 : DBRefSource.UNIPROT + ":" + uniprotRecordVersion;
266         dbr.setAccessionId(vrs[0]);
267         dbr.setVersion(version);
268         /*
269          * e.g. Uniprot accession Q9BXM7 has
270          * <dbReference type="Ensembl" id="ENST00000321556">
271          *   <molecule id="Q9BXM7-1"/>
272          *   <property type="protein sequence ID" value="ENSP00000364204"/>
273          *   <property type="gene ID" value="ENSG00000158828"/>
274          * </dbReference> 
275          */
276         String cdsId = getProperty(dbref.getProperty(),
277                 "protein sequence ID");
278         if (cdsId != null && cdsId.trim().length() > 0)
279         {
280           // remove version
281           String[] cdsVrs = cdsId.split("\\.");
282           String cdsVersion = cdsVrs.length > 1 ? cdsVrs[1]
283                   : DBRefSource.UNIPROT + ":" + uniprotRecordVersion;
284           dbr = new DBRefEntry(DBRefSource.ENSEMBL,
285                   DBRefSource.UNIPROT + ":" + cdsVersion, cdsVrs[0]);
286           dbRefs.add(dbr);
287         }
288       }
289     }
290
291     /*
292      * create features; they have either begin and end, or position, in XML
293      */
294     sequence.setPDBId(pdbRefs);
295     if (entry.getFeature() != null)
296     {
297       for (FeatureType uf : entry.getFeature())
298       {
299         LocationType location = uf.getLocation();
300         int start = 0;
301         int end = 0;
302         String uncertain_start = null, uncertain_end = null,
303                 uncertain_pos = null;
304         if (location.getPosition() != null)
305         {
306           if (location.getPosition().getPosition() == null
307                   || "unknown".equals(location.getPosition().getStatus()))
308           {
309             Console.warn(
310                     "Ignoring single position feature with uncertain location "
311                             + uf.getType() + ":" + getDescription(uf));
312             uncertain_pos = location.getPosition().getStatus() == null
313                     ? "unknown"
314                     : location.getPosition().getStatus();
315           }
316           else
317           {
318             start = location.getPosition().getPosition().intValue();
319             end = start;
320           }
321         }
322         else
323         {
324           if (location.getBegin().getPosition() == null)
325           {
326             Console.warn(
327                     "Setting start position of feature with uncertain start to 1: "
328                             + uf.getType() + ":" + getDescription(uf));
329             start = sequence.getStart();
330             uncertain_start = location.getBegin().getStatus();
331           }
332           else
333           {
334             start = location.getBegin().getPosition().intValue();
335           }
336           if (location.getEnd().getPosition() == null)
337           {
338             Console.warn(
339                     "Setting start position of feature with uncertain start to 1: "
340                             + uf.getType() + ":" + getDescription(uf));
341             end = sequence.getEnd();
342             uncertain_end = location.getEnd().getStatus();
343           }
344           else
345           {
346             end = location.getEnd().getPosition().intValue();
347           }
348         }
349         SequenceFeature sf = new SequenceFeature(uf.getType(),
350                 getDescription(uf), start, end, "Uniprot");
351         sf.setStatus(uf.getStatus());
352         if (uncertain_end != null)
353         {
354           sf.setValue("end_status", uncertain_end);
355         }
356         if (uncertain_start != null)
357         {
358           sf.setValue("start_status", uncertain_start);
359         }
360         if (uncertain_pos != null)
361         {
362           sf.setValue("pos_status", uncertain_pos);
363         }
364         sequence.addSequenceFeature(sf);
365       }
366     }
367     for (DBRefEntry dbr : dbRefs)
368     {
369       sequence.addDBRef(dbr);
370     }
371     return sequence;
372   }
373
374   /**
375    * A helper method that builds a sequence feature description
376    * 
377    * @param feature
378    * @return
379    */
380   static String getDescription(FeatureType feature)
381   {
382     String orig = feature.getOriginal();
383     List<String> variants = feature.getVariation();
384     StringBuilder sb = new StringBuilder();
385
386     /*
387      * append variant in standard format if present
388      * e.g. p.Arg59Lys
389      * multiple variants are split over lines using <br>
390      */
391     boolean asHtml = false;
392     if (orig != null && !orig.isEmpty() && variants != null
393             && !variants.isEmpty())
394     {
395       int p = 0;
396       for (String var : variants)
397       {
398         // TODO proper HGVS nomenclature for delins structural variations
399         // http://varnomen.hgvs.org/recommendations/protein/variant/delins/
400         // for now we are pragmatic - any orig/variant sequence longer than
401         // three characters is shown with single-character notation rather than
402         // three-letter notation
403         sb.append("p.");
404         if (orig.length() < 4)
405         {
406           for (int c = 0, clen = orig.length(); c < clen; c++)
407           {
408             char origchar = orig.charAt(c);
409             String orig3 = ResidueProperties.aa2Triplet.get("" + origchar);
410             sb.append(orig3 == null ? origchar
411                     : StringUtils.toSentenceCase(orig3));
412           }
413         }
414         else
415         {
416           sb.append(orig);
417         }
418
419         LocationType location = feature.getLocation();
420         PositionType start = location.getPosition() == null
421                 ? location.getBegin()
422                 : location.getPosition();
423         sb.append(Integer.toString(start.getPosition().intValue()));
424
425         if (var.length() < 4)
426         {
427           for (int c = 0, clen = var.length(); c < clen; c++)
428           {
429             char varchar = var.charAt(c);
430             String var3 = ResidueProperties.aa2Triplet.get("" + varchar);
431
432             sb.append(var3 != null ? StringUtils.toSentenceCase(var3)
433                     : "" + varchar);
434           }
435         }
436         else
437         {
438           sb.append(var);
439         }
440         if (++p != variants.size())
441         {
442           sb.append("<br/>&nbsp;&nbsp;");
443           asHtml = true;
444         }
445         else
446         {
447           sb.append(" ");
448         }
449       }
450     }
451     String description = feature.getDescription();
452     if (description != null)
453     {
454       sb.append(description);
455     }
456     if (asHtml)
457     {
458       sb.insert(0, "<html>");
459       sb.append("</html>");
460     }
461
462     return sb.toString();
463   }
464
465   /**
466    * A helper method that searches the list of properties for one with the given
467    * key, and if found returns the property value, else returns null
468    * 
469    * @param properties
470    * @param key
471    * @return
472    */
473   static String getProperty(List<PropertyType> properties, String key)
474   {
475     String value = null;
476     if (properties != null)
477     {
478       for (PropertyType prop : properties)
479       {
480         if (key.equals(prop.getType()))
481         {
482           value = prop.getValue();
483           break;
484         }
485       }
486     }
487     return value;
488   }
489
490   /**
491    * Extracts xml element entry/protein/recommendedName/fullName
492    * 
493    * @param entry
494    * @return
495    */
496   static String getUniprotEntryDescription(Entry entry)
497   {
498     String desc = "";
499     if (entry.getProtein() != null
500             && entry.getProtein().getRecommendedName() != null)
501     {
502       // fullName is mandatory if recommendedName is present
503       desc = entry.getProtein().getRecommendedName().getFullName()
504               .getValue();
505     }
506     return desc;
507   }
508
509   /**
510    * Constructs a sequence id by concatenating all entry/name elements with '|'
511    * separator
512    * 
513    * @param entry
514    * @return
515    */
516   static String getUniprotEntryId(Entry entry)
517   {
518     StringBuilder name = new StringBuilder(32);
519     for (String n : entry.getName())
520     {
521       if (name.length() > 0)
522       {
523         name.append(BAR_DELIMITER);
524       }
525       name.append(n);
526     }
527     return name.toString();
528   }
529
530   /*
531    * (non-Javadoc)
532    * 
533    * @see jalview.ws.DbSourceProxy#isValidReference(java.lang.String)
534    */
535   @Override
536   public boolean isValidReference(String accession)
537   {
538     // TODO: make the following a standard validator
539     return (accession == null || accession.length() < 2) ? false
540             : getAccessionValidator().search(accession);
541   }
542
543   /**
544    * return LDHA_CHICK uniprot entry
545    */
546   @Override
547   public String getTestQuery()
548   {
549     return "P00340";
550   }
551
552   @Override
553   public String getDbName()
554   {
555     return "Uniprot"; // getDbSource();
556   }
557
558   @Override
559   public int getTier()
560   {
561     return 0;
562   }
563
564   /**
565    * Reads the reply to the EBI Fetch Uniprot data query, unmarshals it to an
566    * Uniprot object, and returns the enclosed Entry objects, or null on any
567    * failure
568    * 
569    * @param is
570    * @return
571    */
572   public List<Entry> getUniprotEntries(InputStream is)
573   {
574     List<Entry> entries = null;
575     try
576     {
577       JAXBContext jc = JAXBContext
578               .newInstance("jalview.xml.binding.uniprot");
579       XMLStreamReader streamReader = XMLInputFactory.newInstance()
580               .createXMLStreamReader(is);
581       javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
582       JAXBElement<jalview.xml.binding.uniprot.Uniprot> uniprotElement = um
583               .unmarshal(streamReader,
584                       jalview.xml.binding.uniprot.Uniprot.class);
585       jalview.xml.binding.uniprot.Uniprot uniprot = uniprotElement
586               .getValue();
587
588       if (uniprot != null && !uniprot.getEntry().isEmpty())
589       {
590         entries = uniprot.getEntry();
591       }
592     } catch (JAXBException | XMLStreamException
593             | FactoryConfigurationError e)
594     {
595       if (e instanceof javax.xml.bind.UnmarshalException
596               && e.getCause() != null
597               && e.getCause() instanceof XMLStreamException
598               && e.getCause().getMessage().contains("[row,col]:[1,1]"))
599       {
600         // trying to parse an empty stream
601         return null;
602       }
603       e.printStackTrace();
604     }
605     return entries;
606   }
607 }