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, 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.site.xmlsitemap; 029 030import org.opencms.ade.configuration.CmsADEConfigData.DetailInfo; 031import org.opencms.file.CmsProperty; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.file.types.I_CmsResourceType; 035import org.opencms.main.CmsException; 036import org.opencms.main.CmsLog; 037import org.opencms.main.OpenCms; 038import org.opencms.util.CmsFileUtil; 039import org.opencms.util.CmsPathMap; 040import org.opencms.util.CmsStringUtil; 041 042import java.util.ArrayList; 043import java.util.Collection; 044import java.util.Collections; 045import java.util.Comparator; 046import java.util.HashMap; 047import java.util.List; 048import java.util.Locale; 049 050import org.apache.commons.logging.Log; 051 052import com.google.common.collect.ArrayListMultimap; 053import com.google.common.collect.Lists; 054import com.google.common.collect.Maps; 055import com.google.common.collect.Multimap; 056 057/** 058 * Sitemap generator class which tries to eliminate duplicate detail pages for the same content and locale.<p> 059 * 060 * In principle, any detail page for a type somewhere in the system could be used to display contents anywhere 061 * else in the system. This sitemap generator, instead of generating all detail page URLs that could possibly be generated, 062 * instead tries to find only the best candidate URL for each content / locale combination. 063 */ 064public class CmsDetailPageDuplicateEliminatingSitemapGenerator extends CmsXmlSitemapGenerator { 065 066 /** The logger instance for this class. */ 067 private static final Log LOG = CmsLog.getLog(CmsDetailPageDuplicateEliminatingSitemapGenerator.class); 068 069 /** The detail page information. */ 070 protected List<DetailInfo> m_detailInfos = new ArrayList<DetailInfo>(); 071 072 /** Multimap of detail infos with the detail page as key. */ 073 private Multimap<String, DetailInfo> m_detailInfosByPage; 074 075 /** Cache for path maps containing the content resources. */ 076 private HashMap<String, CmsPathMap<CmsResource>> m_pathMapsByType = Maps.newHashMap(); 077 078 /** 079 * Constructor.<p> 080 * 081 * @param sitemapPath the sitemap path 082 * @throws CmsException if something goes wrong 083 */ 084 public CmsDetailPageDuplicateEliminatingSitemapGenerator(String sitemapPath) 085 throws CmsException { 086 087 super(sitemapPath); 088 List<DetailInfo> rawDetailInfo = OpenCms.getADEManager().getDetailInfo(m_guestCms); 089 List<DetailInfo> filteredDetailInfo = Lists.newArrayList(); 090 for (DetailInfo item : rawDetailInfo) { 091 String path = item.getFolderPath(); 092 if (OpenCms.getSiteManager().startsWithShared(path) || CmsStringUtil.isPrefixPath(m_siteRoot, path)) { 093 filteredDetailInfo.add(item); 094 } else { 095 if (LOG.isDebugEnabled()) { 096 LOG.debug("Filtered detail info: " + item); 097 } 098 } 099 } 100 m_detailInfos = filteredDetailInfo; 101 102 } 103 104 /** 105 * @see org.opencms.site.xmlsitemap.CmsXmlSitemapGenerator#generateSitemapBeans() 106 */ 107 @Override 108 public List<CmsXmlSitemapUrlBean> generateSitemapBeans() throws CmsException { 109 110 List<CmsXmlSitemapUrlBean> parentResult = super.generateSitemapBeans(); 111 List<CmsXmlSitemapUrlBean> result = Lists.newArrayList(); 112 Multimap<String, CmsXmlSitemapUrlBean> detailPageBeans = ArrayListMultimap.create(); 113 114 // We want to eliminate duplicate detail pages for the same detail content and locale, 115 // so first we group the XML sitemap beans belonging to detail pages by their locale/content combination, 116 // and then we sort each group by the sitemap configuration where the detail page is coming from, 117 // and then only take the last element in each group. 118 119 for (CmsXmlSitemapUrlBean urlBean : parentResult) { 120 if (urlBean.getDetailPageResource() == null) { 121 result.add(urlBean); 122 } else { 123 String localeKey = urlBean.getOriginalResource().getStructureId() + "_" + urlBean.getLocale(); 124 detailPageBeans.put(localeKey, urlBean); 125 } 126 } 127 Comparator<CmsXmlSitemapUrlBean> pathComparator = new Comparator<CmsXmlSitemapUrlBean>() { 128 129 public int compare(CmsXmlSitemapUrlBean urlbean1, CmsXmlSitemapUrlBean urlbean2) { 130 131 String subsite1 = urlbean1.getSubsite(); 132 if (subsite1 == null) { 133 subsite1 = ""; 134 } 135 String subsite2 = urlbean2.getSubsite(); 136 if (subsite2 == null) { 137 subsite2 = ""; 138 } 139 return subsite1.compareTo(subsite2); 140 } 141 }; 142 for (String key : detailPageBeans.keySet()) { 143 result.add(Collections.max(detailPageBeans.get(key), pathComparator)); 144 } 145 return result; 146 } 147 148 /** 149 * @see org.opencms.site.xmlsitemap.CmsXmlSitemapGenerator#addDetailLinks(org.opencms.file.CmsResource, java.util.Locale) 150 */ 151 @Override 152 protected void addDetailLinks(CmsResource containerPage, Locale locale) throws CmsException { 153 154 Collection<DetailInfo> detailInfos = getDetailInfosForPage(containerPage); 155 for (DetailInfo info : detailInfos) { 156 List<CmsResource> contents = getContents(info.getFolderPath(), info.getType()); 157 for (CmsResource detailRes : contents) { 158 if (OpenCms.getADEManager().getDetailPageHandler().isValidDetailPage( 159 m_guestCms, 160 containerPage, 161 detailRes)) { 162 List<CmsProperty> detailProps = m_guestCms.readPropertyObjects(detailRes, true); 163 String detailLink = getDetailLink(containerPage, detailRes, locale); 164 detailLink = CmsFileUtil.removeTrailingSeparator(detailLink); 165 CmsXmlSitemapUrlBean detailUrlBean = new CmsXmlSitemapUrlBean( 166 replaceServerUri(detailLink), 167 detailRes.getDateLastModified(), 168 getChangeFrequency(detailProps), 169 getPriority(detailProps)); 170 detailUrlBean.setLocale(locale); 171 detailUrlBean.setOriginalResource(detailRes); 172 detailUrlBean.setDetailPageResource(containerPage); 173 detailUrlBean.setSubsite(info.getBasePath()); 174 addResult(detailUrlBean, 2); 175 } 176 } 177 } 178 } 179 180 /** 181 * Gets the contents for the given folder path and type name.<p> 182 * 183 * @param folderPath the content folder path 184 * @param type the type name 185 * @return the list of contents 186 * 187 * @throws CmsException if something goes wrong 188 */ 189 private List<CmsResource> getContents(String folderPath, String type) throws CmsException { 190 191 CmsPathMap<CmsResource> pathMap = getPathMapForType(type); 192 return pathMap.getChildValues(folderPath); 193 } 194 195 /** 196 * Gets the detail information for the given container page.<p> 197 * 198 * @param containerPage the container page 199 * @return the detail information 200 */ 201 private Collection<DetailInfo> getDetailInfosForPage(CmsResource containerPage) { 202 203 if (m_detailInfosByPage == null) { 204 m_detailInfosByPage = ArrayListMultimap.create(); 205 for (DetailInfo detailInfo : m_detailInfos) { 206 m_detailInfosByPage.put(detailInfo.getDetailPageInfo().getUri(), detailInfo); 207 } 208 } 209 String folderPath = CmsResource.getParentFolder(containerPage.getRootPath()); 210 Collection<DetailInfo> result = m_detailInfosByPage.get(containerPage.getRootPath()); 211 if (result.isEmpty()) { 212 result = m_detailInfosByPage.get(folderPath); 213 } 214 return result; 215 } 216 217 /** 218 * Gets the path map containing the contents for the given type.<p> 219 * 220 * @param typeName the type name 221 * @return the path map with the content resources 222 * 223 * @throws CmsException if something goes wrong 224 */ 225 private CmsPathMap<CmsResource> getPathMapForType(String typeName) throws CmsException { 226 227 if (!m_pathMapsByType.containsKey(typeName)) { 228 CmsPathMap<CmsResource> pathMap = readPathMapForType( 229 OpenCms.getResourceManager().getResourceType(typeName)); 230 m_pathMapsByType.put(typeName, pathMap); 231 } 232 return m_pathMapsByType.get(typeName); 233 } 234 235 /** 236 * Reads the contents of a given type and stores them in a path map.<p> 237 * 238 * @param type the type for which to read the contents 239 * @return the path map containing the contents 240 */ 241 private CmsPathMap<CmsResource> readPathMapForType(I_CmsResourceType type) { 242 243 List<CmsResource> result = new ArrayList<CmsResource>(); 244 CmsResourceFilter filter = CmsResourceFilter.DEFAULT_FILES.addRequireType(type); 245 try { 246 List<CmsResource> siteFiles = m_guestCms.readResources(m_siteRoot, filter, true); 247 result.addAll(siteFiles); 248 } catch (CmsException e) { 249 LOG.error("XML sitemap generator error: " + e.getLocalizedMessage(), e); 250 } 251 String shared = CmsFileUtil.removeTrailingSeparator(OpenCms.getSiteManager().getSharedFolder()); 252 if (shared != null) { 253 try { 254 List<CmsResource> sharedFiles = m_guestCms.readResources(shared, filter, true); 255 result.addAll(sharedFiles); 256 } catch (CmsException e) { 257 LOG.error("XML sitemap generator error: " + e.getLocalizedMessage(), e); 258 } 259 } 260 CmsPathMap<CmsResource> resultMap = new CmsPathMap<CmsResource>(); 261 for (CmsResource resource : result) { 262 resultMap.add(resource.getRootPath(), resource); 263 } 264 return resultMap; 265 } 266 267}