View Javadoc
1   /**
2    * This file Copyright (c) 2009-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.link;
35  
36  import info.magnolia.cms.beans.config.URI2RepositoryManager;
37  import info.magnolia.cms.core.Content;
38  import info.magnolia.cms.core.NodeData;
39  import info.magnolia.cms.i18n.I18nContentSupport;
40  import info.magnolia.context.MgnlContext;
41  import info.magnolia.jcr.util.NodeTypes;
42  import info.magnolia.objectfactory.Components;
43  import info.magnolia.repository.RepositoryConstants;
44  
45  import java.io.UnsupportedEncodingException;
46  import java.net.URLEncoder;
47  import java.util.Calendar;
48  import java.util.regex.Matcher;
49  import java.util.regex.Pattern;
50  
51  import javax.jcr.Node;
52  import javax.jcr.PathNotFoundException;
53  import javax.jcr.Property;
54  import javax.jcr.PropertyType;
55  import javax.jcr.RepositoryException;
56  import javax.jcr.Session;
57  
58  import org.apache.commons.lang3.StringUtils;
59  import org.apache.commons.lang3.time.FastDateFormat;
60  import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
61  import org.apache.jackrabbit.spi.commons.conversion.PathParser;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  /**
66   * Utility methods for various operations necessary for link transformations and handling.
67   * This is actually a Business Facade providing an entry point to the link transformations.
68   * Hence it will be renamed to LinkManager (MAGNOLIA-4765) soon.
69   */
70  public class LinkUtil {
71  
72      private static final FastDateFormat FINGERPRINT_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd-HH-mm-ss");
73  
74      /**
75       * Pattern that matches external and mailto: links.
76       */
77      public static final Pattern EXTERNAL_LINK_PATTERN = Pattern.compile("^(\\w*://|mailto:|javascript:|tel:).*");
78  
79      public static final String DEFAULT_EXTENSION = "html";
80  
81      public static final String DEFAULT_REPOSITORY = RepositoryConstants.WEBSITE;
82  
83      /**
84       * Pattern to find a link.
85       */
86      public static final Pattern LINK_OR_IMAGE_PATTERN = Pattern.compile(
87              "(<(a|img|embed) " + // start <a or <img
88                      "[^>]*" +  // some attributes
89                      "(href|src)[ ]*=[ ]*\")" + // start href or src
90                      "([^\"]*)" + // the link
91                      "(\"" + // ending "
92                      "[^>]*" + // any attributes
93                      ">)"); // end the tag
94  
95      /**
96       * Pattern to find a magnolia formatted uuid link.
97       */
98      public static Pattern UUID_PATTERN = Pattern.compile(
99              "\\$\\{link:\\{uuid:\\{([^\\}]*)\\}," // the uuid of the node
100                     + "repository:\\{([^\\}]*)\\},"
101                     + "(workspace:\\{[^\\}]*\\},)?" // is not supported anymore
102                     + "(path|handle):\\{([^\\}]*)\\}"        // fallback handle should not be used unless the uuid is invalid
103                     + "(,nodeData:\\{([^\\}]*)\\}," // in case we point to a binary (node data has no uuid!)
104                     + "extension:\\{([^\\}]*)\\})?" // the extension to be used in rendering
105                     + "\\}\\}"  // the handle
106                     + "(#([^\\?\"]*))?" // anchor
107                     + "(\\?([^\"]*))?"); // parameters
108 
109     /**
110      * Pattern to find a link.
111      */
112     public static final Pattern LINK_PATTERN = Pattern.compile(
113             "(/[^\\.\"#\\?]*)" + // the handle
114                     "(\\.([\\w[^#\\?]]+))?" + // extension (if any)
115                     "(#([^\\?\"]*))?" + // anchor
116                     "(\\?([^\"]*))?" // parameters
117     );
118 
119     private static final Logger log = LoggerFactory.getLogger(LinkUtil.class);
120 
121 
122     //-- conversions from UUID - singles
123 
124     /**
125      * Transforms a uuid to a handle beginning with a /. This path is used to get the page from the repository.
126      * The editor needs this kind of links.
127      */
128     public static String convertUUIDtoHandle(String uuid, String workspaceName) throws LinkException {
129         return createLinkInstance(workspaceName, uuid).getPath();
130     }
131 
132     /**
133      * Transforms a uuid to an uri. It does not add the context path. In difference from {@link Link#getHandle()},
134      * this method will apply all uri to repository mappings as well as i18n.
135      */
136     public static String convertUUIDtoURI(String uuid, String workspaceName) throws LinkException {
137         return LinkTransformerManager.getInstance().getAbsolute(false).transform(createLinkInstance(workspaceName, uuid));
138     }
139 
140     //-- conversions to UUID - bulk
141 
142     /**
143      * Parses provided html and transforms all the links to the magnolia format. Used during storing.
144      *
145      * @param html html code with links to be converted
146      * @return html with changed hrefs
147      */
148     public static String convertAbsoluteLinksToUUIDs(String html) {
149         // get all link tags
150         Matcher matcher = LINK_OR_IMAGE_PATTERN.matcher(html);
151         StringBuffer res = new StringBuffer();
152         while (matcher.find()) {
153             final String href = matcher.group(4);
154             if (!isExternalLinkOrAnchor(href)) {
155                 try {
156                     Link link = parseLink(href);
157                     String linkStr = toPattern(link);
158                     linkStr = StringUtils.replace(linkStr, "\\", "\\\\");
159                     linkStr = StringUtils.replace(linkStr, "$", "\\$");
160                     matcher.appendReplacement(res, "$1" + linkStr + "$5");
161                 } catch (LinkException e) {
162                     // this is expected if the link is an absolute path to something else
163                     // than content stored in the repository
164                     matcher.appendReplacement(res, "$0");
165                     log.debug("can't parse link", e);
166                 }
167             } else {
168                 matcher.appendReplacement(res, "$0");
169             }
170         }
171         matcher.appendTail(res);
172         return res.toString();
173     }
174 
175     //-- conversions from UUID - bulk
176 
177     /**
178      * Converts provided html with links in UUID pattern format to any other kind of links based on provided link transformer.
179      *
180      * @param str Html with UUID links
181      * @param transformer Link transformer
182      * @return converted html with links as created by provided transformer.
183      * @see LinkTransformerManager
184      */
185     public static String convertLinksFromUUIDPattern(String str, LinkTransformer transformer) throws LinkException {
186         Matcher matcher = UUID_PATTERN.matcher(str);
187         StringBuffer res = new StringBuffer();
188         while (matcher.find()) {
189             Link link = createLinkInstance(matcher.group(1), matcher.group(2), matcher.group(5), matcher.group(7), matcher.group(8), matcher.group(10), matcher.group(12));
190             String replacement = transformer.transform(link);
191             // Replace "\" with "\\" and "$" with "\$" since Matcher.appendReplacement treats these characters specially
192             replacement = StringUtils.replace(replacement, "\\", "\\\\");
193             replacement = StringUtils.replace(replacement, "$", "\\$");
194             matcher.appendReplacement(res, replacement);
195         }
196         matcher.appendTail(res);
197         return res.toString();
198     }
199 
200     public static String convertLinksFromUUIDPattern(String str) throws LinkException {
201         LinkTransformer transformer = LinkTransformerManager.getInstance().getBrowserLink(null);
202         return convertLinksFromUUIDPattern(str, transformer);
203     }
204 
205     /**
206      * Determines if the given link is internal and relative.
207      */
208     public static boolean isInternalRelativeLink(String href) {
209         // TODO : this could definitely be improved
210         return !isExternalLinkOrAnchor(href) && !href.startsWith("/");
211     }
212 
213     /**
214      * Determines whether the given link is external link or anchor (i.e. returns true for all non translatable links).
215      */
216     public static boolean isExternalLinkOrAnchor(String href) {
217         return LinkUtil.EXTERNAL_LINK_PATTERN.matcher(href).matches() || href.startsWith("#");
218     }
219 
220     /**
221      * Make a absolute path relative. It adds ../ until the root is reached
222      *
223      * @param absolutePath absolute path
224      * @param url page to be relative to
225      * @return relative path
226      */
227     public static String makePathRelative(String url, String absolutePath) {
228         String fromPath = StringUtils.substringBeforeLast(url, "/");
229         String toPath = StringUtils.substringBeforeLast(absolutePath, "/");
230 
231         // reference to parent folder
232         if (StringUtils.equals(fromPath, toPath) && StringUtils.endsWith(absolutePath, "/")) {
233             return ".";
234         }
235 
236         String[] fromDirectories = StringUtils.split(fromPath, "/");
237         String[] toDirectories = StringUtils.split(toPath, "/");
238 
239         int pos = 0;
240         while (pos < fromDirectories.length && pos < toDirectories.length && fromDirectories[pos].equals(toDirectories[pos])) {
241             pos++;
242         }
243 
244         StringBuilder rel = new StringBuilder();
245         for (int i = pos; i < fromDirectories.length; i++) {
246             rel.append("../");
247         }
248 
249         for (int i = pos; i < toDirectories.length; i++) {
250             rel.append(toDirectories[i]).append("/");
251         }
252 
253         rel.append(StringUtils.substringAfterLast(absolutePath, "/"));
254 
255         return rel.toString();
256     }
257 
258     /**
259      * Maps a path to a repository.
260      *
261      * @param path URI
262      * @return repository denoted by the provided URI.
263      */
264     public static String mapPathToRepository(String path) {
265         String workspaceName = getURI2RepositoryManager().getRepository(path);
266         if (StringUtils.isEmpty(workspaceName)) {
267             workspaceName = DEFAULT_REPOSITORY;
268         }
269         return workspaceName;
270     }
271 
272     /**
273      * Appends a parameter to the given url, using ?, or & if there are already
274      * parameters in the given url. <strong>Warning:</strong> It does not
275      * <strong>replace</strong> an existing parameter with the same name.
276      */
277     public static void addParameter(StringBuffer uri, String name, String value) {
278         if (uri.indexOf("?") < 0) {
279             uri.append('?');
280         } else {
281             uri.append('&');
282         }
283         uri.append(name).append('=');
284         try {
285             uri.append(URLEncoder.encode(value, "UTF-8"));
286         } catch (UnsupportedEncodingException e) {
287             throw new RuntimeException("It seems your system does not support UTF-8 !?", e);
288         }
289     }
290 
291     /**
292      * Creates absolute link including context path for provided node data.
293      *
294      * @param nodedata Node data to create link for.
295      * @return Absolute link to the provided node data.
296      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
297      * @deprecated since 5.0 use {@link #createAbsoluteLink(Property)} instead.
298      */
299     @Deprecated
300     public static String createAbsoluteLink(NodeData nodedata) throws LinkException {
301         if (nodedata == null || !nodedata.isExist()) {
302             return null;
303         }
304         try {
305             if (nodedata.getType() != PropertyType.BINARY) {
306                 return createAbsoluteLink(nodedata.getJCRProperty());
307             }
308             return createAbsoluteLink(MgnlContext.getJCRSession(nodedata.getHierarchyManager().getWorkspace().getName()).getNode(nodedata.getHandle()));
309         } catch (RepositoryException e) {
310             throw new LinkException(e);
311         }
312     }
313 
314     /**
315      * Creates absolute link including context path for provided Property.
316      *
317      * @return Absolute link to the provided Property.
318      */
319     public static String createAbsoluteLink(Property property) throws LinkException {
320         if (property == null) {
321             return null;
322         }
323         return LinkTransformerManager.getInstance().getAbsolute().transform(createLinkInstance(property));
324     }
325 
326     /**
327      * Creates absolute link including context path to the provided content and performing all URI2Repository mappings and applying locales.
328      *
329      * @param uuid UUID of content to create link to.
330      * @param workspaceName Name of the repository where content is located.
331      * @return Absolute link to the provided content.
332      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
333      */
334     public static String createAbsoluteLink(String workspaceName, String uuid) throws RepositoryException {
335         Node jcrNode = MgnlContext.getJCRSession(workspaceName).getNodeByIdentifier(uuid);
336         return createAbsoluteLink(jcrNode);
337     }
338 
339     /**
340      * Creates absolute link including context path to the provided content and performing all URI2Repository mappings and applying locales.
341      *
342      * @param content content to create link to.
343      * @return Absolute link to the provided content.
344      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
345      * @deprecated since 5.0 use {@link #createAbsoluteLink(Node)} instead.
346      */
347     @Deprecated
348     public static String createAbsoluteLink(Content content) {
349         if (content == null) {
350             return null;
351         }
352         return createAbsoluteLink(content.getJCRNode());
353     }
354 
355     /**
356      * Creates absolute link including context path to the provided node and performing all URI2Repository mappings and applying locales.
357      *
358      * @return Absolute link to the provided content.
359      */
360     public static String createAbsoluteLink(Node node) {
361         if (node == null) {
362             return null;
363         }
364         return LinkTransformerManager.getInstance().getAbsolute().transform(createLinkInstance(node));
365     }
366 
367     /**
368      * Creates a complete url to access given content from external systems applying all the URI2Repository mappings and locales.
369      *
370      * @deprecated since 5.0 use {@link #createExternalLink(Node)} instead.
371      */
372     @Deprecated
373     public static String createExternalLink(Content content) {
374         if (content == null) {
375             return null;
376         }
377         return createExternalLink(content.getJCRNode());
378     }
379 
380     /**
381      * Creates a complete url to access given node from external systems applying all the URI2Repository mappings and locales.
382      */
383     public static String createExternalLink(Node node) {
384         if (node == null) {
385             return null;
386         }
387         return LinkTransformerManager.getInstance().getCompleteUrl().transform(createLinkInstance(node));
388     }
389 
390     /**
391      * Creates link guessing best possible link format from current site and provided node.
392      *
393      * @param node Node to create link for.
394      * @return Absolute link to the provided node data.
395      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
396      * @deprecated since 5.0 use {@link #createLink(Node)} instead.
397      */
398     @Deprecated
399     public static String createLink(Content node) {
400         if (node == null) {
401             return null;
402         }
403         return createLink(node.getJCRNode());
404     }
405 
406     /**
407      * Creates link guessing best possible link format from current site and provided node.
408      *
409      * @param node Node to create link for.
410      * @return Absolute link to the provided Node.
411      */
412     public static String createLink(Node node) {
413         if (node == null) {
414             return null;
415         }
416         try {
417             return LinkTransformerManager.getInstance().getBrowserLink(node.getPath()).transform(createLinkInstance(node));
418         } catch (RepositoryException e) {
419             log.debug(e.getMessage(), e);
420         }
421         return null;
422     }
423 
424     /**
425      * Creates link guessing best possible link format from current site and provided node data.
426      *
427      * @param nodedata Node data to create link for.
428      * @return Absolute link to the provided node data.
429      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
430      * @deprecated Since 5.0 use {@link #createAbsoluteLink(Property)} instead.
431      */
432     @Deprecated
433     public static String createLink(NodeData nodedata) throws LinkException {
434         if (nodedata == null || !nodedata.isExist()) {
435             return null;
436         }
437         try {
438             if (nodedata.getType() != PropertyType.BINARY) {
439                 return createLink(nodedata.getJCRProperty());
440             }
441             return createLink(MgnlContext.getJCRSession(nodedata.getHierarchyManager().getWorkspace().getName()).getNode(nodedata.getHandle()));
442         } catch (RepositoryException e) {
443             throw new LinkException(e.getMessage(), e);
444         }
445     }
446 
447     /**
448      * Creates link guessing best possible link format from current site and provided Property.
449      *
450      * @param property Property to create link for.
451      * @return Absolute link to the provided Property.
452      */
453     public static String createLink(Property property) throws LinkException {
454         if (property == null) {
455             return null;
456         }
457         try {
458             return LinkTransformerManager.getInstance().getBrowserLink(property.getParent().getPath()).transform(createLinkInstance(property));
459         } catch (RepositoryException e) {
460             throw new LinkException(e.getMessage(), e);
461         }
462     }
463 
464     /**
465      * Creates link guessing best possible link format from current site and provided content.
466      *
467      * @param uuid UUID of content to create link to.
468      * @param workspaceName Name of the repository where content is located.
469      * @return Absolute link to the provided content.
470      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
471      */
472     public static String createLink(String workspaceName, String uuid) throws RepositoryException {
473         Node node = MgnlContext.getJCRSession(workspaceName).getNodeByIdentifier(uuid);
474         return createLink(node);
475     }
476 
477     /**
478      * Creates Link to provided Content.
479      *
480      * @return Link to provided Content.
481      * @deprecated since 5.0 use {@link #createLinkInstance(Node)} instead.
482      */
483     @Deprecated
484     public static Link createLinkInstance(Content node) {
485         return createLinkInstance(node.getJCRNode());
486     }
487 
488     /**
489      * Creates Link to provided Node.
490      *
491      * @return Link to provided Node.
492      */
493     public static Link createLinkInstance(Node node) {
494         return new Link(node);
495     }
496 
497     /**
498      * Creates Link to provided NodeData.
499      *
500      * @return Link to provided NodeData.
501      * @deprecated since 5.0 use {@link #createLinkInstance(Property)} instead.
502      */
503     @Deprecated
504     public static Link createLinkInstance(NodeData nodeData) throws LinkException {
505         try {
506             if (nodeData.getType() != PropertyType.BINARY) {
507                 return createLinkInstance(nodeData.getJCRProperty());
508             }
509             return createLinkInstance(MgnlContext.getJCRSession(nodeData.getHierarchyManager().getWorkspace().getName()).getNode(nodeData.getHandle()));
510         } catch (RepositoryException e) {
511             throw new LinkException("can't find node " + nodeData, e);
512         }
513     }
514 
515     public static Link createLinkInstance(Property property) throws LinkException {
516         return new Link(property);
517     }
518 
519     /**
520      * Creates link to the content denoted by repository and uuid.
521      *
522      * @param workspaceName Parent repository of the content of interest.
523      * @param uuid UUID of the content to create link to.
524      * @return link to the content with provided UUID.
525      */
526     public static Link createLinkInstance(String workspaceName, String uuid) throws LinkException {
527         try {
528             return new Link(MgnlContext.getJCRSession(workspaceName).getNodeByIdentifier(uuid));
529         } catch (RepositoryException e) {
530             throw new LinkException("can't get node with uuid " + uuid + " and repository " + workspaceName);
531         }
532     }
533 
534     /**
535      * Creates link to the content identified by the repository and path. Link will use specified extension and will also contain the anchor and parameters if specified.
536      *
537      * @param workspaceName Source repository for the content.
538      * @param path Path to the content of interest.
539      * @param extension Optional extension to be used in the link
540      * @param anchor Optional link anchor.
541      * @param parameters Optional link parameters.
542      * @return Link pointing to the content denoted by repository and path including extension, anchor and parameters if such were provided.
543      */
544     public static Link createLinkInstance(String workspaceName, String path, String extension, String anchor, String parameters) throws LinkException {
545         Node node = null;
546         String fileName = null;
547         String nodeDataName = null;
548         Property property = null;
549         try {
550             Session session = MgnlContext.getJCRSession(workspaceName);
551 
552             boolean exists = false;
553             try {
554                 PathParser.checkFormat(path);
555             } catch (MalformedPathException e) {
556                 // we first check for path incl. the file name. While file name might not be necessarily part of the path, it might contain also non ascii chars. If that is the case, parsing exception will occur so we know that path with filename can't exist.
557             }
558             exists = session.itemExists(path) && !session.propertyExists(path);
559             if (exists) {
560                 node = session.getNode(path);
561             }
562             if (node == null) {
563                 if (session.nodeExists(path)) {
564                     node = session.getNode(path);
565                 }
566                 if (node != null && node.isNodeType(NodeTypes.Resource.NAME) && node.hasProperty("fileName")) {
567                     fileName = node.getProperty("fileName").getString();
568                 }
569                 if (session.propertyExists(path)) {
570                     nodeDataName = StringUtils.substringAfterLast(path, "/");
571                     path = StringUtils.substringBeforeLast(path, "/");
572                     property = node.getProperty(nodeDataName);
573                 }
574             }
575             if (node == null) {
576                 throw new LinkException("can't find node " + path + " in repository " + workspaceName);
577             }
578         } catch (RepositoryException e) {
579             throw new LinkException("can't get node with path " + path + " from repository " + workspaceName);
580         }
581 
582         Link link = new Link(node);
583         link.setAnchor(anchor);
584         link.setExtension(extension);
585         link.setParameters(parameters);
586         link.setFileName(fileName);
587         link.setPropertyName(nodeDataName);
588         link.setProperty(property);
589         link.setPath(path);
590         return link;
591     }
592 
593     /**
594      * Creates link based on provided parameters. Should the uuid be non existent or the fallback handle invalid, creates nonetheless an <em>"undefined"</em> {@link Link} object,
595      * pointing to the non existing uuid so that broken link detection tools can find it.
596      *
597      * @param uuid UUID of the content
598      * @param workspaceName Content repository name.
599      * @param fallbackHandle Optional fallback content handle.
600      * @param nodeDataName Content node data name for binary data.
601      * @param extension Optional link extension.
602      * @param anchor Optional link anchor.
603      * @param parameters Optional link parameters.
604      * @return Link pointing to the content denoted by uuid and repository. Link is created using all provided optional values if present.
605      */
606     public static Link createLinkInstance(String uuid, String workspaceName, String fallbackHandle, String nodeDataName, String extension, String anchor, String parameters) throws LinkException {
607         final String defaultRepository = StringUtils.defaultIfEmpty(workspaceName, RepositoryConstants.WEBSITE);
608         Link link;
609         try {
610             link = createLinkInstance(defaultRepository, uuid);
611         } catch (LinkException e) {
612             try {
613                 final Node node = MgnlContext.getJCRSession(defaultRepository).getNode(fallbackHandle != null ? fallbackHandle : "");
614                 link = createLinkInstance(node);
615             } catch (PathNotFoundException pnfe) {
616                 log.warn("Can't find node with uuid {} or handle {} in repository {}", uuid, fallbackHandle, defaultRepository);
617                 link = new Link();
618                 link.setUUID(uuid);
619             } catch (RepositoryException re) {
620                 log.warn("Can't find node with uuid {} or handle {} in repository {}", uuid, fallbackHandle, defaultRepository);
621                 link = new Link();
622                 link.setUUID(uuid);
623             }
624         }
625         link.setFallbackPath(fallbackHandle);
626         link.setPropertyName(nodeDataName);
627         link.setExtension(extension);
628         link.setAnchor(anchor);
629         link.setParameters(parameters);
630 
631         return link;
632     }
633 
634     /**
635      * Parses UUID link pattern string and converts it into a Link object.
636      *
637      * @param uuidLink String containing reference to content as a UUID link pattern.
638      * @return Link to content referenced in the provided text.
639      */
640     public static Link parseUUIDLink(String uuidLink) throws LinkException {
641         Matcher matcher = UUID_PATTERN.matcher(uuidLink);
642         if (matcher.matches()) {
643             return createLinkInstance(matcher.group(1), matcher.group(2), matcher.group(5), matcher.group(7), matcher.group(8), matcher.group(10), matcher.group(12));
644         }
645         throw new LinkException("can't parse [ " + uuidLink + "]");
646     }
647 
648     /**
649      * Parses provided URI to the link.
650      *
651      * @param link URI representing path to piece of content
652      * @return Link pointing to the content represented by provided URI
653      */
654     public static Link parseLink(String link) throws LinkException {
655         // ignore context handle if existing
656         link = StringUtils.removeStart(link, MgnlContext.getContextPath());
657 
658         Matcher matcher = LINK_PATTERN.matcher(link);
659         if (matcher.matches()) {
660             String orgHandle = matcher.group(1);
661             orgHandle = Components.getComponent(I18nContentSupport.class).toRawURI(orgHandle);
662             String workspaceName = getURI2RepositoryManager().getRepository(orgHandle);
663             String handle = getURI2RepositoryManager().getHandle(orgHandle);
664             return createLinkInstance(workspaceName, handle, matcher.group(3), matcher.group(5), matcher.group(7));
665         }
666         throw new LinkException("can't parse [ " + link + "]");
667     }
668 
669     /**
670      * Converts provided Link to an UUID link pattern.
671      *
672      * @param link Link to convert.
673      * @return UUID link pattern representation of provided link.
674      */
675     public static String toPattern(Link link) {
676         return "${link:{"
677                 + "uuid:{" + link.getUUID() + "},"
678                 + "repository:{" + link.getWorkspace() + "},"
679                 + "path:{" + link.getPath() + "}," // original handle represented by the uuid
680                 + "nodeData:{" + StringUtils.defaultString(link.getNodeDataName()) + "}," // in case of binaries
681                 + "extension:{" + StringUtils.defaultString(link.getExtension()) + "}" // the extension to use if no extension can be resolved otherwise
682                 + "}}"
683                 + (StringUtils.isNotEmpty(link.getAnchor()) ? "#" + link.getAnchor() : "")
684                 + (StringUtils.isNotEmpty(link.getParameters()) ? "?" + link.getParameters() : "");
685     }
686 
687     private static URI2RepositoryManager getURI2RepositoryManager() {
688         return Components.getComponent(URI2RepositoryManager.class);
689     }
690 
691     /**
692      * Insert a finger-print into a link that is based on the last modified date.
693      * This way we can use far future cache headers on images/assets, and simply change the filename for the
694      * asset when the asset has changed and we want browsers & proxies to take up the new asset from the server.
695      * Appends the date as a string directly before the file extension.
696      *
697      * @return The original link with the date based finger-print inserted, null if the passed link is null or the original link if lastModified is null.
698      */
699     public static String addFingerprintToLink(String link, Calendar lastModified) {
700         if (StringUtils.isBlank(link)) {
701             return null;
702         }
703         String fingerprintedLink = "";
704         if (lastModified == null) {
705             return link;
706         }
707 
708         String fingerprint = FINGERPRINT_FORMAT.format(lastModified.getTime());
709 
710         // Determine where to place the fingerprint.
711         int lastDot = link.lastIndexOf('.');
712         int lastSlash = link.lastIndexOf('/');
713 
714         if (lastDot > lastSlash && lastDot != -1) {
715             fingerprintedLink = link.substring(0, lastDot) + "." + fingerprint + link.substring(lastDot);
716         } else {
717             // No file extension - just add fingerprint at end.
718             fingerprintedLink = link + "." + fingerprint;
719         }
720 
721         return fingerprintedLink;
722     }
723 
724     /**
725      * Remove the extension and fingerPrint if present.
726      * Example: (print-logo.2012-11-20-12-15-20.pdf --> print-logo)
727      */
728     public static String removeFingerprintAndExtensionFromLink(String originalPath) {
729 
730         String subPath = StringUtils.substringBeforeLast(originalPath, ".");
731         // Get Finger print
732         String fingerPrint = StringUtils.substringAfterLast(subPath, ".");
733         if (fingerPrint != null && fingerPrint.matches("\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}")) {
734             return StringUtils.substringBeforeLast(subPath, ".");
735         } else {
736             return subPath;
737         }
738     }
739 }