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