View Javadoc

1   /**
2    * This file Copyright (c) 2009-2011 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.HierarchyManager;
39  import info.magnolia.cms.core.MgnlNodeType;
40  import info.magnolia.cms.core.NodeData;
41  import info.magnolia.cms.i18n.I18nContentSupport;
42  import info.magnolia.cms.util.ContentUtil;
43  import info.magnolia.context.MgnlContext;
44  import info.magnolia.objectfactory.Components;
45  import info.magnolia.repository.RepositoryConstants;
46  
47  import java.io.UnsupportedEncodingException;
48  import java.net.URLEncoder;
49  import java.util.regex.Matcher;
50  import java.util.regex.Pattern;
51  
52  import javax.jcr.Node;
53  import javax.jcr.PathNotFoundException;
54  import javax.jcr.RepositoryException;
55  
56  import org.apache.commons.lang.StringUtils;
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   */
65  public class LinkUtil {
66  
67      /**
68       * Pattern that matches external and mailto: links.
69       */
70      public static final Pattern EXTERNAL_LINK_PATTERN = Pattern.compile("^(\\w*://|mailto:|javascript:|tel:).*");
71  
72      public static final String DEFAULT_EXTENSION = "html";
73  
74      public static final String DEFAULT_REPOSITORY = RepositoryConstants.WEBSITE;
75  
76      // is this proxied or not? Tests says no.
77      //private static final LinkTransformerManager linkManager = LinkTransformerManager.getInstance();
78  
79      /**
80       * Pattern to find a link.
81       */
82      public static final Pattern LINK_OR_IMAGE_PATTERN = Pattern.compile(
83              "(<(a|img|embed) " + // start <a or <img
84                      "[^>]*" +  // some attributes
85                      "(href|src)[ ]*=[ ]*\")" + // start href or src
86                      "([^\"]*)" + // the link
87                      "(\"" + // ending "
88                      "[^>]*" + // any attributes
89              ">)"); // end the tag
90  
91      /**
92       * Pattern to find a magnolia formatted uuid link.
93       */
94      public static Pattern UUID_PATTERN = Pattern.compile(
95          "\\$\\{link:\\{uuid:\\{([^\\}]*)\\}," // the uuid of the node
96          + "repository:\\{([^\\}]*)\\},"
97          + "(workspace:\\{[^\\}]*\\},)?" // is not supported anymore
98          + "(path|handle):\\{([^\\}]*)\\}"        // fallback handle should not be used unless the uuid is invalid
99          + "(,nodeData:\\{([^\\}]*)\\}," // in case we point to a binary (node data has no uuid!)
100         + "extension:\\{([^\\}]*)\\})?" // the extension to be used in rendering
101         + "\\}\\}"  // the handle
102         + "(#([^\\?\"]*))?" // anchor
103         + "(\\?([^\"]*))?"); // parameters
104 
105     /**
106      * Pattern to find a link.
107      */
108     public static final Pattern LINK_PATTERN = Pattern.compile(
109         "(/[^\\.\"#\\?]*)" + // the handle
110         "(\\.([\\w[^#\\?]]+))?" + // extension (if any)
111         "(#([^\\?\"]*))?" + // anchor
112         "(\\?([^\"]*))?" // parameters
113     );
114 
115     /**
116      * Logger.
117      */
118     private static final Logger log = LoggerFactory.getLogger(LinkUtil.class);
119 
120 
121     //-- conversions from UUID - singles
122     /**
123      * Transforms a uuid to a handle beginning with a /. This path is used to get the page from the repository.
124      * The editor needs this kind of links.
125      */
126     public static String convertUUIDtoHandle(String uuid, String repository) throws LinkException {
127         return createLinkInstance(repository, uuid).getHandle();
128     }
129 
130     /**
131      * Transforms a uuid to an uri. It does not add the context path. In difference from {@link Link#getHandle()},
132      * this method will apply all uri to repository mappings as well as i18n.
133      */
134     public static String convertUUIDtoURI(String uuid, String repository) throws LinkException {
135         return LinkTransformerManager.getInstance().getAbsolute(false).transform(createLinkInstance(repository, uuid));
136     }
137 
138     //-- conversions to UUID - bulk
139     /**
140      * Parses provided html and transforms all the links to the magnolia format. Used during storing.
141      * @param html html code with links to be converted
142      * @return html with changed hrefs
143      */
144     public static String convertAbsoluteLinksToUUIDs(String html) {
145         // get all link tags
146         Matcher matcher = LINK_OR_IMAGE_PATTERN.matcher(html);
147         StringBuffer res = new StringBuffer();
148         while (matcher.find()) {
149             final String href = matcher.group(4);
150             if (!isExternalLinkOrAnchor(href)) {
151                 try {
152                     Link link = parseLink(href);
153                     String linkStr = toPattern(link);
154                     linkStr = StringUtils.replace(linkStr, "\\", "\\\\");
155                     linkStr = StringUtils.replace(linkStr, "$", "\\$");
156                     matcher.appendReplacement(res, "$1" + linkStr + "$5");
157                 }
158                 catch (LinkException e) {
159                     // this is expected if the link is an absolute path to something else
160                     // than content stored in the repository
161                     matcher.appendReplacement(res, "$0");
162                     log.debug("can't parse link", e);
163                 }
164             }
165             else{
166                 matcher.appendReplacement(res, "$0");
167             }
168         }
169         matcher.appendTail(res);
170         return res.toString();
171     }
172 
173     //-- conversions from UUID - bulk
174 
175     /**
176      * Converts provided html with links in UUID pattern format to any other kind of links based on provided link transformer.
177      * @param str Html with UUID links
178      * @param transformer Link transformer
179      * @return converted html with links as created by provided transformer.
180      * @see LinkTransformerManager
181      */
182     public static String convertLinksFromUUIDPattern(String str, LinkTransformer transformer) throws LinkException {
183         Matcher matcher = UUID_PATTERN.matcher(str);
184         StringBuffer res = new StringBuffer();
185         while (matcher.find()) {
186             Link link = createLinkInstance(matcher.group(1), matcher.group(2), matcher.group(5), matcher.group(7), matcher.group(8), matcher.group(10), matcher.group(12));
187             String replacement = transformer.transform(link);
188             // Replace "\" with "\\" and "$" with "\$" since Matcher.appendReplacement treats these characters specially
189             replacement = StringUtils.replace(replacement, "\\", "\\\\");
190             replacement = StringUtils.replace(replacement,"$", "\\$");
191             matcher.appendReplacement(res, replacement);
192         }
193         matcher.appendTail(res);
194         return res.toString();
195     }
196 
197     public static String convertLinksFromUUIDPattern(String str) throws LinkException {
198         LinkTransformer transformer = LinkTransformerManager.getInstance().getBrowserLink(null);
199         return convertLinksFromUUIDPattern(str, transformer);
200     }
201     /**
202      * Determines if the given link is internal and relative.
203      */
204     public static boolean isInternalRelativeLink(String href) {
205         // TODO : this could definitely be improved
206         return !isExternalLinkOrAnchor(href) && !href.startsWith("/");
207     }
208 
209     /**
210      * Determines whether the given link is external link or anchor (i.e. returns true for all non translatable links).
211      */
212     public static boolean isExternalLinkOrAnchor(String href) {
213         return LinkUtil.EXTERNAL_LINK_PATTERN.matcher(href).matches() || href.startsWith("#");
214     }
215 
216     /**
217      * Make a absolute path relative. It adds ../ until the root is reached
218      * @param absolutePath absolute path
219      * @param url page to be relative to
220      * @return relative path
221      */
222     public static String makePathRelative(String url, String absolutePath){
223         String fromPath = StringUtils.substringBeforeLast(url, "/");
224         String toPath = StringUtils.substringBeforeLast(absolutePath, "/");
225 
226         // reference to parent folder
227         if (StringUtils.equals(fromPath, toPath) && StringUtils.endsWith(absolutePath, "/")) {
228             return ".";
229         }
230 
231         String[] fromDirectories = StringUtils.split(fromPath, "/");
232         String[] toDirectories = StringUtils.split(toPath, "/");
233 
234         int pos=0;
235         while(pos < fromDirectories.length && pos < toDirectories.length && fromDirectories[pos].equals(toDirectories[pos])){
236             pos++;
237         }
238 
239         StringBuilder rel = new StringBuilder();
240         for(int i=pos; i < fromDirectories.length; i++ ){
241             rel.append("../");
242         }
243 
244         for(int i=pos; i < toDirectories.length; i++ ){
245             rel.append(toDirectories[i] + "/");
246         }
247 
248         rel.append(StringUtils.substringAfterLast(absolutePath, "/"));
249 
250         return rel.toString();
251     }
252 
253     /**
254      * Maps a path to a repository.
255      * @param path URI
256      * @return repository denoted by the provided URI.
257      */
258     public static String mapPathToRepository(String path) {
259         //String repository = URI2RepositoryManager.getInstance().getRepository(path);
260         String repository = getURI2RepositoryManager().getRepository(path);
261         if(StringUtils.isEmpty(repository)){
262             repository = DEFAULT_REPOSITORY;
263         }
264         return repository;
265     }
266 
267     /**
268      * Appends a parameter to the given url, using ?, or & if there are already
269      * parameters in the given url. <strong>Warning:</strong> It does not
270      * <strong>replace</strong> an existing parameter with the same name.
271      */
272     public static void addParameter(StringBuffer uri, String name, String value) {
273         if (uri.indexOf("?") < 0) {
274             uri.append('?');
275         } else {
276             uri.append('&');
277         }
278         uri.append(name).append('=');
279         try {
280             uri.append(URLEncoder.encode(value, "UTF-8"));
281         } catch (UnsupportedEncodingException e) {
282             throw new RuntimeException("It seems your system does not support UTF-8 !?", e);
283         }
284     }
285 
286     /**
287      * Creates absolute link including context path for provided node data.
288      *
289      * @param nodedata
290      *            Node data to create link for.
291      * @return Absolute link to the provided node data.
292      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
293      */
294     public static String createAbsoluteLink(NodeData nodedata) throws LinkException {
295         if(nodedata == null || !nodedata.isExist()){
296             return null;
297         }
298         return LinkTransformerManager.getInstance().getAbsolute().transform(createLinkInstance(nodedata));
299     }
300 
301     /**
302      * Creates absolute link including context path to the provided content and performing all URI2Repository mappings and applying locales.
303      *
304      * @param uuid
305      *            UUID of content to create link to.
306      * @param repository
307      *            Name of the repository where content is located.
308      * @return Absolute link to the provided content.
309      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
310      */
311     public static String createAbsoluteLink(String repository, String uuid) throws RepositoryException {
312         Node jcrNode = MgnlContext.getJCRSession(repository).getNodeByIdentifier(uuid);
313         /*TODO update with Node method*/
314         return createAbsoluteLink(ContentUtil.asContent(jcrNode));
315     }
316 
317     /**
318      * Creates absolute link including context path to the provided content and performing all URI2Repository mappings and applying locales.
319      *
320      * @param content
321      *            content to create link to.
322      * @return Absolute link to the provided content.
323      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
324      */
325     public static String createAbsoluteLink(Content content) {
326         if(content == null){
327             return null;
328         }
329         return LinkTransformerManager.getInstance().getAbsolute().transform(createLinkInstance(content));
330     }
331 
332     /**
333      * Creates a complete url to access given content from external systems applying all the URI2Repository mappings and locales.
334      */
335     public static String createExternalLink(Content content) {
336         if(content == null){
337             return null;
338         }
339         return LinkTransformerManager.getInstance().getCompleteUrl().transform(createLinkInstance(content));
340     }
341 
342     /**
343      * Creates link guessing best possible link format from current site and provided node.
344      *
345      * @param nodedata
346      *            Node data to create link for.
347      * @return Absolute link to the provided node data.
348      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
349      */
350     public static String createLink(Content node) {
351         if(node == null){
352             return null;
353         }
354         if (node.isNodeType(MgnlNodeType.NT_RESOURCE)) {
355             try {
356                 // This content is what was formerly hidden as binary node data and still needs to be treated as such until new Link API is ready.
357                 String name = node.getName();
358                 Content parent = node.getParent();
359                 return LinkUtil.createLink(parent.getNodeData(name));
360             } catch (RepositoryException e) {
361                 log.debug(e.getMessage(), e);
362             } catch (LinkException e) {
363                 log.debug(e.getMessage(), e);
364             }
365         }
366         return LinkTransformerManager.getInstance().getBrowserLink(node.getHandle()).transform(createLinkInstance(node));
367     }
368 
369     /**
370      * Creates link guessing best possible link format from current site and provided node data.
371      *
372      * @param nodedata
373      *            Node data to create link for.
374      * @return Absolute link to the provided node data.
375      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
376      */
377     public static String createLink(NodeData nodedata) throws LinkException {
378         if(nodedata == null || !nodedata.isExist()){
379             return null;
380         }
381         try {
382             return LinkTransformerManager.getInstance().getBrowserLink(nodedata.getParent().getHandle()).transform(LinkFactory.createLink(nodedata));
383         } catch (RepositoryException e) {
384             throw new LinkException(e.getMessage(), e);
385         }
386     }
387 
388     /**
389      * Creates link guessing best possible link format from current site and provided content.
390      *
391      * @param uuid
392      *            UUID of content to create link to.
393      * @param repository
394      *            Name of the repository where content is located.
395      * @return Absolute link to the provided content.
396      * @see info.magnolia.cms.i18n.AbstractI18nContentSupport
397      */
398     public static String createLink(String repository, String uuid) throws RepositoryException {
399         Node node = MgnlContext.getJCRSession(repository).getNodeByIdentifier(uuid);
400         /*TODO update with Node method*/
401         return createLink(ContentUtil.asContent(node));
402     }
403 
404     public static Link createLinkInstance(Content node) {
405         return new Link(node);
406     }
407 
408     public static Link createLinkInstance(NodeData nodeData) throws LinkException{
409         try {
410             return new Link(nodeData.getParent().getWorkspace().getName(), nodeData.getParent(), nodeData);
411         } catch (RepositoryException e) {
412             throw new LinkException("can't find node " + nodeData , e);
413         }
414     }
415 
416     /**
417      * Creates link to the content denoted by repository and uuid.
418      * @param repository Parent repository of the content of interest.
419      * @param uuid UUID of the content to create link to.
420      * @return link to the content with provided UUID.
421      */
422     public static Link createLinkInstance(String repository, String uuid) throws LinkException {
423         try {
424             return new Link(MgnlContext.getHierarchyManager(repository).getContentByUUID(uuid));
425         } catch (RepositoryException e) {
426             throw new LinkException("can't get node with uuid " + uuid + " and repository " + repository);
427         }
428     }
429 
430     /**
431      * 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.
432      * @param repository Source repository for the content.
433      * @param path Path to the content of interest.
434      * @param extension Optional extension to be used in the link
435      * @param anchor Optional link anchor.
436      * @param parameters Optional link parameters.
437      * @return Link pointing to the content denoted by repository and path including extension, anchor and parameters if such were provided.
438      * @throws LinkException
439      */
440     public static Link createLinkInstance(String repository, String path, String extension, String anchor, String parameters) throws LinkException {
441         Content node = null;
442         String fileName = null;
443         String nodeDataName = null;
444         NodeData nodeData = null;
445         try {
446             HierarchyManager hm = MgnlContext.getHierarchyManager(repository);
447             boolean exists = false;
448             try {
449                 // jackrabbit own path parser
450                 // TODO: rewrite this as Magnolia method or allow configuration of parser per JCR impl
451                 PathParser.checkFormat(path);
452             } catch (MalformedPathException e) {
453                 // 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.
454                     exists = false;
455             }
456             exists = hm.isExist(path) && !hm.isNodeData(path);
457             if (exists) {
458                 node = hm.getContent(path);
459             }
460             if (node == null) {
461                 // this is a binary containing the name at the end
462                 // this name is stored as an attribute but is not part of the handle
463                 if (hm.isNodeData(StringUtils.substringBeforeLast(path, "/"))) {
464                     fileName = StringUtils.substringAfterLast(path, "/");
465                     path = StringUtils.substringBeforeLast(path, "/");
466                 }
467 
468                 // link to the binary node data
469                 if (hm.isNodeData(path)) {
470                     nodeDataName = StringUtils.substringAfterLast(path, "/");
471                     path = StringUtils.substringBeforeLast(path, "/");
472                     node = hm.getContent(path);
473                     nodeData = node.getNodeData(nodeDataName);
474                 }
475             }
476             if (node == null) {
477                 throw new LinkException("can't find node " + path + " in repository " + repository);
478             }
479         } catch (RepositoryException e) {
480             throw new LinkException("can't get node with path " + path + " from repository " + repository);
481         }
482 
483         Link link = new Link(node);
484         link.setAnchor(anchor);
485         link.setExtension(extension);
486         link.setParameters(parameters);
487         link.setFileName(fileName);
488         link.setNodeDataName(nodeDataName);
489         link.setNodeData(nodeData);
490         link.setHandle(path);
491         return link;
492     }
493 
494     /**
495      * 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,
496      * pointing to the non existing uuid so that broken link detection tools can find it.
497      * @param uuid UUID of the content
498      * @param repository Content repository name.
499      * @param fallbackHandle Optional fallback content handle.
500      * @param nodeDataName Content node data name for binary data.
501      * @param extension Optional link extension.
502      * @param anchor Optional link anchor.
503      * @param parameters Optional link parameters.
504      * @return Link pointing to the content denoted by uuid and repository. Link is created using all provided optional values if present.
505      * @throws LinkException
506      */
507     public static Link createLinkInstance(String uuid, String repository, String fallbackHandle, String nodeDataName, String extension, String anchor, String parameters) throws LinkException {
508         final String defaultRepository = StringUtils.defaultIfEmpty(repository, RepositoryConstants.WEBSITE);
509         Link link;
510         try {
511             link = createLinkInstance(defaultRepository, uuid);
512         } catch (LinkException e) {
513             try {
514                 final Content node = MgnlContext.getHierarchyManager(defaultRepository).getContent(fallbackHandle != null? fallbackHandle:"");
515                 link = createLinkInstance(node);
516             } catch (PathNotFoundException pnfe) {
517                 log.warn("Can't find node with uuid {} or handle {} in repository {}", new Object[]{ uuid, fallbackHandle, defaultRepository});
518                 link = new Link();
519                 link.setUUID(uuid);
520             } catch (RepositoryException re) {
521                 log.warn("Can't find node with uuid {} or handle {} in repository {}", new Object[]{ uuid, fallbackHandle, defaultRepository});
522                 link = new Link();
523                 link.setUUID(uuid);
524             }
525         }
526         link.setFallbackHandle(fallbackHandle);
527         link.setNodeDataName(nodeDataName);
528         link.setExtension(extension);
529         link.setAnchor(anchor);
530         link.setParameters(parameters);
531 
532         return link;
533     }
534 
535     /**
536      * Parses UUID link pattern string and converts it into a Link object.
537      * @param uuidLink String containing reference to content as a UUID link pattern.
538      * @return Link to content referenced in the provided text.
539      */
540     public static Link parseUUIDLink(String uuidLink) throws LinkException{
541         Matcher matcher = UUID_PATTERN.matcher(uuidLink);
542         if(matcher.matches()){
543             return createLinkInstance(matcher.group(1), matcher.group(2), matcher.group(5), matcher.group(7), matcher.group(8), matcher.group(10), matcher.group(12));
544         }
545         throw new LinkException("can't parse [ " + uuidLink + "]");
546     }
547 
548     /**
549      * Parses provided URI to the link.
550      * @param link URI representing path to piece of content
551      * @return Link pointing to the content represented by provided URI
552      */
553     public static Link parseLink(String link) throws LinkException{
554         // ignore context handle if existing
555         link = StringUtils.removeStart(link, MgnlContext.getContextPath());
556 
557         Matcher matcher = LINK_PATTERN.matcher(link);
558         if(matcher.matches()){
559             String orgHandle = matcher.group(1);
560             orgHandle = Components.getComponent(I18nContentSupport.class).toRawURI(orgHandle);
561             String repository = getURI2RepositoryManager().getRepository(orgHandle);
562             String handle = getURI2RepositoryManager().getHandle(orgHandle);
563             return createLinkInstance(repository, handle, matcher.group(3),matcher.group(5),matcher.group(7));
564         }
565         throw new LinkException("can't parse [ " + link + "]");
566     }
567 
568     /**
569      * Converts provided Link to an UUID link pattern.
570      * @param link Link to convert.
571      * @return UUID link pattern representation of provided link.
572      * @throws RepositoryException
573      */
574     public static String toPattern(Link link) {
575         return "${link:{"
576             + "uuid:{" + link.getUUID() + "},"
577             + "repository:{" + link.getRepository() + "},"
578             + "handle:{" + link.getHandle() + "}," // original handle represented by the uuid
579             + "nodeData:{" + StringUtils.defaultString(link.getNodeDataName()) + "}," // in case of binaries
580             + "extension:{" + StringUtils.defaultString(link.getExtension()) + "}" // the extension to use if no extension can be resolved otherwise
581             + "}}"
582             + (StringUtils.isNotEmpty(link.getAnchor())? "#" + link.getAnchor():"")
583             + (StringUtils.isNotEmpty(link.getParameters())? "?" + link.getParameters() : "");
584     }
585 
586     private static URI2RepositoryManager getURI2RepositoryManager(){
587         return Components.getComponent(URI2RepositoryManager.class);
588     }
589 }