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}