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}