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