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