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