Merge branch 'develop' into bug/JAL-2399textColour
[jalview.git] / src / jalview / util / UrlLink.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.util;
22
23 import static jalview.util.UrlConstants.DB_ACCESSION;
24 import static jalview.util.UrlConstants.DELIM;
25 import static jalview.util.UrlConstants.SEP;
26 import static jalview.util.UrlConstants.SEQUENCE_ID;
27
28 import jalview.datamodel.DBRefEntry;
29 import jalview.datamodel.SequenceI;
30
31 import java.util.Arrays;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Vector;
35
36 public class UrlLink
37 {
38   /**
39    * helper class to parse URL Link strings taken from applet parameters or
40    * jalview properties file using the com.stevesoft.pat.Regex implementation.
41    * Jalview 2.4 extension allows regular expressions to be used to parse ID
42    * strings and replace the result in the URL. Regex's operate on the whole ID
43    * string given to the matchURL method, if no regex is supplied, then only
44    * text following the first pipe symbol will be substituted. Usage
45    * documentation todo.
46    */
47
48   private static final String EQUALS = "=";
49
50   private static final String SPACE = " ";
51
52   private String urlSuffix;
53
54   private String urlPrefix;
55
56   private String target;
57
58   private String label;
59
60   private String dbname;
61
62   private String regexReplace;
63
64   private boolean dynamic = false;
65
66   private boolean usesDBaccession = false;
67
68   private String invalidMessage = null;
69
70   /**
71    * parse the given linkString of the form '<label>SEP<url>' into parts url may
72    * contain a string $SEQUENCE_ID<=optional regex=>$ where <=optional regex=>
73    * must be of the form =/<perl style regex>/=$
74    * 
75    * @param link
76    */
77   public UrlLink(String link)
78   {
79     int sep = link.indexOf(SEP);
80     int psqid = link.indexOf(DELIM + DB_ACCESSION);
81     int nsqid = link.indexOf(DELIM + SEQUENCE_ID);
82     if (psqid > -1)
83     {
84       dynamic = true;
85       usesDBaccession = true;
86
87       sep = parseLabel(sep, psqid, link);
88
89       int endOfRegex = parseUrl(link, DB_ACCESSION, psqid, sep);
90       parseTarget(link, sep, endOfRegex);
91     }
92     else if (nsqid > -1)
93     {
94       dynamic = true;
95       sep = parseLabel(sep, nsqid, link);
96
97       int endOfRegex = parseUrl(link, SEQUENCE_ID, nsqid, sep);
98
99       parseTarget(link, sep, endOfRegex);
100     }
101     else
102     {
103       label = link.substring(0, sep).trim();
104
105       // if there's a third element in the url link string
106       // it is the target name, otherwise target=label
107       int lastsep = link.lastIndexOf(SEP);
108       if (lastsep != sep)
109       {
110         urlPrefix = link.substring(sep + 1, lastsep).trim();
111         target = link.substring(lastsep + 1).trim();
112       }
113       else
114       {
115         urlPrefix = link.substring(sep + 1).trim();
116         target = label;
117       }
118
119       regexReplace = null; // implies we trim any prefix if necessary //
120       urlSuffix = null;
121     }
122
123     label = label.trim();
124     target = target.trim();
125   }
126
127   /**
128    * Alternative constructor for separate name, link and description
129    * 
130    * @param name
131    *          The string used to match the link to a DB reference id
132    * @param url
133    *          The url to link to
134    * @param desc
135    *          The description of the associated target DB
136    */
137   public UrlLink(String name, String url, String desc)
138   {
139     this(name + SEP + url + SEP + desc);
140   }
141
142   /**
143    * @return the url_suffix
144    */
145   public String getUrlSuffix()
146   {
147     return urlSuffix;
148   }
149
150   /**
151    * @return the url_prefix
152    */
153   public String getUrlPrefix()
154   {
155     return urlPrefix;
156   }
157
158   /**
159    * @return the target
160    */
161   public String getTarget()
162   {
163     return target;
164   }
165
166   /**
167    * @return the label
168    */
169   public String getLabel()
170   {
171     return label;
172   }
173
174   public String getUrlWithToken()
175   {
176     String var = (usesDBaccession ? DB_ACCESSION : SEQUENCE_ID);
177
178     return urlPrefix
179             + (dynamic ? (DELIM + var + ((regexReplace != null) ? EQUALS
180                     + regexReplace + EQUALS + DELIM : DELIM)) : "")
181             + ((urlSuffix == null) ? "" : urlSuffix);
182   }
183
184   /**
185    * @return the regexReplace
186    */
187   public String getRegexReplace()
188   {
189     return regexReplace;
190   }
191
192   /**
193    * @return the invalidMessage
194    */
195   public String getInvalidMessage()
196   {
197     return invalidMessage;
198   }
199
200   /**
201    * Check if URL string was parsed properly.
202    * 
203    * @return boolean - if false then <code>getInvalidMessage</code> returns an
204    *         error message
205    */
206   public boolean isValid()
207   {
208     return invalidMessage == null;
209   }
210
211   /**
212    * 
213    * @return whether link is dynamic
214    */
215   public boolean isDynamic()
216   {
217     return dynamic;
218   }
219
220   /**
221    * 
222    * @return whether link uses DB Accession id
223    */
224   public boolean usesDBAccession()
225   {
226     return usesDBaccession;
227   }
228
229   /**
230    * Set the label
231    * 
232    * @param newlabel
233    */
234   public void setLabel(String newlabel)
235   {
236     this.label = newlabel;
237   }
238
239   /**
240    * Set the target
241    * 
242    * @param desc
243    */
244   public void setTarget(String desc)
245   {
246     target = desc;
247   }
248
249   /**
250    * return one or more URL strings by applying regex to the given idstring
251    * 
252    * @param idstring
253    * @param onlyIfMatches
254    *          - when true url strings are only made if regex is defined and
255    *          matches
256    * @return String[] { part of idstring substituted, full substituted url , ..
257    *         next part, next url..}
258    */
259   public String[] makeUrls(String idstring, boolean onlyIfMatches)
260   {
261     if (dynamic)
262     {
263       if (regexReplace != null)
264       {
265         com.stevesoft.pat.Regex rg = com.stevesoft.pat.Regex.perlCode("/"
266                 + regexReplace + "/");
267         if (rg.search(idstring))
268         {
269           int ns = rg.numSubs();
270           if (ns == 0)
271           {
272             // take whole regex
273             return new String[] { rg.stringMatched(),
274                 urlPrefix + rg.stringMatched() + urlSuffix };
275           } /*
276              * else if (ns==1) { // take only subgroup match return new String[]
277              * { rg.stringMatched(1), url_prefix+rg.stringMatched(1)+url_suffix
278              * }; }
279              */
280           else
281           {
282             // debug
283             for (int s = 0; s <= rg.numSubs(); s++)
284             {
285               System.err.println("Sub " + s + " : " + rg.matchedFrom(s)
286                       + " : " + rg.matchedTo(s) + " : '"
287                       + rg.stringMatched(s) + "'");
288             }
289             // try to collate subgroup matches
290             Vector<String> subs = new Vector<String>();
291             // have to loop through submatches, collating them at top level
292             // match
293             int s = 0; // 1;
294             while (s <= ns)
295             {
296               if (s + 1 <= ns && rg.matchedTo(s) > -1
297                       && rg.matchedTo(s + 1) > -1
298                       && rg.matchedTo(s + 1) < rg.matchedTo(s))
299               {
300                 // s is top level submatch. search for submatches enclosed by
301                 // this one
302                 int r = s + 1;
303                 String mtch = "";
304                 while (r <= ns && rg.matchedTo(r) <= rg.matchedTo(s))
305                 {
306                   if (rg.matchedFrom(r) > -1)
307                   {
308                     mtch += rg.stringMatched(r);
309                   }
310                   r++;
311                 }
312                 if (mtch.length() > 0)
313                 {
314                   subs.addElement(mtch);
315                   subs.addElement(urlPrefix + mtch + urlSuffix);
316                 }
317                 s = r;
318               }
319               else
320               {
321                 if (rg.matchedFrom(s) > -1)
322                 {
323                   subs.addElement(rg.stringMatched(s));
324                   subs.addElement(urlPrefix + rg.stringMatched(s)
325                           + urlSuffix);
326                 }
327                 s++;
328               }
329             }
330
331             String[] res = new String[subs.size()];
332             for (int r = 0, rs = subs.size(); r < rs; r++)
333             {
334               res[r] = subs.elementAt(r);
335             }
336             subs.removeAllElements();
337             return res;
338           }
339         }
340         if (onlyIfMatches)
341         {
342           return null;
343         }
344       }
345       /* Otherwise - trim off any 'prefix' - pre 2.4 Jalview behaviour */
346       if (idstring.indexOf(SEP) > -1)
347       {
348         idstring = idstring.substring(idstring.lastIndexOf(SEP) + 1);
349       }
350
351       // just return simple url substitution.
352       return new String[] { idstring, urlPrefix + idstring + urlSuffix };
353     }
354     else
355     {
356       return new String[] { "", urlPrefix };
357     }
358   }
359
360   @Override
361   public String toString()
362   {
363     return label + SEP + getUrlWithToken();
364   }
365
366   /**
367    * @return delimited string containing label, url and target
368    */
369   public String toStringWithTarget()
370   {
371     return label + SEP + getUrlWithToken() + SEP + target;
372   }
373
374   /**
375    * Parse the label from the link string
376    * 
377    * @param firstSep
378    *          Location of first occurrence of separator in link string
379    * @param psqid
380    *          Position of sequence id or name in link string
381    * @param link
382    *          Link string containing database name and url
383    * @return Position of last separator symbol prior to any regex symbols
384    */
385   protected int parseLabel(int firstSep, int psqid, String link)
386   {
387     int p = firstSep;
388     int sep = firstSep;
389     do
390     {
391       sep = p;
392       p = link.indexOf(SEP, sep + 1);
393     } while (p > sep && p < psqid);
394     // Assuming that the URL itself does not contain any SEP symbols
395     // sep now contains last pipe symbol position prior to any regex symbols
396     label = link.substring(0, sep);
397
398     return sep;
399   }
400
401   /**
402    * Parse the target from the link string
403    * 
404    * @param link
405    *          Link string containing database name and url
406    * @param sep
407    *          Location of first separator symbol
408    * @param endOfRegex
409    *          Location of end of any regular expression in link string
410    */
411   protected void parseTarget(String link, int sep, int endOfRegex)
412   {
413     int lastsep = link.lastIndexOf(SEP);
414
415     if ((lastsep != sep) && (lastsep > endOfRegex))
416     {
417       // final element in link string is the target
418       target = link.substring(lastsep + 1).trim();
419     }
420     else
421     {
422       target = label;
423     }
424
425     if (target.indexOf(SEP) > -1)
426     {
427       // SEP terminated database name / www target at start of Label
428       target = target.substring(0, target.indexOf(SEP));
429     }
430     else if (target.indexOf(SPACE) > 2)
431     {
432       // space separated label - first word matches database name
433       target = target.substring(0, target.indexOf(SPACE));
434     }
435   }
436
437   /**
438    * Parse the URL part of the link string
439    * 
440    * @param link
441    *          Link string containing database name and url
442    * @param varName
443    *          Name of variable in url string (e.g. SEQUENCE_ID, SEQUENCE_NAME)
444    * @param sqidPos
445    *          Position of id or name in link string
446    * @param sep
447    *          Position of separator in link string
448    * @return Location of end of any regex in link string
449    */
450   protected int parseUrl(String link, String varName, int sqidPos, int sep)
451   {
452     urlPrefix = link.substring(sep + 1, sqidPos).trim();
453
454     // delimiter at start of regex: e.g. $SEQUENCE_ID=/
455     String startDelimiter = DELIM + varName + "=/";
456
457     // delimiter at end of regex: /=$
458     String endDelimiter = "/=" + DELIM;
459
460     int startLength = startDelimiter.length();
461
462     // Parse URL : Whole URL string first
463     int p = link.indexOf(endDelimiter, sqidPos + startLength);
464
465     if (link.indexOf(startDelimiter) == sqidPos
466             && (p > sqidPos + startLength))
467     {
468       // Extract Regex and suffix
469       urlSuffix = link.substring(p + endDelimiter.length());
470       regexReplace = link.substring(sqidPos + startLength, p);
471       try
472       {
473         com.stevesoft.pat.Regex rg = com.stevesoft.pat.Regex.perlCode("/"
474                 + regexReplace + "/");
475         if (rg == null)
476         {
477           invalidMessage = "Invalid Regular Expression : '" + regexReplace
478                   + "'\n";
479         }
480       } catch (Exception e)
481       {
482         invalidMessage = "Invalid Regular Expression : '" + regexReplace
483                 + "'\n";
484       }
485     }
486     else
487     {
488       // no regex
489       regexReplace = null;
490       // verify format is really correct.
491       if (link.indexOf(DELIM + varName + DELIM) == sqidPos)
492       {
493         int lastsep = link.lastIndexOf(SEP);
494         if (lastsep < sqidPos + startLength - 1)
495         {
496           // the last SEP character was before the regex, ignore
497           lastsep = link.length();
498         }
499         urlSuffix = link.substring(sqidPos + startLength - 1, lastsep)
500                 .trim();
501         regexReplace = null;
502       }
503       else
504       {
505         invalidMessage = "Warning: invalid regex structure for URL link : "
506                 + link;
507       }
508     }
509
510     return p;
511   }
512
513   /**
514    * Create a set of URL links for a sequence
515    * 
516    * @param seq
517    *          The sequence to create links for
518    * @param linkset
519    *          Map of links: key = id + SEP + link, value = [target, label, id,
520    *          link]
521    */
522   public void createLinksFromSeq(final SequenceI seq,
523           Map<String, List<String>> linkset)
524   {
525     if (seq != null && dynamic)
526     {
527       createDynamicLinks(seq, linkset);
528     }
529     else
530     {
531       createStaticLink(linkset);
532     }
533   }
534
535   /**
536    * Create a static URL link
537    * 
538    * @param linkset
539    *          Map of links: key = id + SEP + link, value = [target, label, id,
540    *          link]
541    */
542   protected void createStaticLink(Map<String, List<String>> linkset)
543   {
544     if (!linkset.containsKey(label + SEP + getUrlPrefix()))
545     {
546       // Add a non-dynamic link
547       linkset.put(label + SEP + getUrlPrefix(),
548               Arrays.asList(target, label, null, getUrlPrefix()));
549     }
550   }
551
552   /**
553    * Create dynamic URL links
554    * 
555    * @param seq
556    *          The sequence to create links for
557    * @param linkset
558    *          Map of links: key = id + SEP + link, value = [target, label, id,
559    *          link]
560    */
561   protected void createDynamicLinks(final SequenceI seq,
562           Map<String, List<String>> linkset)
563   {
564     // collect id string too
565     String id = seq.getName();
566     String descr = seq.getDescription();
567     if (descr != null && descr.length() < 1)
568     {
569       descr = null;
570     }
571
572     if (usesDBAccession()) // link is ID
573     {
574       // collect matching db-refs
575       DBRefEntry[] dbr = DBRefUtils.selectRefs(seq.getDBRefs(),
576               new String[] { target });
577
578       // if there are any dbrefs which match up with the link
579       if (dbr != null)
580       {
581         for (int r = 0; r < dbr.length; r++)
582         {
583           // create Bare ID link for this URL
584           createBareURLLink(dbr[r].getAccessionId(), true, linkset);
585         }
586       }
587     }
588     else if (!usesDBAccession() && id != null) // link is name
589     {
590       // create Bare ID link for this URL
591       createBareURLLink(id, false, linkset);
592     }
593
594     // Create urls from description but only for URL links which are regex
595     // links
596     if (descr != null && getRegexReplace() != null)
597     {
598       // create link for this URL from description where regex matches
599       createBareURLLink(descr, false, linkset);
600     }
601   }
602
603   /*
604    * Create a bare URL Link
605    * Returns map where key = id + SEP + link, and value = [target, label, id, link]
606    */
607   protected void createBareURLLink(String id, Boolean combineLabel,
608           Map<String, List<String>> linkset)
609   {
610     String[] urls = makeUrls(id, true);
611     if (urls != null)
612     {
613       for (int u = 0; u < urls.length; u += 2)
614       {
615         if (!linkset.containsKey(urls[u] + SEP + urls[u + 1]))
616         {
617           String thisLabel = label;
618           if (combineLabel)
619           {
620             // incorporate label with idstring
621             thisLabel = label + SEP + urls[u];
622           }
623
624           linkset.put(urls[u] + SEP + urls[u + 1],
625                   Arrays.asList(target, thisLabel, urls[u], urls[u + 1]));
626         }
627       }
628     }
629   }
630 }