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    /** The anchor of the URI, if any. */
098    private String m_anchor;
099
100    /** The XML element reference. */
101    private Element m_element;
102
103    /** Indicates if the link is an internal link within the OpenCms VFS. */
104    private boolean m_internal;
105
106    /** The internal name of the link. */
107    private String m_name;
108
109    /** The parameters of the query, if any. */
110    private Map<String, String[]> m_parameters;
111
112    /** The query, if any. */
113    private String m_query;
114
115    /** The site root of the (internal) link. */
116    private String m_siteRoot;
117
118    /** The structure id of the linked resource. */
119    private CmsUUID m_structureId;
120
121    /** The link target (destination). */
122    private String m_target;
123
124    /** The type of the link. */
125    private CmsRelationType m_type;
126
127    /** The raw uri. */
128    private String m_uri;
129
130    /**
131     * Reconstructs a link object from the given XML node.<p>
132     *
133     * @param element the XML node containing the link information
134     */
135    public CmsLink(Element element) {
136
137        m_element = element;
138        Attribute attrName = element.attribute(ATTRIBUTE_NAME);
139        if (attrName != null) {
140            m_name = attrName.getValue();
141        } else {
142            m_name = DEFAULT_NAME;
143        }
144        Attribute attrType = element.attribute(ATTRIBUTE_TYPE);
145        if (attrType != null) {
146            m_type = CmsRelationType.valueOfXml(attrType.getValue());
147        } else {
148            m_type = DEFAULT_TYPE;
149        }
150        Attribute attrInternal = element.attribute(ATTRIBUTE_INTERNAL);
151        if (attrInternal != null) {
152            m_internal = Boolean.valueOf(attrInternal.getValue()).booleanValue();
153        } else {
154            m_internal = true;
155        }
156
157        Element uuid = element.element(NODE_UUID);
158        Element target = element.element(NODE_TARGET);
159        Element anchor = element.element(NODE_ANCHOR);
160        Element query = element.element(NODE_QUERY);
161
162        m_structureId = (uuid != null) ? new CmsUUID(uuid.getText()) : null;
163        m_target = (target != null) ? target.getText() : null;
164        m_anchor = (anchor != null) ? anchor.getText() : null;
165        setQuery((query != null) ? query.getText() : null);
166
167        // update the uri from the components
168        setUri();
169    }
170
171    /**
172     * Creates a new link object without a reference to the xml page link element.<p>
173     *
174     * @param name the internal name of this link
175     * @param type the type of this link
176     * @param structureId the structure id of the link
177     * @param uri the link uri
178     * @param internal indicates if the link is internal within OpenCms
179     */
180    public CmsLink(String name, CmsRelationType type, CmsUUID structureId, String uri, boolean internal) {
181
182        m_element = null;
183        m_name = name;
184        m_type = type;
185        m_internal = internal;
186        m_structureId = structureId;
187        m_uri = uri;
188        // update component members from the uri
189        setComponents();
190    }
191
192    /**
193     * Creates a new link object without a reference to the xml page link element.<p>
194     *
195     * @param name the internal name of this link
196     * @param type the type of this link
197     * @param uri the link uri
198     * @param internal indicates if the link is internal within OpenCms
199     */
200    public CmsLink(String name, CmsRelationType type, String uri, boolean internal) {
201
202        this(name, type, null, uri, internal);
203    }
204
205    /**
206     *  Empty constructor for NULL constant.<p>
207     */
208    private CmsLink() {
209
210        // empty constructor for NULL constant
211    }
212
213    /**
214     * Checks and updates the structure id or the path of the target.<p>
215     *
216     * @param cms the cms context
217     */
218    public void checkConsistency(CmsObject cms) {
219
220        if (!m_internal || (cms == null)) {
221            return;
222        }
223
224        // in case of static resource links use the null UUID
225        if (CmsStaticResourceHandler.isStaticResourceUri(m_target)) {
226            m_structureId = CmsUUID.getNullUUID();
227            return;
228        }
229
230        try {
231            if (m_structureId == null) {
232                // try by path
233                throw new CmsException(Messages.get().container(Messages.LOG_BROKEN_LINK_NO_ID_0));
234            }
235            // first look for the resource with the given structure id
236            String rootPath = null;
237            CmsResource res;
238            try {
239                res = cms.readResource(m_structureId, CmsResourceFilter.ALL);
240                rootPath = res.getRootPath();
241                if (!res.getRootPath().equals(m_target)) {
242                    // update path if needed
243                    if (LOG.isDebugEnabled()) {
244                        LOG.debug(
245                            Messages.get().getBundle().key(
246                                Messages.LOG_BROKEN_LINK_UPDATED_BY_ID_3,
247                                m_structureId,
248                                m_target,
249                                res.getRootPath()));
250                    }
251
252                }
253            } catch (CmsException e) {
254                // not found
255                throw new CmsVfsResourceNotFoundException(
256                    org.opencms.db.generic.Messages.get().container(
257                        org.opencms.db.generic.Messages.ERR_READ_RESOURCE_1,
258                        m_target),
259                    e);
260            }
261            if ((rootPath != null) && !rootPath.equals(m_target)) {
262                // set the new target
263                m_target = res.getRootPath();
264                setUri();
265                // update xml node
266                CmsLinkUpdateUtil.updateXml(this, m_element, true);
267            }
268        } catch (CmsException e) {
269            if (LOG.isDebugEnabled()) {
270                LOG.debug(Messages.get().getBundle().key(Messages.LOG_BROKEN_LINK_BY_ID_2, m_target, m_structureId), e);
271            }
272            if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_target)) {
273                // no correction is possible
274                return;
275            }
276            // go on with the resource with the given path
277            String siteRoot = cms.getRequestContext().getSiteRoot();
278            try {
279                cms.getRequestContext().setSiteRoot("");
280                // now look for the resource with the given path
281                CmsResource res = cms.readResource(m_target, CmsResourceFilter.ALL);
282                if (!res.getStructureId().equals(m_structureId)) {
283                    // update structure id if needed
284                    if (LOG.isDebugEnabled()) {
285                        LOG.debug(
286                            Messages.get().getBundle().key(
287                                Messages.LOG_BROKEN_LINK_UPDATED_BY_NAME_3,
288                                m_target,
289                                m_structureId,
290                                res.getStructureId()));
291                    }
292                    m_target = res.getRootPath(); // could change by a translation rule
293                    m_structureId = res.getStructureId();
294                    CmsLinkUpdateUtil.updateXml(this, m_element, true);
295                }
296            } catch (CmsException e1) {
297                // no correction was possible
298                if (LOG.isDebugEnabled()) {
299                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_BROKEN_LINK_BY_NAME_1, m_target), e1);
300                }
301                m_structureId = null;
302            } finally {
303                cms.getRequestContext().setSiteRoot(siteRoot);
304            }
305        }
306    }
307
308    /**
309     * A link is considered equal if the link target and the link type is equal.<p>
310     *
311     * @see java.lang.Object#equals(java.lang.Object)
312     */
313    @Override
314    public boolean equals(Object obj) {
315
316        if (obj == this) {
317            return true;
318        }
319        if (obj instanceof CmsLink) {
320            CmsLink other = (CmsLink)obj;
321            return (m_type == other.m_type) && CmsStringUtil.isEqual(m_target, other.m_target);
322        }
323        return false;
324    }
325
326    /**
327     * Returns the anchor of this link.<p>
328     *
329     * @return the anchor or null if undefined
330     */
331    public String getAnchor() {
332
333        return m_anchor;
334    }
335
336    /**
337     * Returns the xml node element representing this link object.<p>
338     *
339     * @return the xml node element representing this link object
340     */
341    public Element getElement() {
342
343        return m_element;
344    }
345
346    /**
347     * Returns the processed link.<p>
348     *
349     * @param cms the current OpenCms user context, can be <code>null</code>
350     *
351     * @return the processed link
352     */
353    public String getLink(CmsObject cms) {
354
355        if (m_internal) {
356            // if we have a local link, leave it unchanged
357            // cms may be null for unit tests
358            if ((cms == null) || (m_uri.length() == 0) || (m_uri.charAt(0) == '#')) {
359                return m_uri;
360            }
361
362            checkConsistency(cms);
363            //String target = replaceTargetWithDetailPageIfNecessary(cms, m_resource, m_target);
364            String target = m_target;
365            String uri = computeUri(target, m_query, m_anchor);
366
367            CmsObjectWrapper wrapper = (CmsObjectWrapper)cms.getRequestContext().getAttribute(
368                CmsObjectWrapper.ATTRIBUTE_NAME);
369            if (wrapper != null) {
370                // if an object wrapper is used, rewrite the URI
371                m_uri = wrapper.rewriteLink(m_uri);
372                uri = wrapper.rewriteLink(uri);
373            }
374
375            if ((cms.getRequestContext().getSiteRoot().length() == 0)
376                && (cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_EDITOR) == null)) {
377                // Explanation why this check is required:
378                // If the site root name length is 0, this means that a user has switched
379                // the site root to the root site "/" in the Workplace.
380                // In this case the workplace site must also be the active site.
381                // If the editor is opened in the root site, because of this code the links are
382                // always generated _with_ server name / port so that the source code looks identical to code
383                // that would normally be created when running in a regular site.
384                // If normal link processing would be used, the site information in the link
385                // would be lost.
386                return OpenCms.getLinkManager().substituteLink(cms, uri);
387            }
388
389            // get the site root for this URI / link
390            // if there is no site root, we either have a /system link, or the site was deleted,
391            // return the full URI prefixed with the opencms context
392            String siteRoot = getSiteRoot();
393            if (siteRoot == null) {
394                return OpenCms.getLinkManager().substituteLink(cms, uri);
395            }
396
397            if (cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_FULLLINKS) != null) {
398                // full links should be generated even if we are in the same site
399                return OpenCms.getLinkManager().getServerLink(cms, uri);
400            }
401
402            // return the link with the server prefix, if necessary
403            return OpenCms.getLinkManager().substituteLink(cms, getSitePath(uri), siteRoot);
404        } else {
405
406            // don't touch external links
407            return m_uri;
408        }
409    }
410
411    /**
412     * Returns the processed link.<p>
413     *
414     * @param cms the current OpenCms user context, can be <code>null</code>
415     * @param processEditorLinks this parameter is not longer used
416     *
417     * @return the processed link
418     *
419     * @deprecated use {@link #getLink(CmsObject)} instead,
420     *      the process editor option is set using the OpenCms request context attributes
421     */
422    @Deprecated
423    public String getLink(CmsObject cms, boolean processEditorLinks) {
424
425        return getLink(cms);
426    }
427
428    /**
429     * Returns the macro name of this link.<p>
430     *
431     * @return the macro name name of this link
432     */
433    public String getName() {
434
435        return m_name;
436    }
437
438    /**
439     * Returns the first parameter value for the given parameter name.<p>
440     *
441     * @param name the name of the parameter
442     * @return the first value for this name or <code>null</code>
443     */
444    public String getParameter(String name) {
445
446        String[] p = getParameterMap().get(name);
447        if (p != null) {
448            return p[0];
449        }
450
451        return null;
452    }
453
454    /**
455     * Returns the map of parameters of this link.<p>
456     *
457     * @return the map of parameters
458     */
459    public Map<String, String[]> getParameterMap() {
460
461        if (m_parameters == null) {
462            m_parameters = CmsRequestUtil.createParameterMap(m_query);
463        }
464        return m_parameters;
465    }
466
467    /**
468     * Returns the set of available parameter names for this link.<p>
469     *
470     * @return the parameter names
471     */
472    public Set<String> getParameterNames() {
473
474        return getParameterMap().keySet();
475    }
476
477    /**
478     * Returns all parameter values for the given name.<p>
479     *
480     * @param name the name of the parameter
481     *
482     * @return all parameter values or <code>null</code>
483     */
484    public String[] getParameterValues(String name) {
485
486        return getParameterMap().get(name);
487    }
488
489    /**
490     * Returns the query of this link.<p>
491     *
492     * @return the query or null if undefined
493     */
494    public String getQuery() {
495
496        return m_query;
497    }
498
499    /**
500     * Returns the vfs link of the target if it is internal.<p>
501     *
502     * @return the full link destination or null if the link is not internal
503     *
504     * @deprecated use {@link #getSitePath(CmsObject)} instead
505     */
506    @Deprecated
507    public String getSitePath() {
508
509        return getSitePath(m_uri);
510    }
511
512    /**
513     * Returns the path of the link target relative to the current site.<p>
514     *
515     * @param cms the CMS context
516     *
517     * @return the site path
518     */
519    public String getSitePath(CmsObject cms) {
520
521        return cms.getRequestContext().removeSiteRoot(m_uri);
522    }
523
524    /**
525     * Return the site root if the target of this link is internal, or <code>null</code> otherwise.<p>
526     *
527     * @return the site root if the target of this link is internal, or <code>null</code> otherwise
528     */
529    public String getSiteRoot() {
530
531        if (m_internal && (m_siteRoot == null)) {
532            m_siteRoot = OpenCms.getSiteManager().getSiteRoot(m_target);
533            if (m_siteRoot == null) {
534                m_siteRoot = "";
535            }
536        }
537        return m_siteRoot;
538    }
539
540    /**
541     * The structure id of the linked resource.<p>
542     *
543     * @return structure id of the linked resource
544     */
545    public CmsUUID getStructureId() {
546
547        return m_structureId;
548    }
549
550    /**
551     * Returns the target (destination) of this link.<p>
552     *
553     * @return the target the target (destination) of this link
554     */
555    public String getTarget() {
556
557        return m_target;
558    }
559
560    /**
561     * Returns the type of this link.<p>
562     *
563     * @return the type of this link
564     */
565    public CmsRelationType getType() {
566
567        return m_type;
568    }
569
570    /**
571     * Returns the raw uri of this link.<p>
572     *
573     * @return the uri
574     */
575    public String getUri() {
576
577        return m_uri;
578    }
579
580    /**
581     * Returns the vfs link of the target if it is internal.<p>
582     *
583     * @return the full link destination or null if the link is not internal
584     *
585     * @deprecated Use {@link #getSitePath()} instead
586     */
587    @Deprecated
588    public String getVfsUri() {
589
590        return getSitePath();
591    }
592
593    /**
594     * @see java.lang.Object#hashCode()
595     */
596    @Override
597    public int hashCode() {
598
599        int result = m_type.hashCode();
600        if (m_target != null) {
601            result += m_target.hashCode();
602        }
603        return result;
604    }
605
606    /**
607     * Returns if the link is internal.<p>
608     *
609     * @return true if the link is a local link
610     */
611    public boolean isInternal() {
612
613        return m_internal;
614    }
615
616    /**
617     * @see java.lang.Object#toString()
618     */
619    @Override
620    public String toString() {
621
622        return m_uri;
623    }
624
625    /**
626     * Updates the uri of this link with a new value.<p>
627     *
628     * Also updates the structure of the underlying XML page document this link belongs to.<p>
629     *
630     * Note that you can <b>not</b> update the "internal" or "type" values of the link,
631     * so the new link must be of same type (A, IMG) and also remain either an internal or external link.<p>
632     *
633     * @param uri the uri to update this link with <code>scheme://authority/path#anchor?query</code>
634     */
635    public void updateLink(String uri) {
636
637        // set the uri
638        m_uri = uri;
639
640        // update the components
641        setComponents();
642
643        // update the xml
644        CmsLinkUpdateUtil.updateXml(this, m_element, true);
645    }
646
647    /**
648     * Updates the uri of this link with a new target, anchor and query.<p>
649     *
650     * If anchor and/or query are <code>null</code>, this features are not used.<p>
651     *
652     * Note that you can <b>not</b> update the "internal" or "type" values of the link,
653     * so the new link must be of same type (A, IMG) and also remain either an internal or external link.<p>
654     *
655     * Also updates the structure of the underlying XML page document this link belongs to.<p>
656     *
657     * @param target the target (destination) of this link
658     * @param anchor the anchor or null if undefined
659     * @param query the query or null if undefined
660     */
661    public void updateLink(String target, String anchor, String query) {
662
663        // set the components
664        m_target = target;
665        m_anchor = anchor;
666        setQuery(query);
667
668        // create the uri from the components
669        setUri();
670
671        // update the xml
672        CmsLinkUpdateUtil.updateXml(this, m_element, true);
673    }
674
675    /**
676     * Helper method for getting the site path for a uri.<p>
677     *
678     * @param uri a VFS uri
679     * @return the site path
680     */
681    protected String getSitePath(String uri) {
682
683        if (m_internal) {
684            String siteRoot = getSiteRoot();
685            if (siteRoot != null) {
686                return uri.substring(siteRoot.length());
687            } else {
688                return uri;
689            }
690        }
691        return null;
692    }
693
694    /**
695     * Helper method for creating a uri from its components.<p>
696     *
697     * @param target the uri target
698     * @param query the uri query component
699     * @param anchor the uri anchor component
700     *
701     * @return the uri
702     */
703    private String computeUri(String target, String query, String anchor) {
704
705        StringBuffer uri = new StringBuffer(64);
706        uri.append(target);
707        if (query != null) {
708            uri.append('?');
709            uri.append(query);
710        }
711        if (anchor != null) {
712            uri.append('#');
713            uri.append(anchor);
714        }
715        return uri.toString();
716
717    }
718
719    /**
720     * Sets the component member variables (target, anchor, query)
721     * by splitting the uri <code>scheme://authority/path#anchor?query</code>.<p>
722     */
723    private void setComponents() {
724
725        CmsUriSplitter splitter = new CmsUriSplitter(m_uri, true);
726        m_target = splitter.getPrefix();
727        m_anchor = CmsLinkProcessor.unescapeLink(splitter.getAnchor());
728        setQuery(splitter.getQuery());
729    }
730
731    /**
732     * Sets the query of the link.<p>
733     *
734     * @param query the query to set.
735     */
736    private void setQuery(String query) {
737
738        m_query = CmsLinkProcessor.unescapeLink(query);
739        m_parameters = null;
740    }
741
742    /**
743     * Joins the internal target, anchor and query components
744     * to one uri string, setting the internal uri and parameters fields.<p>
745     */
746    private void setUri() {
747
748        m_uri = computeUri(m_target, m_query, m_anchor);
749    }
750}