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