View Javadoc
1   /**
2    * This file Copyright (c) 2009-2017 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         if (html == null) {
150             return null;
151         }
152         // get all link tags
153         Matcher matcher = LINK_OR_IMAGE_PATTERN.matcher(html);
154         StringBuffer res = new StringBuffer();
155         while (matcher.find()) {
156             final String href = matcher.group(4);
157             if (!isExternalLinkOrAnchor(href)) {
158                 try {
159                     Link link = parseLink(href);
160                     String linkStr = toPattern(link);
161                     linkStr = StringUtils.replace(linkStr, "\\", "\\\\");
162                     linkStr = StringUtils.replace(linkStr, "$", "\\$");
163                     matcher.appendReplacement(res, "$1" + linkStr + "$5");
164                 } catch (LinkException e) {
165                     // this is expected if the link is an absolute path to something else
166                     // than content stored in the repository
167                     matcher.appendReplacement(res, "$0");
168                     log.debug("can't parse link", e);
169                 }
170             } else {
171                 matcher.appendReplacement(res, "$0");
172             }
173         }
174         matcher.appendTail(res);
175         return res.toString();
176     }
177 
178     //-- conversions from UUID - bulk
179 
180     /**
181      * Converts provided html with links in UUID pattern format to any other kind of links based on provided link transformer.
182      *
183      * @param str Html with UUID links
184      * @param transformer Link transformer
185      * @return converted html with links as created by provided transformer.
186      * @see LinkTransformerManager
187      */
188     public static String convertLinksFromUUIDPattern(String str, LinkTransformer transformer) throws LinkException {
189         if (str == null) {
190             return null;
191         }
192         Matcher matcher = UUID_PATTERN.matcher(str);
193         StringBuffer res = new StringBuffer();
194         while (matcher.find()) {
195             Link link = createLinkInstance(matcher.group(1), matcher.group(2), matcher.group(5), matcher.group(7), matcher.group(8), matcher.group(10), matcher.group(12));
196             String replacement = transformer.transform(link);
197             // Replace "\" with "\\" and "$" with "\$" since Matcher.appendReplacement treats these characters specially
198             replacement = StringUtils.replace(replacement, "\\", "\\\\");
199             replacement = StringUtils.replace(replacement, "$", "\\$");
200             matcher.appendReplacement(res, replacement);
201         }
202         matcher.appendTail(res);
203         return res.toString();
204     }
205 
206     public static String convertLinksFromUUIDPattern(String str) throws LinkException {
207         LinkTransformer transformer = LinkTransformerManager.getInstance().getBrowserLink(null);
208         return convertLinksFromUUIDPattern(str, transformer);
209     }
210 
211     /**
212      * Determines if the given link is internal and relative.
213      */
214     public static boolean isInternalRelativeLink(String href) {
215         // TODO : this could definitely be improved
216         return !isExternalLinkOrAnchor(href) && !href.startsWith("/");
217     }
218 
219     /**
220      * Determines whether the given link is external link or anchor (i.e. returns true for all non translatable links).
221      */
222     public static boolean isExternalLinkOrAnchor(String href) {
223         return LinkUtil.EXTERNAL_LINK_PATTERN.matcher(href).matches() || href.startsWith("#");
224     }
225 
226     /**
227      * Make a absolute path relative. It adds ../ until the root is reached
228      *
229      * @param absolutePath absolute path
230      * @param url page to be relative to
231      * @return relative path
232      */
233     public static String makePathRelative(String url, String absolutePath) {
234         String fromPath = StringUtils.substringBeforeLast(url, "/");
235         String toPath = StringUtils.substringBeforeLast(absolutePath, "/");
236 
237         // reference to parent folder
238         if (StringUtils.equals(fromPath, toPath) && StringUtils.endsWith(absolutePath, "/")) {
239             return ".";
240         }
241 
242         String[] fromDirectories = StringUtils.split(fromPath, "/");
243         String[] toDirectories = StringUtils.split(toPath, "/");
244 
245         int pos = 0;
246         while (pos < fromDirectories.length && pos < toDirectories.length && fromDirectories[pos].equals(toDirectories[pos])) {
247             pos++;
248         }
249 
250         StringBuilder rel = new StringBuilder();
251         for (int i = pos; i < fromDirectories.length; i++) {
252             rel.append("../");
253         }
254 
255         for (int i = pos; i < toDirectories.length; i++) {
256             rel.append(toDirectories[i]).append("/");
257         }
258 
259         rel.append(StringUtils.substringAfterLast(absolutePath, "/"));
260 
261         return rel.toString();
262     }
263 
264     /**
265      * Maps a path to a repository.
266      *
267      * @param path URI
268      * @return repository denoted by the provided URI.
269      */
270     public static String mapPathToRepository(String path) {
271         String workspaceName = getURI2RepositoryManager().getRepository(path);
272         if (StringUtils.isEmpty(workspaceName)) {
273             workspaceName = DEFAULT_REPOSITORY;
274         }
275         return workspaceName;
276     }
277 
278     /**
279      * Appends a parameter to the given url, using ?, or & if there are already
280      * parameters in the given url. <strong>Warning:</strong> It does not
281      * <strong>replace</strong> an existing parameter with the same name.
282      */
283     public static void addParameter(StringBuffer uri, String name, String value) {
284         if (uri.indexOf("?") < 0) {
285             uri.append('?');
286         } else {
287             uri.append('&');
288         }
289         uri.append(name).append('=');
290         try {
291             uri.append(URLEncoder.encode(value, "UTF-8"));
292         } catch (UnsupportedEncodingException e) {
293             throw new RuntimeException("It seems your system does not support UTF-8 !?", e);
294         }
295     }
296 
297     /**
298      * Creates absolute link including context path for provided node data.
299      *
300      * @param nodedata Node data to create link for.
301      * @return Absolute link to the provided node data.
302      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
303      * @deprecated since 5.0 use {@link #createAbsoluteLink(Property)} instead.
304      */
305     @Deprecated
306     public static String createAbsoluteLink(NodeData nodedata) throws LinkException {
307         if (nodedata == null || !nodedata.isExist()) {
308             return null;
309         }
310         try {
311             if (nodedata.getType() != PropertyType.BINARY) {
312                 return createAbsoluteLink(nodedata.getJCRProperty());
313             }
314             return createAbsoluteLink(MgnlContext.getJCRSession(nodedata.getHierarchyManager().getWorkspace().getName()).getNode(nodedata.getHandle()));
315         } catch (RepositoryException e) {
316             throw new LinkException(e);
317         }
318     }
319 
320     /**
321      * Creates absolute link including context path for provided Property.
322      *
323      * @return Absolute link to the provided Property.
324      */
325     public static String createAbsoluteLink(Property property) throws LinkException {
326         if (property == null) {
327             return null;
328         }
329         return LinkTransformerManager.getInstance().getAbsolute().transform(createLinkInstance(property));
330     }
331 
332     /**
333      * Creates absolute link including context path to the provided content and performing all URI2Repository mappings and applying locales.
334      *
335      * @param uuid UUID of content to create link to.
336      * @param workspaceName Name of the repository where content is located.
337      * @return Absolute link to the provided content.
338      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
339      */
340     public static String createAbsoluteLink(String workspaceName, String uuid) throws RepositoryException {
341         Node jcrNode = MgnlContext.getJCRSession(workspaceName).getNodeByIdentifier(uuid);
342         return createAbsoluteLink(jcrNode);
343     }
344 
345     /**
346      * Creates absolute link including context path to the provided content and performing all URI2Repository mappings and applying locales.
347      *
348      * @param content content to create link to.
349      * @return Absolute link to the provided content.
350      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
351      * @deprecated since 5.0 use {@link #createAbsoluteLink(Node)} instead.
352      */
353     @Deprecated
354     public static String createAbsoluteLink(Content content) {
355         if (content == null) {
356             return null;
357         }
358         return createAbsoluteLink(content.getJCRNode());
359     }
360 
361     /**
362      * Creates absolute link including context path to the provided node and performing all URI2Repository mappings and applying locales.
363      *
364      * @return Absolute link to the provided content.
365      */
366     public static String createAbsoluteLink(Node node) {
367         if (node == null) {
368             return null;
369         }
370         return LinkTransformerManager.getInstance().getAbsolute().transform(createLinkInstance(node));
371     }
372 
373     /**
374      * Creates a complete url to access given content from external systems applying all the URI2Repository mappings and locales.
375      *
376      * @deprecated since 5.0 use {@link #createExternalLink(Node)} instead.
377      */
378     @Deprecated
379     public static String createExternalLink(Content content) {
380         if (content == null) {
381             return null;
382         }
383         return createExternalLink(content.getJCRNode());
384     }
385 
386     /**
387      * Creates a complete url to access given node from external systems applying all the URI2Repository mappings and locales.
388      */
389     public static String createExternalLink(Node node) {
390         if (node == null) {
391             return null;
392         }
393         return LinkTransformerManager.getInstance().getCompleteUrl().transform(createLinkInstance(node));
394     }
395 
396     /**
397      * Creates link guessing best possible link format from current site and provided node.
398      *
399      * @param node Node to create link for.
400      * @return Absolute link to the provided node data.
401      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
402      * @deprecated since 5.0 use {@link #createLink(Node)} instead.
403      */
404     @Deprecated
405     public static String createLink(Content node) {
406         if (node == null) {
407             return null;
408         }
409         return createLink(node.getJCRNode());
410     }
411 
412     /**
413      * Creates link guessing best possible link format from current site and provided node.
414      *
415      * @param node Node to create link for.
416      * @return Absolute link to the provided Node.
417      */
418     public static String createLink(Node node) {
419         if (node == null) {
420             return null;
421         }
422         try {
423             return LinkTransformerManager.getInstance().getBrowserLink(node.getPath()).transform(createLinkInstance(node));
424         } catch (RepositoryException e) {
425             log.debug(e.getMessage(), e);
426         }
427         return null;
428     }
429 
430     /**
431      * Creates link guessing best possible link format from current site and provided node data.
432      *
433      * @param nodedata Node data to create link for.
434      * @return Absolute link to the provided node data.
435      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
436      * @deprecated Since 5.0 use {@link #createAbsoluteLink(Property)} instead.
437      */
438     @Deprecated
439     public static String createLink(NodeData nodedata) throws LinkException {
440         if (nodedata == null || !nodedata.isExist()) {
441             return null;
442         }
443         try {
444             if (nodedata.getType() != PropertyType.BINARY) {
445                 return createLink(nodedata.getJCRProperty());
446             }
447             return createLink(MgnlContext.getJCRSession(nodedata.getHierarchyManager().getWorkspace().getName()).getNode(nodedata.getHandle()));
448         } catch (RepositoryException e) {
449             throw new LinkException(e.getMessage(), e);
450         }
451     }
452 
453     /**
454      * Creates link guessing best possible link format from current site and provided Property.
455      *
456      * @param property Property to create link for.
457      * @return Absolute link to the provided Property.
458      */
459     public static String createLink(Property property) throws LinkException {
460         if (property == null) {
461             return null;
462         }
463         try {
464             return LinkTransformerManager.getInstance().getBrowserLink(property.getParent().getPath()).transform(createLinkInstance(property));
465         } catch (RepositoryException e) {
466             throw new LinkException(e.getMessage(), e);
467         }
468     }
469 
470     /**
471      * Creates link guessing best possible link format from current site and provided content.
472      *
473      * @param uuid UUID of content to create link to.
474      * @param workspaceName Name of the repository where content is located.
475      * @return Absolute link to the provided content.
476      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
477      */
478     public static String createLink(String workspaceName, String uuid) throws RepositoryException {
479         Node node = MgnlContext.getJCRSession(workspaceName).getNodeByIdentifier(uuid);
480         return createLink(node);
481     }
482 
483     /**
484      * Creates Link to provided Content.
485      *
486      * @return Link to provided Content.
487      * @deprecated since 5.0 use {@link #createLinkInstance(Node)} instead.
488      */
489     @Deprecated
490     public static Link createLinkInstance(Content node) {
491         return createLinkInstance(node.getJCRNode());
492     }
493 
494     /**
495      * Creates Link to provided Node.
496      *
497      * @return Link to provided Node.
498      */
499     public static Link createLinkInstance(Node node) {
500         return new Link(node);
501     }
502 
503     /**
504      * Creates Link to provided NodeData.
505      *
506      * @return Link to provided NodeData.
507      * @deprecated since 5.0 use {@link #createLinkInstance(Property)} instead.
508      */
509     @Deprecated
510     public static Link createLinkInstance(NodeData nodeData) throws LinkException {
511         try {
512             if (nodeData.getType() != PropertyType.BINARY) {
513                 return createLinkInstance(nodeData.getJCRProperty());
514             }
515             return createLinkInstance(MgnlContext.getJCRSession(nodeData.getHierarchyManager().getWorkspace().getName()).getNode(nodeData.getHandle()));
516         } catch (RepositoryException e) {
517             throw new LinkException("can't find node " + nodeData, e);
518         }
519     }
520 
521     public static Link createLinkInstance(Property property) throws LinkException {
522         return new Link(property);
523     }
524 
525     /**
526      * Creates link to the content denoted by repository and uuid.
527      *
528      * @param workspaceName Parent repository of the content of interest.
529      * @param uuid UUID of the content to create link to.
530      * @return link to the content with provided UUID.
531      */
532     public static Link createLinkInstance(String workspaceName, String uuid) throws LinkException {
533         try {
534             return new Link(MgnlContext.getJCRSession(workspaceName).getNodeByIdentifier(uuid));
535         } catch (RepositoryException e) {
536             throw new LinkException("can't get node with uuid " + uuid + " and repository " + workspaceName);
537         }
538     }
539 
540     /**
541      * 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.
542      *
543      * @param workspaceName Source repository for the content.
544      * @param path Path to the content of interest.
545      * @param extension Optional extension to be used in the link
546      * @param anchor Optional link anchor.
547      * @param parameters Optional link parameters.
548      * @return Link pointing to the content denoted by repository and path including extension, anchor and parameters if such were provided.
549      */
550     public static Link createLinkInstance(String workspaceName, String path, String extension, String anchor, String parameters) throws LinkException {
551         Node node = null;
552         String fileName = null;
553         String nodeDataName = null;
554         Property property = null;
555         try {
556             Session session = MgnlContext.getJCRSession(workspaceName);
557 
558             boolean exists = false;
559             try {
560                 PathParser.checkFormat(path);
561             } catch (MalformedPathException e) {
562                 // 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.
563             }
564             exists = session.itemExists(path) && !session.propertyExists(path);
565             if (exists) {
566                 node = session.getNode(path);
567             }
568             if (node == null) {
569                 if (session.nodeExists(path)) {
570                     node = session.getNode(path);
571                 }
572                 if (node != null && node.isNodeType(NodeTypes.Resource.NAME) && node.hasProperty("fileName")) {
573                     fileName = node.getProperty("fileName").getString();
574                 }
575                 if (session.propertyExists(path)) {
576                     nodeDataName = StringUtils.substringAfterLast(path, "/");
577                     path = StringUtils.substringBeforeLast(path, "/");
578                     property = node.getProperty(nodeDataName);
579                 }
580             }
581             if (node == null) {
582                 throw new LinkException("can't find node " + path + " in repository " + workspaceName);
583             }
584         } catch (RepositoryException e) {
585             throw new LinkException("can't get node with path " + path + " from repository " + workspaceName);
586         }
587 
588         Link link = new Link(node);
589         link.setAnchor(anchor);
590         link.setExtension(extension);
591         link.setParameters(parameters);
592         link.setFileName(fileName);
593         link.setPropertyName(nodeDataName);
594         link.setProperty(property);
595         link.setPath(path);
596         return link;
597     }
598 
599     /**
600      * 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,
601      * pointing to the non existing uuid so that broken link detection tools can find it.
602      *
603      * @param uuid UUID of the content
604      * @param workspaceName Content repository name.
605      * @param fallbackHandle Optional fallback content handle.
606      * @param nodeDataName Content node data name for binary data.
607      * @param extension Optional link extension.
608      * @param anchor Optional link anchor.
609      * @param parameters Optional link parameters.
610      * @return Link pointing to the content denoted by uuid and repository. Link is created using all provided optional values if present.
611      */
612     public static Link createLinkInstance(String uuid, String workspaceName, String fallbackHandle, String nodeDataName, String extension, String anchor, String parameters) throws LinkException {
613         final String defaultRepository = StringUtils.defaultIfEmpty(workspaceName, RepositoryConstants.WEBSITE);
614         Link link;
615         try {
616             link = createLinkInstance(defaultRepository, uuid);
617         } catch (LinkException e) {
618             try {
619                 final Node node = MgnlContext.getJCRSession(defaultRepository).getNode(fallbackHandle != null ? fallbackHandle : "");
620                 link = createLinkInstance(node);
621             } catch (PathNotFoundException pnfe) {
622                 log.warn("Can't find node with uuid {} or handle {} in repository {}", uuid, fallbackHandle, defaultRepository);
623                 link = new Link();
624                 link.setUUID(uuid);
625             } catch (RepositoryException re) {
626                 log.warn("Can't find node with uuid {} or handle {} in repository {}", uuid, fallbackHandle, defaultRepository);
627                 link = new Link();
628                 link.setUUID(uuid);
629             }
630         }
631         link.setFallbackPath(fallbackHandle);
632         link.setPropertyName(nodeDataName);
633         link.setExtension(extension);
634         link.setAnchor(anchor);
635         link.setParameters(parameters);
636 
637         return link;
638     }
639 
640     /**
641      * Parses UUID link pattern string and converts it into a Link object.
642      *
643      * @param uuidLink String containing reference to content as a UUID link pattern.
644      * @return Link to content referenced in the provided text.
645      */
646     public static Link parseUUIDLink(String uuidLink) throws LinkException {
647         Matcher matcher = UUID_PATTERN.matcher(uuidLink);
648         if (matcher.matches()) {
649             return createLinkInstance(matcher.group(1), matcher.group(2), matcher.group(5), matcher.group(7), matcher.group(8), matcher.group(10), matcher.group(12));
650         }
651         throw new LinkException("can't parse [ " + uuidLink + "]");
652     }
653 
654     /**
655      * Parses provided URI to the link.
656      *
657      * @param link URI representing path to piece of content
658      * @return Link pointing to the content represented by provided URI
659      */
660     public static Link parseLink(String link) throws LinkException {
661         // ignore context handle if existing
662         link = StringUtils.removeStart(link, MgnlContext.getContextPath());
663 
664         Matcher matcher = LINK_PATTERN.matcher(link);
665         if (matcher.matches()) {
666             String orgHandle = matcher.group(1);
667             orgHandle = Components.getComponent(I18nContentSupport.class).toRawURI(orgHandle);
668             String workspaceName = getURI2RepositoryManager().getRepository(orgHandle);
669             String handle = getURI2RepositoryManager().getHandle(orgHandle);
670             return createLinkInstance(workspaceName, handle, matcher.group(3), matcher.group(5), matcher.group(7));
671         }
672         throw new LinkException("can't parse [ " + link + "]");
673     }
674 
675     /**
676      * Converts provided Link to an UUID link pattern.
677      *
678      * @param link Link to convert.
679      * @return UUID link pattern representation of provided link.
680      */
681     public static String toPattern(Link link) {
682         return "${link:{"
683                 + "uuid:{" + link.getUUID() + "},"
684                 + "repository:{" + link.getWorkspace() + "},"
685                 + "path:{" + link.getPath() + "}," // original handle represented by the uuid
686                 + "nodeData:{" + StringUtils.defaultString(link.getNodeDataName()) + "}," // in case of binaries
687                 + "extension:{" + StringUtils.defaultString(link.getExtension()) + "}" // the extension to use if no extension can be resolved otherwise
688                 + "}}"
689                 + (StringUtils.isNotEmpty(link.getAnchor()) ? "#" + link.getAnchor() : "")
690                 + (StringUtils.isNotEmpty(link.getParameters()) ? "?" + link.getParameters() : "");
691     }
692 
693     private static URI2RepositoryManager getURI2RepositoryManager() {
694         return Components.getComponent(URI2RepositoryManager.class);
695     }
696 
697     /**
698      * Insert a finger-print into a link that is based on the last modified date.
699      * This way we can use far future cache headers on images/assets, and simply change the filename for the
700      * asset when the asset has changed and we want browsers & proxies to take up the new asset from the server.
701      * Appends the date as a string directly before the file extension.
702      *
703      * @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.
704      */
705     public static String addFingerprintToLink(String link, Calendar lastModified) {
706         if (StringUtils.isBlank(link)) {
707             return null;
708         }
709         String fingerprintedLink = "";
710         if (lastModified == null) {
711             return link;
712         }
713 
714         String fingerprint = FINGERPRINT_FORMAT.format(lastModified.getTime());
715 
716         // Determine where to place the fingerprint.
717         int lastDot = link.lastIndexOf('.');
718         int lastSlash = link.lastIndexOf('/');
719 
720         if (lastDot > lastSlash && lastDot != -1) {
721             fingerprintedLink = link.substring(0, lastDot) + "." + fingerprint + link.substring(lastDot);
722         } else {
723             // No file extension - just add fingerprint at end.
724             fingerprintedLink = link + "." + fingerprint;
725         }
726 
727         return fingerprintedLink;
728     }
729 
730     /**
731      * Remove the extension and fingerPrint if present.
732      * Example: (print-logo.2012-11-20-12-15-20.pdf --> print-logo)
733      */
734     public static String removeFingerprintAndExtensionFromLink(String originalPath) {
735 
736         String subPath = StringUtils.substringBeforeLast(originalPath, ".");
737         // Get Finger print
738         String fingerPrint = StringUtils.substringAfterLast(subPath, ".");
739         if (fingerPrint != null && fingerPrint.matches("\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}")) {
740             return StringUtils.substringBeforeLast(subPath, ".");
741         } else {
742             return subPath;
743         }
744     }
745 }