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