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