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