001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (https://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, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://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.site.xmlsitemap; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.ade.configuration.CmsADEManager; 032import org.opencms.ade.detailpage.CmsDetailPageInfo; 033import org.opencms.db.CmsAlias; 034import org.opencms.file.CmsObject; 035import org.opencms.file.CmsProperty; 036import org.opencms.file.CmsPropertyDefinition; 037import org.opencms.file.CmsRequestContext; 038import org.opencms.file.CmsResource; 039import org.opencms.file.CmsResourceFilter; 040import org.opencms.file.CmsVfsResourceNotFoundException; 041import org.opencms.file.types.CmsResourceTypeHtmlRedirect; 042import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 043import org.opencms.file.types.I_CmsResourceType; 044import org.opencms.gwt.shared.alias.CmsAliasMode; 045import org.opencms.jsp.CmsJspNavBuilder; 046import org.opencms.jsp.CmsJspNavElement; 047import org.opencms.loader.CmsLoaderException; 048import org.opencms.loader.CmsResourceManager; 049import org.opencms.main.CmsException; 050import org.opencms.main.CmsLog; 051import org.opencms.main.OpenCms; 052import org.opencms.relations.CmsRelation; 053import org.opencms.relations.CmsRelationFilter; 054import org.opencms.relations.CmsRelationType; 055import org.opencms.site.CmsSite; 056import org.opencms.staticexport.CmsLinkManager; 057import org.opencms.util.CmsFileUtil; 058import org.opencms.util.CmsStringUtil; 059import org.opencms.util.CmsUUID; 060 061import java.net.URI; 062import java.net.URISyntaxException; 063import java.util.ArrayList; 064import java.util.Collection; 065import java.util.HashMap; 066import java.util.HashSet; 067import java.util.Iterator; 068import java.util.LinkedHashMap; 069import java.util.List; 070import java.util.Locale; 071import java.util.Map; 072import java.util.Set; 073 074import org.apache.commons.logging.Log; 075 076import com.google.common.collect.ArrayListMultimap; 077import com.google.common.collect.Multimap; 078 079/** 080 * Class for generating XML sitemaps for SEO purposes, as described in 081 * <a href="http://www.sitemaps.org/protocol.html">http://www.sitemaps.org/protocol.html</a>.<p> 082 */ 083public class CmsXmlSitemapGenerator { 084 085 /** 086 * A bean that consists of a sitemap URL bean and a priority score, to determine which of multiple entries with the same 087 * URL are to be preferred.<p> 088 */ 089 protected class ResultEntry { 090 091 /** Internal priority to determine which of multiple entries with the same URL is used. 092 * Note that this has nothing to do with the priority in the URL bean itself! 093 */ 094 private int m_priority; 095 096 /** The URL bean. */ 097 private CmsXmlSitemapUrlBean m_urlBean; 098 099 /** 100 * Creates a new result entry.<p> 101 * 102 * @param urlBean the url bean 103 * 104 * @param priority the internal priority 105 */ 106 public ResultEntry(CmsXmlSitemapUrlBean urlBean, int priority) { 107 108 m_priority = priority; 109 m_urlBean = urlBean; 110 } 111 112 /** 113 * Gets the internal priority used to determine which of multiple entries with the same URL to use.<p> 114 * This has nothing to do with the priority defined in the URL beans themselves! 115 * 116 * @return the internal priority 117 */ 118 public int getPriority() { 119 120 return m_priority; 121 } 122 123 /** 124 * Gets the URL bean.<p> 125 * 126 * @return the URL bean 127 */ 128 public CmsXmlSitemapUrlBean getUrlBean() { 129 130 return m_urlBean; 131 } 132 } 133 134 /** The default change frequency. */ 135 public static final String DEFAULT_CHANGE_FREQUENCY = "daily"; 136 137 /** The default priority. */ 138 public static final double DEFAULT_PRIORITY = 0.5; 139 140 /** Sitemap attribute to exclude empty detail pages relevant for settings only. */ 141 public static final String ATTR_DETAIL_SETTINGS_PAGE_EXCLUDE = "template.detailsettingspage.exclude"; 142 143 /** The logger instance for this class. */ 144 private static final Log LOG = CmsLog.getLog(CmsXmlSitemapGenerator.class); 145 146 /** The root path for the sitemap root folder. */ 147 protected String m_baseFolderRootPath; 148 149 /** The site path of the base folder. */ 150 protected String m_baseFolderSitePath; 151 152 /** Flag to control whether container page dates should be computed. */ 153 protected boolean m_computeContainerPageDates; 154 155 /** The list of detail page info beans. */ 156 protected List<CmsDetailPageInfo> m_detailPageInfos = new ArrayList<CmsDetailPageInfo>(); 157 158 /** A map from type names to lists of potential detail resources of that type. */ 159 protected Map<String, List<CmsResource>> m_detailResources = new HashMap<String, List<CmsResource>>(); 160 161 /** A multimap from detail page root paths to corresponding types. */ 162 protected Multimap<String, String> m_detailTypesByPage = ArrayListMultimap.create(); 163 164 /** A CMS context with guest privileges. */ 165 protected CmsObject m_guestCms; 166 167 /** The include/exclude configuration used for choosing pages for the XML sitemap. */ 168 protected CmsPathIncludeExcludeSet m_includeExcludeSet = new CmsPathIncludeExcludeSet(); 169 170 /** A map from structure ids to page aliases below the base folder which point to the given structure id. */ 171 protected Multimap<CmsUUID, CmsAlias> m_pageAliasesBelowBaseFolderByStructureId = ArrayListMultimap.create(); 172 173 /** The map used for storing the results, with URLs as keys. */ 174 protected Map<String, ResultEntry> m_resultMap = new LinkedHashMap<String, ResultEntry>(); 175 176 /** A guest user CMS object with the site root of the base folder. */ 177 protected CmsObject m_siteGuestCms; 178 179 /** The site root of the base folder. */ 180 protected String m_siteRoot; 181 182 /** A link to the site root. */ 183 protected String m_siteRootLink; 184 185 /** Configured replacement server URL. */ 186 private String m_serverUrl; 187 188 /** 189 * Creates a new sitemap generator instance.<p> 190 * 191 * @param folderRootPath the root folder for the XML sitemap to generate 192 * 193 * @throws CmsException if something goes wrong 194 */ 195 public CmsXmlSitemapGenerator(String folderRootPath) 196 throws CmsException { 197 198 m_baseFolderRootPath = CmsFileUtil.removeTrailingSeparator(folderRootPath); 199 m_guestCms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest()); 200 m_siteGuestCms = OpenCms.initCmsObject(m_guestCms); 201 CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(CmsStringUtil.joinPaths(folderRootPath, "/")); 202 m_siteRoot = site.getSiteRoot(); 203 204 m_siteGuestCms.getRequestContext().setSiteRoot(m_siteRoot); 205 m_baseFolderSitePath = CmsStringUtil.joinPaths( 206 "/", 207 m_siteGuestCms.getRequestContext().removeSiteRoot(m_baseFolderRootPath)); 208 } 209 210 /** 211 * Replaces the protocol/host/port of a link with the ones from the given server URI, if it's not empty.<p> 212 * 213 * @param link the link to change 214 * @param server the server URI string 215 216 * @return the changed link 217 */ 218 public static String replaceServerUri(String link, String server) { 219 220 String serverUriStr = server; 221 222 if (CmsStringUtil.isEmptyOrWhitespaceOnly(serverUriStr)) { 223 return link; 224 } 225 try { 226 URI serverUri = new URI(serverUriStr); 227 URI linkUri = new URI(link); 228 URI result = new URI( 229 serverUri.getScheme(), 230 serverUri.getAuthority(), 231 linkUri.getPath(), 232 linkUri.getQuery(), 233 linkUri.getFragment()); 234 return result.toString(); 235 } catch (URISyntaxException e) { 236 LOG.error(e.getLocalizedMessage(), e); 237 return link; 238 } 239 240 } 241 242 /** 243 * Gets the change frequency for a sitemap entry from a list of properties.<p> 244 * 245 * If the change frequency is not defined in the properties, this method will return null.<p> 246 * 247 * @param properties the properties from which the change frequency should be obtained 248 * 249 * @return the change frequency string 250 */ 251 protected static String getChangeFrequency(List<CmsProperty> properties) { 252 253 CmsProperty prop = CmsProperty.get(CmsPropertyDefinition.PROPERTY_XMLSITEMAP_CHANGEFREQ, properties); 254 if (prop.isNullProperty()) { 255 return null; 256 } 257 String result = prop.getValue().trim(); 258 return result; 259 } 260 261 /** 262 * Gets the page priority from a list of properties.<p> 263 * 264 * If the page priority can't be found among the properties, -1 will be returned.<p> 265 * 266 * @param properties the properties of a resource 267 * 268 * @return the page priority read from the properties, or -1 269 */ 270 protected static double getPriority(List<CmsProperty> properties) { 271 272 CmsProperty prop = CmsProperty.get(CmsPropertyDefinition.PROPERTY_XMLSITEMAP_PRIORITY, properties); 273 if (prop.isNullProperty()) { 274 return -1.0; 275 } 276 try { 277 double result = Double.parseDouble(prop.getValue().trim()); 278 return result; 279 } catch (NumberFormatException e) { 280 return -1.0; 281 } 282 } 283 284 /** 285 * Removes files marked as internal from a resource list.<p> 286 * 287 * @param resources the list which should be replaced 288 */ 289 protected static void removeInternalFiles(List<CmsResource> resources) { 290 291 Iterator<CmsResource> iter = resources.iterator(); 292 while (iter.hasNext()) { 293 CmsResource resource = iter.next(); 294 if (resource.isInternal()) { 295 iter.remove(); 296 } 297 } 298 } 299 300 /** 301 * Generates a list of XML sitemap entry beans for the root folder which has been set in the constructor.<p> 302 * 303 * @return the list of XML sitemap entries 304 * 305 * @throws CmsException if something goes wrong 306 */ 307 public List<CmsXmlSitemapUrlBean> generateSitemapBeans() throws CmsException { 308 309 String baseSitePath = m_siteGuestCms.getRequestContext().removeSiteRoot(m_baseFolderRootPath); 310 initializeFileData(baseSitePath); 311 for (CmsResource resource : getDirectPages()) { 312 if (CmsResourceTypeHtmlRedirect.isRedirect(resource)) { 313 continue; 314 } 315 String sitePath = m_siteGuestCms.getSitePath(resource); 316 List<CmsProperty> propertyList = m_siteGuestCms.readPropertyObjects(resource, true); 317 String onlineLink = OpenCms.getLinkManager().getOnlineLink(m_siteGuestCms, sitePath); 318 boolean isContainerPage = CmsResourceTypeXmlContainerPage.isContainerPage(resource); 319 long dateModified = resource.getDateLastModified(); 320 if (isContainerPage) { 321 if (m_computeContainerPageDates) { 322 dateModified = computeContainerPageModificationDate(resource); 323 } else { 324 dateModified = -1; 325 } 326 } 327 CmsXmlSitemapUrlBean urlBean = new CmsXmlSitemapUrlBean( 328 replaceServerUri(onlineLink), 329 dateModified, 330 getChangeFrequency(propertyList), 331 getPriority(propertyList)); 332 urlBean.setOriginalResource(resource); 333 boolean isDefaultDetailPage = isDefaultDetailPage(resource); 334 List<I_CmsResourceType> types = getDetailTypesForPage(resource); 335 if (isDefaultDetailPage) { // default detail page 336 if (!excludeDetailPage(resource)) { 337 addResult(urlBean, 3); 338 } 339 } else if (types.isEmpty()) { // not a detail page 340 addResult(urlBean, 3); 341 } else { // typed detail page 342 if (!excludeDetailPage(resource)) { 343 addResult(urlBean, 3); 344 } 345 Locale locale = getLocale(resource, propertyList); 346 addDetailLinks(resource, locale, types); 347 } 348 } 349 350 for (CmsUUID aliasStructureId : m_pageAliasesBelowBaseFolderByStructureId.keySet()) { 351 addAliasLinks(aliasStructureId); 352 } 353 354 List<CmsXmlSitemapUrlBean> result = new ArrayList<CmsXmlSitemapUrlBean>(); 355 for (ResultEntry resultEntry : m_resultMap.values()) { 356 result.add(resultEntry.getUrlBean()); 357 } 358 return result; 359 } 360 361 /** 362 * Gets the include/exclude configuration of this XML sitemap generator.<p> 363 * 364 * @return the include/exclude configuration 365 */ 366 public CmsPathIncludeExcludeSet getIncludeExcludeSet() { 367 368 return m_includeExcludeSet; 369 } 370 371 /** 372 * Generates and formats the XML sitemap as a string.<p> 373 * 374 * @return the sitemap XML data 375 * 376 * @throws CmsException if something goes wrong 377 */ 378 public String renderSitemap() throws Exception { 379 380 List<CmsXmlSitemapUrlBean> urlBeans = generateSitemapBeans(); 381 382 StringBuffer buffer = new StringBuffer(); 383 buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 384 buffer.append(getUrlSetOpenTag() + "\n"); 385 for (CmsXmlSitemapUrlBean bean : urlBeans) { 386 buffer.append(getXmlForEntry(bean)); 387 buffer.append("\n"); 388 } 389 buffer.append("</urlset>"); 390 return buffer.toString(); 391 } 392 393 /** 394 * Enables or disables computation of container page dates.<p> 395 * 396 * @param computeContainerPageDates the new value 397 */ 398 public void setComputeContainerPageDates(boolean computeContainerPageDates) { 399 400 m_computeContainerPageDates = computeContainerPageDates; 401 } 402 403 /** 404 * Sets the replacement server URL.<p> 405 * 406 * The replacement server URL will replace the scheme/host/port from the URLs returned by getOnlineLink. 407 * 408 * @param serverUrl the server URL 409 */ 410 public void setServerUrl(String serverUrl) { 411 412 m_serverUrl = serverUrl; 413 } 414 415 /** 416 * Adds the detail page links for a given page to the results.<p> 417 * 418 * @param containerPage the container page resource 419 * @param locale the locale of the container page 420 * 421 * @throws CmsException if something goes wrong 422 */ 423 protected void addDetailLinks(CmsResource containerPage, Locale locale) throws CmsException { 424 425 List<I_CmsResourceType> types = getDetailTypesForPage(containerPage); 426 addDetailLinks(containerPage, locale, types); 427 } 428 429 /** 430 * Adds an URL bean to the internal map of results, but only if there is no existing entry with higher internal priority 431 * than the priority given as an argument.<p> 432 * 433 * @param result the result URL bean to add 434 * 435 * @param resultPriority the internal priority to use for updating the map of results 436 */ 437 protected void addResult(CmsXmlSitemapUrlBean result, int resultPriority) { 438 439 String url = CmsFileUtil.removeTrailingSeparator(result.getUrl()); 440 boolean writeEntry = true; 441 if (m_resultMap.containsKey(url)) { 442 LOG.warn("Encountered duplicate URL with while generating sitemap: " + result.getUrl()); 443 ResultEntry entry = m_resultMap.get(url); 444 writeEntry = entry.getPriority() <= resultPriority; 445 } 446 if (writeEntry) { 447 m_resultMap.put(url, new ResultEntry(result, resultPriority)); 448 } 449 } 450 451 /** 452 * Computes the container the container page modification date from its referenced contents.<p> 453 * 454 * @param containerPage the container page 455 * 456 * @return the computed modification date 457 * 458 * @throws CmsException if something goes wrong 459 */ 460 protected long computeContainerPageModificationDate(CmsResource containerPage) throws CmsException { 461 462 CmsRelationFilter filter = CmsRelationFilter.relationsFromStructureId( 463 containerPage.getStructureId()).filterType(CmsRelationType.XML_STRONG); 464 List<CmsRelation> relations = m_guestCms.readRelations(filter); 465 long result = containerPage.getDateLastModified(); 466 for (CmsRelation relation : relations) { 467 try { 468 CmsResource target = relation.getTarget( 469 m_guestCms, 470 CmsResourceFilter.DEFAULT_FILES.addRequireVisible()); 471 long targetDate = target.getDateLastModified(); 472 if (targetDate > result) { 473 result = targetDate; 474 } 475 } catch (CmsException e) { 476 LOG.warn( 477 "Could not get relation target for relation " 478 + relation.toString() 479 + " | " 480 + e.getLocalizedMessage(), 481 e); 482 } 483 } 484 485 return result; 486 } 487 488 /** 489 * Returns whether to exclude the given detail page. 490 * @param detailPage the detail page 491 * @return whether to exclude the given detail page 492 */ 493 protected boolean excludeDetailPage(CmsResource detailPage) { 494 495 CmsADEConfigData adeConfigData = OpenCms.getADEManager().lookupConfigurationWithCache( 496 m_guestCms, 497 detailPage.getRootPath()); 498 String exclude = adeConfigData.getAttribute(ATTR_DETAIL_SETTINGS_PAGE_EXCLUDE, null); 499 return Boolean.valueOf(exclude); 500 } 501 502 /** 503 * Gets the detail link for a given container page and detail content.<p> 504 * 505 * Note: The actual container page used for the result link is not necessarily the container page passed 506 * in as parameter - the default detail page in the sitemap containing the page is used. 507 * 508 * @param pageRes the container page 509 * @param detailRes the detail content 510 * @param locale the locale for which we want the link 511 * 512 * @return the detail page link 513 */ 514 protected String getDetailLink(CmsResource pageRes, CmsResource detailRes, Locale locale) { 515 516 String pageSitePath = m_siteGuestCms.getSitePath(pageRes); 517 String detailSitePath = m_siteGuestCms.getSitePath(detailRes); 518 CmsRequestContext requestContext = m_siteGuestCms.getRequestContext(); 519 String originalUri = requestContext.getUri(); 520 Locale originalLocale = requestContext.getLocale(); 521 try { 522 requestContext.setUri(pageSitePath); 523 requestContext.setLocale(locale); 524 return OpenCms.getLinkManager().getOnlineLink(m_siteGuestCms, detailSitePath, true); 525 } finally { 526 requestContext.setUri(originalUri); 527 requestContext.setLocale(originalLocale); 528 } 529 } 530 531 /** 532 * Gets the types for which a given resource is configured as a detail page.<p> 533 * 534 * @param resource a resource for which we want to find the detail page types 535 * 536 * @return the list of resource types for which the given page is configured as a detail page 537 */ 538 protected List<I_CmsResourceType> getDetailTypesForPage(CmsResource resource) { 539 540 Collection<String> typesForPage = m_detailTypesByPage.get(resource.getRootPath()); 541 String parentPath = CmsFileUtil.removeTrailingSeparator(CmsResource.getParentFolder(resource.getRootPath())); 542 Collection<String> typesForFolder = m_detailTypesByPage.get(parentPath); 543 Set<String> allTypes = new HashSet<String>(); 544 allTypes.addAll(typesForPage); 545 allTypes.addAll(typesForFolder); 546 List<I_CmsResourceType> resTypes = new ArrayList<I_CmsResourceType>(); 547 CmsResourceManager resMan = OpenCms.getResourceManager(); 548 for (String typeName : allTypes) { 549 if (typeName.startsWith(CmsDetailPageInfo.FUNCTION_PREFIX)) { 550 continue; 551 } 552 try { 553 I_CmsResourceType resType = resMan.getResourceType(typeName); 554 resTypes.add(resType); 555 } catch (CmsLoaderException e) { 556 LOG.warn("Invalid resource type name" + typeName + "! " + e.getLocalizedMessage(), e); 557 } 558 } 559 return resTypes; 560 } 561 562 /** 563 * Gets the list of pages which should be directly added to the XML sitemap.<p> 564 * 565 * @return the list of resources which should be directly added to the XML sitemap 566 * 567 * @throws CmsException if something goes wrong 568 */ 569 protected List<CmsResource> getDirectPages() throws CmsException { 570 571 List<CmsResource> result = new ArrayList<CmsResource>(); 572 result.addAll(getNavigationPages()); 573 Set<String> includeRoots = m_includeExcludeSet.getIncludeRoots(); 574 for (String includeRoot : includeRoots) { 575 try { 576 CmsResource resource = m_guestCms.readResource(includeRoot); 577 if (resource.isFile()) { 578 result.add(resource); 579 } else { 580 List<CmsResource> subtreeFiles = m_guestCms.readResources( 581 includeRoot, 582 CmsResourceFilter.DEFAULT_FILES, 583 true); 584 result.addAll(subtreeFiles); 585 } 586 } catch (CmsVfsResourceNotFoundException e) { 587 LOG.warn("Could not read include resource: " + includeRoot); 588 } 589 } 590 Iterator<CmsResource> filterIter = result.iterator(); 591 while (filterIter.hasNext()) { 592 CmsResource currentResource = filterIter.next(); 593 if (currentResource.isInternal() || m_includeExcludeSet.isExcluded(currentResource.getRootPath())) { 594 filterIter.remove(); 595 } 596 } 597 return result; 598 } 599 600 /** 601 * Writes the inner node content for an url element to a buffer.<p> 602 * 603 * @param entry the entry for which the content should be written 604 * @return the inner XML 605 */ 606 protected String getInnerXmlForEntry(CmsXmlSitemapUrlBean entry) { 607 608 StringBuffer buffer = new StringBuffer(); 609 entry.writeElement(buffer, "loc", entry.getUrl()); 610 entry.writeLastmod(buffer); 611 entry.writeChangefreq(buffer); 612 entry.writePriority(buffer); 613 return buffer.toString(); 614 } 615 616 /** 617 * Gets the list of pages from the navigation which should be directly added to the XML sitemap.<p> 618 * 619 * @return the list of pages to add to the XML sitemap 620 */ 621 protected List<CmsResource> getNavigationPages() { 622 623 List<CmsResource> result = new ArrayList<CmsResource>(); 624 CmsJspNavBuilder navBuilder = new CmsJspNavBuilder(m_siteGuestCms); 625 try { 626 CmsResource rootDefaultFile = m_siteGuestCms.readDefaultFile( 627 m_siteGuestCms.getRequestContext().removeSiteRoot(m_baseFolderRootPath), 628 CmsResourceFilter.DEFAULT); 629 if (rootDefaultFile != null) { 630 result.add(rootDefaultFile); 631 } 632 } catch (Exception e) { 633 LOG.info(e.getLocalizedMessage(), e); 634 } 635 List<CmsJspNavElement> navElements = navBuilder.getSiteNavigation( 636 m_baseFolderSitePath, 637 CmsJspNavBuilder.Visibility.includeHidden, 638 -1); 639 for (CmsJspNavElement navElement : navElements) { 640 CmsResource navResource = navElement.getResource(); 641 if (navResource.isFolder()) { 642 try { 643 CmsResource defaultFile = m_guestCms.readDefaultFile(navResource, CmsResourceFilter.DEFAULT_FILES); 644 if (defaultFile != null) { 645 result.add(defaultFile); 646 } else { 647 LOG.warn("Could not get default file for " + navResource.getRootPath()); 648 } 649 } catch (CmsException e) { 650 LOG.warn("Could not get default file for " + navResource.getRootPath()); 651 } 652 } else { 653 result.add(navResource); 654 } 655 } 656 return result; 657 } 658 659 /** 660 * Gets the opening tag for the urlset element (can be overridden to add e.g. more namespaces.<p> 661 * 662 * @return the opening tag 663 */ 664 protected String getUrlSetOpenTag() { 665 666 return "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"; 667 } 668 669 /** 670 * Writes the XML for an URL entry to a buffer.<p> 671 * 672 * @param entry the XML sitemap entry bean 673 * 674 * @return an XML representation of this bean 675 */ 676 protected String getXmlForEntry(CmsXmlSitemapUrlBean entry) { 677 678 StringBuffer buffer = new StringBuffer(); 679 buffer.append("<url>"); 680 buffer.append(getInnerXmlForEntry(entry)); 681 buffer.append("</url>"); 682 return buffer.toString(); 683 } 684 685 /** 686 * Checks whether the given alias is below the base folder.<p> 687 * 688 * @param alias the alias to check 689 * 690 * @return true if the alias is below the base folder 691 */ 692 protected boolean isAliasBelowBaseFolder(CmsAlias alias) { 693 694 boolean isBelowBaseFolder = CmsStringUtil.isPrefixPath(m_baseFolderSitePath, alias.getAliasPath()); 695 return isBelowBaseFolder; 696 } 697 698 /** 699 * Returns whether the given page is a default detail page. 700 * @param resource the page resource 701 * @return whether the given page is a default detail page 702 */ 703 protected boolean isDefaultDetailPage(CmsResource resource) { 704 705 Collection<String> typesForPage = m_detailTypesByPage.get(resource.getRootPath()); 706 String parentPath = CmsFileUtil.removeTrailingSeparator(CmsResource.getParentFolder(resource.getRootPath())); 707 Collection<String> typesForFolder = m_detailTypesByPage.get(parentPath); 708 Set<String> allTypes = new HashSet<String>(); 709 allTypes.addAll(typesForPage); 710 allTypes.addAll(typesForFolder); 711 for (String typeName : allTypes) { 712 if (typeName.equals(CmsADEManager.DEFAULT_DETAILPAGE_TYPE)) { 713 return true; 714 } 715 } 716 return false; 717 } 718 719 /** 720 * Checks whether the page/detail content combination is a valid detail page.<p> 721 * 722 * @param page the container page 723 * @param locale the locale 724 * @param detailRes the detail content resource 725 * 726 * @return true if this is a valid detail page combination 727 */ 728 protected boolean isValidDetailPageCombination(CmsResource page, Locale locale, CmsResource detailRes) { 729 730 return OpenCms.getADEManager().getDetailPageHandler().isValidDetailPage(m_guestCms, page, detailRes); 731 } 732 733 /** 734 * Replaces the protocol/host/port of a link with the ones from the configured server URI, if it's not empty.<p> 735 * 736 * @param link the link to change 737 * 738 * @return the changed link 739 */ 740 protected String replaceServerUri(String link) { 741 742 return replaceServerUri(link, m_serverUrl); 743 } 744 745 /** 746 * Adds the alias links for a given structure id to the results.<p> 747 * 748 * @param aliasStructureId the alias target structure id 749 */ 750 private void addAliasLinks(CmsUUID aliasStructureId) { 751 752 try { 753 CmsResource aliasTarget = m_guestCms.readResource(aliasStructureId); 754 List<CmsProperty> properties = m_guestCms.readPropertyObjects(aliasTarget, true); 755 double priority = getPriority(properties); 756 String changeFrequency = getChangeFrequency(properties); 757 Collection<CmsAlias> aliases = m_pageAliasesBelowBaseFolderByStructureId.get(aliasStructureId); 758 for (CmsAlias alias : aliases) { 759 String aliasLink = (m_siteRootLink + "/" + alias.getAliasPath()).replaceAll("(?<!:)//+", "/"); 760 CmsXmlSitemapUrlBean aliasUrlBean = new CmsXmlSitemapUrlBean( 761 replaceServerUri(aliasLink), 762 -1, 763 changeFrequency, 764 priority); 765 aliasUrlBean.setOriginalResource(aliasTarget); 766 addResult(aliasUrlBean, 1); 767 } 768 } catch (CmsException e) { 769 LOG.error(e.getLocalizedMessage(), e); 770 } 771 } 772 773 /** 774 * Adds the detail page links for a given page to the results.<p> 775 * 776 * @param containerPage the container page resource 777 * @param locale the locale of the container page 778 * @param types the detail types 779 * 780 * @throws CmsException if something goes wrong 781 */ 782 private void addDetailLinks(CmsResource containerPage, Locale locale, List<I_CmsResourceType> types) 783 throws CmsException { 784 785 for (I_CmsResourceType type : types) { 786 List<CmsResource> resourcesForType = getDetailResources(type); 787 for (CmsResource detailRes : resourcesForType) { 788 if (!isValidDetailPageCombination(containerPage, locale, detailRes)) { 789 continue; 790 } 791 List<CmsProperty> detailProps = m_guestCms.readPropertyObjects(detailRes, true); 792 String detailLink = getDetailLink(containerPage, detailRes, locale); 793 String detailLinkRootPath = detailLink; 794 try { 795 detailLinkRootPath = (new URI(detailLink)).getPath(); 796 detailLinkRootPath = CmsLinkManager.removeOpenCmsContext(detailLinkRootPath); 797 detailLinkRootPath = m_siteGuestCms.addSiteRoot(detailLinkRootPath); 798 } catch (URISyntaxException e) { 799 // should not happen 800 } 801 if (!m_includeExcludeSet.isExcluded(detailLinkRootPath)) { 802 CmsXmlSitemapUrlBean detailUrlBean = new CmsXmlSitemapUrlBean( 803 replaceServerUri(detailLink), 804 detailRes.getDateLastModified(), 805 getChangeFrequency(detailProps), 806 getPriority(detailProps)); 807 detailUrlBean.setOriginalResource(detailRes); 808 detailUrlBean.setDetailPageResource(containerPage); 809 addResult(detailUrlBean, 2); 810 } 811 } 812 } 813 } 814 815 /** 816 * Gets all resources from the folder tree beneath the base folder or the shared folder which have a given type.<p> 817 * 818 * @param type the type to filter by 819 * 820 * @return the list of resources with the given type 821 * 822 * @throws CmsException if something goes wrong 823 */ 824 private List<CmsResource> getDetailResources(I_CmsResourceType type) throws CmsException { 825 826 String typeName = type.getTypeName(); 827 if (!m_detailResources.containsKey(typeName)) { 828 List<CmsResource> result = new ArrayList<CmsResource>(); 829 CmsResourceFilter filter = CmsResourceFilter.DEFAULT_FILES.addRequireType(type); 830 List<CmsResource> siteFiles = m_guestCms.readResources(m_siteRoot, filter, true); 831 result.addAll(siteFiles); 832 String shared = CmsFileUtil.removeTrailingSeparator(OpenCms.getSiteManager().getSharedFolder()); 833 if (shared != null) { 834 List<CmsResource> sharedFiles = m_guestCms.readResources(shared, filter, true); 835 result.addAll(sharedFiles); 836 } 837 m_detailResources.put(typeName, result); 838 } 839 return m_detailResources.get(typeName); 840 } 841 842 /** 843 * Gets the locale to use for the given resource.<p> 844 * 845 * @param resource the resource 846 * @param propertyList the properties of the resource 847 * 848 * @return the locale to use for the given resource 849 */ 850 private Locale getLocale(CmsResource resource, List<CmsProperty> propertyList) { 851 852 return OpenCms.getLocaleManager().getDefaultLocale(m_guestCms, m_guestCms.getSitePath(resource)); 853 } 854 855 /** 856 * Reads the data necessary for building the sitemap from the VFS and initializes the internal data structures.<p> 857 * 858 * @param baseSitePath the base site path 859 * 860 * @throws CmsException if something goes wrong 861 */ 862 private void initializeFileData(String baseSitePath) throws CmsException { 863 864 m_resultMap.clear(); 865 m_siteRootLink = OpenCms.getLinkManager().getOnlineLink(m_siteGuestCms, "/"); 866 m_siteRootLink = CmsFileUtil.removeTrailingSeparator(m_siteRootLink); 867 m_detailPageInfos = OpenCms.getADEManager().getAllDetailPages(m_guestCms); 868 for (CmsDetailPageInfo detailPageInfo : m_detailPageInfos) { 869 String type = detailPageInfo.getType(); 870 String path = detailPageInfo.getUri(); 871 path = CmsFileUtil.removeTrailingSeparator(path); 872 m_detailTypesByPage.put(path, type); 873 } 874 List<CmsAlias> siteAliases = OpenCms.getAliasManager().getAliasesForSite( 875 m_siteGuestCms, 876 m_siteGuestCms.getRequestContext().getSiteRoot()); 877 for (CmsAlias alias : siteAliases) { 878 if (isAliasBelowBaseFolder(alias) && (alias.getMode() == CmsAliasMode.page)) { 879 CmsUUID aliasId = alias.getStructureId(); 880 m_pageAliasesBelowBaseFolderByStructureId.put(aliasId, alias); 881 } 882 } 883 884 } 885}