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