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