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