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}