Jalview.isJS() --> Platform.isJS(), DBRefEntry[] --> List<DBRefEntry>
[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
180                     ? (DELIM + var
181                             + ((regexReplace != null)
182                                     ? EQUALS + regexReplace + EQUALS + DELIM
183                                     : DELIM))
184                     : "")
185             + ((urlSuffix == null) ? "" : urlSuffix);
186   }
187
188   /**
189    * @return the regexReplace
190    */
191   public String getRegexReplace()
192   {
193     return regexReplace;
194   }
195
196   /**
197    * @return the invalidMessage
198    */
199   public String getInvalidMessage()
200   {
201     return invalidMessage;
202   }
203
204   /**
205    * Check if URL string was parsed properly.
206    * 
207    * @return boolean - if false then <code>getInvalidMessage</code> returns an
208    *         error message
209    */
210   public boolean isValid()
211   {
212     return invalidMessage == null;
213   }
214
215   /**
216    * 
217    * @return whether link is dynamic
218    */
219   public boolean isDynamic()
220   {
221     return dynamic;
222   }
223
224   /**
225    * 
226    * @return whether link uses DB Accession id
227    */
228   public boolean usesDBAccession()
229   {
230     return usesDBaccession;
231   }
232
233   /**
234    * Set the label
235    * 
236    * @param newlabel
237    */
238   public void setLabel(String newlabel)
239   {
240     this.label = newlabel;
241   }
242
243   /**
244    * Set the target
245    * 
246    * @param desc
247    */
248   public void setTarget(String desc)
249   {
250     target = desc;
251   }
252
253   /**
254    * return one or more URL strings by applying regex to the given idstring
255    * 
256    * @param idstring
257    * @param onlyIfMatches
258    *          - when true url strings are only made if regex is defined and
259    *          matches
260    * @return String[] { part of idstring substituted, full substituted url , ..
261    *         next part, next url..}
262    */
263   public String[] makeUrls(String idstring, boolean onlyIfMatches)
264   {
265     if (dynamic)
266     {
267       if (regexReplace != null)
268       {
269         com.stevesoft.pat.Regex rg = com.stevesoft.pat.Regex
270                 .perlCode("/" + regexReplace + "/");
271         if (rg.search(idstring))
272         {
273           int ns = rg.numSubs();
274           if (ns == 0)
275           {
276             // take whole regex
277             return new String[] { rg.stringMatched(),
278                 urlPrefix + rg.stringMatched() + urlSuffix };
279           } /*
280              * else if (ns==1) { // take only subgroup match return new String[]
281              * { rg.stringMatched(1), url_prefix+rg.stringMatched(1)+url_suffix
282              * }; }
283              */
284           else
285           {
286             // debug
287             for (int s = 0; s <= rg.numSubs(); s++)
288             {
289               System.err.println("Sub " + s + " : " + rg.matchedFrom(s)
290                       + " : " + rg.matchedTo(s) + " : '"
291                       + rg.stringMatched(s) + "'");
292             }
293             // try to collate subgroup matches
294             Vector<String> subs = new Vector<String>();
295             // have to loop through submatches, collating them at top level
296             // match
297             int s = 0; // 1;
298             while (s <= ns)
299             {
300               if (s + 1 <= ns && rg.matchedTo(s) > -1
301                       && rg.matchedTo(s + 1) > -1
302                       && rg.matchedTo(s + 1) < rg.matchedTo(s))
303               {
304                 // s is top level submatch. search for submatches enclosed by
305                 // this one
306                 int r = s + 1;
307                 String mtch = "";
308                 while (r <= ns && rg.matchedTo(r) <= rg.matchedTo(s))
309                 {
310                   if (rg.matchedFrom(r) > -1)
311                   {
312                     mtch += rg.stringMatched(r);
313                   }
314                   r++;
315                 }
316                 if (mtch.length() > 0)
317                 {
318                   subs.addElement(mtch);
319                   subs.addElement(urlPrefix + mtch + urlSuffix);
320                 }
321                 s = r;
322               }
323               else
324               {
325                 if (rg.matchedFrom(s) > -1)
326                 {
327                   subs.addElement(rg.stringMatched(s));
328                   subs.addElement(
329                           urlPrefix + rg.stringMatched(s) + urlSuffix);
330                 }
331                 s++;
332               }
333             }
334
335             String[] res = new String[subs.size()];
336             for (int r = 0, rs = subs.size(); r < rs; r++)
337             {
338               res[r] = subs.elementAt(r);
339             }
340             subs.removeAllElements();
341             return res;
342           }
343         }
344         if (onlyIfMatches)
345         {
346           return null;
347         }
348       }
349       /* Otherwise - trim off any 'prefix' - pre 2.4 Jalview behaviour */
350       if (idstring.indexOf(SEP) > -1)
351       {
352         idstring = idstring.substring(idstring.lastIndexOf(SEP) + 1);
353       }
354
355       // just return simple url substitution.
356       return new String[] { idstring, urlPrefix + idstring + urlSuffix };
357     }
358     else
359     {
360       return new String[] { "", urlPrefix };
361     }
362   }
363
364   @Override
365   public String toString()
366   {
367     return label + SEP + getUrlWithToken();
368   }
369
370   /**
371    * @return delimited string containing label, url and target
372    */
373   public String toStringWithTarget()
374   {
375     return label + SEP + getUrlWithToken() + SEP + target;
376   }
377
378   /**
379    * Parse the label from the link string
380    * 
381    * @param firstSep
382    *          Location of first occurrence of separator in link string
383    * @param psqid
384    *          Position of sequence id or name in link string
385    * @param link
386    *          Link string containing database name and url
387    * @return Position of last separator symbol prior to any regex symbols
388    */
389   protected int parseLabel(int firstSep, int psqid, String link)
390   {
391     int p = firstSep;
392     int sep = firstSep;
393     do
394     {
395       sep = p;
396       p = link.indexOf(SEP, sep + 1);
397     } while (p > sep && p < psqid);
398     // Assuming that the URL itself does not contain any SEP symbols
399     // sep now contains last pipe symbol position prior to any regex symbols
400     label = link.substring(0, sep);
401
402     return sep;
403   }
404
405   /**
406    * Parse the target from the link string
407    * 
408    * @param link
409    *          Link string containing database name and url
410    * @param sep
411    *          Location of first separator symbol
412    * @param endOfRegex
413    *          Location of end of any regular expression in link string
414    */
415   protected void parseTarget(String link, int sep, int endOfRegex)
416   {
417     int lastsep = link.lastIndexOf(SEP);
418
419     if ((lastsep != sep) && (lastsep > endOfRegex))
420     {
421       // final element in link string is the target
422       target = link.substring(lastsep + 1).trim();
423     }
424     else
425     {
426       target = label;
427     }
428
429     if (target.indexOf(SEP) > -1)
430     {
431       // SEP terminated database name / www target at start of Label
432       target = target.substring(0, target.indexOf(SEP));
433     }
434     else if (target.indexOf(SPACE) > 2)
435     {
436       // space separated label - first word matches database name
437       target = target.substring(0, target.indexOf(SPACE));
438     }
439   }
440
441   /**
442    * Parse the URL part of the link string
443    * 
444    * @param link
445    *          Link string containing database name and url
446    * @param varName
447    *          Name of variable in url string (e.g. SEQUENCE_ID, SEQUENCE_NAME)
448    * @param sqidPos
449    *          Position of id or name in link string
450    * @param sep
451    *          Position of separator in link string
452    * @return Location of end of any regex in link string
453    */
454   protected int parseUrl(String link, String varName, int sqidPos, int sep)
455   {
456     urlPrefix = link.substring(sep + 1, sqidPos).trim();
457
458     // delimiter at start of regex: e.g. $SEQUENCE_ID=/
459     String startDelimiter = DELIM + varName + "=/";
460
461     // delimiter at end of regex: /=$
462     String endDelimiter = "/=" + DELIM;
463
464     int startLength = startDelimiter.length();
465
466     // Parse URL : Whole URL string first
467     int p = link.indexOf(endDelimiter, sqidPos + startLength);
468
469     if (link.indexOf(startDelimiter) == sqidPos
470             && (p > sqidPos + startLength))
471     {
472       // Extract Regex and suffix
473       urlSuffix = link.substring(p + endDelimiter.length());
474       regexReplace = link.substring(sqidPos + startLength, p);
475       try
476       {
477         com.stevesoft.pat.Regex rg = com.stevesoft.pat.Regex
478                 .perlCode("/" + regexReplace + "/");
479         if (rg == null)
480         {
481           invalidMessage = "Invalid Regular Expression : '" + regexReplace
482                   + "'\n";
483         }
484       } catch (Exception e)
485       {
486         invalidMessage = "Invalid Regular Expression : '" + regexReplace
487                 + "'\n";
488       }
489     }
490     else
491     {
492       // no regex
493       regexReplace = null;
494       // verify format is really correct.
495       if (link.indexOf(DELIM + varName + DELIM) == sqidPos)
496       {
497         int lastsep = link.lastIndexOf(SEP);
498         if (lastsep < sqidPos + startLength - 1)
499         {
500           // the last SEP character was before the regex, ignore
501           lastsep = link.length();
502         }
503         urlSuffix = link.substring(sqidPos + startLength - 1, lastsep)
504                 .trim();
505         regexReplace = null;
506       }
507       else
508       {
509         invalidMessage = "Warning: invalid regex structure for URL link : "
510                 + link;
511       }
512     }
513
514     return p;
515   }
516
517   /**
518    * Create a set of URL links for a sequence
519    * 
520    * @param seq
521    *          The sequence to create links for
522    * @param linkset
523    *          Map of links: key = id + SEP + link, value = [target, label, id,
524    *          link]
525    */
526   public void createLinksFromSeq(final SequenceI seq,
527           Map<String, List<String>> linkset)
528   {
529     if (seq != null && dynamic)
530     {
531       createDynamicLinks(seq, linkset);
532     }
533     else
534     {
535       createStaticLink(linkset);
536     }
537   }
538
539   /**
540    * Create a static URL link
541    * 
542    * @param linkset
543    *          Map of links: key = id + SEP + link, value = [target, label, id,
544    *          link]
545    */
546   protected void createStaticLink(Map<String, List<String>> linkset)
547   {
548     if (!linkset.containsKey(label + SEP + getUrlPrefix()))
549     {
550       // Add a non-dynamic link
551       linkset.put(label + SEP + getUrlPrefix(),
552               Arrays.asList(target, label, null, getUrlPrefix()));
553     }
554   }
555
556   /**
557    * Create dynamic URL links
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   protected void createDynamicLinks(final SequenceI seq,
566           Map<String, List<String>> linkset)
567   {
568     // collect id string too
569     String id = seq.getName();
570     String descr = seq.getDescription();
571     if (descr != null && descr.length() < 1)
572     {
573       descr = null;
574     }
575
576     if (usesDBAccession()) // link is ID
577     {
578       // collect matching db-refs
579       List<DBRefEntry> dbr = DBRefUtils.selectRefs(seq.getDBRefs(),
580               new String[]
581               { target });
582
583       // if there are any dbrefs which match up with the link
584       if (dbr != null)
585       {
586         for (int r = 0, nd = dbr.size(); r < nd; r++)
587         {
588           // create Bare ID link for this URL
589           createBareURLLink(dbr.get(r).getAccessionId(), true, linkset);
590         }
591       }
592     }
593     else if (!usesDBAccession() && id != null) // link is name
594     {
595       // create Bare ID link for this URL
596       createBareURLLink(id, false, linkset);
597     }
598
599     // Create urls from description but only for URL links which are regex
600     // links
601     if (descr != null && getRegexReplace() != null)
602     {
603       // create link for this URL from description where regex matches
604       createBareURLLink(descr, false, linkset);
605     }
606   }
607
608   /*
609    * Create a bare URL Link
610    * Returns map where key = id + SEP + link, and value = [target, label, id, link]
611    */
612   protected void createBareURLLink(String id, Boolean combineLabel,
613           Map<String, List<String>> linkset)
614   {
615     String[] urls = makeUrls(id, true);
616     if (urls != null)
617     {
618       for (int u = 0; u < urls.length; u += 2)
619       {
620         if (!linkset.containsKey(urls[u] + SEP + urls[u + 1]))
621         {
622           String thisLabel = label;
623           if (combineLabel)
624           {
625             // incorporate label with idstring
626             thisLabel = label + SEP + urls[u];
627           }
628
629           linkset.put(urls[u] + SEP + urls[u + 1],
630                   Arrays.asList(target, thisLabel, urls[u], urls[u + 1]));
631         }
632       }
633     }
634   }
635 }