001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.loader;
029
030import org.opencms.configuration.CmsParameterConfiguration;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsRequestContext;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.CmsVfsResourceNotFoundException;
038import org.opencms.file.history.CmsHistoryResourceHandler;
039import org.opencms.flex.CmsFlexCache;
040import org.opencms.flex.CmsFlexController;
041import org.opencms.flex.CmsFlexRequest;
042import org.opencms.flex.CmsFlexResponse;
043import org.opencms.gwt.shared.CmsGwtConstants;
044import org.opencms.i18n.CmsEncoder;
045import org.opencms.i18n.CmsMessageContainer;
046import org.opencms.jsp.CmsJspTagEnableAde;
047import org.opencms.jsp.jsonpart.CmsJsonPartFilter;
048import org.opencms.jsp.util.CmsJspLinkMacroResolver;
049import org.opencms.jsp.util.CmsJspStandardContextBean;
050import org.opencms.main.CmsEvent;
051import org.opencms.main.CmsException;
052import org.opencms.main.CmsLog;
053import org.opencms.main.I_CmsEventListener;
054import org.opencms.main.OpenCms;
055import org.opencms.monitor.CmsMemoryMonitor;
056import org.opencms.relations.CmsRelation;
057import org.opencms.relations.CmsRelationFilter;
058import org.opencms.relations.CmsRelationType;
059import org.opencms.staticexport.CmsLinkManager;
060import org.opencms.util.CmsFileUtil;
061import org.opencms.util.CmsRequestUtil;
062import org.opencms.util.CmsStringUtil;
063import org.opencms.util.I_CmsRegexSubstitution;
064import org.opencms.workplace.CmsWorkplaceManager;
065
066import java.io.File;
067import java.io.FileNotFoundException;
068import java.io.FileOutputStream;
069import java.io.IOException;
070import java.io.UnsupportedEncodingException;
071import java.io.Writer;
072import java.net.SocketException;
073import java.util.Collection;
074import java.util.Collections;
075import java.util.HashMap;
076import java.util.HashSet;
077import java.util.Iterator;
078import java.util.LinkedHashSet;
079import java.util.Locale;
080import java.util.Map;
081import java.util.Set;
082import java.util.concurrent.locks.ReentrantReadWriteLock;
083import java.util.regex.Matcher;
084import java.util.regex.Pattern;
085
086import javax.servlet.ServletException;
087import javax.servlet.ServletRequest;
088import javax.servlet.ServletResponse;
089import javax.servlet.http.HttpServletRequest;
090import javax.servlet.http.HttpServletResponse;
091
092import org.apache.commons.logging.Log;
093
094import com.google.common.base.Splitter;
095
096/**
097 * The JSP loader which enables the execution of JSP in OpenCms.<p>
098 *
099 * Parameters supported by this loader:<dl>
100 *
101 * <dt>jsp.repository</dt><dd>
102 * (Optional) This is the root directory in the "real" file system where generated JSPs are stored.
103 * The default is the web application path, e.g. in Tomcat if your web application is
104 * names "opencms" it would be <code>${TOMCAT_HOME}/webapps/opencms/</code>.
105 * The <code>jsp.folder</code> (see below) is added to this path.
106 * Usually the <code>jsp.repository</code> is not changed.
107 * </dd>
108 *
109 * <dt>jsp.folder</dt><dd>
110 * (Optional) A path relative to the <code>jsp.repository</code> path where the
111 * JSPs generated by OpenCms are stored. The default is to store the generated JSP in
112 * <code>/WEB-INF/jsp/</code>.
113 * This works well in Tomcat 4, and the JSPs are
114 * not accessible directly from the outside this way, only through the OpenCms servlet.
115 * <i>Please note:</i> Some servlet environments (e.g. BEA Weblogic) do not permit
116 * JSPs to be stored under <code>/WEB-INF</code>. For environments like these,
117 * set the path to some place where JSPs can be accessed, e.g. <code>/jsp/</code> only.
118 * </dd>
119 *
120 * <dt>jsp.errorpage.committed</dt><dd>
121 * (Optional) This parameter controls behavior of JSP error pages
122 * i.e. <code>&lt;% page errorPage="..." %&gt;</code>. If you find that these don't work
123 * in your servlet environment, you should try to change the value here.
124 * The default <code>true</code> has been tested with Tomcat 4.1 and 5.0.
125 * Older versions of Tomcat like 4.0 require a setting of <code>false</code>.</dd>
126 * </dl>
127 *
128 * @since 6.0.0
129 *
130 * @see I_CmsResourceLoader
131 */
132public class CmsJspLoader implements I_CmsResourceLoader, I_CmsFlexCacheEnabledLoader, I_CmsEventListener {
133
134    /** Property value for "cache" that indicates that the FlexCache should be bypassed. */
135    public static final String CACHE_PROPERTY_BYPASS = "bypass";
136
137    /** Property value for "cache" that indicates that the output should be streamed. */
138    public static final String CACHE_PROPERTY_STREAM = "stream";
139
140    /** Default jsp folder constant. */
141    public static final String DEFAULT_JSP_FOLDER = "/WEB-INF/jsp/";
142
143    /** Special JSP directive tag start (<code>%&gt;</code>). */
144    public static final String DIRECTIVE_END = "%>";
145
146    /** Special JSP directive tag start (<code>&lt;%&#0040;</code>). */
147    public static final String DIRECTIVE_START = "<%@";
148
149    /** Extension for JSP managed by OpenCms (<code>.jsp</code>). */
150    public static final String JSP_EXTENSION = ".jsp";
151
152    /** Cache max age parameter name. */
153    public static final String PARAM_CLIENT_CACHE_MAXAGE = "client.cache.maxage";
154
155    /** Jsp cache size parameter name. */
156    public static final String PARAM_JSP_CACHE_SIZE = "jsp.cache.size";
157
158    /** Error page committed parameter name. */
159    public static final String PARAM_JSP_ERRORPAGE_COMMITTED = "jsp.errorpage.committed";
160
161    /** Jsp folder parameter name. */
162    public static final String PARAM_JSP_FOLDER = "jsp.folder";
163
164    /** Jsp repository parameter name. */
165    public static final String PARAM_JSP_REPOSITORY = "jsp.repository";
166
167    /** The id of this loader. */
168    public static final int RESOURCE_LOADER_ID = 6;
169
170    /** The log object for this class. */
171    private static final Log LOG = CmsLog.getLog(CmsJspLoader.class);
172
173    /** The maximum age for delivered contents in the clients cache. */
174    private static long m_clientCacheMaxAge;
175
176    /** Read write locks for jsp files. */
177    private static Map<String, ReentrantReadWriteLock> m_fileLocks = CmsMemoryMonitor.createLRUCacheMap(10000);
178
179    /** The directory to store the generated JSP pages in (absolute path). */
180    private static String m_jspRepository;
181
182    /** The directory to store the generated JSP pages in (relative path in web application). */
183    private static String m_jspWebAppRepository;
184
185    /** The CmsFlexCache used to store generated cache entries in. */
186    private CmsFlexCache m_cache;
187
188    /** The resource loader configuration. */
189    private CmsParameterConfiguration m_configuration;
190
191    /** Flag to indicate if error pages are marked as "committed". */
192    private boolean m_errorPagesAreNotCommitted;
193
194    /** The offline JSPs. */
195    private Map<String, Boolean> m_offlineJsps;
196
197    /** The online JSPs. */
198    private Map<String, Boolean> m_onlineJsps;
199
200    /** A map from taglib names to their URIs. */
201    private Map<String, String> m_taglibs = new HashMap<String, String>();
202
203    /** Lock used to prevent JSP repository from being accessed while it is purged. The read lock is needed for accessing the JSP repository, the write lock is needed for purging it. */
204    private ReentrantReadWriteLock m_purgeLock = new ReentrantReadWriteLock(true);
205
206    /**
207     * The constructor of the class is empty, the initial instance will be
208     * created by the resource manager upon startup of OpenCms.<p>
209     *
210     * @see org.opencms.loader.CmsResourceManager
211     */
212    public CmsJspLoader() {
213
214        m_configuration = new CmsParameterConfiguration();
215        OpenCms.addCmsEventListener(
216            this,
217            new int[] {EVENT_CLEAR_CACHES, EVENT_CLEAR_OFFLINE_CACHES, EVENT_CLEAR_ONLINE_CACHES});
218        m_fileLocks = CmsMemoryMonitor.createLRUCacheMap(10000);
219        initCaches(1000);
220    }
221
222    /**
223     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
224     */
225    public void addConfigurationParameter(String paramName, String paramValue) {
226
227        m_configuration.add(paramName, paramValue);
228        if (paramName.startsWith("taglib.")) {
229            m_taglibs.put(paramName.replaceFirst("^taglib\\.", ""), paramValue.trim());
230        }
231    }
232
233    /**
234     * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent)
235     */
236    public void cmsEvent(CmsEvent event) {
237
238        switch (event.getType()) {
239            case EVENT_CLEAR_CACHES:
240                m_offlineJsps.clear();
241                m_onlineJsps.clear();
242                return;
243            case EVENT_CLEAR_OFFLINE_CACHES:
244                m_offlineJsps.clear();
245                return;
246            case EVENT_CLEAR_ONLINE_CACHES:
247                m_onlineJsps.clear();
248                return;
249            default:
250                // do nothing
251        }
252    }
253
254    /**
255     * Destroy this ResourceLoder, this is a NOOP so far.
256     */
257    public void destroy() {
258
259        // NOOP
260    }
261
262    /**
263     * @see org.opencms.loader.I_CmsResourceLoader#dump(org.opencms.file.CmsObject, org.opencms.file.CmsResource, java.lang.String, java.util.Locale, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
264     */
265    public byte[] dump(
266        CmsObject cms,
267        CmsResource file,
268        String element,
269        Locale locale,
270        HttpServletRequest req,
271        HttpServletResponse res)
272    throws ServletException, IOException {
273
274        // get the current Flex controller
275        CmsFlexController controller = CmsFlexController.getController(req);
276        CmsFlexController oldController = null;
277
278        if (controller != null) {
279            // for dumping we must create an new "top level" controller, save the old one to be restored later
280            oldController = controller;
281        }
282
283        byte[] result = null;
284        try {
285            // now create a new, temporary Flex controller
286            controller = getController(cms, file, req, res, false, false);
287            if (element != null) {
288                // add the element parameter to the included request
289                String[] value = new String[] {element};
290                Map<String, String[]> parameters = Collections.singletonMap(
291                    I_CmsResourceLoader.PARAMETER_ELEMENT,
292                    value);
293                controller.getCurrentRequest().addParameterMap(parameters);
294            }
295            Map<String, Object> attrs = controller.getCurrentRequest().addAttributeMap(
296                CmsRequestUtil.getAtrributeMap(req));
297            // dispatch to the JSP
298            result = dispatchJsp(controller);
299
300            // the standard context bean still references the nested request, we need to reset it to the old request
301            // (using the nested request is bad because it references the flex controller that is going to be nulled out by removeController(), so operations
302            // which use the flex controller might fail).
303
304            CmsJspStandardContextBean standardContext = (CmsJspStandardContextBean)attrs.get(
305                CmsJspStandardContextBean.ATTRIBUTE_NAME);
306            if ((standardContext != null) && (req instanceof CmsFlexRequest)) {
307                standardContext.updateRequestData((CmsFlexRequest)req);
308            }
309            // remove temporary controller
310            CmsFlexController.removeController(req);
311        } finally {
312            if ((oldController != null) && (controller != null)) {
313                // update "date last modified"
314                oldController.updateDates(controller.getDateLastModified(), controller.getDateExpires());
315                // reset saved controller
316                CmsFlexController.setController(req, oldController);
317            }
318        }
319
320        return result;
321    }
322
323    /**
324     * @see org.opencms.loader.I_CmsResourceLoader#export(org.opencms.file.CmsObject, org.opencms.file.CmsResource, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
325     */
326    public byte[] export(CmsObject cms, CmsResource resource, HttpServletRequest req, HttpServletResponse res)
327    throws ServletException, IOException {
328
329        // get the Flex controller
330        CmsFlexController controller = getController(cms, resource, req, res, false, true);
331
332        // dispatch to the JSP
333        byte[] result = dispatchJsp(controller);
334
335        // remove the controller from the request
336        CmsFlexController.removeController(req);
337
338        // return the contents
339        return result;
340    }
341
342    /**
343     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
344     */
345    public CmsParameterConfiguration getConfiguration() {
346
347        // return the configuration in an immutable form
348        return m_configuration;
349    }
350
351    /**
352     * Returns the absolute path in the "real" file system for the JSP repository
353     * toplevel directory.<p>
354     *
355     * @return The full path to the JSP repository
356     */
357    public String getJspRepository() {
358
359        return m_jspRepository;
360    }
361
362    /**
363     * @see org.opencms.loader.I_CmsResourceLoader#getLoaderId()
364     */
365    public int getLoaderId() {
366
367        return RESOURCE_LOADER_ID;
368    }
369
370    /**
371     * Returns a set of root paths of files that are including the given resource using the 'link.strong' macro.<p>
372     *
373     * @param cms the current cms context
374     * @param resource the resource to check
375     * @param referencingPaths the set of already referencing paths, also return parameter
376     *
377     * @throws CmsException if something goes wrong
378     */
379    public void getReferencingStrongLinks(CmsObject cms, CmsResource resource, Set<String> referencingPaths)
380    throws CmsException {
381
382        CmsRelationFilter filter = CmsRelationFilter.SOURCES.filterType(CmsRelationType.JSP_STRONG);
383        Iterator<CmsRelation> it = cms.getRelationsForResource(resource, filter).iterator();
384        while (it.hasNext()) {
385            CmsRelation relation = it.next();
386            try {
387                CmsResource source = relation.getSource(cms, CmsResourceFilter.DEFAULT);
388                // check if file was already included
389                if (referencingPaths.contains(source.getRootPath())) {
390                    // no need to include this file more than once
391                    continue;
392                }
393                referencingPaths.add(source.getRootPath());
394                getReferencingStrongLinks(cms, source, referencingPaths);
395            } catch (CmsException e) {
396                if (LOG.isErrorEnabled()) {
397                    LOG.error(e.getLocalizedMessage(), e);
398                }
399            }
400        }
401    }
402
403    /**
404     * Return a String describing the ResourceLoader,
405     * which is (localized to the system default locale)
406     * <code>"The OpenCms default resource loader for JSP"</code>.<p>
407     *
408     * @return a describing String for the ResourceLoader
409     */
410    public String getResourceLoaderInfo() {
411
412        return Messages.get().getBundle().key(Messages.GUI_LOADER_JSP_DEFAULT_DESC_0);
413    }
414
415    /**
416     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
417     */
418    public void initConfiguration() {
419
420        m_jspRepository = m_configuration.get(PARAM_JSP_REPOSITORY);
421        if (m_jspRepository == null) {
422            m_jspRepository = OpenCms.getSystemInfo().getWebApplicationRfsPath();
423        }
424        m_jspWebAppRepository = m_configuration.getString(PARAM_JSP_FOLDER, DEFAULT_JSP_FOLDER);
425        if (!m_jspWebAppRepository.endsWith("/")) {
426            m_jspWebAppRepository += "/";
427        }
428        m_jspRepository = CmsFileUtil.normalizePath(m_jspRepository + m_jspWebAppRepository);
429
430        String maxAge = m_configuration.get(PARAM_CLIENT_CACHE_MAXAGE);
431        if (maxAge == null) {
432            m_clientCacheMaxAge = -1;
433        } else {
434            m_clientCacheMaxAge = Long.parseLong(maxAge);
435        }
436
437        // get the "error pages are committed or not" flag from the configuration
438        m_errorPagesAreNotCommitted = m_configuration.getBoolean(PARAM_JSP_ERRORPAGE_COMMITTED, true);
439
440        int cacheSize = m_configuration.getInteger(PARAM_JSP_CACHE_SIZE, -1);
441        if (cacheSize > 0) {
442            initCaches(cacheSize);
443        }
444
445        // output setup information
446        if (CmsLog.INIT.isInfoEnabled()) {
447            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_JSP_REPOSITORY_ABS_PATH_1, m_jspRepository));
448            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_WEBAPP_PATH_1, m_jspWebAppRepository));
449            CmsLog.INIT.info(
450                Messages.get().getBundle().key(
451                    Messages.INIT_JSP_REPOSITORY_ERR_PAGE_COMMOTED_1,
452                    Boolean.valueOf(m_errorPagesAreNotCommitted)));
453            if (m_clientCacheMaxAge > 0) {
454                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_CLIENT_CACHE_MAX_AGE_1, maxAge));
455            }
456            if (cacheSize > 0) {
457                CmsLog.INIT.info(
458                    Messages.get().getBundle().key(Messages.INIT_JSP_CACHE_SIZE_1, String.valueOf(cacheSize)));
459            }
460            CmsLog.INIT.info(
461                Messages.get().getBundle().key(Messages.INIT_LOADER_INITIALIZED_1, this.getClass().getName()));
462        }
463    }
464
465    /**
466     * @see org.opencms.loader.I_CmsResourceLoader#isStaticExportEnabled()
467     */
468    public boolean isStaticExportEnabled() {
469
470        return true;
471    }
472
473    /**
474     * @see org.opencms.loader.I_CmsResourceLoader#isStaticExportProcessable()
475     */
476    public boolean isStaticExportProcessable() {
477
478        return true;
479    }
480
481    /**
482     * @see org.opencms.loader.I_CmsResourceLoader#isUsableForTemplates()
483     */
484    public boolean isUsableForTemplates() {
485
486        return true;
487    }
488
489    /**
490     * @see org.opencms.loader.I_CmsResourceLoader#isUsingUriWhenLoadingTemplate()
491     */
492    public boolean isUsingUriWhenLoadingTemplate() {
493
494        return false;
495    }
496
497    /**
498     * @see org.opencms.loader.I_CmsResourceLoader#load(org.opencms.file.CmsObject, org.opencms.file.CmsResource, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
499     */
500    public void load(CmsObject cms, CmsResource file, HttpServletRequest req, HttpServletResponse res)
501    throws ServletException, IOException, CmsException {
502
503        CmsRequestContext context = cms.getRequestContext();
504        // If we load template jsp or template-element jsp (xml contents or xml pages) don't show source (2nd test)
505        if ((CmsHistoryResourceHandler.isHistoryRequest(req))
506            && (context.getUri().equals(context.removeSiteRoot(file.getRootPath())))) {
507            showSource(cms, file, req, res);
508        } else {
509            // load and process the JSP
510            boolean streaming = false;
511            boolean bypass = false;
512
513            // read "cache" property for requested VFS resource to check for special "stream" and "bypass" values
514            String cacheProperty = cms.readPropertyObject(file, CmsPropertyDefinition.PROPERTY_CACHE, true).getValue();
515            if (cacheProperty != null) {
516                cacheProperty = cacheProperty.trim();
517                if (CACHE_PROPERTY_STREAM.equals(cacheProperty)) {
518                    streaming = true;
519                } else if (CACHE_PROPERTY_BYPASS.equals(cacheProperty)) {
520                    streaming = true;
521                    bypass = true;
522                }
523            }
524
525            // For now, disable flex caching when the __json parameter is used
526            if (CmsJsonPartFilter.isJsonRequest(req)) {
527                streaming = true;
528                bypass = true;
529            }
530
531            // get the Flex controller
532            CmsFlexController controller = getController(cms, file, req, res, streaming, true);
533            if (bypass || controller.isForwardMode()) {
534                // initialize the standard contex bean to be available for all requests
535                CmsJspStandardContextBean.getInstance(controller.getCurrentRequest());
536                // once in forward mode, always in forward mode (for this request)
537                controller.setForwardMode(true);
538                // bypass Flex cache for this page, update the JSP first if necessary
539                String target = updateJsp(file, controller, new HashSet<String>());
540                // dispatch to external JSP
541                req.getRequestDispatcher(target).forward(controller.getCurrentRequest(), res);
542            } else {
543                // Flex cache not bypassed, dispatch to internal JSP
544                dispatchJsp(controller);
545            }
546
547            // remove the controller from the request if not forwarding
548            if (!controller.isForwardMode()) {
549                CmsFlexController.removeController(req);
550            }
551        }
552    }
553
554    /**
555     * Replaces taglib attributes in page directives with taglib directives.<p>
556     *
557     * @param content the JSP source text
558     *
559     * @return the transformed JSP text
560     */
561    @Deprecated
562    public String processTaglibAttributes(String content) {
563
564        // matches a whole page directive
565        final Pattern directivePattern = Pattern.compile("(?sm)<%@\\s*page.*?%>");
566        // matches a taglibs attribute and captures its values
567        final Pattern taglibPattern = Pattern.compile("(?sm)taglibs\\s*=\\s*\"(.*?)\"");
568        final Pattern commaPattern = Pattern.compile("(?sm)\\s*,\\s*");
569        final Set<String> taglibs = new LinkedHashSet<String>();
570        // we insert the marker after the first page directive
571        final String marker = ":::TAGLIBS:::";
572        I_CmsRegexSubstitution directiveSub = new I_CmsRegexSubstitution() {
573
574            private boolean m_first = true;
575
576            public String substituteMatch(String string, Matcher matcher) {
577
578                String match = string.substring(matcher.start(), matcher.end());
579                I_CmsRegexSubstitution taglibSub = new I_CmsRegexSubstitution() {
580
581                    public String substituteMatch(String string1, Matcher matcher1) {
582
583                        // values of the taglibs attribute
584                        String match1 = string1.substring(matcher1.start(1), matcher1.end(1));
585                        for (String taglibKey : Splitter.on(commaPattern).split(match1)) {
586                            taglibs.add(taglibKey);
587                        }
588                        return "";
589                    }
590                };
591                String result = CmsStringUtil.substitute(taglibPattern, match, taglibSub);
592                if (m_first) {
593                    result += marker;
594                    m_first = false;
595                }
596                return result;
597            }
598        };
599        String substituted = CmsStringUtil.substitute(directivePattern, content, directiveSub);
600        // insert taglib inclusion
601        substituted = substituted.replaceAll(marker, generateTaglibInclusions(taglibs));
602        // remove empty page directives
603        substituted = substituted.replaceAll("(?sm)<%@\\s*page\\s*%>", "");
604        return substituted;
605    }
606
607    /**
608     * Removes the given resources from the cache.<p>
609     *
610     * @param rootPaths the set of root paths to remove
611     * @param online if online or offline
612     */
613    public void removeFromCache(Set<String> rootPaths, boolean online) {
614
615        Map<String, Boolean> cache;
616        if (online) {
617            cache = m_onlineJsps;
618        } else {
619            cache = m_offlineJsps;
620        }
621        Iterator<String> itRemove = rootPaths.iterator();
622        while (itRemove.hasNext()) {
623            String rootPath = itRemove.next();
624            cache.remove(rootPath);
625        }
626    }
627
628    /**
629     * Removes a JSP from an offline project from the RFS.<p>
630     *
631     * @param resource the offline JSP resource to remove from the RFS
632     *
633     * @throws CmsLoaderException if accessing the loader fails
634     */
635    public void removeOfflineJspFromRepository(CmsResource resource) throws CmsLoaderException {
636
637        String jspName = getJspRfsPath(resource, false);
638        Set<String> pathSet = new HashSet<String>();
639        pathSet.add(resource.getRootPath());
640        ReentrantReadWriteLock lock = getFileLock(jspName);
641        lock.writeLock().lock();
642        try {
643            removeFromCache(pathSet, false);
644            File jspFile = new File(jspName);
645            jspFile.delete();
646        } finally {
647            lock.writeLock().unlock();
648        }
649    }
650
651    /**
652     * @see org.opencms.loader.I_CmsResourceLoader#service(org.opencms.file.CmsObject, org.opencms.file.CmsResource, javax.servlet.ServletRequest, javax.servlet.ServletResponse)
653     */
654    public void service(CmsObject cms, CmsResource resource, ServletRequest req, ServletResponse res)
655    throws ServletException, IOException, CmsLoaderException {
656
657        CmsFlexController controller = CmsFlexController.getController(req);
658        // get JSP target name on "real" file system
659        String target = updateJsp(resource, controller, new HashSet<String>(8));
660        // important: Indicate that all output must be buffered
661        controller.getCurrentResponse().setOnlyBuffering(true);
662        // initialize the standard contex bean to be available for all requests
663        CmsJspStandardContextBean.getInstance(controller.getCurrentRequest());
664        // dispatch to external file
665        controller.getCurrentRequest().getRequestDispatcherToExternal(cms.getSitePath(resource), target).include(
666            req,
667            res);
668    }
669
670    /**
671     * @see org.opencms.loader.I_CmsFlexCacheEnabledLoader#setFlexCache(org.opencms.flex.CmsFlexCache)
672     */
673    public void setFlexCache(CmsFlexCache cache) {
674
675        m_cache = cache;
676        // output setup information
677        if (CmsLog.INIT.isInfoEnabled()) {
678            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_ADD_FLEX_CACHE_0));
679        }
680    }
681
682    /**
683     * Triggers an asynchronous purge of the JSP repository.<p>
684     *
685     * @param afterPurgeAction the action to execute after purging
686     */
687    public void triggerPurge(final Runnable afterPurgeAction) {
688
689        OpenCms.getExecutor().execute(new Runnable() {
690
691            @SuppressWarnings("synthetic-access")
692            public void run() {
693
694                try {
695                    m_purgeLock.writeLock().lock();
696                    for (ReentrantReadWriteLock lock : m_fileLocks.values()) {
697                        lock.writeLock().lock();
698                    }
699                    doPurge(afterPurgeAction);
700                } catch (Exception e) {
701                    LOG.error("Error while purging jsp repository: " + e.getLocalizedMessage(), e);
702                } finally {
703                    for (ReentrantReadWriteLock lock : m_fileLocks.values()) {
704                        try {
705                            lock.writeLock().unlock();
706                        } catch (Exception e) {
707                            LOG.warn(e.getLocalizedMessage(), e);
708                        }
709                    }
710                    m_purgeLock.writeLock().unlock();
711                }
712            }
713        });
714    }
715
716    /**
717     * Updates a JSP page in the "real" file system in case the VFS resource has changed.<p>
718     *
719     * Also processes the <code>&lt;%@ cms %&gt;</code> tags before the JSP is written to the real FS.
720     * Also recursively updates all files that are referenced by a <code>&lt;%@ cms %&gt;</code> tag
721     * on this page to make sure the file actually exists in the real FS.
722     * All <code>&lt;%@ include %&gt;</code> tags are parsed and the name in the tag is translated
723     * from the OpenCms VFS path to the path in the real FS.
724     * The same is done for filenames in <code>&lt;%@ page errorPage=... %&gt;</code> tags.<p>
725     *
726     * @param resource the requested JSP file resource in the VFS
727     * @param controller the controller for the JSP integration
728     * @param updatedFiles a Set containing all JSP pages that have been already updated
729     *
730     * @return the file name of the updated JSP in the "real" FS
731     *
732     * @throws ServletException might be thrown in the process of including the JSP
733     * @throws IOException might be thrown in the process of including the JSP
734     * @throws CmsLoaderException if the resource type can not be read
735     */
736    public String updateJsp(CmsResource resource, CmsFlexController controller, Set<String> updatedFiles)
737    throws IOException, ServletException, CmsLoaderException {
738
739        String jspVfsName = resource.getRootPath();
740        String extension;
741        boolean isHardInclude;
742        int loaderId = OpenCms.getResourceManager().getResourceType(resource.getTypeId()).getLoaderId();
743        if ((loaderId == CmsJspLoader.RESOURCE_LOADER_ID) && (!jspVfsName.endsWith(JSP_EXTENSION))) {
744            // this is a true JSP resource that does not end with ".jsp"
745            extension = JSP_EXTENSION;
746            isHardInclude = false;
747        } else {
748            // not a JSP resource or already ends with ".jsp"
749            extension = "";
750            // if this is a JSP we don't treat it as hard include
751            isHardInclude = (loaderId != CmsJspLoader.RESOURCE_LOADER_ID);
752        }
753
754        String jspTargetName = CmsFileUtil.getRepositoryName(
755            m_jspWebAppRepository,
756            jspVfsName + extension,
757            controller.getCurrentRequest().isOnline());
758
759        // check if page was already updated
760        if (updatedFiles.contains(jspTargetName)) {
761            // no need to write the already included file to the real FS more then once
762            return jspTargetName;
763        }
764
765        String jspPath = CmsFileUtil.getRepositoryName(
766            m_jspRepository,
767            jspVfsName + extension,
768            controller.getCurrentRequest().isOnline());
769
770        File d = new File(jspPath).getParentFile();
771        if ((d == null) || (d.exists() && !(d.isDirectory() && d.canRead()))) {
772            CmsMessageContainer message = Messages.get().container(Messages.LOG_ACCESS_DENIED_1, jspPath);
773            LOG.error(message.key());
774            // can not continue
775            throw new ServletException(message.key());
776        }
777
778        if (!d.exists()) {
779            // create directory structure
780            d.mkdirs();
781        }
782        ReentrantReadWriteLock readWriteLock = getFileLock(jspVfsName);
783        try {
784            // get a read lock for this jsp
785            readWriteLock.readLock().lock();
786            File jspFile = new File(jspPath);
787            // check if the JSP must be updated
788            boolean mustUpdate = false;
789            long jspModificationDate = 0;
790            if (!jspFile.exists()) {
791                // file does not exist in real FS
792                mustUpdate = true;
793                // make sure the parent folder exists
794                File folder = jspFile.getParentFile();
795                if (!folder.exists()) {
796                    boolean success = folder.mkdirs();
797                    if (!success) {
798                        LOG.error(
799                            org.opencms.db.Messages.get().getBundle().key(
800                                org.opencms.db.Messages.LOG_CREATE_FOLDER_FAILED_1,
801                                folder.getAbsolutePath()));
802                    }
803                }
804            } else {
805                jspModificationDate = jspFile.lastModified();
806                if (jspModificationDate < resource.getDateLastModified()) {
807                    // file in real FS is older then file in VFS
808                    mustUpdate = true;
809                } else if (controller.getCurrentRequest().isDoRecompile()) {
810                    // recompile is forced with parameter
811                    mustUpdate = true;
812                } else {
813                    // check if update is needed
814                    if (controller.getCurrentRequest().isOnline()) {
815                        mustUpdate = !m_onlineJsps.containsKey(jspVfsName);
816                    } else {
817                        mustUpdate = !m_offlineJsps.containsKey(jspVfsName);
818                    }
819                    // check strong links only if update is needed
820                    if (mustUpdate) {
821                        // update strong link dependencies
822                        mustUpdate = updateStrongLinks(resource, controller, updatedFiles);
823                    }
824                }
825            }
826            if (mustUpdate) {
827                if (LOG.isDebugEnabled()) {
828                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_WRITING_JSP_1, jspTargetName));
829                }
830                // jsp needs updating, acquire a write lock
831                readWriteLock.readLock().unlock();
832                readWriteLock.writeLock().lock();
833                try {
834                    // check again if updating is still necessary as this might have happened while waiting for the write lock
835                    if (!jspFile.exists() || (jspModificationDate == jspFile.lastModified())) {
836                        updatedFiles.add(jspTargetName);
837                        byte[] contents;
838                        String encoding;
839                        try {
840                            CmsObject cms = controller.getCmsObject();
841                            contents = cms.readFile(resource).getContents();
842                            // check the "content-encoding" property for the JSP, use system default if not found on path
843                            encoding = cms.readPropertyObject(
844                                resource,
845                                CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
846                                true).getValue();
847                            if (encoding == null) {
848                                encoding = OpenCms.getSystemInfo().getDefaultEncoding();
849                            } else {
850                                encoding = CmsEncoder.lookupEncoding(encoding.trim(), encoding);
851                            }
852                        } catch (CmsException e) {
853                            controller.setThrowable(e, jspVfsName);
854                            throw new ServletException(
855                                Messages.get().getBundle().key(Messages.ERR_LOADER_JSP_ACCESS_1, jspVfsName),
856                                e);
857                        }
858
859                        try {
860                            // parse the JSP and modify OpenCms critical directives
861                            contents = parseJsp(contents, encoding, controller, updatedFiles, isHardInclude);
862                            if (LOG.isInfoEnabled()) {
863                                // check for existing file and display some debug info
864                                LOG.info(
865                                    Messages.get().getBundle().key(
866                                        Messages.LOG_JSP_PERMCHECK_4,
867                                        new Object[] {
868                                            jspFile.getAbsolutePath(),
869                                            Boolean.valueOf(jspFile.exists()),
870                                            Boolean.valueOf(jspFile.isFile()),
871                                            Boolean.valueOf(jspFile.canWrite())}));
872                            }
873                            // write the parsed JSP content to the real FS
874                            synchronized (CmsJspLoader.class) {
875                                // this must be done only one file at a time
876                                FileOutputStream fs = new FileOutputStream(jspFile);
877                                fs.write(contents);
878                                fs.close();
879
880                                // we set the modification date to (approximately) that of the VFS resource. This is needed because in the Online project, the old version of a JSP
881                                // may be generated in the RFS JSP repository *after* the JSP has been changed, but *before* it has been published, which would lead
882                                // to it not being updated after the changed JSP is published.
883
884                                // Note: the RFS may only support second precision for the last modification date
885                                jspFile.setLastModified((1 + (resource.getDateLastModified() / 1000)) * 1000);
886                            }
887                            if (controller.getCurrentRequest().isOnline()) {
888                                m_onlineJsps.put(jspVfsName, Boolean.TRUE);
889                            } else {
890                                m_offlineJsps.put(jspVfsName, Boolean.TRUE);
891                            }
892                            if (LOG.isInfoEnabled()) {
893                                LOG.info(
894                                    Messages.get().getBundle().key(
895                                        Messages.LOG_UPDATED_JSP_2,
896                                        jspTargetName,
897                                        jspVfsName));
898                            }
899                        } catch (FileNotFoundException e) {
900                            throw new ServletException(
901                                Messages.get().getBundle().key(Messages.ERR_LOADER_JSP_WRITE_1, jspFile.getName()),
902                                e);
903                        }
904                    }
905                } finally {
906                    readWriteLock.readLock().lock();
907                    readWriteLock.writeLock().unlock();
908                }
909            }
910
911            // update "last modified" and "expires" date on controller
912            controller.updateDates(jspFile.lastModified(), CmsResource.DATE_EXPIRED_DEFAULT);
913        } finally {
914            //m_processingFiles.remove(jspVfsName);
915            readWriteLock.readLock().unlock();
916        }
917
918        return jspTargetName;
919    }
920
921    /**
922     * Updates the internal jsp repository when the servlet container
923     * tries to compile a jsp file that may not exist.<p>
924     *
925     * @param servletPath the servlet path, just to avoid unneeded recursive calls
926     * @param request the current request
927     */
928    public void updateJspFromRequest(String servletPath, CmsFlexRequest request) {
929
930        // assemble the RFS name of the requested jsp
931        String jspUri = servletPath;
932        String pathInfo = request.getPathInfo();
933        if (pathInfo != null) {
934            jspUri += pathInfo;
935        }
936
937        // check the file name
938        if ((jspUri == null) || !jspUri.startsWith(m_jspWebAppRepository)) {
939            // nothing to do, this kind of request are handled by the CmsJspLoader#service method
940            return;
941        }
942
943        // remove prefixes
944        jspUri = jspUri.substring(m_jspWebAppRepository.length());
945        if (jspUri.startsWith(CmsFlexCache.REPOSITORY_ONLINE)) {
946            jspUri = jspUri.substring(CmsFlexCache.REPOSITORY_ONLINE.length());
947        } else if (jspUri.startsWith(CmsFlexCache.REPOSITORY_OFFLINE)) {
948            jspUri = jspUri.substring(CmsFlexCache.REPOSITORY_OFFLINE.length());
949        } else {
950            // this is not an OpenCms jsp file
951            return;
952        }
953
954        // read the resource from OpenCms
955        CmsFlexController controller = CmsFlexController.getController(request);
956        try {
957            CmsResource includeResource;
958            try {
959                // first try to read the resource assuming no additional jsp extension was needed
960                includeResource = readJspResource(controller, jspUri);
961            } catch (CmsVfsResourceNotFoundException e) {
962                // try removing the additional jsp extension
963                if (jspUri.endsWith(JSP_EXTENSION)) {
964                    jspUri = jspUri.substring(0, jspUri.length() - JSP_EXTENSION.length());
965                }
966                includeResource = readJspResource(controller, jspUri);
967            }
968            // make sure the jsp referenced file is generated
969            updateJsp(includeResource, controller, new HashSet<String>(8));
970        } catch (Exception e) {
971            if (LOG.isDebugEnabled()) {
972                LOG.debug(e.getLocalizedMessage(), e);
973            }
974        }
975    }
976
977    /**
978     * Dispatches the current request to the OpenCms internal JSP.<p>
979     *
980     * @param controller the current controller
981     *
982     * @return the content of the processed JSP
983     *
984     * @throws ServletException if inclusion does not work
985     * @throws IOException if inclusion does not work
986     */
987    protected byte[] dispatchJsp(CmsFlexController controller) throws ServletException, IOException {
988
989        // get request / response wrappers
990        CmsFlexRequest f_req = controller.getCurrentRequest();
991        CmsFlexResponse f_res = controller.getCurrentResponse();
992        try {
993            f_req.getRequestDispatcher(controller.getCmsObject().getSitePath(controller.getCmsResource())).include(
994                f_req,
995                f_res);
996        } catch (SocketException e) {
997            // uncritical, might happen if client (browser) does not wait until end of page delivery
998            LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e);
999        }
1000
1001        byte[] result = null;
1002        HttpServletResponse res = controller.getTopResponse();
1003
1004        if (!controller.isStreaming() && !f_res.isSuspended()) {
1005            try {
1006                // if a JSP error page was triggered the response will be already committed here
1007                if (!res.isCommitted() || m_errorPagesAreNotCommitted) {
1008
1009                    // check if the current request was done by a workplace user
1010                    boolean isWorkplaceUser = CmsWorkplaceManager.isWorkplaceUser(f_req);
1011
1012                    // check if the content was modified since the last request
1013                    if (controller.isTop()
1014                        && !isWorkplaceUser
1015                        && CmsFlexController.isNotModifiedSince(f_req, controller.getDateLastModified())) {
1016                        if (f_req.getParameterMap().size() == 0) {
1017                            // only use "expires" header on pages that have no parameters,
1018                            // otherwise some browsers (e.g. IE 6) will not even try to request
1019                            // updated versions of the page
1020                            CmsFlexController.setDateExpiresHeader(
1021                                res,
1022                                controller.getDateExpires(),
1023                                m_clientCacheMaxAge);
1024                        }
1025                        res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
1026                        return null;
1027                    }
1028
1029                    // get the result byte array
1030                    result = f_res.getWriterBytes();
1031                    HttpServletRequest req = controller.getTopRequest();
1032                    if (req.getHeader(CmsRequestUtil.HEADER_OPENCMS_EXPORT) != null) {
1033                        // this is a non "on-demand" static export request, don't write to the response stream
1034                        req.setAttribute(
1035                            CmsRequestUtil.HEADER_OPENCMS_EXPORT,
1036                            Long.valueOf(controller.getDateLastModified()));
1037                    } else if (controller.isTop()) {
1038                        // process headers and write output if this is the "top" request/response
1039                        res.setContentLength(result.length);
1040                        // check for preset error code
1041                        Integer errorCode = (Integer)req.getAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE);
1042                        if (errorCode == null) {
1043                            // set last modified / no cache headers only if this is not an error page
1044                            if (isWorkplaceUser) {
1045                                res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis());
1046                                CmsRequestUtil.setNoCacheHeaders(res);
1047                            } else {
1048                                // set date last modified header
1049                                CmsFlexController.setDateLastModifiedHeader(res, controller.getDateLastModified());
1050                                if ((f_req.getParameterMap().size() == 0) && (controller.getDateLastModified() > -1)) {
1051                                    // only use "expires" header on pages that have no parameters
1052                                    // and that are cachable (i.e. 'date last modified' is set)
1053                                    // otherwise some browsers (e.g. IE 6) will not even try to request
1054                                    // updated versions of the page
1055                                    CmsFlexController.setDateExpiresHeader(
1056                                        res,
1057                                        controller.getDateExpires(),
1058                                        m_clientCacheMaxAge);
1059                                }
1060                            }
1061                            // set response status to "200 - OK" (required for static export "on-demand")
1062                            res.setStatus(HttpServletResponse.SC_OK);
1063                        } else {
1064                            // set previously saved error code
1065                            res.setStatus(errorCode.intValue());
1066                        }
1067                        // process the headers
1068                        CmsFlexResponse.processHeaders(f_res.getHeaders(), res);
1069                        res.getOutputStream().write(result);
1070                        res.getOutputStream().flush();
1071                    }
1072                }
1073            } catch (IllegalStateException e) {
1074                // uncritical, might happen if JSP error page was used
1075                LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e);
1076            } catch (SocketException e) {
1077                // uncritical, might happen if client (browser) does not wait until end of page delivery
1078                LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e);
1079            }
1080        }
1081
1082        return result;
1083    }
1084
1085    /**
1086     * Purges the JSP repository.<p<
1087     *
1088     * @param afterPurgeAction the action to execute after purging
1089     */
1090    protected void doPurge(Runnable afterPurgeAction) {
1091
1092        if (LOG.isInfoEnabled()) {
1093            LOG.info(
1094                org.opencms.flex.Messages.get().getBundle().key(
1095                    org.opencms.flex.Messages.LOG_FLEXCACHE_WILL_PURGE_JSP_REPOSITORY_0));
1096        }
1097
1098        File d;
1099        d = new File(getJspRepository() + CmsFlexCache.REPOSITORY_ONLINE + File.separator);
1100        CmsFileUtil.purgeDirectory(d);
1101
1102        d = new File(getJspRepository() + CmsFlexCache.REPOSITORY_OFFLINE + File.separator);
1103        CmsFileUtil.purgeDirectory(d);
1104        if (afterPurgeAction != null) {
1105            afterPurgeAction.run();
1106        }
1107
1108        if (LOG.isInfoEnabled()) {
1109            LOG.info(
1110                org.opencms.flex.Messages.get().getBundle().key(
1111                    org.opencms.flex.Messages.LOG_FLEXCACHE_PURGED_JSP_REPOSITORY_0));
1112        }
1113
1114    }
1115
1116    /**
1117     * Generates the taglib directives for a collection of taglib identifiers.<p>
1118     *
1119     * @param taglibs the taglib identifiers
1120     *
1121     * @return a string containing taglib directives
1122     */
1123    protected String generateTaglibInclusions(Collection<String> taglibs) {
1124
1125        StringBuffer buffer = new StringBuffer();
1126        for (String taglib : taglibs) {
1127            String uri = m_taglibs.get(taglib);
1128            if (uri != null) {
1129                buffer.append("<%@ taglib prefix=\"" + taglib + "\" uri=\"" + uri + "\" %>");
1130            }
1131        }
1132        return buffer.toString();
1133    }
1134
1135    /**
1136     * Delivers a Flex controller, either by creating a new one, or by re-using an existing one.<p>
1137     *
1138     * @param cms the initial CmsObject to wrap in the controller
1139     * @param resource the resource requested
1140     * @param req the current request
1141     * @param res the current response
1142     * @param streaming indicates if the response is streaming
1143     * @param top indicates if the response is the top response
1144     *
1145     * @return a Flex controller
1146     */
1147    protected CmsFlexController getController(
1148        CmsObject cms,
1149        CmsResource resource,
1150        HttpServletRequest req,
1151        HttpServletResponse res,
1152        boolean streaming,
1153        boolean top) {
1154
1155        CmsFlexController controller = null;
1156        if (top) {
1157            // only check for existing controller if this is the "top" request/response
1158            controller = CmsFlexController.getController(req);
1159        }
1160        if (controller == null) {
1161            // create new request / response wrappers
1162            if (!cms.getRequestContext().getCurrentProject().isOnlineProject()
1163                && (CmsHistoryResourceHandler.isHistoryRequest(req) || CmsJspTagEnableAde.isDirectEditDisabled(req))) {
1164                cms.getRequestContext().setAttribute(CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT, Boolean.TRUE);
1165            }
1166            controller = new CmsFlexController(cms, resource, m_cache, req, res, streaming, top);
1167            CmsFlexController.setController(req, controller);
1168            CmsFlexRequest f_req = new CmsFlexRequest(req, controller);
1169            CmsFlexResponse f_res = new CmsFlexResponse(res, controller, streaming, true);
1170            controller.push(f_req, f_res);
1171        } else if (controller.isForwardMode()) {
1172            // reset CmsObject (because of URI) if in forward mode
1173            controller = new CmsFlexController(cms, controller);
1174            CmsFlexController.setController(req, controller);
1175        }
1176        return controller;
1177    }
1178
1179    /**
1180     * Initializes the caches.<p>
1181     *
1182     * @param cacheSize the cache size
1183     */
1184    protected void initCaches(int cacheSize) {
1185
1186        m_offlineJsps = CmsMemoryMonitor.createLRUCacheMap(cacheSize);
1187        m_onlineJsps = CmsMemoryMonitor.createLRUCacheMap(cacheSize);
1188    }
1189
1190    /**
1191     * Parses the JSP and modifies OpenCms critical directive information.<p>
1192     *
1193     * @param byteContent the original JSP content
1194     * @param encoding the encoding to use for the JSP
1195     * @param controller the controller for the JSP integration
1196     * @param updatedFiles a Set containing all JSP pages that have been already updated
1197     * @param isHardInclude indicated if this page is actually a "hard" include with <code>&lt;%@ include file="..." &gt;</code>
1198     *
1199     * @return the modified JSP content
1200     */
1201    protected byte[] parseJsp(
1202        byte[] byteContent,
1203        String encoding,
1204        CmsFlexController controller,
1205        Set<String> updatedFiles,
1206        boolean isHardInclude) {
1207
1208        String content;
1209        // make sure encoding is set correctly
1210        try {
1211            content = new String(byteContent, encoding);
1212        } catch (UnsupportedEncodingException e) {
1213            // encoding property is not set correctly
1214            LOG.error(
1215                Messages.get().getBundle().key(
1216                    Messages.LOG_UNSUPPORTED_ENC_1,
1217                    controller.getCurrentRequest().getElementUri()),
1218                e);
1219            try {
1220                encoding = OpenCms.getSystemInfo().getDefaultEncoding();
1221                content = new String(byteContent, encoding);
1222            } catch (UnsupportedEncodingException e2) {
1223                // should not happen since default encoding is always a valid encoding (checked during system startup)
1224                content = new String(byteContent);
1225            }
1226        }
1227
1228        // parse for special %(link:...) macros
1229        content = parseJspLinkMacros(content, controller);
1230        // parse for special <%@cms file="..." %> tag
1231        content = parseJspCmsTag(content, controller, updatedFiles);
1232        // parse for included files in tags
1233        content = parseJspIncludes(content, controller, updatedFiles);
1234        // parse for <%@page pageEncoding="..." %> tag
1235        content = parseJspEncoding(content, encoding, isHardInclude);
1236        // Processes magic taglib attributes in page directives
1237        content = processTaglibAttributes(content);
1238        // convert the result to bytes and return it
1239        try {
1240            return content.getBytes(encoding);
1241        } catch (UnsupportedEncodingException e) {
1242            // should not happen since encoding was already checked
1243            return content.getBytes();
1244        }
1245    }
1246
1247    /**
1248     * Parses the JSP content for the special <code>&lt;%cms file="..." %&gt;</code> tag.<p>
1249     *
1250     * @param content the JSP content to parse
1251     * @param controller the current JSP controller
1252     * @param updatedFiles a set of already updated jsp files
1253     *
1254     * @return the parsed JSP content
1255     */
1256    protected String parseJspCmsTag(String content, CmsFlexController controller, Set<String> updatedFiles) {
1257
1258        // check if a JSP directive occurs in the file
1259        int i1 = content.indexOf(DIRECTIVE_START);
1260        if (i1 < 0) {
1261            // no directive occurs
1262            return content;
1263        }
1264
1265        StringBuffer buf = new StringBuffer(content.length());
1266        int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length(), elen = DIRECTIVE_END.length();
1267
1268        while (i1 >= 0) {
1269            // parse the file and replace JSP filename references
1270            i2 = content.indexOf(DIRECTIVE_END, i1 + slen);
1271            if (i2 < 0) {
1272                // wrong syntax (missing end directive) - let the JSP compiler produce the error message
1273                return content;
1274            } else if (i2 > i1) {
1275                String directive = content.substring(i1 + slen, i2);
1276                if (LOG.isDebugEnabled()) {
1277                    LOG.debug(
1278                        Messages.get().getBundle().key(
1279                            Messages.LOG_DIRECTIVE_DETECTED_3,
1280                            DIRECTIVE_START,
1281                            directive,
1282                            DIRECTIVE_END));
1283                }
1284
1285                int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
1286                while (directive.charAt(t1) == ' ') {
1287                    t1++;
1288                }
1289                String argument = null;
1290                if (directive.startsWith("cms", t1)) {
1291                    if (LOG.isDebugEnabled()) {
1292                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "cms"));
1293                    }
1294                    t2 = directive.indexOf("file", t1 + 3);
1295                    t5 = 4;
1296                }
1297
1298                if (t2 > 0) {
1299                    String sub = directive.substring(t2 + t5);
1300                    char c1 = sub.charAt(t3);
1301                    while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) {
1302                        c1 = sub.charAt(++t3);
1303                    }
1304                    t4 = t3;
1305                    while (c1 != '"') {
1306                        c1 = sub.charAt(++t4);
1307                    }
1308                    if (t4 > t3) {
1309                        argument = sub.substring(t3, t4);
1310                    }
1311                    if (LOG.isDebugEnabled()) {
1312                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument));
1313                    }
1314                }
1315
1316                if (argument != null) {
1317                    //  try to update the referenced file
1318                    String jspname = updateJsp(argument, controller, updatedFiles);
1319                    if (jspname != null) {
1320                        directive = jspname;
1321                        if (LOG.isDebugEnabled()) {
1322                            LOG.debug(
1323                                Messages.get().getBundle().key(
1324                                    Messages.LOG_DIRECTIVE_CHANGED_3,
1325                                    DIRECTIVE_START,
1326                                    directive,
1327                                    DIRECTIVE_END));
1328                        }
1329                    }
1330                    // cms directive was found
1331                    buf.append(content.substring(p0, i1));
1332                    buf.append(directive);
1333                    p0 = i2 + elen;
1334                    i1 = content.indexOf(DIRECTIVE_START, p0);
1335                } else {
1336                    // cms directive was not found
1337                    buf.append(content.substring(p0, i1 + slen));
1338                    buf.append(directive);
1339                    p0 = i2;
1340                    i1 = content.indexOf(DIRECTIVE_START, p0);
1341                }
1342            }
1343        }
1344        if (i2 > 0) {
1345            // the content of the JSP was changed
1346            buf.append(content.substring(p0, content.length()));
1347            content = buf.toString();
1348        }
1349        return content;
1350    }
1351
1352    /**
1353     * Parses the JSP content for the  <code>&lt;%page pageEncoding="..." %&gt;</code> tag
1354     * and ensures that the JSP page encoding is set according to the OpenCms
1355     * "content-encoding" property value of the JSP.<p>
1356     *
1357     * @param content the JSP content to parse
1358     * @param encoding the encoding to use for the JSP
1359     * @param isHardInclude indicated if this page is actually a "hard" include with <code>&lt;%@ include file="..." &gt;</code>
1360     *
1361     * @return the parsed JSP content
1362     */
1363    protected String parseJspEncoding(String content, String encoding, boolean isHardInclude) {
1364
1365        // check if a JSP directive occurs in the file
1366        int i1 = content.indexOf(DIRECTIVE_START);
1367        if (i1 < 0) {
1368            // no directive occurs
1369            if (isHardInclude) {
1370                return content;
1371            }
1372        }
1373
1374        StringBuffer buf = new StringBuffer(content.length() + 64);
1375        int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length();
1376        boolean found = false;
1377
1378        if (i1 < 0) {
1379            // no directive found at all, append content to buffer
1380            buf.append(content);
1381        }
1382
1383        while (i1 >= 0) {
1384            // parse the file and set/replace page encoding
1385            i2 = content.indexOf(DIRECTIVE_END, i1 + slen);
1386            if (i2 < 0) {
1387                // wrong syntax (missing end directive) - let the JSP compiler produce the error message
1388                return content;
1389            } else if (i2 > i1) {
1390                String directive = content.substring(i1 + slen, i2);
1391                if (LOG.isDebugEnabled()) {
1392                    LOG.debug(
1393                        Messages.get().getBundle().key(
1394                            Messages.LOG_DIRECTIVE_DETECTED_3,
1395                            DIRECTIVE_START,
1396                            directive,
1397                            DIRECTIVE_END));
1398                }
1399
1400                int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
1401                while (directive.charAt(t1) == ' ') {
1402                    t1++;
1403                }
1404                String argument = null;
1405                if (directive.startsWith("page", t1)) {
1406                    if (LOG.isDebugEnabled()) {
1407                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "page"));
1408                    }
1409                    t2 = directive.indexOf("pageEncoding", t1 + 4);
1410                    t5 = 12;
1411                    if (t2 > 0) {
1412                        found = true;
1413                    }
1414                }
1415
1416                if (t2 > 0) {
1417                    String sub = directive.substring(t2 + t5);
1418                    char c1 = sub.charAt(t3);
1419                    while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) {
1420                        c1 = sub.charAt(++t3);
1421                    }
1422                    t4 = t3;
1423                    while (c1 != '"') {
1424                        c1 = sub.charAt(++t4);
1425                    }
1426                    if (t4 > t3) {
1427                        argument = sub.substring(t3, t4);
1428                    }
1429                    if (LOG.isDebugEnabled()) {
1430                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument));
1431                    }
1432                }
1433
1434                if (argument != null) {
1435                    // a pageEncoding setting was found, changes have to be made
1436                    String pre = directive.substring(0, t2 + t3 + t5);
1437                    String suf = directive.substring(t2 + t3 + t5 + argument.length());
1438                    // change the encoding
1439                    directive = pre + encoding + suf;
1440                    if (LOG.isDebugEnabled()) {
1441                        LOG.debug(
1442                            Messages.get().getBundle().key(
1443                                Messages.LOG_DIRECTIVE_CHANGED_3,
1444                                DIRECTIVE_START,
1445                                directive,
1446                                DIRECTIVE_END));
1447                    }
1448                }
1449
1450                buf.append(content.substring(p0, i1 + slen));
1451                buf.append(directive);
1452                p0 = i2;
1453                i1 = content.indexOf(DIRECTIVE_START, p0);
1454            }
1455        }
1456        if (i2 > 0) {
1457            // the content of the JSP was changed
1458            buf.append(content.substring(p0, content.length()));
1459        }
1460        if (found) {
1461            content = buf.toString();
1462        } else if (!isHardInclude) {
1463            // encoding setting was not found
1464            // if this is not a "hard" include then add the encoding to the top of the page
1465            // checking for the hard include is important to prevent errors with
1466            // multiple page encoding settings if a template is composed from several hard included elements
1467            // this is an issue in Tomcat 4.x but not 5.x
1468            StringBuffer buf2 = new StringBuffer(buf.length() + 32);
1469            buf2.append("<%@ page pageEncoding=\"");
1470            buf2.append(encoding);
1471            buf2.append("\" %>");
1472            buf2.append(buf);
1473            content = buf2.toString();
1474        }
1475        return content;
1476    }
1477
1478    /**
1479     * Parses the JSP content for includes and replaces all OpenCms VFS
1480     * path information with information for the real FS.<p>
1481     *
1482     * @param content the JSP content to parse
1483     * @param controller the current JSP controller
1484     * @param updatedFiles a set of already updated files
1485     *
1486     * @return the parsed JSP content
1487     */
1488    protected String parseJspIncludes(String content, CmsFlexController controller, Set<String> updatedFiles) {
1489
1490        // check if a JSP directive occurs in the file
1491        int i1 = content.indexOf(DIRECTIVE_START);
1492        if (i1 < 0) {
1493            // no directive occurs
1494            return content;
1495        }
1496
1497        StringBuffer buf = new StringBuffer(content.length());
1498        int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length();
1499
1500        while (i1 >= 0) {
1501            // parse the file and replace JSP filename references
1502            i2 = content.indexOf(DIRECTIVE_END, i1 + slen);
1503            if (i2 < 0) {
1504                // wrong syntax (missing end directive) - let the JSP compiler produce the error message
1505                return content;
1506            } else if (i2 > i1) {
1507                String directive = content.substring(i1 + slen, i2);
1508                if (LOG.isDebugEnabled()) {
1509                    LOG.debug(
1510                        Messages.get().getBundle().key(
1511                            Messages.LOG_DIRECTIVE_DETECTED_3,
1512                            DIRECTIVE_START,
1513                            directive,
1514                            DIRECTIVE_END));
1515                }
1516
1517                int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
1518                while (directive.charAt(t1) == ' ') {
1519                    t1++;
1520                }
1521                String argument = null;
1522                if (directive.startsWith("include", t1)) {
1523                    if (LOG.isDebugEnabled()) {
1524                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "include"));
1525                    }
1526                    t2 = directive.indexOf("file", t1 + 7);
1527                    t5 = 6;
1528                } else if (directive.startsWith("page", t1)) {
1529                    if (LOG.isDebugEnabled()) {
1530                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "page"));
1531                    }
1532                    t2 = directive.indexOf("errorPage", t1 + 4);
1533                    t5 = 11;
1534                }
1535
1536                if (t2 > 0) {
1537                    String sub = directive.substring(t2 + t5);
1538                    char c1 = sub.charAt(t3);
1539                    while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) {
1540                        c1 = sub.charAt(++t3);
1541                    }
1542                    t4 = t3;
1543                    while (c1 != '"') {
1544                        c1 = sub.charAt(++t4);
1545                    }
1546                    if (t4 > t3) {
1547                        argument = sub.substring(t3, t4);
1548                    }
1549                    if (LOG.isDebugEnabled()) {
1550                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument));
1551                    }
1552                }
1553
1554                if (argument != null) {
1555                    // a file was found, changes have to be made
1556                    String pre = directive.substring(0, t2 + t3 + t5);
1557                    String suf = directive.substring(t2 + t3 + t5 + argument.length());
1558                    // now try to update the referenced file
1559                    String jspname = updateJsp(argument, controller, updatedFiles);
1560                    if (jspname != null) {
1561                        // only change something in case no error had occurred
1562                        directive = pre + jspname + suf;
1563                        if (LOG.isDebugEnabled()) {
1564                            LOG.debug(
1565                                Messages.get().getBundle().key(
1566                                    Messages.LOG_DIRECTIVE_CHANGED_3,
1567                                    DIRECTIVE_START,
1568                                    directive,
1569                                    DIRECTIVE_END));
1570                        }
1571                    }
1572                }
1573
1574                buf.append(content.substring(p0, i1 + slen));
1575                buf.append(directive);
1576                p0 = i2;
1577                i1 = content.indexOf(DIRECTIVE_START, p0);
1578            }
1579        }
1580        if (i2 > 0) {
1581            // the content of the JSP was changed
1582            buf.append(content.substring(p0, content.length()));
1583            content = buf.toString();
1584        }
1585        return content;
1586    }
1587
1588    /**
1589     * Parses all jsp link macros, and replace them by the right target path.<p>
1590     *
1591     * @param content the content to parse
1592     * @param controller the request controller
1593     *
1594     * @return the parsed content
1595     */
1596    protected String parseJspLinkMacros(String content, CmsFlexController controller) {
1597
1598        CmsJspLinkMacroResolver macroResolver = new CmsJspLinkMacroResolver(controller.getCmsObject(), null, true);
1599        return macroResolver.resolveMacros(content);
1600    }
1601
1602    /**
1603     * Returns the jsp resource identified by the given name, using the controllers cms context.<p>
1604     *
1605     * @param controller the flex controller
1606     * @param jspName the name of the jsp
1607     *
1608     * @return an OpenCms resource
1609     *
1610     * @throws CmsException if something goes wrong
1611     */
1612    protected CmsResource readJspResource(CmsFlexController controller, String jspName) throws CmsException {
1613
1614        // create an OpenCms user context that operates in the root site
1615        CmsObject cms = OpenCms.initCmsObject(controller.getCmsObject());
1616        // we only need to change the site, but not the project,
1617        // since the request has already the right project set
1618        cms.getRequestContext().setSiteRoot("");
1619        // try to read the resource
1620        return cms.readResource(jspName);
1621    }
1622
1623    /**
1624     * Delivers the plain uninterpreted resource with escaped XML.<p>
1625     *
1626     * This is intended for viewing historical versions.<p>
1627     *
1628     * @param cms the initialized CmsObject which provides user permissions
1629     * @param file the requested OpenCms VFS resource
1630     * @param req the servlet request
1631     * @param res the servlet response
1632     *
1633     * @throws IOException might be thrown by the servlet environment
1634     * @throws CmsException in case of errors accessing OpenCms functions
1635     */
1636    protected void showSource(CmsObject cms, CmsResource file, HttpServletRequest req, HttpServletResponse res)
1637    throws CmsException, IOException {
1638
1639        CmsResource historyResource = (CmsResource)CmsHistoryResourceHandler.getHistoryResource(req);
1640        if (historyResource == null) {
1641            historyResource = file;
1642        }
1643        CmsFile historyFile = cms.readFile(historyResource);
1644        String content = new String(historyFile.getContents());
1645        // change the content-type header so that browsers show plain text
1646        res.setContentLength(content.length());
1647        res.setContentType("text/plain");
1648
1649        Writer out = res.getWriter();
1650        out.write(content);
1651        out.close();
1652    }
1653
1654    /**
1655     * Updates a JSP page in the "real" file system in case the VFS resource has changed based on the resource name.<p>
1656     *
1657     * Generates a resource based on the provided name and calls {@link #updateJsp(CmsResource, CmsFlexController, Set)}.<p>
1658     *
1659     * @param vfsName the name of the JSP file resource in the VFS
1660     * @param controller the controller for the JSP integration
1661     * @param updatedFiles a Set containing all JSP pages that have been already updated
1662     *
1663     * @return the file name of the updated JSP in the "real" FS
1664     */
1665    protected String updateJsp(String vfsName, CmsFlexController controller, Set<String> updatedFiles) {
1666
1667        String jspVfsName = CmsLinkManager.getAbsoluteUri(vfsName, controller.getCurrentRequest().getElementRootPath());
1668        if (LOG.isDebugEnabled()) {
1669            LOG.debug(Messages.get().getBundle().key(Messages.LOG_UPDATE_JSP_1, jspVfsName));
1670        }
1671        String jspRfsName;
1672        try {
1673            CmsResource includeResource;
1674            try {
1675                // first try a root path
1676                includeResource = readJspResource(controller, jspVfsName);
1677            } catch (CmsVfsResourceNotFoundException e) {
1678                // if fails, try a site relative path
1679                includeResource = readJspResource(
1680                    controller,
1681                    controller.getCmsObject().getRequestContext().addSiteRoot(jspVfsName));
1682            }
1683            // make sure the jsp referenced file is generated
1684            jspRfsName = updateJsp(includeResource, controller, updatedFiles);
1685            if (LOG.isDebugEnabled()) {
1686                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NAME_REAL_FS_1, jspRfsName));
1687            }
1688        } catch (Exception e) {
1689            jspRfsName = null;
1690            if (LOG.isDebugEnabled()) {
1691                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_UPDATE_1, jspVfsName), e);
1692            }
1693        }
1694        return jspRfsName;
1695    }
1696
1697    /**
1698     * Updates all jsp files that include the given jsp file using the 'link.strong' macro.<p>
1699     *
1700     * @param resource the current updated jsp file
1701     * @param controller the controller for the jsp integration
1702     * @param updatedFiles the already updated files
1703     *
1704     * @return <code>true</code> if the given JSP file should be updated due to dirty included files
1705     *
1706     * @throws ServletException might be thrown in the process of including the JSP
1707     * @throws IOException might be thrown in the process of including the JSP
1708     * @throws CmsLoaderException if the resource type can not be read
1709     */
1710    protected boolean updateStrongLinks(CmsResource resource, CmsFlexController controller, Set<String> updatedFiles)
1711    throws CmsLoaderException, IOException, ServletException {
1712
1713        int numberOfUpdates = updatedFiles.size();
1714        CmsObject cms = controller.getCmsObject();
1715        CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType(CmsRelationType.JSP_STRONG);
1716        Iterator<CmsRelation> it;
1717        try {
1718            it = cms.getRelationsForResource(resource, filter).iterator();
1719        } catch (CmsException e) {
1720            // should never happen
1721            if (LOG.isErrorEnabled()) {
1722                LOG.error(e.getLocalizedMessage(), e);
1723            }
1724            return false;
1725        }
1726        while (it.hasNext()) {
1727            CmsRelation relation = it.next();
1728            CmsResource target = null;
1729            try {
1730                target = relation.getTarget(cms, CmsResourceFilter.DEFAULT);
1731            } catch (CmsException e) {
1732                // should never happen
1733                if (LOG.isErrorEnabled()) {
1734                    LOG.error(e.getLocalizedMessage(), e);
1735                }
1736                continue;
1737            }
1738            // prevent recursive update when including the same file
1739            if (resource.equals(target)) {
1740                continue;
1741            }
1742            // update the target
1743            updateJsp(target, controller, updatedFiles);
1744        }
1745        // the current jsp file should be updated only if one of the included jsp has been updated
1746        return numberOfUpdates < updatedFiles.size();
1747    }
1748
1749    /**
1750     * Returns the read-write-lock for the given jsp vfs name.<p>
1751     *
1752     * @param jspVfsName the jsp vfs name
1753     *
1754     * @return the read-write-lock
1755     */
1756    private ReentrantReadWriteLock getFileLock(String jspVfsName) {
1757
1758        ReentrantReadWriteLock lock = m_fileLocks.get(jspVfsName);
1759        if (lock == null) {
1760            // acquire the purge lock before adding new file lock entries
1761            // in case of a JSP repository purge, adding new file lock entries is blocked
1762            // and all present file locks will be locked for purge
1763            // @see #triggerPurge()
1764            m_purgeLock.readLock().lock();
1765            synchronized (m_fileLocks) {
1766                if (!m_fileLocks.containsKey(jspVfsName)) {
1767                    m_fileLocks.put(jspVfsName, new ReentrantReadWriteLock(true));
1768                }
1769                lock = m_fileLocks.get(jspVfsName);
1770            }
1771            m_purgeLock.readLock().unlock();
1772        }
1773        return lock;
1774    }
1775
1776    /**
1777     * Returns the RFS path for a JSP resource.<p>
1778     *
1779     * This does not check whether there actually exists a file at the returned path.
1780     *
1781     * @param resource the JSP resource
1782     * @param online true if the path for the online project should be returned
1783     *
1784     * @return the RFS path for the JSP
1785     *
1786     * @throws CmsLoaderException if accessing the resource loader fails
1787     */
1788    private String getJspRfsPath(CmsResource resource, boolean online) throws CmsLoaderException {
1789
1790        String jspVfsName = resource.getRootPath();
1791        String extension;
1792        int loaderId = OpenCms.getResourceManager().getResourceType(resource.getTypeId()).getLoaderId();
1793        if ((loaderId == CmsJspLoader.RESOURCE_LOADER_ID) && (!jspVfsName.endsWith(JSP_EXTENSION))) {
1794            // this is a true JSP resource that does not end with ".jsp"
1795            extension = JSP_EXTENSION;
1796        } else {
1797            // not a JSP resource or already ends with ".jsp"
1798            extension = "";
1799        }
1800        String jspPath = CmsFileUtil.getRepositoryName(m_jspRepository, jspVfsName + extension, online);
1801        return jspPath;
1802    }
1803}