001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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 GmbH & Co. KG, 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.staticexport;
029
030import org.opencms.ade.detailpage.CmsDetailPageUtil;
031import org.opencms.ade.detailpage.I_CmsDetailPageHandler;
032import org.opencms.db.CmsExportPoint;
033import org.opencms.file.CmsFile;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsProperty;
036import org.opencms.file.CmsPropertyDefinition;
037import org.opencms.file.CmsResource;
038import org.opencms.file.CmsVfsResourceNotFoundException;
039import org.opencms.file.types.CmsResourceTypeJsp;
040import org.opencms.i18n.CmsAcceptLanguageHeaderParser;
041import org.opencms.i18n.CmsI18nInfo;
042import org.opencms.i18n.CmsLocaleManager;
043import org.opencms.loader.CmsDumpLoader;
044import org.opencms.loader.CmsImageLoader;
045import org.opencms.loader.I_CmsResourceLoader;
046import org.opencms.loader.I_CmsStaticExportDirectResponseLoader;
047import org.opencms.loader.I_CmsStaticExportStreamLoader;
048import org.opencms.loader.I_CmsStoredContentDirectDeliveryLoader;
049import org.opencms.main.CmsContextInfo;
050import org.opencms.main.CmsEvent;
051import org.opencms.main.CmsException;
052import org.opencms.main.CmsIllegalArgumentException;
053import org.opencms.main.CmsLog;
054import org.opencms.main.CmsSystemInfo;
055import org.opencms.main.I_CmsEventListener;
056import org.opencms.main.OpenCms;
057import org.opencms.monitor.CmsMemoryMonitor;
058import org.opencms.report.CmsLogReport;
059import org.opencms.report.I_CmsReport;
060import org.opencms.security.CmsSecurityException;
061import org.opencms.site.CmsSite;
062import org.opencms.site.CmsSiteManagerImpl;
063import org.opencms.site.xmlsitemap.CmsXmlSeoConfiguration;
064import org.opencms.staticexport.CmsExportname.CmsExportNameComparator;
065import org.opencms.util.CmsFileUtil;
066import org.opencms.util.CmsMacroResolver;
067import org.opencms.util.CmsRequestUtil;
068import org.opencms.util.CmsStringUtil;
069import org.opencms.util.CmsUUID;
070import org.opencms.workplace.CmsWorkplace;
071
072import java.io.File;
073import java.io.FileOutputStream;
074import java.io.IOException;
075import java.net.MalformedURLException;
076import java.net.URL;
077import java.util.ArrayList;
078import java.util.Collections;
079import java.util.HashMap;
080import java.util.HashSet;
081import java.util.Iterator;
082import java.util.LinkedHashMap;
083import java.util.List;
084import java.util.Locale;
085import java.util.Map;
086import java.util.Map.Entry;
087import java.util.Set;
088import java.util.TreeMap;
089import java.util.regex.Pattern;
090
091import org.apache.commons.lang3.Strings;
092import org.apache.commons.logging.Log;
093
094import jakarta.servlet.RequestDispatcher;
095import jakarta.servlet.ServletException;
096import jakarta.servlet.http.HttpServletRequest;
097import jakarta.servlet.http.HttpServletResponse;
098
099/**
100 * Provides the functionality to export resources from the OpenCms VFS
101 * to the file system.<p>
102 *
103 * @since 6.0.0
104 */
105public class CmsStaticExportManager implements I_CmsEventListener {
106
107    /** Name for the default file. */
108    public static final String DEFAULT_FILE = "index.html";
109
110    /** Marker for error message attribute. */
111    public static final String EXPORT_ATTRIBUTE_ERROR_MESSAGE = RequestDispatcher.ERROR_MESSAGE;
112
113    /** Marker for error request uri attribute. */
114    public static final String EXPORT_ATTRIBUTE_ERROR_REQUEST_URI = RequestDispatcher.ERROR_REQUEST_URI;
115
116    /** Marker for error servlet name attribute. */
117    public static final String EXPORT_ATTRIBUTE_ERROR_SERVLET_NAME = RequestDispatcher.ERROR_SERVLET_NAME;
118
119    /** Marker for error status code attribute. */
120    public static final String EXPORT_ATTRIBUTE_ERROR_STATUS_CODE = RequestDispatcher.ERROR_STATUS_CODE;
121
122    /** Name for the backup folder default name. */
123    public static final String EXPORT_BACKUP_FOLDER_NAME = "backup";
124
125    /** Name for the default work path. */
126    public static final Integer EXPORT_DEFAULT_BACKUPS = Integer.valueOf(0);
127
128    /** Name for the folder default index file. */
129    public static final String EXPORT_DEFAULT_FILE = "index_export.html";
130
131    /** Name for the default work path. */
132    public static final String EXPORT_DEFAULT_WORKPATH = CmsSystemInfo.FOLDER_WEBINF + "temp";
133
134    /** Flag value for links without parameters. */
135    public static final int EXPORT_LINK_WITH_PARAMETER = 2;
136
137    /** Flag value for links without parameters. */
138    public static final int EXPORT_LINK_WITHOUT_PARAMETER = 1;
139
140    /** Marker for externally redirected 404 uri's. */
141    public static final String EXPORT_MARKER = "exporturi";
142
143    /** Time given (in seconds) to the static export handler to finish a publish task. */
144    public static final int HANDLER_FINISH_TIME = 60;
145
146    /** Pattern to find '..' as a path component. */
147    public static final Pattern PATTERN_PARENT_PATH_COMPONENT = Pattern.compile("(^|[/\\\\])\\.\\.([/\\\\]|$)");
148
149    /**
150     * If the property 'secure' is set to this value,
151     * the resource will be delivered through http and https depending on the link source.
152     */
153    public static final String SECURE_PROPERTY_VALUE_BOTH = "both";
154
155    /** Cache value to indicate a true 404 error. */
156    private static final String CACHEVALUE_404 = "?404";
157
158    /** The log object for this class. */
159    private static final Log LOG = CmsLog.getLog(CmsStaticExportManager.class);
160
161    /** HTTP header Accept-Charset. */
162    private String m_acceptCharsetHeader;
163
164    /** HTTP header Accept-Language. */
165    private String m_acceptLanguageHeader;
166
167    /** CMS context with admin permissions. */
168    private CmsObject m_adminCms;
169
170    /** Cache for the export links. */
171    private Map<String, Boolean> m_cacheExportLinks;
172
173    /** Cache for the export uris. */
174    private Map<String, CmsStaticExportData> m_cacheExportUris;
175
176    /** Cache for the online links. */
177    private Map<String, String> m_cacheOnlineLinks;
178
179    /** Cache for the secure links. */
180    private Map<String, String> m_cacheSecureLinks;
181
182    /** OpenCms default charset header. */
183    private String m_defaultAcceptCharsetHeader;
184
185    /** OpenCms default locale header. */
186    private String m_defaultAcceptLanguageHeader;
187
188    /** Matcher for  selecting those resources which should be part of the static export. */
189    private CmsExportFolderMatcher m_exportFolderMatcher;
190
191    /** List of export resources which should be part of the static export. */
192    private List<String> m_exportFolders;
193
194    /** The additional http headers for the static export. */
195    private List<String> m_exportHeaders;
196
197    /** List of all resources that have the "exportname" property set: &lt;system-wide unique export name, root path&gt;. */
198    private Map<CmsExportname, String> m_exportnameResources;
199
200    /** Indicates if <code>true</code> is the default value for the property "export". */
201    private boolean m_exportPropertyDefault;
202
203    /** Indicates if links in the static export should be relative. */
204    private boolean m_exportRelativeLinks;
205
206    /** List of export rules. */
207    private List<CmsStaticExportExportRule> m_exportRules;
208
209    /** List of export suffixes where the "export" property default is always <code>true</code>. */
210    private List<String> m_exportSuffixes;
211
212    /** Temporary variable for reading the xml config file. */
213    private CmsStaticExportExportRule m_exportTmpRule;
214
215    /** Export url to send internal requests to. */
216    private String m_exportUrl;
217
218    /** Export url with unsubstituted context values. */
219    private String m_exportUrlConfigured;
220
221    /** Export url to send internal requests to without http://servername. */
222    private String m_exportUrlPrefix;
223
224    /** Boolean value if the export is a full static export. */
225    private boolean m_fullStaticExport;
226
227    /** Handler class for static export. */
228    private I_CmsStaticExportHandler m_handler;
229
230    /** The configured link substitution handler. */
231    private I_CmsLinkSubstitutionHandler m_linkSubstitutionHandler;
232
233    /** Lock object for write access to the {@link #cmsEvent(CmsEvent)} method. */
234    private Object m_lockCmsEvent;
235
236    /** Lock object for export folder deletion in {@link #scrubExportFolders(I_CmsReport)}. */
237    private Object m_lockScrubExportFolders;
238
239    /** Lock object for write access to the {@link #m_exportnameResources} map in {@link #computeVfsExportnames()}. */
240    private Object m_lockSetExportnames;
241
242    /** The protected export path. */
243    private String m_protectedExportPath;
244
245    /** The protected export points. */
246    private Map<String, String> m_protectedExportPoints = new LinkedHashMap<String, String>();
247
248    /** Indicates if the quick static export for plain resources is enabled. */
249    private boolean m_quickPlainExport;
250
251    /** Remote address. */
252    private String m_remoteAddr;
253
254    /** Prefix to use for exported files. */
255    private String m_rfsPrefix;
256
257    /** Prefix to use for exported files with unsubstituted context values. */
258    private String m_rfsPrefixConfigured;
259
260    /** List of configured rfs rules. */
261    private List<CmsStaticExportRfsRule> m_rfsRules;
262
263    /** Temporary variable for reading the xml config file. */
264    private CmsStaticExportRfsRule m_rfsTmpRule;
265
266    /** The number of backups stored for the export folder. */
267    private Integer m_staticExportBackups;
268
269    /** Indicates if the static export is enabled or disabled. */
270    private boolean m_staticExportEnabled;
271
272    /** The shared cache configuration. */
273    private CmsSharedCacheConfiguration m_sharedCacheConfiguration = new CmsSharedCacheConfiguration();
274
275    /** The path to where the static export will be written. */
276    private String m_staticExportPath;
277
278    /** The path to where the static export will be written without the complete rfs path. */
279    private String m_staticExportPathConfigured;
280
281    /** The path to where the static export will be written during the static export process. */
282    private String m_staticExportWorkPath;
283
284    /** The path to where the static export will be written during the static export process without the complete rfs path. */
285    private String m_staticExportWorkPathConfigured;
286
287    /** The stored content delivery configuration. */
288    private CmsStoredContentDeliveryConfiguration m_storedContentDeliveryConfiguration = new CmsStoredContentDeliveryConfiguration();
289
290    /** Support for stored content delivery during static export. */
291    private CmsStoredContentDeliverySupport m_storedContentDeliverySupport = new CmsStoredContentDeliverySupport();
292
293    /** Vfs Name of a resource used to do a "static export required" test. */
294    private String m_testResource;
295
296    /** If there are several identical export paths the usage of temporary directories has to be disabled. */
297    private boolean m_useTempDirs = true;
298
299    /** Prefix to use for internal OpenCms files. */
300    private String m_vfsPrefix;
301
302    /** Prefix to use for internal OpenCms files with unsubstituted context values. */
303    private String m_vfsPrefixConfigured;
304
305    /**
306     * Creates a new static export property object.<p>
307     *
308     */
309    public CmsStaticExportManager() {
310
311        m_lockCmsEvent = new Object();
312        m_lockScrubExportFolders = new Object();
313        m_lockSetExportnames = new Object();
314        m_exportSuffixes = new ArrayList<String>();
315        m_exportFolders = new ArrayList<String>();
316        m_exportHeaders = new ArrayList<String>();
317        m_rfsRules = new ArrayList<CmsStaticExportRfsRule>();
318        m_exportRules = new ArrayList<CmsStaticExportExportRule>();
319        m_exportTmpRule = new CmsStaticExportExportRule("", "");
320        m_rfsTmpRule = new CmsStaticExportRfsRule("", "", "", "", "", "", null, null);
321        m_fullStaticExport = false;
322    }
323
324    /**
325     * Creates unique, valid RFS name for the given filename that contains
326     * a coded version of the given parameters, with the given file extension appended.<p>
327     *
328     * Adapted from CmsFileUtil.getRfsPath().
329     *
330     * @param filename the base file name
331     * @param extension the extension to use
332     * @param parameters the parameters to code in the result file name
333     *
334     * @return a unique, valid RFS name for the given parameters
335     *
336     * @see org.opencms.staticexport.CmsStaticExportManager
337     */
338    public static String getRfsPath(String filename, String extension, String parameters) {
339
340        boolean appendSlash = false;
341        if (filename.endsWith("/")) {
342            appendSlash = true;
343            filename = filename.substring(0, filename.length() - 1);
344        }
345        StringBuffer buf = new StringBuffer(128);
346        buf.append(filename);
347        buf.append('_');
348        int h = parameters.hashCode();
349        // ensure we do have a positive id value
350        buf.append(h > 0 ? h : -h);
351        buf.append(extension);
352        if (appendSlash) {
353            buf.append("/");
354        }
355        return buf.toString();
356    }
357
358    /**
359     * Returns the real file system name plus the default file name.<p>
360     *
361     * @param rfsName the real file system name to append the default file name to
362     * @param isFolder signals whether the according virtual file system resource is an folder or not
363     *
364     * @return the real file system name plus the default file name
365     */
366    public String addDefaultFileNameToFolder(String rfsName, boolean isFolder) {
367
368        StringBuffer name = new StringBuffer(rfsName);
369
370        if (isFolder) {
371            // vfs folder case
372            name.append(EXPORT_DEFAULT_FILE);
373        }
374        return name.toString();
375    }
376
377    /**
378     * Adds a new export rule to the configuration.<p>
379     *
380     * @param name the name of the rule
381     * @param description the description for the rule
382     */
383    public void addExportRule(String name, String description) {
384
385        m_exportRules.add(
386            new CmsStaticExportExportRule(
387                name,
388                description,
389                m_exportTmpRule.getModifiedResources(),
390                m_exportTmpRule.getExportResourcePatterns()));
391        m_exportTmpRule = new CmsStaticExportExportRule("", "");
392    }
393
394    /**
395     * Adds a regex to the latest export rule.<p>
396     *
397     * @param regex the regex to add
398     */
399    public void addExportRuleRegex(String regex) {
400
401        m_exportTmpRule.addModifiedResource(regex);
402    }
403
404    /**
405     * Adds a export uri to the latest export rule.<p>
406     *
407     * @param exportUri the export uri to add
408     */
409    public void addExportRuleUri(String exportUri) {
410
411        m_exportTmpRule.addExportResourcePattern(exportUri);
412    }
413
414    /**
415     * Adds an protected export point.<p>
416     *
417     * @param uri the source URI
418     * @param destination the export destination
419     */
420    public void addProtectedExportPoint(String uri, String destination) {
421
422        m_protectedExportPoints.put(uri, destination);
423    }
424
425    /**
426     * Adds a new rfs rule to the configuration.<p>
427     *
428     * @param name the name of the rule
429     * @param description the description for the rule
430     * @param source the source regex
431     * @param rfsPrefix the url prefix
432     * @param exportPath the rfs export path
433     * @param exportWorkPath the rfs export work path
434     * @param exportBackups the number of backups
435     * @param useRelativeLinks the relative links value
436     */
437    public void addRfsRule(
438        String name,
439        String description,
440        String source,
441        String rfsPrefix,
442        String exportPath,
443        String exportWorkPath,
444        String exportBackups,
445        String useRelativeLinks) {
446
447        if ((m_staticExportPathConfigured != null) && exportPath.equals(m_staticExportPathConfigured)) {
448            m_useTempDirs = false;
449        }
450        Iterator<CmsStaticExportRfsRule> itRules = m_rfsRules.iterator();
451        while (m_useTempDirs && itRules.hasNext()) {
452            CmsStaticExportRfsRule rule = itRules.next();
453            if (exportPath.equals(rule.getExportPathConfigured())) {
454                m_useTempDirs = false;
455            }
456        }
457        Boolean relativeLinks = (useRelativeLinks == null ? null : Boolean.valueOf(useRelativeLinks));
458        Integer backups = (exportBackups == null ? null : Integer.valueOf(exportBackups));
459
460        m_rfsRules.add(
461            new CmsStaticExportRfsRule(
462                name,
463                description,
464                source,
465                rfsPrefix,
466                exportPath,
467                exportWorkPath,
468                backups,
469                relativeLinks,
470                m_rfsTmpRule.getRelatedSystemResources()));
471        m_rfsTmpRule = new CmsStaticExportRfsRule("", "", "", "", "", "", null, null);
472    }
473
474    /**
475     * Adds a regex of related system resources to the latest rfs-rule.<p>
476     *
477     * @param regex the regex to add
478     */
479    public void addRfsRuleSystemRes(String regex) {
480
481        m_rfsTmpRule.addRelatedSystemRes(regex);
482    }
483
484    /**
485     * Caches a calculated online link.<p>
486     *
487     * @param linkName the link
488     * @param vfsName the name of the VFS resource
489     */
490    public void cacheOnlineLink(String linkName, String vfsName) {
491
492        m_cacheOnlineLinks.put(linkName, vfsName);
493    }
494
495    /**
496     * Implements the CmsEvent interface,
497     * the static export properties uses the events to clear
498     * the list of cached keys in case a project is published.<p>
499     *
500     * @param event CmsEvent that has occurred
501     */
502    public void cmsEvent(CmsEvent event) {
503
504        if (!isStaticExportEnabled()) {
505            if (LOG.isWarnEnabled()) {
506                LOG.warn(Messages.get().getBundle().key(Messages.LOG_STATIC_EXPORT_DISABLED_0));
507            }
508            // we still need to clear all caches
509            switch (event.getType()) {
510                case I_CmsEventListener.EVENT_UPDATE_EXPORTS:
511                case I_CmsEventListener.EVENT_PUBLISH_PROJECT:
512                case I_CmsEventListener.EVENT_CLEAR_CACHES:
513                    clearCaches(event);
514                    break;
515                default:
516                    // no operation
517            }
518            return;
519        }
520        if (m_sharedCacheConfiguration.isEnabled()) {
521            switch (event.getType()) {
522                case I_CmsEventListener.EVENT_UPDATE_EXPORTS:
523                case I_CmsEventListener.EVENT_PUBLISH_PROJECT:
524                case I_CmsEventListener.EVENT_CLEAR_CACHES:
525                    clearCaches(event);
526                    break;
527                default:
528                    // no operation
529            }
530            return;
531        }
532        I_CmsReport report = null;
533        Map<String, Object> data = event.getData();
534        if (data != null) {
535            report = (I_CmsReport)data.get(I_CmsEventListener.KEY_REPORT);
536        }
537        if (report == null) {
538            report = new CmsLogReport(CmsLocaleManager.getDefaultLocale(), getClass());
539        }
540        switch (event.getType()) {
541            case I_CmsEventListener.EVENT_UPDATE_EXPORTS:
542                scrubExportFolders(report);
543                clearCaches(event);
544                break;
545            case I_CmsEventListener.EVENT_PUBLISH_PROJECT:
546                if (data == null) {
547                    if (LOG.isErrorEnabled()) {
548                        LOG.error(Messages.get().getBundle().key(Messages.ERR_EMPTY_EVENT_DATA_0));
549                    }
550                    return;
551                }
552                // event data contains a list of the published resources
553                CmsUUID publishHistoryId = new CmsUUID((String)data.get(I_CmsEventListener.KEY_PUBLISHID));
554                if (LOG.isDebugEnabled()) {
555                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_EVENT_PUBLISH_PROJECT_1, publishHistoryId));
556                }
557                synchronized (m_lockCmsEvent) {
558                    getHandler().performEventPublishProject(publishHistoryId, report);
559                }
560                clearCaches(event);
561
562                if (LOG.isDebugEnabled()) {
563                    LOG.debug(
564                        Messages.get().getBundle().key(
565                            Messages.LOG_EVENT_PUBLISH_PROJECT_FINISHED_1,
566                            publishHistoryId));
567                }
568
569                break;
570            case I_CmsEventListener.EVENT_CLEAR_CACHES:
571                clearCaches(event);
572                break;
573            default:
574                // no operation
575        }
576    }
577
578    /**
579     * Exports the requested uri and at the same time writes the uri to the response output stream
580     * if required.<p>
581     *
582     * @param req the current request
583     * @param res the current response
584     * @param cms an initialised cms context (should be initialised with the "Guest" user only)
585     * @param data the static export data set
586     *
587     * @return status code of the export operation, status codes are the same as http status codes (200,303,304)
588     *
589     * @throws CmsException in case of errors accessing the VFS
590     * @throws ServletException in case of errors accessing the servlet
591     * @throws IOException in case of errors writing to the export output stream
592     * @throws CmsStaticExportException if static export is disabled
593     */
594    public int export(HttpServletRequest req, HttpServletResponse res, CmsObject cms, CmsStaticExportData data)
595    throws CmsException, IOException, ServletException, CmsStaticExportException {
596
597        if (m_sharedCacheConfiguration.isEnabled() && ((req == null) || (res == null))) {
598            throw new CmsStaticExportException(Messages.get().container(Messages.ERR_SHAREDCACHE_ON_DEMAND_ONLY_0));
599        }
600
601        CmsResource resource = data.getResource();
602        String vfsName = data.getVfsName();
603        String rfsName;
604        if (data.isDetailPage()) {
605            rfsName = CmsStringUtil.joinPaths(data.getRfsName(), CmsStaticExportManager.DEFAULT_FILE);
606        } else if (data.getParameters() != null) {
607            rfsName = data.getRfsName();
608        } else {
609            rfsName = addDefaultFileNameToFolder(data.getRfsName(), resource.isFolder());
610        }
611
612        // cut the site root from the vfsName and switch to the correct site
613        String siteRoot = OpenCms.getSiteManager().getSiteRoot(vfsName);
614
615        CmsI18nInfo i18nInfo = OpenCms.getLocaleManager().getI18nInfo(
616            req,
617            cms.getRequestContext().getCurrentUser(),
618            cms.getRequestContext().getCurrentProject(),
619            vfsName);
620
621        String remoteAddr = m_remoteAddr;
622        if (remoteAddr == null) {
623            remoteAddr = CmsContextInfo.LOCALHOST;
624        }
625
626        if (siteRoot != null) {
627            vfsName = vfsName.substring(siteRoot.length());
628        } else {
629            siteRoot = "/";
630        }
631
632        if (LOG.isDebugEnabled()) {
633            LOG.debug(Messages.get().getBundle().key(Messages.LOG_STATIC_EXPORT_SITE_ROOT_2, siteRoot, vfsName));
634        }
635
636        boolean usesSecureSite = (req != null) && OpenCms.getSiteManager().usesSecureSite(req);
637        CmsContextInfo contextInfo = new CmsContextInfo(
638            cms.getRequestContext().getCurrentUser(),
639            cms.getRequestContext().getCurrentProject(),
640            vfsName,
641            cms.getRequestContext().getRequestMatcher(),
642            siteRoot,
643            usesSecureSite,
644            i18nInfo.getLocale(),
645            i18nInfo.getEncoding(),
646            remoteAddr,
647            CmsContextInfo.CURRENT_TIME,
648            cms.getRequestContext().getOuFqn(),
649            cms.getRequestContext().isForceAbsoluteLinks());
650        CmsObject exportCms = OpenCms.initCmsObject(null, contextInfo);
651
652        // only export those resources where the export property is set
653        if (!isExportLink(exportCms, exportCms.getRequestContext().removeSiteRoot(data.getVfsName()))) {
654            // the resource was not used for export, so return HttpServletResponse.SC_SEE_OTHER
655            // as a signal for not exported resource
656            return HttpServletResponse.SC_SEE_OTHER;
657        }
658
659        // this flag signals if the export method is used for "on demand" or "after publish".
660        // if no request and result stream are available, it was called during "export on publish"
661        boolean exportOnDemand = ((req != null) && (res != null));
662        HttpServletRequest exportRequest = req;
663        if (exportOnDemand && (data.getParameters() != null)) {
664            exportRequest = new CmsStaticExportRequest(req, data);
665        }
666        CmsStaticExportResponseWrapper wrapRes = null;
667        if (res != null) {
668            wrapRes = new CmsStaticExportResponseWrapper(res);
669        }
670        boolean exportWithResponse = true;
671        if (LOG.isDebugEnabled()) {
672            LOG.debug(Messages.get().getBundle().key(Messages.LOG_SE_RESOURCE_START_1, data));
673        }
674
675        CmsResource exportResource = OpenCms.initResource(exportCms, vfsName, exportRequest, wrapRes);
676        I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(exportResource);
677        CmsFile file = null;
678        if ((loader == null) || !(loader instanceof I_CmsStaticExportStreamLoader)) {
679            file = exportCms.readFile(exportResource);
680            exportResource = file;
681        }
682        vfsName = exportCms.getSitePath(exportResource);
683
684        // check loader id for resource
685        if ((loader == null) || (!loader.isStaticExportEnabled())) {
686            Object[] arguments = new Object[] {vfsName, Integer.valueOf(exportResource.getTypeId())};
687            throw new CmsStaticExportException(
688                Messages.get().container(Messages.ERR_EXPORT_NOT_SUPPORTED_2, arguments));
689        }
690
691        // ensure we have exactly the same setup as if called "the usual way"
692        // we only have to do this in case of the static export on demand
693        String mimetype = null;
694        if (exportOnDemand) {
695            mimetype = OpenCms.getResourceManager().getMimeType(
696                exportResource.getName(),
697                exportCms.getRequestContext().getEncoding());
698            if (wrapRes != null) {
699                wrapRes.setContentType(mimetype);
700            }
701            exportCms.getRequestContext().setUri(vfsName);
702        }
703
704        if (m_sharedCacheConfiguration.isEnabled()) {
705            return exportToSharedCache(loader, exportCms, exportResource, file, exportRequest, wrapRes, mimetype);
706        }
707
708        // do the export
709        int status = -1;
710        List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(exportCms, vfsName);
711        boolean exported = false;
712        boolean matched = false;
713        // iterate over all rules
714        Iterator<CmsStaticExportRfsRule> it = getRfsRules().iterator();
715        while (it.hasNext()) {
716            CmsStaticExportRfsRule rule = it.next();
717            // normal case
718
719            boolean export = rule.getSource().matcher(CmsStringUtil.joinPaths(siteRoot, vfsName)).matches();
720            matched |= export;
721            // system folder case
722            export |= ((OpenCms.getSiteManager().startsWithShared(vfsName)
723                || vfsName.startsWith(CmsWorkplace.VFS_PATH_SYSTEM)) && rule.match(vfsName));
724            if (export) {
725                // the resource has to exported for this rule
726                CmsObject locCms = exportCms;
727                Locale locale = CmsLocaleManager.getLocale(rule.getName());
728                if (locales.contains(locale)) {
729                    // if the locale is in the default locales for the resource
730                    // so adjust the locale to use for exporting
731                    CmsContextInfo ctxInfo = new CmsContextInfo(exportCms.getRequestContext());
732                    ctxInfo.setLocale(locale);
733                    locCms = OpenCms.initCmsObject(exportCms, ctxInfo);
734                }
735                String locRfsName = rfsName;
736                // in case of the default locale, this would either be wrong or the identity substitution
737                if (!locale.equals(CmsLocaleManager.getDefaultLocale()) && locales.contains(locale)) {
738                    locRfsName = rule.getLocalizedRfsName(rfsName, "/");
739                }
740                if (loader instanceof I_CmsStaticExportStreamLoader) {
741                    writeStreamedResource(
742                        (I_CmsStaticExportStreamLoader)loader,
743                        locCms,
744                        exportResource,
745                        exportRequest,
746                        exportWithResponse ? wrapRes : null,
747                        rule.getExportPath(),
748                        locRfsName,
749                        resource);
750                    // disable writing to response for static resources after the first rule match
751                    // to avoid duplicate response data
752                    // when compression is enabled in Tomcat.
753                    exportWithResponse = false;
754                    exported = true;
755                } else {
756                    // read the content in the matching locale
757                    byte[] content = loader.export(
758                        locCms,
759                        new CmsFile(file),
760                        exportRequest,
761                        exportWithResponse ? wrapRes : null);
762                    if (content != null) {
763                        // write to rfs
764                        exported = true;
765                        writeResource(req, rule.getExportPath(), locRfsName, resource, content);
766                    }
767                }
768            }
769        }
770        if (!matched) {
771            // no rule matched
772            String exportPath = getExportPath(siteRoot + vfsName);
773            if (loader instanceof I_CmsStaticExportStreamLoader) {
774                writeStreamedResource(
775                    (I_CmsStaticExportStreamLoader)loader,
776                    exportCms,
777                    exportResource,
778                    exportRequest,
779                    exportWithResponse ? wrapRes : null,
780                    exportPath,
781                    rfsName,
782                    resource);
783                exported = true;
784            } else {
785                byte[] content = loader.export(
786                    exportCms,
787                    new CmsFile(file),
788                    exportRequest,
789                    exportWithResponse ? wrapRes : null);
790                if (content != null) {
791                    exported = true;
792                    writeResource(req, exportPath, rfsName, resource, content);
793                }
794            }
795        }
796
797        if (exported) {
798            // get the wrapper status that was set
799            status = (wrapRes != null) ? wrapRes.getStatus() : -1;
800            if (status < 0) {
801                // the status was not set, assume everything is o.k.
802                status = HttpServletResponse.SC_OK;
803            }
804        } else {
805            // the resource was not written because it was not modified.
806            // set the status to not modified
807            status = HttpServletResponse.SC_NOT_MODIFIED;
808        }
809
810        return status;
811    }
812
813    /**
814     * Starts a complete static export of all resources.<p>
815     *
816     * @param purgeFirst flag to delete all resources in the export folder of the rfs
817     * @param report an I_CmsReport instance to print output message, or null to write messages to the log file
818     *
819     * @throws CmsException in case of errors accessing the VFS
820     * @throws IOException in case of errors writing to the export output stream
821     * @throws ServletException in case of errors accessing the servlet
822     */
823    public synchronized void exportFullStaticRender(boolean purgeFirst, I_CmsReport report)
824    throws CmsException, IOException, ServletException {
825
826        if (m_sharedCacheConfiguration.isEnabled()) {
827            throw new CmsStaticExportException(Messages.get().container(Messages.ERR_SHAREDCACHE_FULL_EXPORT_0));
828        }
829
830        // set member to true to get temporary export paths for rules
831        m_fullStaticExport = true;
832        // save the real export path
833        String staticExportPathStore = m_staticExportPath;
834
835        if (m_useTempDirs) {
836            // set the export path to the export work path
837            m_staticExportPath = m_staticExportWorkPath;
838        }
839
840        // delete all old exports if the purgeFirst flag is set
841        if (purgeFirst) {
842            Map<String, Object> eventData = new HashMap<String, Object>();
843            eventData.put(I_CmsEventListener.KEY_REPORT, report);
844            CmsEvent clearCacheEvent = new CmsEvent(I_CmsEventListener.EVENT_CLEAR_CACHES, eventData);
845            OpenCms.fireCmsEvent(clearCacheEvent);
846
847            scrubExportFolders(report);
848            // this will always use the root site
849            CmsObject cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserExport());
850            cms.deleteAllStaticExportPublishedResources(EXPORT_LINK_WITHOUT_PARAMETER);
851            cms.deleteAllStaticExportPublishedResources(EXPORT_LINK_WITH_PARAMETER);
852        }
853
854        // do the export
855        CmsAfterPublishStaticExportHandler handler = new CmsAfterPublishStaticExportHandler();
856        // export everything
857        handler.doExportAfterPublish(null, report);
858
859        // set export path to the original one
860        m_staticExportPath = staticExportPathStore;
861
862        // set member to false for further exports
863        m_fullStaticExport = false;
864
865        // check if report contents no errors
866        if (m_useTempDirs && !report.hasError()) {
867            // backup old export folders for default export
868            File staticExport = new File(m_staticExportPath);
869            createExportBackupFolders(staticExport, m_staticExportPath, getExportBackups().intValue(), null);
870
871            // change the name of the used temporary export folder to the original default export path
872            File staticExportWork = new File(m_staticExportWorkPath);
873            staticExportWork.renameTo(new File(m_staticExportPath));
874
875            // backup old export folders of rule based exports
876            Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
877            while (it.hasNext()) {
878                CmsStaticExportRfsRule rule = it.next();
879                File staticExportRule = new File(rule.getExportPath());
880                File staticExportWorkRule = new File(rule.getExportWorkPath());
881                // only backup if a temporary folder exists for this rule
882                if (staticExportWorkRule.exists()) {
883                    createExportBackupFolders(
884                        staticExportRule,
885                        rule.getExportPath(),
886                        rule.getExportBackups().intValue(),
887                        OpenCms.getResourceManager().getFileTranslator().translateResource(rule.getName()));
888                    staticExportWorkRule.renameTo(new File(rule.getExportPath()));
889                }
890            }
891        } else if (report.hasError()) {
892            report.println(Messages.get().container(Messages.ERR_EXPORT_NOT_SUCCESSFUL_0), I_CmsReport.FORMAT_WARNING);
893        }
894    }
895
896    /**
897     * Returns the accept-charset header used for internal requests.<p>
898     *
899     * @return the accept-charset header
900     */
901    public String getAcceptCharsetHeader() {
902
903        return m_acceptCharsetHeader;
904    }
905
906    /**
907     * Returns the accept-language header used for internal requests.<p>
908     *
909     * @return the accept-language header
910     */
911    public String getAcceptLanguageHeader() {
912
913        return m_acceptLanguageHeader;
914    }
915
916    /**
917     * Returns a cached link for the given vfs name.<p>
918     *
919     * @param vfsName the name of the vfs resource to get the cached link for
920     *
921     * @return a cached link for the given vfs name, or null
922     */
923    public String getCachedOnlineLink(String vfsName) {
924
925        return m_cacheOnlineLinks.get(vfsName);
926    }
927
928    /**
929     * Returns the key for the online, export and secure cache.<p>
930     *
931     * @param siteRoot the site root of the resource
932     * @param uri the URI of the resource
933     *
934     * @return a key for the cache
935     */
936    public String getCacheKey(String siteRoot, String uri) {
937
938        return new StringBuffer(siteRoot).append(uri).toString();
939    }
940
941    /**
942     * Gets the default property value as a string representation.<p>
943     *
944     * @return <code>"true"</code> or <code>"false"</code>
945     */
946    public String getDefault() {
947
948        return String.valueOf(m_exportPropertyDefault);
949    }
950
951    /**
952     * Returns the current default charset header.<p>
953     *
954     * @return the current default charset header
955     */
956    public String getDefaultAcceptCharsetHeader() {
957
958        return m_defaultAcceptCharsetHeader;
959    }
960
961    /**
962     * Returns the current default locale header.<p>
963     *
964     * @return the current default locale header
965     */
966    public String getDefaultAcceptLanguageHeader() {
967
968        return m_defaultAcceptLanguageHeader;
969    }
970
971    /**
972     * Returns the default prefix for exported links in the "real" file system.<p>
973     *
974     * @return the default prefix for exported links in the "real" file system
975     */
976    public String getDefaultRfsPrefix() {
977
978        return m_rfsPrefix;
979    }
980
981    /**
982     * Returns the number of stored backups.<p>
983     *
984     * @return the number of stored backups
985     */
986    public Integer getExportBackups() {
987
988        if (m_staticExportBackups != null) {
989            return m_staticExportBackups;
990        }
991        // if backups not configured set to default value
992        return EXPORT_DEFAULT_BACKUPS;
993    }
994
995    /**
996     * Returns the export data for the request, if null is returned no export is required.<p>
997     *
998     * @param request the request to check for export data
999     * @param cms an initialized cms context (should be initialized with the "Guest" user only
1000     *
1001     * @return the export data for the request, if null is returned no export is required
1002     */
1003    public CmsStaticExportData getExportData(HttpServletRequest request, CmsObject cms) {
1004
1005        if (!isStaticExportEnabled()) {
1006            // export is disabled
1007            return null;
1008        }
1009
1010        // build the rfs name for the export "on demand"
1011        String rfsName = request.getParameter(EXPORT_MARKER);
1012        if ((rfsName == null)) {
1013            rfsName = (String)request.getAttribute(EXPORT_ATTRIBUTE_ERROR_REQUEST_URI);
1014        }
1015
1016        if (request.getHeader(CmsRequestUtil.HEADER_OPENCMS_EXPORT) != null) {
1017            // this is a request created by the static export and directly send to 404 handler
1018            // so remove the leading handler identification
1019            int prefix = rfsName.startsWith(getExportUrlPrefix()) ? getExportUrlPrefix().length() : 0;
1020            if (prefix > 0) {
1021                rfsName = rfsName.substring(prefix);
1022            } else {
1023                return null;
1024            }
1025        }
1026
1027        if (!isValidRfsName(rfsName)) {
1028            // this is not an export request, no further processing is required
1029            return null;
1030        }
1031
1032        // store the site root
1033        String storedSiteRoot = cms.getRequestContext().getSiteRoot();
1034        try {
1035            // get the site root according to the HttpServletRequest
1036            CmsSite site = OpenCms.getSiteManager().matchRequest(request);
1037            // set the site root of the request context before getting the export data
1038            cms.getRequestContext().setSiteRoot(site.getSiteRoot());
1039            // get the export data now
1040            CmsStaticExportData data = getRfsExportData(cms, rfsName);
1041
1042            // check if we have an export link,
1043            // only return the data object if we really should export the resource
1044            if ((data != null) && isExportLink(cms, cms.getRequestContext().removeSiteRoot(data.getVfsName()))) {
1045                // if we have an export link return the export data object
1046                return data;
1047            } else {
1048                // otherwise if we have a link vfsName which should not be exported
1049                // return null for better error handling in the OpenCmsServlet
1050                return null;
1051            }
1052        } finally {
1053            // restore the site root
1054            cms.getRequestContext().setSiteRoot(storedSiteRoot);
1055        }
1056    }
1057
1058    /**
1059     * Gets the export enabled value as a string representation.<p>
1060     *
1061     * @return <code>"true"</code> or <code>"false"</code>
1062     */
1063    public String getExportEnabled() {
1064
1065        return String.valueOf(m_staticExportEnabled);
1066    }
1067
1068    /**
1069     * Returns the current folder matcher.<p>
1070     *
1071     * @return the current folder matcher
1072     */
1073    public CmsExportFolderMatcher getExportFolderMatcher() {
1074
1075        return m_exportFolderMatcher;
1076    }
1077
1078    /**
1079     * Returns list of resources patterns which are part of the export.<p>
1080     *
1081     * @return the of resources patterns which are part of the export.
1082     */
1083    public List<String> getExportFolderPatterns() {
1084
1085        return Collections.unmodifiableList(m_exportFolders);
1086    }
1087
1088    /**
1089     * Returns specific http headers for the static export.<p>
1090     *
1091     * If the header <code>Cache-Control</code> is set, OpenCms will not use its default headers.<p>
1092     *
1093     * @return the list of http export headers
1094     */
1095    public List<String> getExportHeaders() {
1096
1097        return Collections.unmodifiableList(m_exportHeaders);
1098    }
1099
1100    /**
1101     * Returns a map of all export names with export name as key
1102     * and the vfs folder path as value.<p>
1103     *
1104     * @return a map of export names
1105     */
1106    public Map<CmsExportname, String> getExportnames() {
1107
1108        if (m_exportnameResources == null) {
1109            try {
1110                TreeMap<CmsExportname, String> sort = new TreeMap<CmsExportname, String>(new CmsExportNameComparator());
1111                sort.putAll(computeVfsExportnames());
1112                m_exportnameResources = sort;
1113            } catch (Throwable t) {
1114                LOG.error(t.getMessage(), t);
1115            }
1116        }
1117        if (LOG.isDebugEnabled()) {
1118            LOG.debug(Messages.get().getBundle().key(Messages.LOG_UPDATE_EXPORTNAME_PROP_FINISHED_0));
1119        }
1120        return Collections.unmodifiableMap(m_exportnameResources);
1121    }
1122
1123    /**
1124     * Returns the export path for the static export, that is the folder where the
1125     * static exported resources will be written to.<p>
1126     *
1127     * The returned value will be a directory like prefix. The value is configured
1128     * in the <code>opencms-importexport.xml</code> configuration file. An optimization
1129     * of the configured value will be performed, where all relative path information is resolved
1130     * (for example <code>/export/../static</code> will be resolved to <code>/export</code>.
1131     * Moreover, if the configured path ends with a <code>/</code>, this will be cut off
1132     * (for example <code>/export/</code> becomes <code>/export</code>.<p>
1133     *
1134     * This is resource name based, and based on the rfs-rules defined in the
1135     * <code>opencms-importexport.xml</code> configuration file.<p>
1136     *
1137     * @param vfsName the name of the resource to export
1138     *
1139     * @return the export path for the static export, that is the folder where the
1140     *
1141     * @see #getRfsPrefix(String)
1142     * @see #getVfsPrefix()
1143     */
1144    public String getExportPath(String vfsName) {
1145
1146        if (vfsName != null) {
1147            Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
1148            while (it.hasNext()) {
1149                CmsStaticExportRfsRule rule = it.next();
1150                if (rule.getSource().matcher(vfsName).matches()) {
1151                    return rule.getExportPath();
1152                }
1153            }
1154        }
1155        if (m_useTempDirs && isFullStaticExport()) {
1156            return getExportWorkPath();
1157        }
1158        return m_staticExportPath;
1159    }
1160
1161    /**
1162     * Returns the original configured export path for the static export without the complete rfs path, to be used
1163     * when re-writing the configuration.<p>
1164     *
1165     * This is required <b>only</b> to serialize the configuration again exactly as it was configured.
1166     * This method should <b>not</b> be used otherwise. Use <code>{@link #getExportPath(String)}</code>
1167     * to obtain the export path to use when exporting.<p>
1168     *
1169     * @return the original configured export path for the static export without the complete rfs path
1170     */
1171    public String getExportPathForConfiguration() {
1172
1173        return m_staticExportPathConfigured;
1174    }
1175
1176    /**
1177     * Returns the protected export points.<p>
1178     *
1179     * @return the protected export points
1180     */
1181    public Set<CmsExportPoint> getExportPoints() {
1182
1183        Set<CmsExportPoint> result = new HashSet<CmsExportPoint>();
1184        for (Entry<String, String> entry : m_protectedExportPoints.entrySet()) {
1185            result.add(
1186                new CmsExportPoint(entry.getKey(), CmsStringUtil.joinPaths(m_protectedExportPath, entry.getValue())));
1187        }
1188        return result;
1189    }
1190
1191    /**
1192     * Returns true if the default value for the resource property "export" is true.<p>
1193     *
1194     * @return true if the default value for the resource property "export" is true
1195     */
1196    public boolean getExportPropertyDefault() {
1197
1198        return m_exportPropertyDefault;
1199    }
1200
1201    /**
1202     * Returns the export Rules.<p>
1203     *
1204     * @return the export Rules
1205     */
1206    public List<CmsStaticExportExportRule> getExportRules() {
1207
1208        return Collections.unmodifiableList(m_exportRules);
1209    }
1210
1211    /**
1212     * Gets the list of resource suffixes which will be exported by default.<p>
1213     *
1214     * @return list of resource suffixes
1215     */
1216    public List<String> getExportSuffixes() {
1217
1218        return m_exportSuffixes;
1219    }
1220
1221    /**
1222     * Returns the export URL used for internal requests for exporting resources that require a
1223     * request / response (like JSP).<p>
1224     *
1225     * @return the export URL used for internal requests for exporting resources like JSP
1226     */
1227    public String getExportUrl() {
1228
1229        return m_exportUrl;
1230    }
1231
1232    /**
1233     * Returns the export URL used for internal requests with unsubstituted context values, to be used
1234     * when re-writing the configuration.<p>
1235     *
1236     * This is required <b>only</b> to serialize the configuration again exactly as it was configured.
1237     * This method should <b>not</b> be used otherwise. Use <code>{@link #getExportUrl()}</code>
1238     * to obtain the export path to use when exporting.<p>
1239     *
1240     * @return the export URL used for internal requests with unsubstituted context values
1241     */
1242    public String getExportUrlForConfiguration() {
1243
1244        return m_exportUrlConfigured;
1245    }
1246
1247    /**
1248     * Returns the export URL used for internal requests for exporting resources that require a
1249     * request / response (like JSP) without http://servername.<p>
1250     *
1251     * @return the export URL used for internal requests for exporting resources like JSP without http://servername
1252     */
1253    public String getExportUrlPrefix() {
1254
1255        return m_exportUrlPrefix;
1256    }
1257
1258    /**
1259     * Returns the export work path for the static export, that is the folder where the
1260     * static exported resources will be written to during the export process.<p>
1261     *
1262     * @return the export work path for the static export
1263     */
1264    public String getExportWorkPath() {
1265
1266        return m_staticExportWorkPath;
1267    }
1268
1269    /**
1270     * Returns the original configured export work path for the static export without the complete rfs path, to be used
1271     * when re-writing the configuration.<p>
1272     *
1273     * @return the original configured export work path for the static export without the complete rfs path
1274     */
1275    public String getExportWorkPathForConfiguration() {
1276
1277        if (m_staticExportWorkPathConfigured != null) {
1278            return m_staticExportWorkPathConfigured;
1279        }
1280        // if work path not configured set to default value
1281        return EXPORT_DEFAULT_WORKPATH;
1282    }
1283
1284    /**
1285     * Returns the configured static export handler class.<p>
1286     *
1287     * If not set, a new <code>{@link CmsAfterPublishStaticExportHandler}</code> is created and returned.<p>
1288     *
1289     * @return the configured static export handler class
1290     */
1291    public I_CmsStaticExportHandler getHandler() {
1292
1293        if (m_handler == null) {
1294            setHandler(CmsOnDemandStaticExportHandler.class.getName());
1295        }
1296        return m_handler;
1297    }
1298
1299    /**
1300     * Returns the configured link substitution handler class.<p>
1301     *
1302     * If not set, a new <code>{@link CmsDefaultLinkSubstitutionHandler}</code> is created and returned.<p>
1303     *
1304     * @return the configured link substitution handler class
1305     */
1306    public I_CmsLinkSubstitutionHandler getLinkSubstitutionHandler() {
1307
1308        if (m_linkSubstitutionHandler == null) {
1309            setLinkSubstitutionHandler(CmsDefaultLinkSubstitutionHandler.class.getName());
1310        }
1311        return m_linkSubstitutionHandler;
1312    }
1313
1314    /**
1315     * Gets the plain export optimization value as a string representation.<p>
1316     *
1317     * @return <code>"true"</code> or <code>"false"</code>
1318     */
1319    public String getPlainExportOptimization() {
1320
1321        return String.valueOf(m_quickPlainExport);
1322    }
1323
1324    /**
1325     * Returns the protected export name for the given root path.<p>
1326     *
1327     * @param rootPath the root path
1328     *
1329     * @return the protected export name
1330     */
1331    public String getProtectedExportName(String rootPath) {
1332
1333        String result = null;
1334        for (Entry<String, String> entry : m_protectedExportPoints.entrySet()) {
1335            if (rootPath.startsWith(entry.getKey())) {
1336                result = CmsStringUtil.joinPaths(
1337                    "/",
1338                    m_protectedExportPath,
1339                    entry.getValue(),
1340                    rootPath.substring(entry.getKey().length()));
1341            }
1342        }
1343        return result;
1344    }
1345
1346    /**
1347     * Returns the protected export path.<p>
1348     *
1349     * @return the protected export path
1350     */
1351    public String getProtectedExportPath() {
1352
1353        return m_protectedExportPath;
1354    }
1355
1356    /**
1357     * Returns the protected export points.<p>
1358     *
1359     * @return the protected export points
1360     */
1361    public Map<String, String> getProtectedExportPoints() {
1362
1363        return m_protectedExportPoints;
1364    }
1365
1366    /**
1367     * Returns true if the quick plain export is enabled.<p>
1368     *
1369     * @return true if the quick plain export is enabled
1370     */
1371    public boolean getQuickPlainExport() {
1372
1373        return m_quickPlainExport;
1374    }
1375
1376    /**
1377     * Gets the relative links value as a string representation.<p>
1378     *
1379     * @return <code>"true"</code> or <code>"false"</code>
1380     */
1381    public String getRelativeLinks() {
1382
1383        return String.valueOf(m_exportRelativeLinks);
1384    }
1385
1386    /**
1387     * Returns the remote address used for internal requests.<p>
1388     *
1389     * @return the remote address
1390     */
1391    public String getRemoteAddr() {
1392
1393        return m_remoteAddr;
1394    }
1395
1396    /**
1397     * Returns the remote address.<p>
1398     *
1399     * @return the remote address
1400     */
1401    public String getRemoteAddress() {
1402
1403        return m_remoteAddr;
1404    }
1405
1406    /**
1407     * Returns the static export rfs name for a given vfs resource.<p>
1408     *
1409     * @param cms an initialized cms context
1410     * @param vfsName the name of the vfs resource
1411     *
1412     * @return the static export rfs name for a give vfs resource
1413     *
1414     * @see #getVfsName(CmsObject, String)
1415     * @see #getRfsName(CmsObject, String, String, String)
1416     */
1417    public String getRfsName(CmsObject cms, String vfsName) {
1418
1419        return getRfsName(cms, vfsName, null, null);
1420    }
1421
1422    /**
1423     * Returns the static export rfs name for a given vfs resource where the link to the
1424     * resource includes request parameters.<p>
1425     *
1426     * @param cms an initialized cms context
1427     * @param vfsName the name of the vfs resource
1428     * @param parameters the parameters of the link pointing to the resource
1429     * @param targetDetailPage the target detail page to use
1430     *
1431     * @return the static export rfs name for a give vfs resource
1432     */
1433    public String getRfsName(CmsObject cms, String vfsName, String parameters, String targetDetailPage) {
1434
1435        String rfsName;
1436        try {
1437            CmsResource vfsRes = null;
1438            if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_4_SERVLET_ACCESS) {
1439                // Accessing the ADEManager during setup may not work.
1440                try {
1441                    vfsRes = cms.readResource(vfsName);
1442                    I_CmsDetailPageHandler finder = OpenCms.getADEManager().getDetailPageHandler();
1443                    String detailPage = finder.getDetailPage(
1444                        cms,
1445                        vfsRes.getRootPath(),
1446                        cms.getRequestContext().getUri(),
1447                        targetDetailPage);
1448                    if (detailPage != null) {
1449                        vfsName = CmsStringUtil.joinPaths(
1450                            detailPage,
1451                            CmsDetailPageUtil.getBestUrlName(cms, vfsRes.getStructureId()),
1452                            "/");
1453                    }
1454                } catch (CmsVfsResourceNotFoundException e) {
1455                    // ignore
1456                }
1457            }
1458            rfsName = getRfsNameWithExportName(cms, vfsName);
1459            String extension = CmsFileUtil.getExtension(rfsName);
1460            // check if the VFS resource is a JSP page with a ".jsp" ending
1461            // in this case the  name suffix must be build with special care,
1462            // usually it must be set to ".html"
1463            boolean isJsp = extension.equals(".jsp");
1464            if (isJsp) {
1465                String suffix = null;
1466                try {
1467                    CmsResource res = cms.readResource(vfsName);
1468                    isJsp = (CmsResourceTypeJsp.isJsp(res));
1469                    // if the resource is a plain resource then no change in suffix is required
1470                    if (isJsp) {
1471                        suffix = cms.readPropertyObject(
1472                            vfsName,
1473                            CmsPropertyDefinition.PROPERTY_EXPORTSUFFIX,
1474                            true).getValue(".html");
1475                    }
1476                } catch (CmsVfsResourceNotFoundException e) {
1477                    // resource has been deleted, so we are not able to get the right extension from the properties
1478                    // try to figure out the right extension from file system
1479                    File rfsFile = new File(
1480                        CmsFileUtil.normalizePath(
1481                            getExportPath(cms.getRequestContext().addSiteRoot(vfsName)) + rfsName));
1482                    File parent = rfsFile.getParentFile();
1483                    if (parent != null) {
1484                        File[] paramVariants = parent.listFiles(new CmsPrefixFileFilter(rfsFile.getName()));
1485                        if ((paramVariants != null) && (paramVariants.length > 0)) {
1486                            // take the first
1487                            suffix = paramVariants[0].getAbsolutePath().substring(rfsFile.getAbsolutePath().length());
1488                        }
1489                    } else {
1490                        // if no luck, try the default extension
1491                        suffix = ".html";
1492                    }
1493                }
1494                if ((suffix != null) && !extension.equals(suffix.toLowerCase())) {
1495                    rfsName += suffix;
1496                    extension = suffix;
1497                }
1498            }
1499            if (parameters != null) {
1500                // build the RFS name for the link with parameters
1501                rfsName = getRfsPath(rfsName, extension, parameters);
1502                // we have found a rfs name for a vfs resource with parameters, save it to the database
1503                try {
1504                    cms.writeStaticExportPublishedResource(
1505                        rfsName,
1506                        CmsStaticExportManager.EXPORT_LINK_WITH_PARAMETER,
1507                        parameters,
1508                        System.currentTimeMillis());
1509                } catch (CmsException e) {
1510                    LOG.error(Messages.get().getBundle().key(Messages.LOG_WRITE_FAILED_1, rfsName), e);
1511                }
1512            }
1513        } catch (CmsException e) {
1514            if (LOG.isDebugEnabled()) {
1515                LOG.debug(e.getLocalizedMessage(), e);
1516            }
1517            // ignore exception, return vfsName as rfsName
1518            rfsName = vfsName;
1519        }
1520
1521        // add export rfs prefix and return result
1522
1523        if (!vfsName.startsWith(CmsWorkplace.VFS_PATH_SYSTEM) && !OpenCms.getSiteManager().startsWithShared(vfsName)) {
1524            return getRfsPrefix(cms.getRequestContext().addSiteRoot(vfsName)).concat(rfsName);
1525        } else {
1526            // check if we are generating a link to a related resource in the same rfs rule
1527            String source = cms.getRequestContext().addSiteRoot(cms.getRequestContext().getUri());
1528            Iterator<CmsStaticExportRfsRule> it = getRfsRules().iterator();
1529            while (it.hasNext()) {
1530                CmsStaticExportRfsRule rule = it.next();
1531                if (rule.getSource().matcher(source).matches() && rule.match(vfsName)) {
1532                    return rule.getRfsPrefix().concat(rfsName);
1533                }
1534            }
1535            // this is a link across rfs rules
1536            return getRfsPrefix(cms.getRequestContext().getSiteRoot() + "/").concat(rfsName);
1537        }
1538    }
1539
1540    /**
1541     * Returns the prefix for exported links in the "real" file system.<p>
1542     *
1543     * The returned value will be a directory like prefix. The value is configured
1544     * in the <code>opencms-importexport.xml</code> configuration file. An optimization
1545     * of the configured value will be performed, where all relative path information is resolved
1546     * (for example <code>/export/../static</code> will be resolved to <code>/export</code>.
1547     * Moreover, if the configured path ends with a <code>/</code>, this will be cut off
1548     * (for example <code>/export/</code> becomes <code>/export</code>.<p>
1549     *
1550     * This is resource name based, and based on the rfs-rules defined in the
1551     * <code>opencms-importexport.xml</code> configuration file.<p>
1552     *
1553     * @param vfsName the name of the resource to export
1554     *
1555     * @return the prefix for exported links in the "real" file system
1556     *
1557     * @see #getExportPath(String)
1558     * @see #getVfsPrefix()
1559     */
1560    public String getRfsPrefix(String vfsName) {
1561
1562        if (vfsName != null) {
1563            Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
1564            while (it.hasNext()) {
1565                CmsStaticExportRfsRule rule = it.next();
1566                if (rule.getSource().matcher(vfsName).matches()) {
1567                    return rule.getRfsPrefix();
1568                }
1569            }
1570        }
1571        return m_rfsPrefix;
1572    }
1573
1574    /**
1575     * Returns the original configured prefix for exported links in the "real" file, to be used
1576     * when re-writing the configuration.<p>
1577     *
1578     * This is required <b>only</b> to serialize the configuration again exactly as it was configured.
1579     * This method should <b>not</b> be used otherwise. Use <code>{@link #getRfsPrefix(String)}</code>
1580     * to obtain the rfs prefix to use for the exported links.<p>
1581     *
1582     * @return the original configured prefix for exported links in the "real" file
1583     */
1584    public String getRfsPrefixForConfiguration() {
1585
1586        return m_rfsPrefixConfigured;
1587    }
1588
1589    /**
1590     * Returns the rfs Rules.<p>
1591     *
1592     * @return the rfs Rules
1593     */
1594    public List<CmsStaticExportRfsRule> getRfsRules() {
1595
1596        return Collections.unmodifiableList(m_rfsRules);
1597    }
1598
1599    /**
1600     * Returns the shared cache configuration.<p>
1601     *
1602     * @return the shared cache configuration
1603     */
1604    public CmsSharedCacheConfiguration getSharedCacheConfiguration() {
1605
1606        return m_sharedCacheConfiguration;
1607    }
1608
1609    /**
1610     * Returns the stored content delivery configuration.<p>
1611     *
1612     * @return the stored content delivery configuration
1613     */
1614    public CmsStoredContentDeliveryConfiguration getStoredContentDeliveryConfiguration() {
1615
1616        return m_storedContentDeliveryConfiguration;
1617    }
1618
1619    /**
1620     * Returns the vfs name of the test resource.<p>
1621     *
1622     * @return the vfs name of the test resource.
1623     */
1624    public String getTestResource() {
1625
1626        return m_testResource;
1627    }
1628
1629    /**
1630     * Returns the export data for a requested resource, if null is returned no export is required.<p>
1631     *
1632     * @param cms an initialized cms context (should be initialized with the "Guest" user only
1633     * @param vfsName the VFS name of the resource requested
1634     *
1635     * @return the export data for the request, if null is returned no export is required
1636     */
1637    public CmsStaticExportData getVfsExportData(CmsObject cms, String vfsName) {
1638
1639        return getRfsExportData(cms, getRfsName(cms, vfsName));
1640    }
1641
1642    /**
1643     * Returns the VFS name for the given RFS name, being the exact reverse of <code>{@link #getRfsName(CmsObject, String)}</code>.<p>
1644     *
1645     * Returns <code>null</code> if no matching VFS resource can be found for the given RFS name.<p>
1646     *
1647     * @param cms the current users OpenCms context
1648     * @param rfsName the RFS name to get the VFS name for
1649     *
1650     * @return the VFS name for the given RFS name, or <code>null</code> if the RFS name does not match to the VFS
1651     *
1652     * @see #getRfsName(CmsObject, String)
1653     */
1654    public String getVfsName(CmsObject cms, String rfsName) {
1655
1656        CmsStaticExportData data = getRfsExportData(cms, rfsName);
1657        if (data != null) {
1658            String result = data.getVfsName();
1659            if ((result != null) && result.startsWith(cms.getRequestContext().getSiteRoot())) {
1660                result = result.substring(cms.getRequestContext().getSiteRoot().length());
1661            }
1662            return result;
1663        }
1664        return null;
1665    }
1666
1667    /**
1668     * Returns the VFS name from a given RFS name.<p>
1669     *
1670     * The RFS name must not contain the RFS prefix.<p>
1671     *
1672     * @param cms an initialized OpenCms user context
1673     * @param rfsName the name of the RFS resource
1674     *
1675     * @return the name of the VFS resource
1676     *
1677     * @throws CmsVfsResourceNotFoundException if something goes wrong
1678     */
1679    public CmsStaticExportData getVfsNameInternal(CmsObject cms, String rfsName)
1680    throws CmsVfsResourceNotFoundException {
1681
1682        String storedSiteRoot = cms.getRequestContext().getSiteRoot();
1683        CmsSite currentSite = OpenCms.getSiteManager().getSiteForSiteRoot(storedSiteRoot);
1684        try {
1685            cms.getRequestContext().setSiteRoot("/");
1686
1687            // try to find a match with the "exportname" folders
1688            String path = rfsName;
1689            // in case of folders, remove the trailing slash
1690            // in case of files, remove the filename and trailing slash
1691            path = path.substring(0, path.lastIndexOf('/'));
1692            // cache the export names
1693            Map<CmsExportname, String> exportnameMapping = getExportnames();
1694            // in case of folders, remove the trailing slash and in case of files, remove the filename and trailing slash
1695            while (true) {
1696                // exportnameResources are only folders!
1697                String expName = exportnameMapping.get(new CmsExportname(path + "/", currentSite));
1698                if (expName == null) {
1699                    expName = exportnameMapping.get(new CmsExportname(path + "/", null));
1700                }
1701                if (expName == null) {
1702                    if (path.length() == 0) {
1703                        break;
1704                    }
1705                    path = path.substring(0, path.lastIndexOf('/'));
1706                    continue;
1707                }
1708                // this will be a root path!
1709                String vfsName = expName + rfsName.substring(path.length() + 1);
1710                try {
1711                    return readResource(cms, vfsName);
1712                } catch (CmsVfsResourceNotFoundException e) {
1713                    // if already checked all parts of the path we can stop here.
1714                    // This is the case if the "/" is set as "exportname" on any vfs resource
1715                    if (path.length() == 0) {
1716                        break;
1717                    }
1718                    // continue with trying out the other exportname to find a match (may be a multiple prefix)
1719                    path = path.substring(0, path.lastIndexOf('/'));
1720                    continue;
1721                } catch (CmsException e) {
1722                    // should never happen
1723                    LOG.error(e.getLocalizedMessage(), e);
1724                    break;
1725                }
1726            }
1727
1728            // try to read name of export resource by reading the resource directly
1729            try {
1730                return readResource(cms, rfsName);
1731            } catch (Throwable t) {
1732                // resource not found
1733                if (LOG.isDebugEnabled()) {
1734                    LOG.debug(
1735                        Messages.get().getBundle().key(Messages.ERR_EXPORT_FILE_FAILED_1, new String[] {rfsName}),
1736                        t);
1737                }
1738            }
1739
1740            // finally check if its a modified jsp resource
1741            int extPos = rfsName.lastIndexOf('.');
1742            // first cut of the last extension
1743            if (extPos >= 0) {
1744                String cutName = rfsName.substring(0, extPos);
1745                int pos = cutName.lastIndexOf('.');
1746                if (pos >= 0) {
1747                    // now check if remaining String ends with ".jsp"
1748                    String extension = cutName.substring(pos).toLowerCase();
1749                    if (".jsp".equals(extension)) {
1750                        return getVfsNameInternal(cms, cutName);
1751                    }
1752                }
1753            }
1754        } finally {
1755            cms.getRequestContext().setSiteRoot(storedSiteRoot);
1756        }
1757        throw new CmsVfsResourceNotFoundException(
1758            org.opencms.db.generic.Messages.get().container(
1759                org.opencms.db.generic.Messages.ERR_READ_RESOURCE_1,
1760                rfsName));
1761    }
1762
1763    /**
1764     * Returns the prefix for the internal in the VFS.<p>
1765     *
1766     * The returned value will be a directory like prefix. The value is configured
1767     * in the <code>opencms-importexport.xml</code> configuration file. An optimization
1768     * of the configured value will be performed, where all relative path information is resolved
1769     * (for example <code>/opencms/../mycms</code> will be resolved to <code>/mycms</code>.
1770     * Moreover, if the configured path ends with a <code>/</code>, this will be cut off
1771     * (for example <code>/opencms/</code> becomes <code>/opencms</code>.<p>
1772     *
1773     * @return the prefix for the internal in the VFS
1774     *
1775     * @see #getExportPath(String)
1776     * @see #getRfsPrefix(String)
1777     */
1778    public String getVfsPrefix() {
1779
1780        return m_vfsPrefix;
1781    }
1782
1783    /**
1784     * Returns the original configured prefix for internal links in the VFS, to be used
1785     * when re-writing the configuration.<p>
1786     *
1787     * This is required <b>only</b> to serialize the configuration again exactly as it was configured.
1788     * This method should <b>not</b> be used otherwise. Use <code>{@link #getVfsPrefix()}</code>
1789     * to obtain the VFS prefix to use for the internal links.<p>
1790     *
1791     * @return the original configured prefix for internal links in the VFS
1792     */
1793    public String getVfsPrefixForConfiguration() {
1794
1795        return m_vfsPrefixConfigured;
1796    }
1797
1798    /**
1799     * Initializes the static export manager with the OpenCms system configuration.<p>
1800     *
1801     * @param cms an OpenCms context object
1802     */
1803    public void initialize(CmsObject cms) {
1804
1805        m_adminCms = cms;
1806        // initialize static export RFS path (relative to web application)
1807        m_staticExportPath = normalizeExportPath(m_staticExportPathConfigured);
1808        m_staticExportWorkPath = normalizeExportPath(getExportWorkPathForConfiguration());
1809        if (m_staticExportPath.equals(OpenCms.getSystemInfo().getWebApplicationRfsPath())) {
1810            throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_INVALID_EXPORT_PATH_0));
1811        }
1812        // initialize prefix variables
1813        m_rfsPrefix = normalizeRfsPrefix(m_rfsPrefixConfigured);
1814        Iterator<CmsStaticExportRfsRule> itRfsRules = m_rfsRules.iterator();
1815        while (itRfsRules.hasNext()) {
1816            CmsStaticExportRfsRule rule = itRfsRules.next();
1817            try {
1818                rule.setExportPath(normalizeExportPath(rule.getExportPathConfigured()));
1819            } catch (CmsIllegalArgumentException e) {
1820                CmsLog.INIT.warn(e.getMessageContainer());
1821                rule.setExportPath(m_staticExportPath);
1822            }
1823            try {
1824                rule.setExportWorkPath(normalizeExportPath(rule.getExportWorkPathConfigured()));
1825            } catch (CmsIllegalArgumentException e) {
1826                CmsLog.INIT.warn(e.getMessageContainer());
1827                rule.setExportWorkPath(m_staticExportWorkPath);
1828            }
1829            rule.setRfsPrefix(normalizeRfsPrefix(rule.getRfsPrefixConfigured()));
1830        }
1831        m_vfsPrefix = insertContextStrings(m_vfsPrefixConfigured);
1832        m_vfsPrefix = CmsFileUtil.normalizePath(m_vfsPrefix, '/');
1833        if (CmsResource.isFolder(m_vfsPrefix)) {
1834            // ensure prefix does NOT end with a folder '/'
1835            m_vfsPrefix = m_vfsPrefix.substring(0, m_vfsPrefix.length() - 1);
1836        }
1837        if (CmsLog.INIT.isDebugEnabled()) {
1838            if (cms != null) {
1839                CmsLog.INIT.debug(Messages.get().getBundle().key(Messages.INIT_SE_MANAGER_CREATED_1, cms));
1840            } else {
1841                CmsLog.INIT.debug(Messages.get().getBundle().key(Messages.INIT_SE_MANAGER_CREATED_0));
1842            }
1843        }
1844
1845        m_cacheOnlineLinks = CmsMemoryMonitor.createLRUCacheMap(2048);
1846        OpenCms.getMemoryMonitor().register(this.getClass().getName() + ".m_cacheOnlineLinks", m_cacheOnlineLinks);
1847
1848        m_cacheExportUris = CmsMemoryMonitor.createLRUCacheMap(2048);
1849        OpenCms.getMemoryMonitor().register(this.getClass().getName() + ".m_cacheExportUris", m_cacheExportUris);
1850
1851        m_cacheSecureLinks = CmsMemoryMonitor.createLRUCacheMap(2048);
1852        OpenCms.getMemoryMonitor().register(this.getClass().getName() + ".m_cacheSecureLinks", m_cacheSecureLinks);
1853
1854        m_cacheExportLinks = CmsMemoryMonitor.createLRUCacheMap(2048);
1855        OpenCms.getMemoryMonitor().register(this.getClass().getName() + ".m_cacheExportLinks", m_cacheExportLinks);
1856
1857        // register this object as event listener
1858        OpenCms.addCmsEventListener(
1859            this,
1860            new int[] {
1861                I_CmsEventListener.EVENT_PUBLISH_PROJECT,
1862                I_CmsEventListener.EVENT_CLEAR_CACHES,
1863                I_CmsEventListener.EVENT_UPDATE_EXPORTS});
1864
1865        m_exportFolderMatcher = new CmsExportFolderMatcher(m_exportFolders, m_testResource);
1866
1867        // get the default accept-language header value
1868        m_defaultAcceptLanguageHeader = CmsAcceptLanguageHeaderParser.createLanguageHeader();
1869
1870        // get the default accept-charset header value
1871        m_defaultAcceptCharsetHeader = OpenCms.getSystemInfo().getDefaultEncoding();
1872
1873        // get the export url prefix
1874        int pos = m_exportUrl.indexOf("://");
1875        if (pos > 0) {
1876            // absolute link, remove http://servername
1877            int pos2 = m_exportUrl.indexOf('/', pos + 3);
1878            if (pos2 > 0) {
1879                m_exportUrlPrefix = m_exportUrl.substring(pos2);
1880            } else {
1881                // should never happen
1882                m_exportUrlPrefix = "";
1883            }
1884        } else {
1885            m_exportUrlPrefix = m_exportUrl;
1886        }
1887        if (CmsLog.INIT.isInfoEnabled()) {
1888            if (isStaticExportEnabled()) {
1889                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_STATIC_EXPORT_ENABLED_0));
1890                CmsLog.INIT.info(
1891                    Messages.get().getBundle().key(
1892                        Messages.INIT_EXPORT_DEFAULT_1,
1893                        Boolean.valueOf(getExportPropertyDefault())));
1894                itRfsRules = m_rfsRules.iterator();
1895                while (itRfsRules.hasNext()) {
1896                    CmsStaticExportRfsRule rfsRule = itRfsRules.next();
1897                    CmsLog.INIT.info(
1898                        Messages.get().getBundle().key(
1899                            Messages.INIT_EXPORT_RFS_RULE_EXPORT_PATH_2,
1900                            rfsRule.getSource(),
1901                            rfsRule.getExportPath()));
1902                    CmsLog.INIT.info(
1903                        Messages.get().getBundle().key(
1904                            Messages.INIT_EXPORT_RFS_RULE_RFS_PREFIX_2,
1905                            rfsRule.getSource(),
1906                            rfsRule.getRfsPrefix()));
1907                    if (rfsRule.getUseRelativeLinks() != null) {
1908                        if (rfsRule.getUseRelativeLinks().booleanValue()) {
1909                            CmsLog.INIT.info(
1910                                Messages.get().getBundle().key(
1911                                    Messages.INIT_EXPORT_RFS_RULE_RELATIVE_LINKS_1,
1912                                    rfsRule.getSource()));
1913                        } else {
1914                            CmsLog.INIT.info(
1915                                Messages.get().getBundle().key(
1916                                    Messages.INIT_EXPORT_RFS_RULE_ABSOLUTE_LINKS_1,
1917                                    rfsRule.getSource()));
1918                        }
1919                    }
1920                }
1921                // default rule
1922                CmsLog.INIT.info(
1923                    Messages.get().getBundle().key(
1924                        Messages.INIT_EXPORT_RFS_RULE_EXPORT_PATH_2,
1925                        "/",
1926                        m_staticExportPath));
1927                CmsLog.INIT.info(
1928                    Messages.get().getBundle().key(Messages.INIT_EXPORT_RFS_RULE_RFS_PREFIX_2, "/", m_rfsPrefix));
1929                if (m_exportRelativeLinks) {
1930                    CmsLog.INIT.info(
1931                        Messages.get().getBundle().key(Messages.INIT_EXPORT_RFS_RULE_RELATIVE_LINKS_1, "/"));
1932                } else {
1933                    CmsLog.INIT.info(
1934                        Messages.get().getBundle().key(Messages.INIT_EXPORT_RFS_RULE_ABSOLUTE_LINKS_1, "/"));
1935                }
1936                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_EXPORT_VFS_PREFIX_1, getVfsPrefix()));
1937                CmsLog.INIT.info(
1938                    Messages.get().getBundle().key(
1939                        Messages.INIT_EXPORT_EXPORT_HANDLER_1,
1940                        getHandler().getClass().getName()));
1941                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_EXPORT_URL_1, getExportUrl()));
1942                CmsLog.INIT.info(
1943                    Messages.get().getBundle().key(Messages.INIT_EXPORT_OPTIMIZATION_1, getPlainExportOptimization()));
1944                CmsLog.INIT.info(
1945                    Messages.get().getBundle().key(Messages.INIT_EXPORT_TESTRESOURCE_1, getTestResource()));
1946                CmsLog.INIT.info(
1947                    Messages.get().getBundle().key(
1948                        Messages.INIT_LINKSUBSTITUTION_HANDLER_1,
1949                        getLinkSubstitutionHandler().getClass().getName()));
1950            } else {
1951                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_STATIC_EXPORT_DISABLED_0));
1952            }
1953        }
1954    }
1955
1956    /**
1957     * Checks if the static export is required for the given VFS resource.<p>
1958     *
1959     * Please note that the given OpenCms user context is NOT used to read the resource.
1960     * The check for export is always done with the permissions of the "Export" user.
1961     * The provided user context is just used to get the current site root.<p>
1962     *
1963     * Since the "Export" user always operates in the "Online" project, the resource
1964     * is also read from the "Online" project, not from the current project of the given
1965     * OpenCms context.<p>
1966     *
1967     * @param cms the current users OpenCms context
1968     * @param vfsName the VFS resource name to check
1969     *
1970     * @return <code>true</code> if static export is required for the given VFS resource
1971     */
1972    public boolean isExportLink(CmsObject cms, String vfsName) {
1973
1974        LOG.info("isExportLink? " + vfsName);
1975        if (!isStaticExportEnabled()) {
1976            return false;
1977        }
1978        String siteRoot = cms.getRequestContext().getSiteRoot();
1979        // vfsname may still be a root path for a site with a different site root
1980        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(vfsName);
1981        if (site != null) {
1982            siteRoot = site.getSiteRoot();
1983            vfsName = CmsStringUtil.joinPaths("/", vfsName.substring(siteRoot.length()));
1984        }
1985        String cacheKey = getCacheKey(siteRoot, vfsName);
1986        Boolean exportResource = getCacheExportLinks().get(cacheKey);
1987        if (exportResource != null) {
1988            return exportResource.booleanValue();
1989        }
1990
1991        boolean result = false;
1992        try {
1993            // static export must always be checked with the export users permissions,
1994            // not the current users permissions
1995            CmsObject exportCms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserExport());
1996            exportCms.getRequestContext().setSiteRoot(siteRoot);
1997            // exportRes is usually the resource at path vfsName, but in case of detail page URIs it's the detail content
1998            CmsResource exportRes = CmsDetailPageUtil.lookupPage(exportCms, vfsName);
1999            // if we are handling request for robots.txt, don't export
2000            if (OpenCms.getResourceManager().matchResourceType(
2001                CmsXmlSeoConfiguration.SEO_FILE_TYPE,
2002                exportRes.getTypeId())) {
2003                if (vfsName.endsWith("robots.txt")) {
2004                    return false;
2005                }
2006            }
2007
2008            if (Strings.builder().get().endsWithAny(vfsName.toLowerCase(), ".jsp", ".jspx")
2009                && !OpenCms.getResourceManager().matchResourceType(
2010                    CmsResourceTypeJsp.getStaticTypeName(),
2011                    exportRes.getTypeId())) {
2012
2013                return false;
2014            }
2015            String exportValue = exportCms.readPropertyObject(
2016                exportCms.getSitePath(exportRes),
2017                CmsPropertyDefinition.PROPERTY_EXPORT,
2018                true).getValue();
2019            if (exportValue == null) {
2020                // no setting found for "export" property
2021                if (getExportPropertyDefault()) {
2022                    // if the default is "true" we always export
2023                    result = true;
2024                } else {
2025                    // check if the resource is exportable by suffix
2026                    result = isSuffixExportable(vfsName);
2027                }
2028            } else {
2029                // "export" value found, if it was "true" we export
2030                result = Boolean.valueOf(exportValue).booleanValue();
2031            }
2032        } catch (CmsException e) {
2033            // no export required (probably security issues, e.g. no access for export user)
2034            LOG.debug(e.getLocalizedMessage(), e);
2035        }
2036        getCacheExportLinks().put(cacheKey, Boolean.valueOf(result));
2037
2038        return result;
2039    }
2040
2041    /**
2042     * Returns true if the export process is a full static export.<p>
2043     *
2044     * @return true if the export process is a full static export
2045     */
2046    public boolean isFullStaticExport() {
2047
2048        return m_fullStaticExport;
2049    }
2050
2051    /**
2052     * Returns <code>true</code> if the given VFS resource should be transported through a secure channel.<p>
2053     *
2054     * The secure mode is only checked in the "Online" project.
2055     * If the given OpenCms context is currently not in the "Online" project,
2056     * <code>false</code> is returned.<p>
2057     *
2058     * The given resource is read from the site root of the provided OpenCms context.<p>
2059     *
2060     * @param cms the current users OpenCms context
2061     * @param vfsName the VFS resource name to check
2062     *
2063     * @return <code>true</code> if the given VFS resource should be transported through a secure channel
2064     *
2065     * @see CmsStaticExportManager#isSecureLink(CmsObject, String, String)
2066     */
2067    public boolean isSecureLink(CmsObject cms, String vfsName) {
2068
2069        return isSecureLink(cms, vfsName, false);
2070    }
2071
2072    /**
2073     * Returns <code>true</code> if the given VFS resource should be transported through a secure channel.<p>
2074     *
2075     * The secure mode is only checked in the "Online" project.
2076     * If the given OpenCms context is currently not in the "Online" project,
2077     * <code>false</code> is returned.<p>
2078     *
2079     * The given resource is read from the site root of the provided OpenCms context.<p>
2080     *
2081     * @param cms the current users OpenCms context
2082     * @param vfsName the VFS resource name to check
2083     * @param fromSecure <code>true</code> if the link source is delivered secure
2084     *
2085     * @return <code>true</code> if the given VFS resource should be transported through a secure channel
2086     *
2087     * @see CmsStaticExportManager#isSecureLink(CmsObject, String, String)
2088     */
2089    public boolean isSecureLink(CmsObject cms, String vfsName, boolean fromSecure) {
2090
2091        if (!cms.getRequestContext().getCurrentProject().isOnlineProject()) {
2092            return false;
2093        }
2094
2095        String cacheKey = OpenCms.getStaticExportManager().getCacheKey(cms.getRequestContext().getSiteRoot(), vfsName);
2096        String secureResource = OpenCms.getStaticExportManager().getCacheSecureLinks().get(cacheKey);
2097        if (secureResource == null) {
2098            CmsObject cmsForReadingProperties = cms;
2099            try {
2100                // the link target resource may not be readable by the current user, so we use a CmsObject with admin permissions
2101                // to read the "secure" property
2102                CmsObject adminCms = OpenCms.initCmsObject(m_adminCms);
2103                adminCms.getRequestContext().setSiteRoot(cms.getRequestContext().getSiteRoot());
2104                adminCms.getRequestContext().setCurrentProject(cms.getRequestContext().getCurrentProject());
2105                adminCms.getRequestContext().setRequestTime(cms.getRequestContext().getRequestTime());
2106                cmsForReadingProperties = adminCms;
2107            } catch (Exception e) {
2108                LOG.error("Could not initialize CmsObject in isSecureLink:" + e.getLocalizedMessage(), e);
2109            }
2110            try {
2111                secureResource = cmsForReadingProperties.readPropertyObject(
2112                    vfsName,
2113                    CmsPropertyDefinition.PROPERTY_SECURE,
2114                    true).getValue();
2115                if (CmsStringUtil.isEmptyOrWhitespaceOnly(secureResource)) {
2116                    secureResource = "false";
2117                }
2118                // only cache result if read was successfull
2119                OpenCms.getStaticExportManager().getCacheSecureLinks().put(cacheKey, secureResource);
2120            } catch (CmsVfsResourceNotFoundException e) {
2121                secureResource = SECURE_PROPERTY_VALUE_BOTH;
2122                OpenCms.getStaticExportManager().getCacheSecureLinks().put(cacheKey, secureResource);
2123            } catch (Exception e) {
2124                // no secure link required (probably security issues, e.g. no access for current user)
2125                // however other users may be allowed to read the resource, so the result can't be cached
2126                return false;
2127            }
2128        }
2129        return Boolean.parseBoolean(secureResource)
2130            || (fromSecure && SECURE_PROPERTY_VALUE_BOTH.equals(secureResource));
2131    }
2132
2133    /**
2134     * Returns <code>true</code> if the given VFS resource that is located under the
2135     * given site root should be transported through a secure channel.<p>
2136     *
2137     * @param cms the current users OpenCms context
2138     * @param vfsName the VFS resource name to check
2139     * @param siteRoot the site root where the the VFS resource should be read
2140     *
2141     * @return <code>true</code> if the given VFS resource should be transported through a secure channel
2142     *
2143     * @see #isSecureLink(CmsObject, String)
2144     */
2145    public boolean isSecureLink(CmsObject cms, String vfsName, String siteRoot) {
2146
2147        return isSecureLink(cms, vfsName, siteRoot, false);
2148    }
2149
2150    /**
2151     * Returns <code>true</code> if the given VFS resource should be transported through a secure channel.<p>
2152     *
2153     * The secure mode is only checked in the "Online" project.
2154     * If the given OpenCms context is currently not in the "Online" project,
2155     * <code>false</code> is returned.<p>
2156     *
2157     * The given resource is read from the site root of the provided OpenCms context.<p>
2158     *
2159     * @param cms the current users OpenCms context
2160     * @param vfsName the VFS resource name to check
2161     * @param siteRoot the site root where the the VFS resource should be read
2162     * @param fromSecure <code>true</code> if the link source is delivered secure
2163     *
2164     * @return <code>true</code> if the given VFS resource should be transported through a secure channel
2165     *
2166     * @see CmsStaticExportManager#isSecureLink(CmsObject, String, String)
2167     */
2168    public boolean isSecureLink(CmsObject cms, String vfsName, String siteRoot, boolean fromSecure) {
2169
2170        if (siteRoot == null) {
2171            return isSecureLink(cms, vfsName, fromSecure);
2172        }
2173
2174        // the site root of the cms object has to be changed so that the property can be read
2175        String storedSiteRoot = cms.getRequestContext().getSiteRoot();
2176        try {
2177            cms.getRequestContext().setSiteRoot(siteRoot);
2178            return isSecureLink(cms, vfsName, fromSecure);
2179        } finally {
2180            cms.getRequestContext().setSiteRoot(storedSiteRoot);
2181        }
2182    }
2183
2184    /**
2185     * Returns true if the static export is enabled.<p>
2186     *
2187     * @return true if the static export is enabled
2188     */
2189    public boolean isStaticExportEnabled() {
2190
2191        return m_staticExportEnabled;
2192    }
2193
2194    /**
2195     * Returns true if the given resource name is exportable because of it's suffix.<p>
2196     *
2197     * @param resourceName the name to check
2198     * @return true if the given resource name is exportable because of it's suffix
2199     */
2200    public boolean isSuffixExportable(String resourceName) {
2201
2202        if (resourceName == null) {
2203            return false;
2204        }
2205        int pos = resourceName.lastIndexOf('.');
2206        if (pos >= 0) {
2207            String suffix = resourceName.substring(pos).toLowerCase();
2208            return m_exportSuffixes.contains(suffix);
2209        }
2210        return false;
2211    }
2212
2213    /**
2214     * Checks if we have to use temporary directories during export.<p>
2215     *
2216     * @return <code>true</code> if using temporary directories
2217     */
2218    public boolean isUseTempDir() {
2219
2220        return m_useTempDirs;
2221    }
2222
2223    /**
2224     * Returns true if the links in the static export should be relative.<p>
2225     *
2226     * @param vfsName the name of the resource to export
2227     *
2228     * @return true if the links in the static export should be relative
2229     */
2230    public boolean relativeLinksInExport(String vfsName) {
2231
2232        if (vfsName != null) {
2233            Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
2234            while (it.hasNext()) {
2235                CmsStaticExportRfsRule rule = it.next();
2236                if (rule.getSource().matcher(vfsName).matches()) {
2237                    return rule.getUseRelativeLinks() != null
2238                    ? rule.getUseRelativeLinks().booleanValue()
2239                    : m_exportRelativeLinks;
2240                }
2241            }
2242        }
2243        return m_exportRelativeLinks;
2244    }
2245
2246    /**
2247     * Removes a local static export file for content delivered directly from storage.<p>
2248     *
2249     * @param exportFileName the export file name
2250     *
2251     * @throws CmsException if the file can not be deleted
2252     */
2253    public void removeStoredContentExportFile(String exportFileName) throws CmsException {
2254
2255        File exportFile = new File(exportFileName);
2256        if (!exportFile.exists()) {
2257            return;
2258        }
2259        if (!exportFile.delete()) {
2260            throw new CmsStaticExportException(Messages.get().container(Messages.ERR_OUTPUT_STREAM_1, exportFileName));
2261        }
2262        if (LOG.isDebugEnabled()) {
2263            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FILE_DELETED_1, exportFileName));
2264        }
2265    }
2266
2267    /**
2268     * Sets the accept-charset header value.<p>
2269     *
2270     * @param value accept-language header value
2271     */
2272    public void setAcceptCharsetHeader(String value) {
2273
2274        m_acceptCharsetHeader = value;
2275    }
2276
2277    /**
2278     * Sets the accept-language header value.<p>
2279     *
2280     * @param value accept-language header value
2281     */
2282    public void setAcceptLanguageHeader(String value) {
2283
2284        m_acceptLanguageHeader = value;
2285    }
2286
2287    /**
2288     * Sets the default property value.<p>
2289     *
2290     * @param value must be <code>true</code> or <code>false</code>
2291     */
2292    public void setDefault(String value) {
2293
2294        m_exportPropertyDefault = Boolean.valueOf(value).booleanValue();
2295    }
2296
2297    /**
2298     * Sets the number of backups for the static export.<p>
2299     *
2300     * @param backup number of backups
2301     */
2302    public void setExportBackups(String backup) {
2303
2304        m_staticExportBackups = Integer.valueOf(backup);
2305    }
2306
2307    /**
2308     * Sets the export enabled value.<p>
2309     *
2310     * @param value must be <code>true</code> or <code>false</code>
2311     */
2312    public void setExportEnabled(String value) {
2313
2314        m_staticExportEnabled = Boolean.valueOf(value).booleanValue();
2315    }
2316
2317    /**
2318     * Adds a resource pattern to the list of resources which are part of the export.<p>
2319     *
2320     * @param folder the folder pattern to add to the list.
2321     */
2322    public void setExportFolderPattern(String folder) {
2323
2324        m_exportFolders.add(folder);
2325    }
2326
2327    /**
2328     * Sets specific http header for the static export.<p>
2329     *
2330     * The format of the headers must be "header:value".<p>
2331     *
2332     * @param exportHeader a specific http header
2333     */
2334    public void setExportHeader(String exportHeader) {
2335
2336        if (CmsStringUtil.splitAsArray(exportHeader, ':').length == 2) {
2337            if (CmsLog.INIT.isInfoEnabled()) {
2338                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_EXPORT_HEADERS_1, exportHeader));
2339            }
2340            m_exportHeaders.add(exportHeader);
2341        } else {
2342            if (CmsLog.INIT.isWarnEnabled()) {
2343                CmsLog.INIT.warn(Messages.get().getBundle().key(Messages.INIT_INVALID_HEADER_1, exportHeader));
2344            }
2345        }
2346    }
2347
2348    /**
2349     * Sets the path where the static export is written.<p>
2350     *
2351     * @param path the path where the static export is written
2352     */
2353    public void setExportPath(String path) {
2354
2355        m_staticExportPathConfigured = path;
2356    }
2357
2358    /**
2359     * Adds a suffix to the list of resource suffixes which will be exported by default.<p>
2360     *
2361     * @param suffix the suffix to add to the list.
2362     */
2363    public void setExportSuffix(String suffix) {
2364
2365        m_exportSuffixes.add(suffix.toLowerCase());
2366    }
2367
2368    /**
2369     * Sets the export url.<p>
2370     *
2371     * @param url the export url
2372     */
2373    public void setExportUrl(String url) {
2374
2375        m_exportUrl = insertContextStrings(url);
2376        m_exportUrlConfigured = url;
2377    }
2378
2379    /**
2380     * Sets the path where the static export is temporarily written.<p>
2381     *
2382     * @param path the path where the static export is temporarily written
2383     */
2384    public void setExportWorkPath(String path) {
2385
2386        m_staticExportWorkPathConfigured = path;
2387    }
2388
2389    /**
2390     * Sets the link substitution handler class.<p>
2391     *
2392     * @param handlerClassName the link substitution handler class name
2393     */
2394    public void setHandler(String handlerClassName) {
2395
2396        try {
2397            m_handler = (I_CmsStaticExportHandler)Class.forName(handlerClassName).newInstance();
2398        } catch (Exception e) {
2399            // should never happen
2400            LOG.error(e.getLocalizedMessage(), e);
2401        }
2402    }
2403
2404    /**
2405     * Sets the static export handler class.<p>
2406     *
2407     * @param handlerClassName the static export handler class name
2408     */
2409    public void setLinkSubstitutionHandler(String handlerClassName) {
2410
2411        try {
2412            m_linkSubstitutionHandler = (I_CmsLinkSubstitutionHandler)Class.forName(handlerClassName).newInstance();
2413        } catch (Exception e) {
2414            // should never happen
2415            LOG.error(e.getLocalizedMessage(), e);
2416        }
2417    }
2418
2419    /**
2420     * Sets the plain export optimization value.<p>
2421     *
2422     * @param value must be <code>true</code> or <code>false</code>
2423     */
2424    public void setPlainExportOptimization(String value) {
2425
2426        m_quickPlainExport = Boolean.valueOf(value).booleanValue();
2427    }
2428
2429    /**
2430     * Sets the protected export path.<p>
2431     *
2432     * @param exportPath the export path to set
2433     */
2434    public void setProtectedExportPath(String exportPath) {
2435
2436        m_protectedExportPath = exportPath;
2437    }
2438
2439    /**
2440     * Sets the relative links value.<p>
2441     *
2442     * @param value must be <code>true</code> or <code>false</code>
2443     */
2444    public void setRelativeLinks(String value) {
2445
2446        m_exportRelativeLinks = Boolean.valueOf(value).booleanValue();
2447    }
2448
2449    /**
2450     * Sets the remote address which will be used for internal requests during the static export.<p>
2451     *
2452     * @param addr the remote address to be used
2453     */
2454    public void setRemoteAddr(String addr) {
2455
2456        m_remoteAddr = addr;
2457    }
2458
2459    /**
2460     * Sets the prefix for exported links in the "real" file system.<p>
2461     *
2462     * @param rfsPrefix the prefix for exported links in the "real" file system
2463     */
2464    public void setRfsPrefix(String rfsPrefix) {
2465
2466        m_rfsPrefixConfigured = rfsPrefix;
2467    }
2468
2469    /**
2470     * Sets the shared cache configuration.<p>
2471     *
2472     * @param configuration the shared cache configuration
2473     */
2474    public void setSharedCacheConfiguration(CmsSharedCacheConfiguration configuration) {
2475
2476        if (configuration == null) {
2477            m_sharedCacheConfiguration = new CmsSharedCacheConfiguration();
2478        } else {
2479            m_sharedCacheConfiguration = configuration;
2480        }
2481    }
2482
2483    /**
2484     * Sets the stored content delivery configuration.<p>
2485     *
2486     * @param configuration the stored content delivery configuration
2487     */
2488    public void setStoredContentDeliveryConfiguration(CmsStoredContentDeliveryConfiguration configuration) {
2489
2490        if (configuration == null) {
2491            m_storedContentDeliveryConfiguration = new CmsStoredContentDeliveryConfiguration();
2492        } else {
2493            m_storedContentDeliveryConfiguration = configuration;
2494        }
2495    }
2496
2497    /**
2498     * Sets the test resource.<p>
2499     *
2500     * @param testResource the vfs name of the test resource
2501     */
2502    public void setTestResource(String testResource) {
2503
2504        m_testResource = testResource;
2505    }
2506
2507    /**
2508     * Sets the prefix for internal links in the vfs.<p>
2509     *
2510     * @param vfsPrefix the prefix for internal links in the vfs
2511     */
2512    public void setVfsPrefix(String vfsPrefix) {
2513
2514        m_vfsPrefixConfigured = vfsPrefix;
2515    }
2516
2517    /**
2518     * Shuts down all this static export manager.<p>
2519     *
2520     * This is required since there may still be a thread running when the system is being shut down.<p>
2521     */
2522    public synchronized void shutDown() {
2523
2524        int count = 0;
2525        // if the handler is still running, we must wait up to 30 seconds until it is finished
2526        while ((count < HANDLER_FINISH_TIME) && m_handler.isBusy()) {
2527            count++;
2528            try {
2529                if (CmsLog.INIT.isInfoEnabled()) {
2530                    CmsLog.INIT.info(
2531                        Messages.get().getBundle().key(
2532                            Messages.INIT_STATIC_EXPORT_SHUTDOWN_3,
2533                            m_handler.getClass().getName(),
2534                            String.valueOf(count),
2535                            String.valueOf(HANDLER_FINISH_TIME)));
2536                }
2537                wait(1000);
2538            } catch (InterruptedException e) {
2539                // if interrupted we ignore the handler, this will produce some log messages but should be ok
2540                count = HANDLER_FINISH_TIME;
2541            }
2542        }
2543
2544        if (CmsLog.INIT.isInfoEnabled()) {
2545            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_1, this.getClass().getName()));
2546        }
2547
2548    }
2549
2550    /**
2551     * Tries to deliver an already resolved scaled image directly from the configured image cache.<p>
2552     *
2553     * This fast path is limited to registered parameterized static-export links which use the exact image loader.
2554     * The caller must have obtained the data through {@link #getExportData}, so the export-link check and parameter
2555     * lookup have already succeeded. The image loader still validates the scale parameters and read permission before
2556     * creating or delivering the derivative. All other image requests continue through {@link #export}.<p>
2557     *
2558     * @param req the wrapped static export request containing the registered scale parameters
2559     * @param res the current response
2560     * @param cms the CMS context initialized with the Export user
2561     * @param data the resolved static export data
2562     *
2563     * @return <code>true</code> if the response has been handled directly
2564     *
2565     * @throws CmsException if accessing the VFS or image cache fails
2566     * @throws IOException if writing the response fails
2567     */
2568    public boolean tryExportImageCache(
2569        HttpServletRequest req,
2570        HttpServletResponse res,
2571        CmsObject cms,
2572        CmsStaticExportData data)
2573    throws CmsException, IOException {
2574
2575        if ((req == null)
2576            || (res == null)
2577            || (cms == null)
2578            || (data == null)
2579            || (data.getResource() == null)
2580            || (data.getParameters() == null)
2581            || data.isDetailPage()) {
2582            return false;
2583        }
2584        CmsStoredContentDeliveryConfiguration configuration = getStoredContentDeliveryConfiguration();
2585        CmsResource resource = data.getResource();
2586        if (!configuration.isEnabled() || !configuration.isSuffixEnabled(resource.getRootPath())) {
2587            return false;
2588        }
2589        I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(resource);
2590        if (!(loader instanceof I_CmsStaticExportDirectResponseLoader)
2591            || (loader.getLoaderId() != CmsImageLoader.RESOURCE_LOADER_ID_IMAGE_LOADER)) {
2592            return false;
2593        }
2594        String mimetype = OpenCms.getResourceManager().getMimeType(
2595            resource.getName(),
2596            cms.getRequestContext().getEncoding());
2597        if (mimetype != null) {
2598            res.setContentType(mimetype);
2599        }
2600        I_CmsStaticExportDirectResponseLoader directResponseLoader = (I_CmsStaticExportDirectResponseLoader)loader;
2601        return directResponseLoader.tryExportDirectResponse(cms, resource, req, res);
2602    }
2603
2604    /**
2605     * Tries to deliver an already resolved on-demand static export resource directly from external storage.<p>
2606     *
2607     * This is a narrowly constrained fast path for requests which have already passed {@link #getExportData}.
2608     * It deliberately only handles plain dump-loader resources without export parameters. All other resources
2609     * continue through {@link #export}, where locale handling, resource initialization and export rules are applied.<p>
2610     *
2611     * @param req the current request
2612     * @param res the current response
2613     * @param cms the CMS context initialized with the Export user
2614     * @param data the resolved static export data
2615     *
2616     * @return <code>true</code> if the response has been handled directly
2617     *
2618     * @throws CmsException if accessing the VFS or storage fails
2619     * @throws IOException if writing the response fails
2620     */
2621    public boolean tryExportStoredContent(
2622        HttpServletRequest req,
2623        HttpServletResponse res,
2624        CmsObject cms,
2625        CmsStaticExportData data)
2626    throws CmsException, IOException {
2627
2628        if ((req == null)
2629            || (res == null)
2630            || (cms == null)
2631            || (data == null)
2632            || (data.getResource() == null)
2633            || (data.getParameters() != null)
2634            || data.isDetailPage()) {
2635            return false;
2636        }
2637        CmsStoredContentDeliveryConfiguration configuration = getStoredContentDeliveryConfiguration();
2638        CmsResource resource = data.getResource();
2639        if (!configuration.isEnabled() || !configuration.isSuffixEnabled(resource.getRootPath())) {
2640            return false;
2641        }
2642        I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(resource);
2643        if (!(loader instanceof CmsDumpLoader)
2644            || (loader.getLoaderId() != CmsDumpLoader.RESOURCE_LOADER_ID)
2645            || !(loader instanceof I_CmsStoredContentDirectDeliveryLoader)) {
2646            return false;
2647        }
2648        I_CmsStoredContentDirectDeliveryLoader directDeliveryLoader = (I_CmsStoredContentDirectDeliveryLoader)loader;
2649        if (!directDeliveryLoader.isStoredContentDirectDeliveryEnabled(cms, resource, req, res)) {
2650            return false;
2651        }
2652        CmsStaticExportResponseWrapper wrapRes = new CmsStaticExportResponseWrapper(res);
2653        if (!directDeliveryLoader.exportStoredContentTo(cms, resource, req, wrapRes)) {
2654            return false;
2655        }
2656        int status = wrapRes.getStatus();
2657        res.setStatus(status < 0 ? HttpServletResponse.SC_OK : status);
2658        return true;
2659    }
2660
2661    /**
2662     * Clears the caches in the export manager.<p>
2663     *
2664     * @param event the event that requested to clear the caches
2665     */
2666    protected void clearCaches(CmsEvent event) {
2667
2668        // synchronization of this method is not required as the individual maps are all synchronized maps anyway,
2669        // and setExportnames() is doing it's own synchronization
2670
2671        // flush all caches
2672        m_cacheOnlineLinks.clear();
2673        m_cacheExportUris.clear();
2674        m_cacheSecureLinks.clear();
2675        m_cacheExportLinks.clear();
2676        m_exportnameResources = null;
2677        if (LOG.isDebugEnabled()) {
2678            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLUSHED_CACHES_1, Integer.valueOf(event.getType())));
2679        }
2680    }
2681
2682    /**
2683     * Creates the backup folders for the given export folder and deletes the oldest if the maximum number is reached.<p>
2684     *
2685     * @param staticExport folder for which a new backup folder has to be created
2686     * @param exportPath export path to create backup path out of it
2687     * @param exportBackups number of maximum
2688     * @param ruleBackupExtension extension for rule based backups
2689     */
2690    protected void createExportBackupFolders(
2691        File staticExport,
2692        String exportPath,
2693        int exportBackups,
2694        String ruleBackupExtension) {
2695
2696        if (staticExport.exists()) {
2697            String backupFolderName = exportPath.substring(0, exportPath.lastIndexOf(File.separator) + 1);
2698            if (ruleBackupExtension != null) {
2699                backupFolderName = backupFolderName + EXPORT_BACKUP_FOLDER_NAME + ruleBackupExtension;
2700            } else {
2701                backupFolderName = backupFolderName + EXPORT_BACKUP_FOLDER_NAME;
2702            }
2703            for (int i = exportBackups; i > 0; i--) {
2704                File staticExportBackupOld = new File(backupFolderName + Integer.valueOf(i).toString());
2705                if (staticExportBackupOld.exists()) {
2706                    if ((i + 1) > exportBackups) {
2707                        // delete folder if it is the last backup folder
2708                        CmsFileUtil.purgeDirectory(staticExportBackupOld);
2709                    } else {
2710                        // set backup folder to the next backup folder name
2711                        staticExportBackupOld.renameTo(new File(backupFolderName + Integer.valueOf(i + 1).toString()));
2712                    }
2713                }
2714                // old export folder rename to first backup folder
2715                if (i == 1) {
2716                    staticExport.renameTo(staticExportBackupOld);
2717                }
2718            }
2719
2720            // if no backups will be stored the old export folder has to be deleted
2721            if (exportBackups == 0) {
2722                CmsFileUtil.purgeDirectory(staticExport);
2723            }
2724        }
2725    }
2726
2727    /**
2728     * Creates the parent folder for a exported resource in the RFS.<p>
2729     *
2730     * @param exportPath the path to export the file
2731     * @param rfsName the rfs name of the resource
2732     *
2733     * @throws CmsException if the folder could not be created
2734     */
2735    protected void createExportFolder(String exportPath, String rfsName) throws CmsException {
2736
2737        String exportFolderName = CmsFileUtil.normalizePath(exportPath + CmsResource.getFolderPath(rfsName));
2738        File exportFolder = new File(exportFolderName);
2739        if (!exportFolder.exists()) {
2740            // in case of concurrent requests to create this folder, check the folder existence again
2741            if (!exportFolder.mkdirs() && !exportFolder.exists()) {
2742                throw new CmsStaticExportException(Messages.get().container(Messages.ERR_CREATE_FOLDER_1, rfsName));
2743            }
2744        }
2745    }
2746
2747    /**
2748     * Returns the cacheExportLinks.<p>
2749     *
2750     * @return the cacheExportLinks
2751     */
2752    protected Map<String, Boolean> getCacheExportLinks() {
2753
2754        return m_cacheExportLinks;
2755    }
2756
2757    /**
2758     * Returns the cacheSecureLinks.<p>
2759     *
2760     * @return the cacheSecureLinks
2761     */
2762    protected Map<String, String> getCacheSecureLinks() {
2763
2764        return m_cacheSecureLinks;
2765    }
2766
2767    /**
2768     * Returns the export data for a requested resource, if null is returned no export is required.<p>
2769     *
2770     * @param cms an initialized cms context (should be initialized with the "Export" user only)
2771     * @param uri the uri, ie RFS name of the requested resource, with or without the 'export' prefix
2772     *
2773     * @return the export data for the request, if null is returned no export is required
2774     */
2775    protected CmsStaticExportData getRfsExportData(CmsObject cms, String uri) {
2776
2777        // cut export prefix from name
2778        String rfsName = uri.substring(getRfsPrefixForRfsName(uri).length());
2779
2780        // check if we have the result already in the cache
2781        CmsStaticExportData data = null;
2782        String siteRoot = OpenCms.getSiteManager().getSiteRoot(cms.getRequestContext().getSiteRoot());
2783        if (siteRoot != null) {
2784            data = m_cacheExportUris.get(siteRoot + ":" + rfsName);
2785        } else {
2786            data = m_cacheExportUris.get(rfsName);
2787        }
2788
2789        if (data == null) {
2790            // export uri not in cache, must look up the file in the VFS
2791            try {
2792                data = getVfsNameInternal(cms, rfsName);
2793            } catch (CmsVfsResourceNotFoundException e) {
2794                // could happen but is the expected behavior because
2795                // the accoring vfs resource for the given rfsname could not be found
2796                // maybe the rfsname has parameters set -> go on
2797            }
2798        }
2799
2800        if (data == null) {
2801            // it could be a translated resourcename with parameters,
2802            // so make a lookup in the published resources table
2803            try {
2804                String parameters = cms.readStaticExportPublishedResourceParameters(rfsName);
2805                // there was a match in the db table, so get the StaticExportData
2806                if (CmsStringUtil.isNotEmpty(parameters)) {
2807                    // get the rfs base string without the parameter hashcode
2808                    String rfsBaseName = rfsName.substring(0, rfsName.lastIndexOf('_'));
2809                    if (rfsBaseName.endsWith(EXPORT_DEFAULT_FILE)) {
2810                        rfsBaseName = rfsBaseName.substring(0, rfsBaseName.length() - EXPORT_DEFAULT_FILE.length());
2811                    }
2812                    // get the vfs base name, which is later used to read the resource in the vfs
2813                    data = getVfsNameInternal(cms, rfsBaseName);
2814                    if (data != null) {
2815                        data.setParameters(parameters);
2816                    }
2817                }
2818            } catch (CmsVfsResourceNotFoundException e) {
2819                if (LOG.isDebugEnabled()) {
2820                    LOG.debug(
2821                        Messages.get().getBundle().key(
2822                            Messages.LOG_NO_INTERNAL_VFS_RESOURCE_FOUND_1,
2823                            new String[] {rfsName}));
2824                }
2825            } catch (CmsException e) {
2826                // ignore, resource does not exist
2827                if (LOG.isWarnEnabled()) {
2828                    LOG.warn(
2829                        Messages.get().getBundle().key(Messages.ERR_EXPORT_FILE_FAILED_1, new String[] {rfsName}),
2830                        e);
2831                }
2832            }
2833        }
2834
2835        if (data == null) {
2836            // no export data found
2837            data = new CmsStaticExportData(CACHEVALUE_404, rfsName, null, null);
2838        }
2839
2840        if (data.getResource() != null) {
2841            siteRoot = OpenCms.getSiteManager().getSiteRoot(data.getResource().getRootPath());
2842        }
2843        if (siteRoot != null) {
2844            m_cacheExportUris.put(siteRoot + ":" + rfsName, data);
2845        } else {
2846            m_cacheExportUris.put(rfsName, data);
2847        }
2848
2849        // this object comparison is safe, see caller method
2850        if (data.getVfsName() != CACHEVALUE_404) {
2851            if (data.getResource().isFolder() && !CmsResource.isFolder(rfsName)) {
2852                // be sure that folders are folders!
2853                rfsName += "/";
2854            }
2855            data.setRfsName(rfsName);
2856            // this uri can be exported
2857            return data;
2858        }
2859        // this uri can not be exported
2860        return null;
2861    }
2862
2863    /**
2864     * Returns the rfs name for a given vfs name with consideration of the export name.<p>
2865     *
2866     * @param cms the cms obejct
2867     * @param vfsName the the name of the vfs resource
2868     *
2869     * @return the rfs name for a given vfs name with consideration of the export name
2870     */
2871    protected String getRfsNameWithExportName(CmsObject cms, String vfsName) {
2872
2873        String rfsName = vfsName;
2874
2875        try {
2876            // check if the resource folder (or a parent folder) has the "exportname" property set
2877            String name = CmsResource.getName(vfsName).replaceAll("/$", "");
2878            CmsUUID detailId = cms.readIdForUrlName(name);
2879            String propertyReadPath;
2880            if (detailId == null) {
2881                propertyReadPath = CmsResource.getFolderPath(rfsName);
2882            } else {
2883                propertyReadPath = CmsResource.getFolderPath(rfsName.replaceAll("/$", ""));
2884            }
2885            CmsProperty exportNameProperty = cms.readPropertyObject(
2886                propertyReadPath,
2887                CmsPropertyDefinition.PROPERTY_EXPORTNAME,
2888                true);
2889
2890            if (exportNameProperty.isNullProperty()) {
2891                // if "exportname" is not set we must add the site root
2892                rfsName = cms.getRequestContext().addSiteRoot(rfsName);
2893            } else {
2894                // "exportname" property is set
2895                String exportname = normalizeExportName(exportNameProperty.getValue(), propertyReadPath);
2896                if (exportname == null) {
2897                    // revert to the default behavior
2898                    rfsName = cms.getRequestContext().addSiteRoot(rfsName);
2899                } else {
2900                    String value = null;
2901                    boolean cont;
2902                    String resourceName = rfsName; // resourceName can be the detail page URI
2903                    do {
2904                        // find out where the export name was set, to replace these parent folders in the RFS name
2905                        try {
2906                            CmsProperty prop = cms.readPropertyObject(
2907                                resourceName,
2908                                CmsPropertyDefinition.PROPERTY_EXPORTNAME,
2909                                false);
2910                            if (prop.isIdentical(exportNameProperty)) {
2911                                // look for the right position in path
2912                                value = prop.getValue();
2913                            }
2914                            cont = (value == null) && (resourceName.length() > 1);
2915                        } catch (CmsVfsResourceNotFoundException e) {
2916                            // this is for publishing deleted resources
2917                            cont = (resourceName.length() > 1);
2918                        } catch (CmsSecurityException se) {
2919                            // a security exception (probably no read permission) we return the current result
2920                            cont = false;
2921                        }
2922                        if (cont) {
2923                            resourceName = CmsResource.getParentFolder(resourceName);
2924                        }
2925                    } while (cont);
2926                    rfsName = exportname + rfsName.substring(resourceName.length());
2927                }
2928            }
2929        } catch (CmsException e) {
2930            if (LOG.isDebugEnabled()) {
2931                LOG.debug(e.getLocalizedMessage(), e);
2932            }
2933            // ignore exception, return vfsName as rfsName
2934            rfsName = vfsName;
2935        }
2936        return rfsName;
2937    }
2938
2939    /**
2940     * Returns the longest rfs prefix matching a given already translated rfs name.<p>
2941     *
2942     * @param rfsName the rfs name
2943     *
2944     * @return its rfs prefix
2945     *
2946     * @see #getRfsPrefix(String)
2947     */
2948    protected String getRfsPrefixForRfsName(String rfsName) {
2949
2950        String retVal = "";
2951        // default case
2952        if (rfsName.startsWith(m_rfsPrefix + "/")) {
2953            retVal = m_rfsPrefix;
2954        }
2955        // additional rules
2956        Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
2957        while (it.hasNext()) {
2958            CmsStaticExportRfsRule rule = it.next();
2959            String rfsPrefix = rule.getRfsPrefix();
2960            if (rfsName.startsWith(rfsPrefix + "/") && (retVal.length() < rfsPrefix.length())) {
2961                retVal = rfsPrefix;
2962            }
2963        }
2964        return retVal;
2965    }
2966
2967    /**
2968     * Substitutes the ${CONTEXT_NAME} and ${SERVLET_NAME} in a path with the real values.<p>
2969     *
2970     * @param path the path to substitute
2971     * @return path with real context values
2972     */
2973    protected String insertContextStrings(String path) {
2974
2975        // create a new macro resolver
2976        CmsMacroResolver resolver = CmsMacroResolver.newInstance();
2977
2978        // add special mappings for macros
2979        resolver.addMacro("CONTEXT_NAME", OpenCms.getSystemInfo().getContextPath());
2980        resolver.addMacro("SERVLET_NAME", OpenCms.getSystemInfo().getServletPath());
2981
2982        // resolve the macros
2983        return resolver.resolveMacros(path);
2984    }
2985
2986    /**
2987     * Returns true if the rfs Name match against any of the defined export urls.<p>
2988     *
2989     * @param rfsName the rfs Name to validate
2990     *
2991     * @return true if the rfs Name match against any of the defined export urls
2992     */
2993    protected boolean isValidRfsName(String rfsName) {
2994
2995        if (rfsName != null) {
2996            // default case
2997            if (rfsName.startsWith(m_rfsPrefix + "/")) {
2998                return true;
2999            }
3000            // additional rules
3001            Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
3002            while (it.hasNext()) {
3003                CmsStaticExportRfsRule rule = it.next();
3004                String rfsPrefix = rule.getRfsPrefix() + "/";
3005                if (rfsName.startsWith(rfsPrefix)) {
3006                    return true;
3007                }
3008            }
3009        }
3010        return false;
3011    }
3012
3013    /**
3014      * Checks if a String is a valid URL.<p>
3015      *
3016      * @param inputString The String to check can be <code>null</code>
3017      *
3018      * @return <code>true</code> if the String is not <code>null</code> and a valid URL
3019      */
3020    protected boolean isValidURL(String inputString) {
3021
3022        boolean isValid = false;
3023        try {
3024            if (inputString != null) {
3025                URL tempURL = new URL(inputString);
3026                isValid = (tempURL.getProtocol() != null);
3027            }
3028        } catch (MalformedURLException mue) {
3029            // ignore because it is not harmful
3030        }
3031        return isValid;
3032    }
3033
3034    /**
3035     * Returns a normalized export path.<p>
3036     *
3037     * Replacing macros, normalizing the path and taking care of relative paths.<p>
3038     *
3039     * @param exportPath the export path to normalize
3040     *
3041     * @return the normalized export path
3042     */
3043    protected String normalizeExportPath(String exportPath) {
3044
3045        String result = insertContextStrings(exportPath);
3046        result = OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebApplication(result);
3047        if (result.endsWith(File.separator)) {
3048            // ensure export path does NOT end with a File.separator
3049            result = result.substring(0, result.length() - 1);
3050        }
3051        return result;
3052    }
3053
3054    /**
3055     * Returns a normalized rfs prefix.<p>
3056     *
3057     * Replacing macros and normalizing the path.<p>
3058     *
3059     * @param rfsPrefix the prefix to normalize
3060     *
3061     * @return the normalized rfs prefix
3062     */
3063    protected String normalizeRfsPrefix(String rfsPrefix) {
3064
3065        String result = insertContextStrings(rfsPrefix);
3066        if (!isValidURL(result)) {
3067            result = CmsFileUtil.normalizePath(result, '/');
3068        }
3069        if (CmsResource.isFolder(result)) {
3070            // ensure prefix does NOT end with a folder '/'
3071            result = result.substring(0, result.length() - 1);
3072        }
3073        return result;
3074    }
3075
3076    /**
3077     * Reads the resource with the given URI.<p>
3078     *
3079     * @param cms the current CMS context
3080     * @param uri the URI to check
3081     *
3082     * @return the resource export data
3083     *
3084     * @throws CmsException if soemthing goes wrong
3085     */
3086    protected CmsStaticExportData readResource(CmsObject cms, String uri) throws CmsException {
3087
3088        CmsResource resource = null;
3089        boolean isDetailPage = false;
3090
3091        try {
3092            resource = cms.readResource(uri);
3093        } catch (CmsVfsResourceNotFoundException e) {
3094
3095            String urlName = CmsResource.getName(uri).replaceAll("/$", "");
3096            CmsUUID id = cms.readIdForUrlName(urlName);
3097            if (id == null) {
3098                throw e;
3099            }
3100            resource = cms.readResource(id);
3101            isDetailPage = true;
3102
3103            //String parent = CmsResource.getParentFolder(uri);
3104            //resource = cms.readDefaultFile(parent);
3105        }
3106        CmsStaticExportData result = new CmsStaticExportData(uri, null, resource, null);
3107        result.setIsDetailPage(isDetailPage);
3108        return result;
3109    }
3110
3111    /**
3112     * Scrubs all the "export" folders.<p>
3113     *
3114     * @param report an I_CmsReport instance to print output message, or null to write messages to the log file
3115     */
3116    protected void scrubExportFolders(I_CmsReport report) {
3117
3118        if (report != null) {
3119            report.println(
3120                Messages.get().container(Messages.RPT_DELETING_EXPORT_FOLDERS_BEGIN_0),
3121                I_CmsReport.FORMAT_HEADLINE);
3122        }
3123        synchronized (m_lockScrubExportFolders) {
3124            int count = 0;
3125            Integer size = Integer.valueOf(m_rfsRules.size() + 1);
3126            // default case
3127            String exportFolderName = CmsFileUtil.normalizePath(m_staticExportPath + '/');
3128            try {
3129                File exportFolder = new File(exportFolderName);
3130                // check if export file exists, if so delete it
3131                if (exportFolder.exists() && exportFolder.canWrite()) {
3132                    CmsFileUtil.purgeDirectory(exportFolder);
3133                }
3134                count++;
3135                if (report != null) {
3136                    report.println(
3137                        Messages.get().container(
3138                            Messages.RPT_DELETE_EXPORT_FOLDER_3,
3139                            Integer.valueOf(count),
3140                            size,
3141                            exportFolderName),
3142                        I_CmsReport.FORMAT_NOTE);
3143                } else {
3144                    // write log message
3145                    if (LOG.isInfoEnabled()) {
3146                        LOG.info(Messages.get().getBundle().key(Messages.LOG_DEL_MAIN_SE_FOLDER_1, exportFolderName));
3147                    }
3148                }
3149            } catch (Throwable t) {
3150                // ignore, nothing to do about the
3151                if (LOG.isWarnEnabled()) {
3152                    LOG.warn(
3153                        Messages.get().getBundle().key(Messages.LOG_FOLDER_DELETION_FAILED_1, exportFolderName),
3154                        t);
3155                }
3156            }
3157            // iterate over the rules
3158            Iterator<CmsStaticExportRfsRule> it = m_rfsRules.iterator();
3159            while (it.hasNext()) {
3160                CmsStaticExportRfsRule rule = it.next();
3161                exportFolderName = CmsFileUtil.normalizePath(rule.getExportPath() + '/');
3162                try {
3163                    File exportFolder = new File(exportFolderName);
3164                    // check if export file exists, if so delete it
3165                    if (exportFolder.exists() && exportFolder.canWrite()) {
3166                        CmsFileUtil.purgeDirectory(exportFolder);
3167                    }
3168                    count++;
3169                    if (report != null) {
3170                        report.println(
3171                            Messages.get().container(
3172                                Messages.RPT_DELETE_EXPORT_FOLDER_3,
3173                                Integer.valueOf(count),
3174                                size,
3175                                exportFolderName),
3176                            I_CmsReport.FORMAT_NOTE);
3177                    } else {
3178                        // write log message
3179                        if (LOG.isInfoEnabled()) {
3180                            LOG.info(
3181                                Messages.get().getBundle().key(Messages.LOG_DEL_MAIN_SE_FOLDER_1, exportFolderName));
3182                        }
3183                    }
3184                } catch (Throwable t) {
3185                    // ignore, nothing to do about the
3186                    if (LOG.isWarnEnabled()) {
3187                        LOG.warn(
3188                            Messages.get().getBundle().key(Messages.LOG_FOLDER_DELETION_FAILED_1, exportFolderName),
3189                            t);
3190                    }
3191                }
3192            }
3193        }
3194        if (report != null) {
3195            report.println(
3196                Messages.get().container(Messages.RPT_DELETING_EXPORT_FOLDERS_END_0),
3197                I_CmsReport.FORMAT_HEADLINE);
3198        }
3199    }
3200
3201    /**
3202     * Handles a streamed static export through the configured stored content delivery handler if possible.<p>
3203     *
3204     * @param loader the resource loader
3205     * @param cms the CMS context
3206     * @param exportResource the resource to export
3207     * @param req the current request
3208     * @param res the current response
3209     * @param exportFileName the target export file name
3210     *
3211     * @return <code>true</code> if the export has been handled without writing the local export file
3212     *
3213     * @throws CmsException if something goes wrong
3214     */
3215    protected boolean tryHandleConfiguredStoredContentDeliveryExport(
3216        I_CmsStaticExportStreamLoader loader,
3217        CmsObject cms,
3218        CmsResource exportResource,
3219        HttpServletRequest req,
3220        HttpServletResponse res,
3221        String exportFileName)
3222    throws CmsException {
3223
3224        CmsStoredContentDeliveryConfiguration configuration = getStoredContentDeliveryConfiguration();
3225        if (!configuration.isEnabled()) {
3226            return false;
3227        }
3228        return m_storedContentDeliverySupport.handle(
3229            this,
3230            configuration,
3231            loader,
3232            cms,
3233            exportResource,
3234            req,
3235            res,
3236            exportFileName);
3237    }
3238
3239    /**
3240     * Writes a resource to the given export path with the given rfs name and the given content.<p>
3241      *
3242      * @param req the current request
3243      * @param exportPath the path to export the resource
3244      * @param rfsName the rfs name
3245      * @param resource the resource
3246      * @param content the content
3247      *
3248      * @throws CmsException if something goes wrong
3249      */
3250    protected void writeResource(
3251        HttpServletRequest req,
3252        String exportPath,
3253        String rfsName,
3254        CmsResource resource,
3255        byte[] content)
3256    throws CmsException {
3257
3258        String exportFileName = CmsFileUtil.normalizePath(exportPath + rfsName);
3259
3260        // make sure all required parent folder exist
3261        createExportFolder(exportPath, rfsName);
3262        // generate export file instance and output stream
3263        File exportFile = new File(exportFileName);
3264        // write new exported file content
3265        try {
3266            FileOutputStream exportStream = new FileOutputStream(exportFile);
3267            exportStream.write(content);
3268            exportStream.close();
3269
3270            // log export success
3271            logStaticExport(resource, exportFileName);
3272
3273        } catch (Throwable t) {
3274            throw new CmsStaticExportException(
3275                Messages.get().container(Messages.ERR_OUTPUT_STREAM_1, exportFileName),
3276                t);
3277        }
3278        setExportFileLastModified(req, exportFile, resource);
3279    }
3280
3281    /**
3282     * Writes a resource to the given export path by streaming the loader output.<p>
3283     *
3284     * @param loader the resource loader
3285     * @param cms the CMS context
3286     * @param exportResource the resource to export
3287     * @param req the current request
3288     * @param res the current response
3289     * @param exportPath the path to export the resource
3290     * @param rfsName the rfs name
3291     * @param logResource the resource used for logging
3292     *
3293     * @throws CmsException if something goes wrong
3294     */
3295    protected void writeStreamedResource(
3296        I_CmsStaticExportStreamLoader loader,
3297        CmsObject cms,
3298        CmsResource exportResource,
3299        HttpServletRequest req,
3300        HttpServletResponse res,
3301        String exportPath,
3302        String rfsName,
3303        CmsResource logResource)
3304    throws CmsException {
3305
3306        String exportFileName = CmsFileUtil.normalizePath(exportPath + rfsName);
3307        if (tryHandleConfiguredStoredContentDeliveryExport(loader, cms, exportResource, req, res, exportFileName)) {
3308            return;
3309        }
3310        createExportFolder(exportPath, rfsName);
3311        File exportFile = new File(exportFileName);
3312        try {
3313            FileOutputStream exportStream = new FileOutputStream(exportFile);
3314            try {
3315                loader.exportTo(cms, exportResource, req, res, exportStream);
3316            } finally {
3317                exportStream.close();
3318            }
3319            logStaticExport(logResource, exportFileName);
3320        } catch (Throwable t) {
3321            throw new CmsStaticExportException(
3322                Messages.get().container(Messages.ERR_OUTPUT_STREAM_1, exportFileName),
3323                t);
3324        }
3325        setExportFileLastModified(req, exportFile, logResource);
3326    }
3327
3328    /**
3329      * Returns the map of vfs exportnames with exportname as key and the vfs folder path as value.<p>
3330      *
3331      * @return the map of vfs exportnames with exportname as key and the vfs folder path as value
3332      */
3333    private Map<CmsExportname, String> computeVfsExportnames() {
3334
3335        if (LOG.isDebugEnabled()) {
3336            LOG.debug(Messages.get().getBundle().key(Messages.LOG_UPDATE_EXPORTNAME_PROP_START_0));
3337        }
3338
3339        CmsSiteManagerImpl sm = OpenCms.getSiteManager();
3340
3341        List<CmsResource> resources;
3342        CmsObject cms = null;
3343        try {
3344            // this will always be in the root site
3345            cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserExport());
3346            resources = cms.readResourcesWithProperty(CmsPropertyDefinition.PROPERTY_EXPORTNAME);
3347
3348            synchronized (m_lockSetExportnames) {
3349                Map<CmsExportname, String> exportnameResources = new HashMap<CmsExportname, String>();
3350                for (int i = 0, n = resources.size(); i < n; i++) {
3351                    CmsResource res = resources.get(i);
3352                    try {
3353                        String foldername = res.getRootPath();
3354                        String exportname = cms.readPropertyObject(
3355                            foldername,
3356                            CmsPropertyDefinition.PROPERTY_EXPORTNAME,
3357                            false).getValue();
3358                        CmsSite site = sm.getSiteForRootPath(foldername);
3359                        exportname = normalizeExportName(exportname, foldername);
3360                        if (exportname != null) {
3361                            // export name has to be system-wide unique
3362                            // the folder name is a root path
3363                            exportnameResources.put(new CmsExportname(exportname, site), foldername);
3364                        }
3365                    } catch (CmsException e) {
3366                        // should never happen, folder will not be added
3367                        LOG.error(e.getLocalizedMessage(), e);
3368                    }
3369                }
3370                return Collections.unmodifiableMap(exportnameResources);
3371            }
3372        } catch (CmsException e) {
3373            // should never happen, no resources will be added at all
3374            LOG.error(e.getLocalizedMessage(), e);
3375            return Collections.emptyMap();
3376        }
3377    }
3378
3379    /**
3380     * Delivers an on-demand static export response without writing an RFS export copy.<p>
3381     *
3382     * @param loader the resource loader
3383     * @param cms the initialized CMS context
3384     * @param resource the resource to export
3385     * @param file the file for non-streaming loaders
3386     * @param req the current request
3387     * @param res the current response
3388     * @param contentType the response content type
3389     *
3390     * @return the HTTP response status
3391     *
3392     * @throws CmsException if accessing the VFS or storage fails
3393     * @throws IOException if writing the response fails
3394     * @throws ServletException if the loader fails to create the response
3395     */
3396    private int exportToSharedCache(
3397        I_CmsResourceLoader loader,
3398        CmsObject cms,
3399        CmsResource resource,
3400        CmsFile file,
3401        HttpServletRequest req,
3402        CmsStaticExportResponseWrapper res,
3403        String contentType)
3404    throws CmsException, IOException, ServletException {
3405
3406        for (String header : m_exportHeaders) {
3407            String[] parts = CmsStringUtil.splitAsArray(header, ':');
3408            if (parts.length == 2) {
3409                res.setHeader(parts[0], parts[1]);
3410            }
3411        }
3412        setSharedCacheHeaders(res, contentType);
3413        boolean exported;
3414        if (loader instanceof I_CmsStaticExportStreamLoader) {
3415            loader.load(cms, resource, req, res);
3416            exported = true;
3417        } else {
3418            exported = loader.export(cms, new CmsFile(file), req, res) != null;
3419        }
3420        if (!exported) {
3421            return HttpServletResponse.SC_NOT_MODIFIED;
3422        }
3423        int status = res.getStatus();
3424        return status < 0 ? HttpServletResponse.SC_OK : status;
3425    }
3426
3427    /**
3428     * Logs a successful static export.<p>
3429     *
3430     * @param resource the exported resource
3431     * @param exportFileName the export file name
3432     */
3433    private void logStaticExport(CmsResource resource, String exportFileName) {
3434
3435        if (LOG.isInfoEnabled()) {
3436            LOG.info(
3437                Messages.get().getBundle().key(Messages.LOG_STATIC_EXPORTED_2, resource.getRootPath(), exportFileName));
3438        }
3439    }
3440
3441    /**
3442     * Normalizes and validates an export name.<p>
3443     *
3444     * @param exportName the export name
3445     * @param resourcePath the path of the resource on which the export name is defined
3446     *
3447     * @return the normalized export name, or <code>null</code> if the export name is invalid
3448     */
3449    private String normalizeExportName(String exportName, String resourcePath) {
3450
3451        if (CmsStringUtil.isEmpty(exportName)) {
3452            LOG.warn("Invalid export name '" + exportName + "' at " + resourcePath);
3453            return null;
3454        }
3455        if (exportName.charAt(0) != '/') {
3456            exportName = '/' + exportName;
3457        }
3458        if (exportName.charAt(exportName.length() - 1) != '/') {
3459            exportName = exportName + '/';
3460        }
3461        if (PATTERN_PARENT_PATH_COMPONENT.matcher(exportName).find()) {
3462            LOG.warn("Invalid export name '" + exportName + "' at " + resourcePath);
3463            return null;
3464        }
3465        return exportName;
3466    }
3467
3468    /**
3469     * Updates the exported file with the modification date from the request or resource.<p>
3470     *
3471     * @param req the current request
3472     * @param exportFile the exported file
3473     * @param resource the exported resource
3474     */
3475    private void setExportFileLastModified(HttpServletRequest req, File exportFile, CmsResource resource) {
3476
3477        if (req != null) {
3478            Long dateLastModified = (Long)req.getAttribute(CmsRequestUtil.HEADER_OPENCMS_EXPORT);
3479            if ((dateLastModified != null) && (dateLastModified.longValue() != -1)) {
3480                long lastModified = (dateLastModified.longValue() / 1000) * 1000;
3481                exportFile.setLastModified(lastModified);
3482                if (LOG.isDebugEnabled()) {
3483                    LOG.debug(
3484                        Messages.get().getBundle().key(
3485                            Messages.LOG_SET_LAST_MODIFIED_2,
3486                            exportFile.getName(),
3487                            Long.valueOf(lastModified)));
3488                }
3489            }
3490        } else {
3491            exportFile.setLastModified((resource.getDateLastModified() / 1000) * 1000);
3492        }
3493    }
3494
3495    /**
3496     * Sets the shared cache response headers for the matching content type policy.<p>
3497     *
3498     * @param res the current response
3499     * @param contentType the response content type
3500     */
3501    private void setSharedCacheHeaders(CmsStaticExportResponseWrapper res, String contentType) {
3502
3503        CmsSharedCachePolicy policy = m_sharedCacheConfiguration.getCachePolicy(contentType);
3504        if (policy == null) {
3505            return;
3506        }
3507        StringBuilder cacheControl = new StringBuilder(64);
3508        cacheControl.append("public, max-age=");
3509        cacheControl.append(policy.getClientMaxAge());
3510        cacheControl.append(", s-maxage=");
3511        cacheControl.append(policy.getSharedMaxAge());
3512        if (policy.getStaleIfError() != CmsSharedCachePolicy.DURATION_UNSET) {
3513            cacheControl.append(", stale-if-error=");
3514            cacheControl.append(policy.getStaleIfError());
3515        }
3516        res.enforceCacheControl(cacheControl.toString());
3517    }
3518}