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            try {
530                I_CmsResourceType resType = resMan.getResourceType(typeName);
531                resTypes.add(resType);
532            } catch (CmsLoaderException e) {
533                LOG.warn("Invalid resource type name" + typeName + "! " + e.getLocalizedMessage(), e);
534            }
535        }
536        return resTypes;
537    }
538
539    /**
540     * Gets the list of pages which should be directly added to the XML sitemap.<p>
541     *
542     * @return the list of resources which should be directly added to the XML sitemap
543     *
544     * @throws CmsException if something goes wrong
545     */
546    protected List<CmsResource> getDirectPages() throws CmsException {
547
548        List<CmsResource> result = new ArrayList<CmsResource>();
549        result.addAll(getNavigationPages());
550        Set<String> includeRoots = m_includeExcludeSet.getIncludeRoots();
551        for (String includeRoot : includeRoots) {
552            try {
553                CmsResource resource = m_guestCms.readResource(includeRoot);
554                if (resource.isFile()) {
555                    result.add(resource);
556                } else {
557                    List<CmsResource> subtreeFiles = m_guestCms.readResources(
558                        includeRoot,
559                        CmsResourceFilter.DEFAULT_FILES,
560                        true);
561                    result.addAll(subtreeFiles);
562                }
563            } catch (CmsVfsResourceNotFoundException e) {
564                LOG.warn("Could not read include resource: " + includeRoot);
565            }
566        }
567        Iterator<CmsResource> filterIter = result.iterator();
568        while (filterIter.hasNext()) {
569            CmsResource currentResource = filterIter.next();
570            if (currentResource.isInternal() || m_includeExcludeSet.isExcluded(currentResource.getRootPath())) {
571                filterIter.remove();
572            }
573        }
574        return result;
575    }
576
577    /**
578     * Writes the inner node content for an url element to a buffer.<p>
579     *
580     * @param entry the entry for which the content should be written
581     * @return the inner XML
582     */
583    protected String getInnerXmlForEntry(CmsXmlSitemapUrlBean entry) {
584
585        StringBuffer buffer = new StringBuffer();
586        entry.writeElement(buffer, "loc", entry.getUrl());
587        entry.writeLastmod(buffer);
588        entry.writeChangefreq(buffer);
589        entry.writePriority(buffer);
590        return buffer.toString();
591    }
592
593    /**
594     * Gets the list of pages from the navigation which should be directly added to the XML sitemap.<p>
595     *
596     * @return the list of pages to add to the XML sitemap
597     */
598    protected List<CmsResource> getNavigationPages() {
599
600        List<CmsResource> result = new ArrayList<CmsResource>();
601        CmsJspNavBuilder navBuilder = new CmsJspNavBuilder(m_siteGuestCms);
602        try {
603            CmsResource rootDefaultFile = m_siteGuestCms.readDefaultFile(
604                m_siteGuestCms.getRequestContext().removeSiteRoot(m_baseFolderRootPath),
605                CmsResourceFilter.DEFAULT);
606            if (rootDefaultFile != null) {
607                result.add(rootDefaultFile);
608            }
609        } catch (Exception e) {
610            LOG.info(e.getLocalizedMessage(), e);
611        }
612        List<CmsJspNavElement> navElements = navBuilder.getSiteNavigation(m_baseFolderSitePath, -1);
613        for (CmsJspNavElement navElement : navElements) {
614            CmsResource navResource = navElement.getResource();
615            if (navResource.isFolder()) {
616                try {
617                    CmsResource defaultFile = m_guestCms.readDefaultFile(navResource, CmsResourceFilter.DEFAULT_FILES);
618                    if (defaultFile != null) {
619                        result.add(defaultFile);
620                    } else {
621                        LOG.warn("Could not get default file for " + navResource.getRootPath());
622                    }
623                } catch (CmsException e) {
624                    LOG.warn("Could not get default file for " + navResource.getRootPath());
625                }
626            } else {
627                result.add(navResource);
628            }
629        }
630        return result;
631    }
632
633    /**
634     * Gets the opening tag for the urlset element (can be overridden to add e.g. more namespaces.<p>
635     *
636     * @return the opening tag
637     */
638    protected String getUrlSetOpenTag() {
639
640        return "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">";
641    }
642
643    /**
644     * Writes the XML for an URL entry to a buffer.<p>
645     *
646     * @param entry the XML sitemap entry bean
647     *
648     * @return an XML representation of this bean
649     */
650    protected String getXmlForEntry(CmsXmlSitemapUrlBean entry) {
651
652        StringBuffer buffer = new StringBuffer();
653        buffer.append("<url>");
654        buffer.append(getInnerXmlForEntry(entry));
655        buffer.append("</url>");
656        return buffer.toString();
657    }
658
659    /**
660     * Checks whether the given alias is below the base folder.<p>
661     *
662     * @param alias the alias to check
663     *
664     * @return true if the alias is below the base folder
665     */
666    protected boolean isAliasBelowBaseFolder(CmsAlias alias) {
667
668        boolean isBelowBaseFolder = CmsStringUtil.isPrefixPath(m_baseFolderSitePath, alias.getAliasPath());
669        return isBelowBaseFolder;
670    }
671
672    /**
673     * Replaces the protocol/host/port of a link with the ones from the configured server URI, if it's not empty.<p>
674     *
675     * @param link the link to change
676     *
677     * @return the changed link
678     */
679    protected String replaceServerUri(String link) {
680
681        return replaceServerUri(link, m_serverUrl);
682    }
683
684    /**
685     * Adds the alias links for a given structure id to the results.<p>
686     *
687     * @param aliasStructureId the alias target structure id
688     */
689    private void addAliasLinks(CmsUUID aliasStructureId) {
690
691        try {
692            CmsResource aliasTarget = m_guestCms.readResource(aliasStructureId);
693            List<CmsProperty> properties = m_guestCms.readPropertyObjects(aliasTarget, true);
694            double priority = getPriority(properties);
695            String changeFrequency = getChangeFrequency(properties);
696            Collection<CmsAlias> aliases = m_pageAliasesBelowBaseFolderByStructureId.get(aliasStructureId);
697            for (CmsAlias alias : aliases) {
698                String aliasLink = (m_siteRootLink + "/" + alias.getAliasPath()).replaceAll("(?<!:)//+", "/");
699                CmsXmlSitemapUrlBean aliasUrlBean = new CmsXmlSitemapUrlBean(
700                    replaceServerUri(aliasLink),
701                    -1,
702                    changeFrequency,
703                    priority);
704                aliasUrlBean.setOriginalResource(aliasTarget);
705                addResult(aliasUrlBean, 1);
706            }
707        } catch (CmsException e) {
708            LOG.error(e.getLocalizedMessage(), e);
709        }
710    }
711
712    /**
713     * Gets all resources from the folder tree beneath the base folder or the shared folder which have a given type.<p>
714     *
715     * @param type the type to filter by
716     *
717     * @return the list of resources with the given type
718     *
719     * @throws CmsException if something goes wrong
720     */
721    private List<CmsResource> getDetailResources(I_CmsResourceType type) throws CmsException {
722
723        String typeName = type.getTypeName();
724        if (!m_detailResources.containsKey(typeName)) {
725            List<CmsResource> result = new ArrayList<CmsResource>();
726            CmsResourceFilter filter = CmsResourceFilter.DEFAULT_FILES.addRequireType(type);
727            List<CmsResource> siteFiles = m_guestCms.readResources(m_siteRoot, filter, true);
728            result.addAll(siteFiles);
729            String shared = CmsFileUtil.removeTrailingSeparator(OpenCms.getSiteManager().getSharedFolder());
730            if (shared != null) {
731                List<CmsResource> sharedFiles = m_guestCms.readResources(shared, filter, true);
732                result.addAll(sharedFiles);
733            }
734            m_detailResources.put(typeName, result);
735        }
736        return m_detailResources.get(typeName);
737    }
738
739    /**
740     * Gets the locale to use for the given resource.<p>
741     *
742     * @param resource the resource
743     * @param propertyList the properties of the resource
744     *
745     * @return the locale to use for the given resource
746     */
747    private Locale getLocale(CmsResource resource, List<CmsProperty> propertyList) {
748
749        return OpenCms.getLocaleManager().getDefaultLocale(m_guestCms, m_guestCms.getSitePath(resource));
750    }
751
752    /**
753     * Reads the data necessary for building the sitemap from the VFS and initializes the internal data structures.<p>
754     *
755     * @param baseSitePath the base site path
756     *
757     * @throws CmsException if something goes wrong
758     */
759    private void initializeFileData(String baseSitePath) throws CmsException {
760
761        m_resultMap.clear();
762        m_siteRootLink = OpenCms.getLinkManager().getOnlineLink(m_siteGuestCms, "/");
763        m_siteRootLink = CmsFileUtil.removeTrailingSeparator(m_siteRootLink);
764        m_detailPageInfos = OpenCms.getADEManager().getAllDetailPages(m_guestCms);
765        for (CmsDetailPageInfo detailPageInfo : m_detailPageInfos) {
766            String type = detailPageInfo.getType();
767            String path = detailPageInfo.getUri();
768            path = CmsFileUtil.removeTrailingSeparator(path);
769            m_detailTypesByPage.put(path, type);
770        }
771        List<CmsAlias> siteAliases = OpenCms.getAliasManager().getAliasesForSite(
772            m_siteGuestCms,
773            m_siteGuestCms.getRequestContext().getSiteRoot());
774        for (CmsAlias alias : siteAliases) {
775            if (isAliasBelowBaseFolder(alias) && (alias.getMode() == CmsAliasMode.page)) {
776                CmsUUID aliasId = alias.getStructureId();
777                m_pageAliasesBelowBaseFolderByStructureId.put(aliasId, alias);
778            }
779        }
780
781    }
782
783    /**
784     * Checks whether the page/detail content combination is a valid detail page.<p>
785     *
786     * @param page the container page
787     * @param locale the locale
788     * @param detailRes the detail content resource
789     *
790     * @return true if this is a valid detail page combination
791     */
792    private boolean isValidDetailPageCombination(CmsResource page, Locale locale, CmsResource detailRes) {
793
794        return true;
795    }
796
797}