JAL-1479 added support for SIFTs mapping output viewing and implemented exception...
[jalview.git] / src / jalview / ws / sifts / SiftsClient.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.sifts;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.api.DBRefEntryI;
25 import jalview.api.SiftsClientI;
26 import jalview.datamodel.DBRefEntry;
27 import jalview.datamodel.SequenceI;
28 import jalview.schemes.ResidueProperties;
29 import jalview.structure.StructureMapping;
30 import jalview.util.Format;
31 import jalview.xml.binding.sifts.Entry;
32 import jalview.xml.binding.sifts.Entry.Entity;
33 import jalview.xml.binding.sifts.Entry.Entity.Segment;
34 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListMapRegion.MapRegion;
35 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue;
36 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.CrossRefDb;
37 import jalview.xml.binding.sifts.Entry.ListDB.Db;
38
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.FileNotFoundException;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.PrintStream;
46 import java.net.URL;
47 import java.net.URLConnection;
48 import java.util.ArrayList;
49 import java.util.HashSet;
50 import java.util.LinkedHashMap;
51 import java.util.List;
52 import java.util.zip.GZIPInputStream;
53
54 import javax.xml.bind.JAXBContext;
55 import javax.xml.bind.JAXBException;
56 import javax.xml.bind.Unmarshaller;
57 import javax.xml.stream.FactoryConfigurationError;
58 import javax.xml.stream.XMLInputFactory;
59 import javax.xml.stream.XMLStreamException;
60 import javax.xml.stream.XMLStreamReader;
61
62 public class SiftsClient implements SiftsClientI
63 {
64   private Entry siftsEntry;
65
66   private String pdbId;
67
68   private String structId;
69
70   private String segStartEnd;
71
72   private static final int BUFFER_SIZE = 4096;
73
74   private static final String SIFTS_FTP_BASE_URL = "ftp://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
75
76   public static final String DEFAULT_SIFTS_DOWNLOAD_DIR = System
77           .getProperty("user.home")
78           + File.separatorChar
79           + ".sifts_downloads" + File.separatorChar;
80
81   public static final String SIFTS_DOWNLOAD_DIR = jalview.bin.Cache
82           .getDefault("sifts_download_dir", DEFAULT_SIFTS_DOWNLOAD_DIR);
83
84   private final static String NEWLINE = System.lineSeparator();
85
86   /**
87    * Fetch SIFTs file for the given PDB Id and construct an instance of
88    * SiftsClient
89    * 
90    * @param pdbId
91    */
92   public SiftsClient(String pdbId)
93   {
94     this.pdbId = pdbId;
95     try
96     {
97       File siftsFile = getSiftsFile(pdbId);
98       siftsEntry = parseSIFTs(siftsFile);
99     } catch (Exception e)
100     {
101       e.printStackTrace();
102     }
103   }
104
105   /**
106    * Construct an instance of SiftsClient using the supplied SIFTs file - 
107    * the SIFTs file should correspond to the given PDB Id
108    * 
109    * @param pdbId
110    * @param siftsFile
111    */
112   public SiftsClient(String pdbId, File siftsFile)
113   {
114     this.pdbId = pdbId;
115     try
116     {
117       siftsEntry = parseSIFTs(siftsFile);
118     } catch (Exception e)
119     {
120       e.printStackTrace();
121     }
122
123   }
124
125   /**
126    * Parse the given SIFTs File and return a JAXB POJO of parsed data
127    * 
128    * @param siftFile
129    *          - the GZipped SIFTs XML file to parse
130    * @return
131    * @throws Exception
132    *           if a problem occurs while parsing the SIFTs XML
133    */
134   private Entry parseSIFTs(File siftFile) throws Exception
135   {
136     try
137     {
138       System.out.println("File : " + siftFile.getAbsolutePath());
139       JAXBContext jc = JAXBContext.newInstance("jalview.xml.binding.sifts");
140       InputStream in = new FileInputStream(siftFile);
141       GZIPInputStream gzis = new GZIPInputStream(in);
142       XMLStreamReader streamReader = XMLInputFactory.newInstance()
143               .createXMLStreamReader(gzis);
144       Unmarshaller um = jc.createUnmarshaller();
145       return (Entry) um.unmarshal(streamReader);
146     } catch (JAXBException e)
147     {
148       e.printStackTrace();
149     } catch (FileNotFoundException e)
150     {
151       e.printStackTrace();
152     } catch (XMLStreamException e)
153     {
154       e.printStackTrace();
155     } catch (FactoryConfigurationError e)
156     {
157       e.printStackTrace();
158     } catch (IOException e)
159     {
160       e.printStackTrace();
161     }
162     throw new Exception("Error parsing siftFile");
163   }
164
165   /**
166    * Get a SIFTs XML file for a given PDB Id
167    * 
168    * @param pdbId
169    * @return SIFTs XML file
170    */
171   public static File getSiftsFile(String pdbId)
172   {
173     File siftsFile = new File(SIFTS_DOWNLOAD_DIR + pdbId.toLowerCase()
174             + ".xml.gz");
175     if (siftsFile.exists())
176     {
177       // TODO it may be worth performing a timestamp age check to determine if a
178       // new SIFTs file should be re-downloaded as SIFTs entries are usually
179       // updated weekly
180       System.out.println(">>> SIFTS File already downloaded for " + pdbId);
181       return siftsFile;
182     }
183     siftsFile = downloadSiftsFile(pdbId.toLowerCase());
184     return siftsFile;
185   }
186
187   /**
188    * Download a SIFTs XML file for a given PDB Id
189    * 
190    * @param pdbId
191    * @return downloaded SIFTs XML file
192    */
193   public static File downloadSiftsFile(String pdbId)
194   {
195     String siftFile = pdbId + ".xml.gz";
196     String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
197     String downloadedSiftsFile = SIFTS_DOWNLOAD_DIR + siftFile;
198     File siftsDownloadDir = new File(SIFTS_DOWNLOAD_DIR);
199     if (!siftsDownloadDir.exists())
200     {
201       siftsDownloadDir.mkdirs();
202     }
203     try
204     {
205       System.out.println(">> Download ftp url : " + siftsFileFTPURL);
206       URL url = new URL(siftsFileFTPURL);
207       URLConnection conn = url.openConnection();
208       InputStream inputStream = conn.getInputStream();
209       FileOutputStream outputStream = new FileOutputStream(
210               downloadedSiftsFile);
211       byte[] buffer = new byte[BUFFER_SIZE];
212       int bytesRead = -1;
213       while ((bytesRead = inputStream.read(buffer)) != -1)
214       {
215         outputStream.write(buffer, 0, bytesRead);
216       }
217       outputStream.close();
218       inputStream.close();
219       System.out.println(">>> File downloaded : " + downloadedSiftsFile);
220     } catch (IOException ex)
221     {
222       ex.printStackTrace();
223     }
224     return new File(downloadedSiftsFile);
225   }
226
227   /**
228    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
229    * directory
230    * 
231    * @param pdbId
232    * @return true if the file was deleted or doesn't exist
233    */
234   public static boolean deleteSiftsFileByPDBId(String pdbId)
235   {
236     File siftsFile = new File(SIFTS_DOWNLOAD_DIR + pdbId.toLowerCase()
237             + ".xml.gz");
238     if (siftsFile.exists())
239     {
240       return siftsFile.delete();
241     }
242     return true;
243   }
244
245
246   /**
247    * Get a valid SIFTs DBRef for the given sequence current SIFTs entry
248    * 
249    * @param seq
250    *          - the target sequence for the operation
251    * @return a valid DBRefEntry that is SIFTs compatible
252    * @throws Exception
253    *           if no valid source DBRefEntry was found for the given sequences
254    */
255   public DBRefEntryI getValidSourceDBRef(SequenceI seq)
256           throws SiftsException
257   {
258     DBRefEntryI sourceDBRef = null;
259     sourceDBRef = seq.getSourceDBRef();
260     if (sourceDBRef != null && isValidDBRefEntry(sourceDBRef))
261     {
262       return sourceDBRef;
263     }
264     else
265     {
266       DBRefEntry[] dbRefs = seq.getDBRefs();
267       if (dbRefs == null || dbRefs.length < 1)
268       {
269         final SequenceI[] seqs = new SequenceI[] { seq };
270         new jalview.ws.DBRefFetcher(seqs, null, null, null, false)
271                 .fetchDBRefs(true);
272         dbRefs = seq.getDBRefs();
273       }
274
275       if (dbRefs == null || dbRefs.length < 1)
276       {
277         throw new SiftsException("Could not get source DB Ref");
278       }
279
280       for (DBRefEntryI dbRef : dbRefs)
281       {
282         if (dbRef == null || dbRef.getAccessionId() == null
283                 || dbRef.getSource() == null)
284         {
285           continue;
286         }
287         if (isFoundInSiftsEntry(dbRef.getAccessionId())
288                 && (dbRef.getSource().equalsIgnoreCase("uniprot") || dbRef
289                         .getSource().equalsIgnoreCase("pdb")))
290         {
291           return dbRef;
292         }
293       }
294     }
295     if (sourceDBRef != null && isValidDBRefEntry(sourceDBRef))
296     {
297       return sourceDBRef;
298     }
299     throw new SiftsException("Could not get source DB Ref");
300   }
301
302
303   /**
304    * Check that the DBRef Entry is properly populated and is available in the
305    * instantiated SIFTs Entry
306    * 
307    * @param entry
308    *          - DBRefEntry to validate
309    * @return true validation is successful otherwise false is returned.
310    */
311   private boolean isValidDBRefEntry(DBRefEntryI entry)
312   {
313     return entry != null && entry.getAccessionId() != null
314             && isFoundInSiftsEntry(entry.getAccessionId());
315     // & entry.getStartRes() > 0;
316   }
317
318   @Override
319   public HashSet<String> getAllMappingAccession()
320   {
321     HashSet<String> accessions = new HashSet<String>();
322     List<Entity> entities = siftsEntry.getEntity();
323     for (Entity entity : entities)
324     {
325       List<Segment> segments = entity.getSegment();
326       for (Segment segment : segments)
327       {
328         List<MapRegion> mapRegions = segment.getListMapRegion()
329                 .getMapRegion();
330         for (MapRegion mapRegion : mapRegions)
331         {
332           accessions.add(mapRegion.getDb().getDbAccessionId());
333         }
334       }
335     }
336     return accessions;
337   }
338
339   @Override
340   public StructureMapping getSiftsStructureMapping(SequenceI seq,
341           String pdbFile, String chain) throws SiftsException
342   {
343     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
344     System.out.println("Getting mapping for: " + pdbId + "|" + chain
345             + " : seq- " + seq.getName());
346
347     final StringBuilder mappingDetails = new StringBuilder(128);
348     PrintStream ps = new PrintStream(System.out)
349     {
350       @Override
351       public void print(String x)
352       {
353         mappingDetails.append(x);
354       }
355
356       @Override
357       public void println()
358       {
359         mappingDetails.append(NEWLINE);
360       }
361     };
362     int[][] mapping = getGreedyMapping(chain, seq, ps);
363
364     String mappingOutput = mappingDetails.toString();
365     return new StructureMapping(seq, pdbFile, pdbId, chain, mapping,
366             mappingOutput);
367   }
368
369   @Override
370   public int[][] getGreedyMapping(String entityId, SequenceI seq,
371           java.io.PrintStream os)
372  throws SiftsException
373   {
374     int matchedResStart = -1;
375     int matchedResEnd = -1;
376     int counter = 0;
377     int pdbStart = -1;
378     int pdbEnd = -1;
379     int sStart = -1;
380     int sEnd = -1;
381     boolean startDetected = false;
382
383     System.out.println("Generating mappings for : " + entityId);
384     Entity entity = null;
385     entity = getEntityById(entityId);
386     String seqStr = AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
387             seq.getSequenceAsString());
388     int mapping[][] = new int[seqStr.length() + seq.getStart()][2];
389     DBRefEntryI sourceDBRef = seq.getSourceDBRef();
390     if (sourceDBRef == null)
391     {
392       sourceDBRef = getValidSourceDBRef(seq);
393       // TODO update sequence start/end with sourceDBRef start/end
394       // seq.setStart(sourceDBRef.getStartRes());
395       // seq.setEnd(sourceDBRef.getEndRes());
396     }
397
398     String crossRefAccessionId = sourceDBRef.getAccessionId();
399     int count = 0;
400     for (int residue[] : mapping)
401     {
402       residue[1] = count++;
403       residue[0] = -1;
404     }
405     
406     LinkedHashMap<Integer, String> resNumMap = new LinkedHashMap<Integer, String>();
407     List<Segment> segments = entity.getSegment();
408     for (Segment segment : segments)
409     {
410       segStartEnd = segment.getStart() + " - " + segment.getEnd();
411       System.out.println("Mappging segments : " + segment.getSegId() + "\\"
412               + segStartEnd);
413       List<Residue> residues = segment.getListResidue().getResidue();
414       for (Residue residue : residues)
415       {
416         int refDbResNum = -1;
417         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
418         for (CrossRefDb cRefDb : cRefDbs)
419         {
420           if (cRefDb.getDbAccessionId().equalsIgnoreCase(
421                   crossRefAccessionId))
422           {
423             refDbResNum = Integer.valueOf(cRefDb.getDbResNum());
424           }
425         }
426         if (refDbResNum == -1)
427         {
428           continue;
429         }
430         int loopCount = 0;
431         for (int[] x : mapping)
432         {
433           if (loopCount > seq.getStart() && x[1] == refDbResNum)
434           {
435             int resNum = Integer.valueOf(residue.getDbResNum());
436             x[0] = resNum;
437             char resCharCode = ResidueProperties
438                     .getSingleCharacterCode(residue.getDbResName());
439             resNumMap.put(resNum, String.valueOf(resCharCode));
440           }
441           ++loopCount;
442         }
443       }
444     }
445
446     for (int[] x : mapping)
447     {
448       if (!startDetected && x[0] > -1)
449       {
450         matchedResStart = counter;
451         // System.out.println(matchedResStart);
452         startDetected = true;
453       }
454
455       if (startDetected && x[0] == -1)
456       {
457         matchedResEnd = counter;
458       }
459       ++counter;
460     }
461
462     String matchedSeqStr = seqStr;
463     if (matchedResStart != -1)
464     {
465       matchedResEnd = (matchedResEnd == -1) ? counter : matchedResEnd;
466       pdbStart = mapping[matchedResStart][0];
467       pdbEnd = mapping[matchedResEnd - 1][0];
468       sStart = mapping[matchedResStart][1];
469       sEnd = mapping[matchedResEnd - 1][1];
470       int seqStart = seq.getStart();
471       if (seqStart > 1)
472       {
473         matchedResStart = matchedResStart - seqStart;
474         matchedResEnd = matchedResEnd - seqStart;
475       }
476       else
477       {
478         --matchedResStart;
479         --matchedResEnd;
480       }
481       matchedSeqStr = seqStr.substring(matchedResStart, matchedResEnd);
482     }
483
484     StringBuilder targetStrucSeqs = new StringBuilder();
485     for (String res : resNumMap.values())
486     {
487       targetStrucSeqs.append(res);
488     }
489
490     try
491     {
492       if (os != null)
493       {
494         MappingOutputPojo mop = new MappingOutputPojo();
495         mop.setSeqStart(sStart);
496         mop.setSeqEnd(sEnd);
497         mop.setSeqName(seq.getName());
498         mop.setSeqResidue(matchedSeqStr);
499
500         mop.setStrStart(pdbStart);
501         mop.setStrEnd(pdbEnd);
502         mop.setStrName(structId);
503         mop.setStrResidue(targetStrucSeqs.toString());
504
505         mop.setType("pep");
506         os.print(getMappingOutput(mop).toString());
507       }
508     } catch (Exception ex)
509     {
510       ex.printStackTrace();
511     }
512     return mapping;
513   }
514
515   @Override
516   public boolean isFoundInSiftsEntry(String accessionId)
517   {
518     return accessionId != null
519             && getAllMappingAccession().contains(accessionId);
520   }
521
522
523   @Override
524   public Entity getEntityById(String id) throws SiftsException
525   {
526     List<Entity> entities = siftsEntry.getEntity();
527     for (Entity entity : entities)
528     {
529       if (!entity.getEntityId().equalsIgnoreCase(id))
530       {
531         continue;
532       }
533       return entity;
534     }
535     throw new SiftsException("Entity " + id + " not found");
536   }
537
538   @Override
539   public String[] getEntryDBs()
540   {
541     System.out.println("\nListing DB entries...");
542     List<String> availDbs = new ArrayList<String>();
543     List<Db> dbs = siftsEntry.getListDB().getDb();
544     for (Db db : dbs)
545     {
546       availDbs.add(db.getDbSource());
547       System.out.println(db.getDbSource() + " | " + db.getDbCoordSys());
548     }
549     return availDbs.toArray(new String[0]);
550   }
551
552   @Override
553   public StringBuffer getMappingOutput(MappingOutputPojo mp)
554   {
555     String seqRes = mp.getSeqResidue();
556     String seqName = mp.getSeqName();
557     int sStart = mp.getSeqStart();
558     int sEnd = mp.getSeqEnd();
559
560     String strRes = mp.getStrResidue();
561     String strName = mp.getStrName();
562     int pdbStart = mp.getStrStart();
563     int pdbEnd = mp.getStrEnd();
564     
565     String type = mp.getType();
566     
567     int maxid = (seqName.length() >= strName.length()) ? seqName.length()
568             : strName.length();
569     int len = 72 - maxid - 1;
570
571     // int nochunks = 2;// mp.getWrapHeight();
572     int nochunks = ((seqRes.length()) / len)
573             + ((seqRes.length()) % len > 0 ? 1 : 0);
574     // output mappings
575     StringBuffer output = new StringBuffer();
576     output.append(NEWLINE);
577     output.append("Sequence ⟷ Structure mapping details:");
578     output.append(NEWLINE).append(NEWLINE);
579
580     output.append(new Format("%" + maxid + "s").form(seqName));
581     output.append(" :  ");
582     output.append(String.valueOf(sStart));
583     output.append(" - ");
584     output.append(String.valueOf(sEnd));
585     output.append(" Maps to ");
586     output.append(NEWLINE);
587     output.append(new Format("%" + maxid + "s").form(structId));
588     output.append(" :  ");
589     output.append(String.valueOf(pdbStart));
590     output.append(" - ");
591     output.append(String.valueOf(pdbEnd));
592     output.append(NEWLINE).append(NEWLINE);
593     
594     float pid = 0;
595     for (int j = 0; j < nochunks; j++)
596     {
597       // Print the first aligned sequence
598       output.append(new Format("%" + (maxid) + "s").form(seqName)).append(
599               " ");
600
601       for (int i = 0; i < len; i++)
602       {
603         if ((i + (j * len)) < seqRes.length())
604         {
605           output.append(seqRes.charAt(i + (j * len)));
606         }
607       }
608
609       output.append(NEWLINE);
610       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
611
612       // Print out the matching chars
613       for (int i = 0; i < len; i++)
614       {
615         if ((i + (j * len)) < seqRes.length())
616         {
617           if (seqRes.charAt(i + (j * len)) == strRes.charAt(i + (j * len))
618                   && !jalview.util.Comparison.isGap(seqRes.charAt(i
619                           + (j * len))))
620           {
621             pid++;
622             output.append("|");
623           }
624           else if (type.equals("pep"))
625           {
626             if (ResidueProperties.getPAM250(seqRes.charAt(i + (j * len)),
627                     strRes.charAt(i + (j * len))) > 0)
628             {
629               output.append(".");
630             }
631             else
632             {
633               output.append(" ");
634             }
635           }
636           else
637           {
638             output.append(" ");
639           }
640         }
641       }
642       // Now print the second aligned sequence
643       output = output.append(NEWLINE);
644       output = output.append(new Format("%" + (maxid) + "s").form(strName))
645               .append(" ");
646       for (int i = 0; i < len; i++)
647       {
648         if ((i + (j * len)) < strRes.length())
649         {
650           output.append(strRes.charAt(i + (j * len)));
651         }
652       }
653       output.append(NEWLINE).append(NEWLINE);
654     }
655     pid = pid / (seqRes.length()) * 100;
656     output.append("Length of alignment = " + seqRes.length())
657             .append(NEWLINE);
658     output.append(new Format("Percentage ID = %2.2f").form(pid));
659     output.append(NEWLINE);
660     output.append("Mapping method: SIFTS").append(NEWLINE);
661     return output;
662   }
663   
664   @Override
665   public int getEntityCount()
666   {
667     return siftsEntry.getEntity().size();
668   }
669
670   @Override
671   public String getDbAccessionId()
672   {
673     return siftsEntry.getDbAccessionId();
674   }
675
676   @Override
677   public String getDbCoordSys()
678   {
679     return siftsEntry.getDbCoordSys();
680   }
681
682   @Override
683   public String getDbEvidence()
684   {
685     return siftsEntry.getDbEvidence();
686   }
687
688   @Override
689   public String getDbSource()
690   {
691     return siftsEntry.getDbSource();
692   }
693
694   @Override
695   public String getDbVersion()
696   {
697     return siftsEntry.getDbVersion();
698   }
699 }