001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.relations;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsRequestContext;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResourceFilter;
034import org.opencms.file.CmsVfsResourceNotFoundException;
035import org.opencms.file.wrapper.CmsObjectWrapper;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.CmsStaticResourceHandler;
039import org.opencms.main.OpenCms;
040import org.opencms.staticexport.CmsLinkProcessor;
041import org.opencms.util.CmsRequestUtil;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.util.CmsUUID;
044import org.opencms.util.CmsUriSplitter;
045
046import java.util.Map;
047import java.util.Set;
048
049import org.apache.commons.logging.Log;
050
051import org.dom4j.Attribute;
052import org.dom4j.Element;
053
054/**
055 * A single link entry in the link table.<p>
056 *
057 * @since 6.0.0
058 */
059public class CmsLink {
060
061    /** Name of the internal attribute of the link node. */
062    public static final String ATTRIBUTE_INTERNAL = "internal";
063
064    /** Name of the name attribute of the elements node. */
065    public static final String ATTRIBUTE_NAME = "name";
066
067    /** Name of the type attribute of the elements node. */
068    public static final String ATTRIBUTE_TYPE = "type";
069
070    /** Default link name. */
071    public static final String DEFAULT_NAME = "ref";
072
073    /** Default link type. */
074    public static final CmsRelationType DEFAULT_TYPE = CmsRelationType.XML_WEAK;
075
076    /** A dummy uri. */
077    public static final String DUMMY_URI = "@@@";
078
079    /** Name of the anchor node. */
080    public static final String NODE_ANCHOR = "anchor";
081
082    /** Name of the query node. */
083    public static final String NODE_QUERY = "query";
084
085    /** Name of the target node. */
086    public static final String NODE_TARGET = "target";
087
088    /** Name of the UUID node. */
089    public static final String NODE_UUID = "uuid";
090
091    /** Constant for the NULL link. */
092    public static final CmsLink NULL_LINK = new CmsLink();
093
094    /** The log object for this class. */
095    private static final Log LOG = CmsLog.getLog(CmsLink.class);
096
097    /** request context attribute to pass in a custom link renderer. */
098    public static final String CUSTOM_LINK_HANDLER = "CmsLink.customLinkHandler";
099
100    /** The anchor of the URI, if any. */
101    private String m_anchor;
102
103    /** The XML element reference. */
104    private Element m_element;
105
106    /** Indicates if the link is an internal link within the OpenCms VFS. */
107    private boolean m_internal;
108
109    /** The internal name of the link. */
110    private String m_name;
111
112    /** The parameters of the query, if any. */
113    private Map<String, String[]> m_parameters;
114
115    /** The query, if any. */
116    private String m_query;
117
118    /** The site root of the (internal) link. */
119    private String m_siteRoot;
120
121    /** The structure id of the linked resource. */
122    private CmsUUID m_structureId;
123
124    /** The link target (destination). */
125    private String m_target;
126
127    /** The type of the link. */
128    private CmsRelationType m_type;
129
130    /** The raw uri. */
131    private String m_uri;
132
133    /** The resource the link points to. */
134    private CmsResource m_resource;
135
136    /**
137     * Creates a new link from the given link info bean.
138     *
139     * @param linkInfo the link info bean
140     */
141    public CmsLink(CmsLinkInfo linkInfo) {
142
143        m_name = DEFAULT_NAME;
144        m_type = linkInfo.getType();
145        m_internal = linkInfo.isInternal();
146        m_structureId = linkInfo.getStructureId();
147        m_target = linkInfo.getTarget();
148        m_anchor = linkInfo.getAnchor();
149        m_query = linkInfo.getQuery();
150        setUri();
151    }
152
153    /**
154     * Reconstructs a link object from the given XML node.<p>
155     *
156     * @param element the XML node containing the link information
157     */
158    public CmsLink(Element element) {
159
160        m_element = element;
161        Attribute attrName = element.attribute(ATTRIBUTE_NAME);
162        if (attrName != null) {
163            m_name = attrName.getValue();
164        } else {
165            m_name = DEFAULT_NAME;
166        }
167        Attribute attrType = element.attribute(ATTRIBUTE_TYPE);
168        if (attrType != null) {
169            m_type = CmsRelationType.valueOfXml(attrType.getValue());
170        } else {
171            m_type = DEFAULT_TYPE;
172        }
173        Attribute attrInternal = element.attribute(ATTRIBUTE_INTERNAL);
174        if (attrInternal != null) {
175            m_internal = Boolean.valueOf(attrInternal.getValue()).booleanValue();
176        } else {
177            m_internal = true;
178        }
179
180        Element uuid = element.element(NODE_UUID);
181        Element target = element.element(NODE_TARGET);
182        Element anchor = element.element(NODE_ANCHOR);
183        Element query = element.element(NODE_QUERY);
184
185        m_structureId = (uuid != null) ? new CmsUUID(uuid.getText()) : null;
186        m_target = (target != null) ? target.getText() : null;
187        m_anchor = (anchor != null) ? anchor.getText() : null;
188        setQuery((query != null) ? query.getText() : null);
189
190        // update the uri from the components
191        setUri();
192    }
193
194    /**
195     * Creates a new link object without a reference to the xml page link element.<p>
196     *
197     * @param name the internal name of this link
198     * @param type the type of this link
199     * @param structureId the structure id of the link
200     * @param uri the link uri
201     * @param internal indicates if the link is internal within OpenCms
202     */
203    public CmsLink(String name, CmsRelationType type, CmsUUID structureId, String uri, boolean internal) {
204
205        m_element = null;
206        m_name = name;
207        m_type = type;
208        m_internal = internal;
209        m_structureId = structureId;
210        m_uri = uri;
211        // update component members from the uri
212        setComponents();
213    }
214
215    /**
216     * Creates a new link object without a reference to the xml page link element.<p>
217     *
218     * @param name the internal name of this link
219     * @param type the type of this link
220     * @param uri the link uri
221     * @param internal indicates if the link is internal within OpenCms
222     */
223    public CmsLink(String name, CmsRelationType type, String uri, boolean internal) {
224
225        this(name, type, null, uri, internal);
226    }
227
228    /**
229     *  Empty constructor for NULL constant.<p>
230     */
231    private CmsLink() {
232
233        // empty constructor for NULL constant
234    }
235
236    /**
237     * Checks and updates the structure id or the path of the target.<p>
238     *
239     * @param cms the cms context
240     */
241    public void checkConsistency(CmsObject cms) {
242
243        if (!m_internal || (cms == null)) {
244            return;
245        }
246
247        // in case of static resource links use the null UUID
248        if (CmsStaticResourceHandler.isStaticResourceUri(m_target)) {
249            m_structureId = CmsUUID.getNullUUID();
250            return;
251        }
252
253        try {
254            if (m_structureId == null) {
255                // try by path
256                throw new CmsException(Messages.get().container(Messages.LOG_BROKEN_LINK_NO_ID_0));
257            }
258            // first look for the resource with the given structure id
259            String rootPath = null;
260            CmsResource res;
261            try {
262                res = cms.readResource(m_structureId, CmsResourceFilter.ALL);
263                m_resource = res;
264                rootPath = res.getRootPath();
265                if (!res.getRootPath().equals(m_target)) {
266                    // update path if needed
267                    if (LOG.isDebugEnabled()) {
268                        LOG.debug(
269                            Messages.get().getBundle().key(
270                                Messages.LOG_BROKEN_LINK_UPDATED_BY_ID_3,
271                                m_structureId,
272                                m_target,
273                                res.getRootPath()));
274                    }
275
276                }
277            } catch (CmsException e) {
278                // not found
279                throw new CmsVfsResourceNotFoundException(
280                    org.opencms.db.generic.Messages.get().container(
281                        org.opencms.db.generic.Messages.ERR_READ_RESOURCE_1,
282                        m_target),
283                    e);
284            }
285            if ((rootPath != null) && !rootPath.equals(m_target)) {
286                // set the new target
287                m_target = res.getRootPath();
288                setUri();
289                // update xml node
290                CmsLinkUpdateUtil.updateXml(this, m_element, true);
291            }
292        } catch (CmsException e) {
293            if (LOG.isDebugEnabled()) {
294                LOG.debug(Messages.get().getBundle().key(Messages.LOG_BROKEN_LINK_BY_ID_2, m_target, m_structureId), e);
295            }
296            if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_target)) {
297                // no correction is possible
298                return;
299            }
300            // go on with the resource with the given path
301            String siteRoot = cms.getRequestContext().getSiteRoot();
302            try {
303                cms.getRequestContext().setSiteRoot("");
304                // now look for the resource with the given path
305                CmsResource res = cms.readResource(m_target, CmsResourceFilter.ALL);
306                m_resource = res;
307                if (!res.getStructureId().equals(m_structureId)) {
308                    // update structure id if needed
309                    if (LOG.isDebugEnabled()) {
310                        LOG.debug(
311                            Messages.get().getBundle().key(
312                                Messages.LOG_BROKEN_LINK_UPDATED_BY_NAME_3,
313                                m_target,
314                                m_structureId,
315                                res.getStructureId()));
316                    }
317                    m_target = res.getRootPath(); // could change by a translation rule
318                    m_structureId = res.getStructureId();
319                    CmsLinkUpdateUtil.updateXml(this, m_element, true);
320                }
321            } catch (CmsException e1) {
322                // no correction was possible
323                if (LOG.isDebugEnabled()) {
324                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_BROKEN_LINK_BY_NAME_1, m_target), e1);
325                }
326                m_structureId = null;
327            } finally {
328                cms.getRequestContext().setSiteRoot(siteRoot);
329            }
330        }
331    }
332
333    /**
334     * A link is considered equal if the link target and the link type is equal.<p>
335     *
336     * @see java.lang.Object#equals(java.lang.Object)
337     */
338    @Override
339    public boolean equals(Object obj) {
340
341        if (obj == this) {
342            return true;
343        }
344        if (obj instanceof CmsLink) {
345            CmsLink other = (CmsLink)obj;
346            return (m_type == other.m_type) && CmsStringUtil.isEqual(m_target, other.m_target);
347        }
348        return false;
349    }
350
351    /**
352     * Returns the anchor of this link.<p>
353     *
354     * @return the anchor or null if undefined
355     */
356    public String getAnchor() {
357
358        return m_anchor;
359    }
360
361    /**
362     * Returns the xml node element representing this link object.<p>
363     *
364     * @return the xml node element representing this link object
365     */
366    public Element getElement() {
367
368        return m_element;
369    }
370
371    /**
372     * Returns the processed link.<p>
373     *
374     * @param cms the current OpenCms user context, can be <code>null</code>
375     *
376     * @return the processed link
377     */
378    public String getLink(CmsObject cms) {
379
380        if (m_internal) {
381            // if we have a local link, leave it unchanged
382            // cms may be null for unit tests
383            if ((cms == null) || (m_uri.length() == 0) || (m_uri.charAt(0) == '#')) {
384                return m_uri;
385            }
386
387            I_CmsCustomLinkRenderer handler = (I_CmsCustomLinkRenderer)cms.getRequestContext().getAttribute(
388                CmsLink.CUSTOM_LINK_HANDLER);
389            if (handler != null) {
390                String handlerResult = handler.getLink(cms, this);
391                if (handlerResult != null) {
392                    return handlerResult;
393                }
394            }
395            checkConsistency(cms);
396            String target = m_target;
397            String uri = computeUri(target, m_query, m_anchor);
398
399            CmsObjectWrapper wrapper = (CmsObjectWrapper)cms.getRequestContext().getAttribute(
400                CmsObjectWrapper.ATTRIBUTE_NAME);
401            if (wrapper != null) {
402                // if an object wrapper is used, rewrite the URI
403                m_uri = wrapper.rewriteLink(m_uri);
404                uri = wrapper.rewriteLink(uri);
405            }
406
407            if ((cms.getRequestContext().getSiteRoot().length() == 0)
408                && (cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_EDITOR) == null)) {
409                // Explanation why this check is required:
410                // If the site root name length is 0, this means that a user has switched
411                // the site root to the root site "/" in the Workplace.
412                // In this case the workplace site must also be the active site.
413                // If the editor is opened in the root site, because of this code the links are
414                // always generated _with_ server name / port so that the source code looks identical to code
415                // that would normally be created when running in a regular site.
416                // If normal link processing would be used, the site information in the link
417                // would be lost.
418                return OpenCms.getLinkManager().substituteLink(cms, uri);
419            }
420
421            // get the site root for this URI / link
422            // if there is no site root, we either have a /system link, or the site was deleted,
423            // return the full URI prefixed with the opencms context
424            String siteRoot = getSiteRoot();
425            if (siteRoot == null) {
426                return OpenCms.getLinkManager().substituteLink(cms, uri);
427            }
428
429            if (cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_FULLLINKS) != null) {
430                // full links should be generated even if we are in the same site
431                return OpenCms.getLinkManager().getServerLink(cms, uri);
432            }
433
434            // return the link with the server prefix, if necessary
435            return OpenCms.getLinkManager().substituteLink(cms, getSitePath(uri), siteRoot);
436        } else {
437
438            // don't touch external links
439            return m_uri;
440        }
441    }
442
443    /**
444     * Returns the processed link.<p>
445     *
446     * @param cms the current OpenCms user context, can be <code>null</code>
447     * @param processEditorLinks this parameter is not longer used
448     *
449     * @return the processed link
450     *
451     * @deprecated use {@link #getLink(CmsObject)} instead,
452     *      the process editor option is set using the OpenCms request context attributes
453     */
454    @Deprecated
455    public String getLink(CmsObject cms, boolean processEditorLinks) {
456
457        return getLink(cms);
458    }
459
460    /**
461     * Returns the macro name of this link.<p>
462     *
463     * @return the macro name name of this link
464     */
465    public String getName() {
466
467        return m_name;
468    }
469
470    /**
471     * Returns the first parameter value for the given parameter name.<p>
472     *
473     * @param name the name of the parameter
474     * @return the first value for this name or <code>null</code>
475     */
476    public String getParameter(String name) {
477
478        String[] p = getParameterMap().get(name);
479        if (p != null) {
480            return p[0];
481        }
482
483        return null;
484    }
485
486    /**
487     * Returns the map of parameters of this link.<p>
488     *
489     * @return the map of parameters
490     */
491    public Map<String, String[]> getParameterMap() {
492
493        if (m_parameters == null) {
494            m_parameters = CmsRequestUtil.createParameterMap(m_query);
495        }
496        return m_parameters;
497    }
498
499    /**
500     * Returns the set of available parameter names for this link.<p>
501     *
502     * @return the parameter names
503     */
504    public Set<String> getParameterNames() {
505
506        return getParameterMap().keySet();
507    }
508
509    /**
510     * Returns all parameter values for the given name.<p>
511     *
512     * @param name the name of the parameter
513     *
514     * @return all parameter values or <code>null</code>
515     */
516    public String[] getParameterValues(String name) {
517
518        return getParameterMap().get(name);
519    }
520
521    /**
522     * Returns the query of this link.<p>
523     *
524     * @return the query or null if undefined
525     */
526    public String getQuery() {
527
528        return m_query;
529    }
530
531    /**
532     * Returns the resource this link points to, if it is an internal link and has already been initialized via checkConsistency.
533     *
534     * <p>Returns null otherwise.
535     *
536     * @return the resource this link points to
537     */
538    public CmsResource getResource() {
539
540        return m_resource;
541    }
542
543    /**
544     * Returns the vfs link of the target if it is internal.<p>
545     *
546     * @return the full link destination or null if the link is not internal
547     *
548     * @deprecated use {@link #getSitePath(CmsObject)} instead
549     */
550    @Deprecated
551    public String getSitePath() {
552
553        return getSitePath(m_uri);
554    }
555
556    /**
557     * Returns the path of the link target relative to the current site.<p>
558     *
559     * @param cms the CMS context
560     *
561     * @return the site path
562     */
563    public String getSitePath(CmsObject cms) {
564
565        return cms.getRequestContext().removeSiteRoot(m_uri);
566    }
567
568    /**
569     * Return the site root if the target of this link is internal, or <code>null</code> otherwise.<p>
570     *
571     * @return the site root if the target of this link is internal, or <code>null</code> otherwise
572     */
573    public String getSiteRoot() {
574
575        if (m_internal && (m_siteRoot == null)) {
576            m_siteRoot = OpenCms.getSiteManager().getSiteRoot(m_target);
577            if (m_siteRoot == null) {
578                m_siteRoot = "";
579            }
580        }
581        return m_siteRoot;
582    }
583
584    /**
585     * The structure id of the linked resource.<p>
586     *
587     * @return structure id of the linked resource
588     */
589    public CmsUUID getStructureId() {
590
591        return m_structureId;
592    }
593
594    /**
595     * Returns the target (destination) of this link.<p>
596     *
597     * @return the target the target (destination) of this link
598     */
599    public String getTarget() {
600
601        return m_target;
602    }
603
604    /**
605     * Gets the target with the query appended, if there is one.
606     *
607     * @return the target with the query
608     */
609
610    public String getTargetWithQuery() {
611
612        return getTarget() + (getQuery() != null ? "?" + getQuery() : "");
613    }
614
615    /**
616     * Returns the type of this link.<p>
617     *
618     * @return the type of this link
619     */
620    public CmsRelationType getType() {
621
622        return m_type;
623    }
624
625    /**
626     * Returns the raw uri of this link.<p>
627     *
628     * @return the uri
629     */
630    public String getUri() {
631
632        return m_uri;
633    }
634
635    /**
636     * Returns the vfs link of the target if it is internal.<p>
637     *
638     * @return the full link destination or null if the link is not internal
639     *
640     * @deprecated Use {@link #getSitePath()} instead
641     */
642    @Deprecated
643    public String getVfsUri() {
644
645        return getSitePath();
646    }
647
648    /**
649     * @see java.lang.Object#hashCode()
650     */
651    @Override
652    public int hashCode() {
653
654        int result = m_type.hashCode();
655        if (m_target != null) {
656            result += m_target.hashCode();
657        }
658        return result;
659    }
660
661    /**
662     * Returns if the link is internal.<p>
663     *
664     * @return true if the link is a local link
665     */
666    public boolean isInternal() {
667
668        return m_internal;
669    }
670
671    /**
672     * Converts this link to a link info object.
673     *
674     * @return the link info object
675     */
676    public CmsLinkInfo toLinkInfo() {
677
678        return new CmsLinkInfo(m_structureId, m_target, m_query, m_anchor, m_type, m_internal);
679    }
680
681    /**
682     * @see java.lang.Object#toString()
683     */
684    @Override
685    public String toString() {
686
687        return m_uri;
688    }
689
690    /**
691     * Updates the uri of this link with a new value.<p>
692     *
693     * Also updates the structure of the underlying XML page document this link belongs to.<p>
694     *
695     * Note that you can <b>not</b> update the "internal" or "type" values of the link,
696     * so the new link must be of same type (A, IMG) and also remain either an internal or external link.<p>
697     *
698     * @param uri the uri to update this link with <code>scheme://authority/path#anchor?query</code>
699     */
700    public void updateLink(String uri) {
701
702        // set the uri
703        m_uri = uri;
704
705        // update the components
706        setComponents();
707
708        // update the xml
709        CmsLinkUpdateUtil.updateXml(this, m_element, true);
710    }
711
712    /**
713     * Updates the uri of this link with a new target, anchor and query.<p>
714     *
715     * If anchor and/or query are <code>null</code>, this features are not used.<p>
716     *
717     * Note that you can <b>not</b> update the "internal" or "type" values of the link,
718     * so the new link must be of same type (A, IMG) and also remain either an internal or external link.<p>
719     *
720     * Also updates the structure of the underlying XML page document this link belongs to.<p>
721     *
722     * @param target the target (destination) of this link
723     * @param anchor the anchor or null if undefined
724     * @param query the query or null if undefined
725     */
726    public void updateLink(String target, String anchor, String query) {
727
728        // set the components
729        m_target = target;
730        m_anchor = anchor;
731        setQuery(query);
732
733        // create the uri from the components
734        setUri();
735
736        // update the xml
737        CmsLinkUpdateUtil.updateXml(this, m_element, true);
738    }
739
740    /**
741     * Helper method for getting the site path for a uri.<p>
742     *
743     * @param uri a VFS uri
744     * @return the site path
745     */
746    protected String getSitePath(String uri) {
747
748        if (m_internal) {
749            String siteRoot = getSiteRoot();
750            if (siteRoot != null) {
751                return uri.substring(siteRoot.length());
752            } else {
753                return uri;
754            }
755        }
756        return null;
757    }
758
759    /**
760     * Helper method for creating a uri from its components.<p>
761     *
762     * @param target the uri target
763     * @param query the uri query component
764     * @param anchor the uri anchor component
765     *
766     * @return the uri
767     */
768    private String computeUri(String target, String query, String anchor) {
769
770        StringBuffer uri = new StringBuffer(64);
771        uri.append(target);
772        if (query != null) {
773            uri.append('?');
774            uri.append(query);
775        }
776        if (anchor != null) {
777            uri.append('#');
778            uri.append(anchor);
779        }
780        return uri.toString();
781
782    }
783
784    /**
785     * Sets the component member variables (target, anchor, query)
786     * by splitting the uri <code>scheme://authority/path#anchor?query</code>.<p>
787     */
788    private void setComponents() {
789
790        CmsUriSplitter splitter = new CmsUriSplitter(m_uri, true);
791        m_target = splitter.getPrefix();
792        m_anchor = CmsLinkProcessor.unescapeLink(splitter.getAnchor());
793        setQuery(splitter.getQuery());
794    }
795
796    /**
797     * Sets the query of the link.<p>
798     *
799     * @param query the query to set.
800     */
801    private void setQuery(String query) {
802
803        m_query = CmsLinkProcessor.unescapeLink(query);
804        m_parameters = null;
805    }
806
807    /**
808     * Joins the internal target, anchor and query components
809     * to one uri string, setting the internal uri and parameters fields.<p>
810     */
811    private void setUri() {
812
813        m_uri = computeUri(m_target, m_query, m_anchor);
814    }
815}