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                    if (controller.isTop() && (controller.getRedirectInfo() != null)) {
1042                        if (!controller.getCmsObject().getRequestContext().getCurrentProject().isOnlineProject()) {
1043                            RedirectInfo info = controller.getRedirectInfo();
1044                            info.executeRedirect(res);
1045                            return null;
1046                        } else {
1047                            if (LOG.isWarnEnabled()) {
1048                                Exception e = new Exception();
1049                                LOG.warn(
1050                                    "Found redirect info in Flex controller in Online project, but response was not suspended (target = "
1051                                        + controller.getRedirectInfo().getTarget()
1052                                        + ")",
1053                                    e);
1054                            }
1055                        }
1056                    }
1057
1058                    // check if the content was modified since the last request
1059                    if (controller.isTop()
1060                        && !isWorkplaceUser
1061                        && CmsFlexController.isNotModifiedSince(f_req, controller.getDateLastModified())) {
1062                        if (f_req.getParameterMap().size() == 0) {
1063                            // only use "expires" header on pages that have no parameters,
1064                            // otherwise some browsers (e.g. IE 6) will not even try to request
1065                            // updated versions of the page
1066                            CmsFlexController.setDateExpiresHeader(
1067                                res,
1068                                controller.getDateExpires(),
1069                                m_clientCacheMaxAge);
1070                        }
1071                        res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
1072                        return null;
1073                    }
1074
1075                    // get the result byte array
1076                    result = f_res.getWriterBytes();
1077                    HttpServletRequest req = controller.getTopRequest();
1078                    if (req.getHeader(CmsRequestUtil.HEADER_OPENCMS_EXPORT) != null) {
1079                        // this is a non "on-demand" static export request, don't write to the response stream
1080                        req.setAttribute(
1081                            CmsRequestUtil.HEADER_OPENCMS_EXPORT,
1082                            Long.valueOf(controller.getDateLastModified()));
1083                    } else if (controller.isTop()) {
1084                        // process headers and write output if this is the "top" request/response
1085                        res.setContentLength(result.length);
1086                        // check for preset error code
1087                        Integer errorCode = (Integer)req.getAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE);
1088                        if (errorCode == null) {
1089                            // set last modified / no cache headers only if this is not an error page
1090                            if (isWorkplaceUser) {
1091                                res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis());
1092                                CmsRequestUtil.setNoCacheHeaders(res);
1093                            } else {
1094                                // set date last modified header
1095                                CmsFlexController.setDateLastModifiedHeader(res, controller.getDateLastModified());
1096                                if ((f_req.getParameterMap().size() == 0) && (controller.getDateLastModified() > -1)) {
1097                                    // only use "expires" header on pages that have no parameters
1098                                    // and that are cachable (i.e. 'date last modified' is set)
1099                                    // otherwise some browsers (e.g. IE 6) will not even try to request
1100                                    // updated versions of the page
1101                                    CmsFlexController.setDateExpiresHeader(
1102                                        res,
1103                                        controller.getDateExpires(),
1104                                        m_clientCacheMaxAge);
1105                                }
1106                            }
1107                            // set response status to "200 - OK" (required for static export "on-demand")
1108                            res.setStatus(HttpServletResponse.SC_OK);
1109                        } else {
1110                            // set previously saved error code
1111                            res.setStatus(errorCode.intValue());
1112                        }
1113                        // process the headers
1114                        CmsFlexResponse.processHeaders(f_res.getHeaders(), res);
1115                        res.getOutputStream().write(result);
1116                        res.getOutputStream().flush();
1117                    }
1118                }
1119            } catch (IllegalStateException e) {
1120                // uncritical, might happen if JSP error page was used
1121                LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e);
1122            } catch (SocketException e) {
1123                // uncritical, might happen if client (browser) does not wait until end of page delivery
1124                LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e);
1125            }
1126        } else if (controller.isTop() && (controller.getRedirectInfo() != null)) {
1127            RedirectInfo info = controller.getRedirectInfo();
1128            info.executeRedirect(res);
1129        }
1130        return result;
1131    }
1132
1133    /**
1134     * Purges the JSP repository.<p<
1135     *
1136     * @param afterPurgeAction the action to execute after purging
1137     */
1138    protected void doPurge(Runnable afterPurgeAction) {
1139
1140        if (LOG.isInfoEnabled()) {
1141            LOG.info(
1142                org.opencms.flex.Messages.get().getBundle().key(
1143                    org.opencms.flex.Messages.LOG_FLEXCACHE_WILL_PURGE_JSP_REPOSITORY_0));
1144        }
1145
1146        File d;
1147        d = new File(getJspRepository() + CmsFlexCache.REPOSITORY_ONLINE + File.separator);
1148        CmsFileUtil.purgeDirectory(d);
1149
1150        d = new File(getJspRepository() + CmsFlexCache.REPOSITORY_OFFLINE + File.separator);
1151        CmsFileUtil.purgeDirectory(d);
1152        if (afterPurgeAction != null) {
1153            afterPurgeAction.run();
1154        }
1155
1156        if (LOG.isInfoEnabled()) {
1157            LOG.info(
1158                org.opencms.flex.Messages.get().getBundle().key(
1159                    org.opencms.flex.Messages.LOG_FLEXCACHE_PURGED_JSP_REPOSITORY_0));
1160        }
1161
1162    }
1163
1164    /**
1165     * Generates the taglib directives for a collection of taglib identifiers.<p>
1166     *
1167     * @param taglibs the taglib identifiers
1168     *
1169     * @return a string containing taglib directives
1170     */
1171    protected String generateTaglibInclusions(Collection<String> taglibs) {
1172
1173        StringBuffer buffer = new StringBuffer();
1174        for (String taglib : taglibs) {
1175            String uri = m_taglibs.get(taglib);
1176            if (uri != null) {
1177                buffer.append("<%@ taglib prefix=\"" + taglib + "\" uri=\"" + uri + "\" %>");
1178            }
1179        }
1180        return buffer.toString();
1181    }
1182
1183    /**
1184     * Delivers a Flex controller, either by creating a new one, or by re-using an existing one.<p>
1185     *
1186     * @param cms the initial CmsObject to wrap in the controller
1187     * @param resource the resource requested
1188     * @param req the current request
1189     * @param res the current response
1190     * @param streaming indicates if the response is streaming
1191     * @param top indicates if the response is the top response
1192     *
1193     * @return a Flex controller
1194     */
1195    protected CmsFlexController getController(
1196        CmsObject cms,
1197        CmsResource resource,
1198        HttpServletRequest req,
1199        HttpServletResponse res,
1200        boolean streaming,
1201        boolean top) {
1202
1203        CmsFlexController controller = null;
1204        if (top) {
1205            // only check for existing controller if this is the "top" request/response
1206            controller = CmsFlexController.getController(req);
1207        }
1208        if (controller == null) {
1209            // create new request / response wrappers
1210            if (!cms.getRequestContext().getCurrentProject().isOnlineProject()
1211                && (CmsHistoryResourceHandler.isHistoryRequest(req) || CmsJspTagEnableAde.isDirectEditDisabled(req))) {
1212                cms.getRequestContext().setAttribute(CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT, Boolean.TRUE);
1213            }
1214            controller = new CmsFlexController(cms, resource, m_cache, req, res, streaming, top);
1215            CmsFlexController.setController(req, controller);
1216            CmsFlexRequest f_req = new CmsFlexRequest(req, controller);
1217            CmsFlexResponse f_res = new CmsFlexResponse(res, controller, streaming, true);
1218            controller.push(f_req, f_res);
1219        } else if (controller.isForwardMode()) {
1220            // reset CmsObject (because of URI) if in forward mode
1221            controller = new CmsFlexController(cms, controller);
1222            CmsFlexController.setController(req, controller);
1223        }
1224        return controller;
1225    }
1226
1227    /**
1228     * Initializes the caches.<p>
1229     *
1230     * @param cacheSize the cache size
1231     */
1232    protected void initCaches(int cacheSize) {
1233
1234        m_offlineJsps = CmsMemoryMonitor.createLRUCacheMap(cacheSize);
1235        m_onlineJsps = CmsMemoryMonitor.createLRUCacheMap(cacheSize);
1236    }
1237
1238    /**
1239     * Parses the JSP and modifies OpenCms critical directive information.<p>
1240     *
1241     * @param byteContent the original JSP content
1242     * @param encoding the encoding to use for the JSP
1243     * @param controller the controller for the JSP integration
1244     * @param updatedFiles a Set containing all JSP pages that have been already updated
1245     * @param isHardInclude indicated if this page is actually a "hard" include with <code>&lt;%@ include file="..." &gt;</code>
1246     *
1247     * @return the modified JSP content
1248     */
1249    protected byte[] parseJsp(
1250        byte[] byteContent,
1251        String encoding,
1252        CmsFlexController controller,
1253        Set<String> updatedFiles,
1254        boolean isHardInclude) {
1255
1256        String content;
1257        // make sure encoding is set correctly
1258        try {
1259            content = new String(byteContent, encoding);
1260        } catch (UnsupportedEncodingException e) {
1261            // encoding property is not set correctly
1262            LOG.error(
1263                Messages.get().getBundle().key(
1264                    Messages.LOG_UNSUPPORTED_ENC_1,
1265                    controller.getCurrentRequest().getElementUri()),
1266                e);
1267            try {
1268                encoding = OpenCms.getSystemInfo().getDefaultEncoding();
1269                content = new String(byteContent, encoding);
1270            } catch (UnsupportedEncodingException e2) {
1271                // should not happen since default encoding is always a valid encoding (checked during system startup)
1272                content = new String(byteContent);
1273            }
1274        }
1275
1276        // parse for special %(link:...) macros
1277        content = parseJspLinkMacros(content, controller);
1278        // parse for special <%@cms file="..." %> tag
1279        content = parseJspCmsTag(content, controller, updatedFiles);
1280        // parse for included files in tags
1281        content = parseJspIncludes(content, controller, updatedFiles);
1282        // parse for <%@page pageEncoding="..." %> tag
1283        content = parseJspEncoding(content, encoding, isHardInclude);
1284        // Processes magic taglib attributes in page directives
1285        content = processTaglibAttributes(content);
1286        // convert the result to bytes and return it
1287        try {
1288            return content.getBytes(encoding);
1289        } catch (UnsupportedEncodingException e) {
1290            // should not happen since encoding was already checked
1291            return content.getBytes();
1292        }
1293    }
1294
1295    /**
1296     * Parses the JSP content for the special <code>&lt;%cms file="..." %&gt;</code> tag.<p>
1297     *
1298     * @param content the JSP content to parse
1299     * @param controller the current JSP controller
1300     * @param updatedFiles a set of already updated jsp files
1301     *
1302     * @return the parsed JSP content
1303     */
1304    protected String parseJspCmsTag(String content, CmsFlexController controller, Set<String> updatedFiles) {
1305
1306        // check if a JSP directive occurs in the file
1307        int i1 = content.indexOf(DIRECTIVE_START);
1308        if (i1 < 0) {
1309            // no directive occurs
1310            return content;
1311        }
1312
1313        StringBuffer buf = new StringBuffer(content.length());
1314        int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length(), elen = DIRECTIVE_END.length();
1315
1316        while (i1 >= 0) {
1317            // parse the file and replace JSP filename references
1318            i2 = content.indexOf(DIRECTIVE_END, i1 + slen);
1319            if (i2 < 0) {
1320                // wrong syntax (missing end directive) - let the JSP compiler produce the error message
1321                return content;
1322            } else if (i2 > i1) {
1323                String directive = content.substring(i1 + slen, i2);
1324                if (LOG.isDebugEnabled()) {
1325                    LOG.debug(
1326                        Messages.get().getBundle().key(
1327                            Messages.LOG_DIRECTIVE_DETECTED_3,
1328                            DIRECTIVE_START,
1329                            directive,
1330                            DIRECTIVE_END));
1331                }
1332
1333                int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
1334                while (directive.charAt(t1) == ' ') {
1335                    t1++;
1336                }
1337                String argument = null;
1338                if (directive.startsWith("cms", t1)) {
1339                    if (LOG.isDebugEnabled()) {
1340                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "cms"));
1341                    }
1342                    t2 = directive.indexOf("file", t1 + 3);
1343                    t5 = 4;
1344                }
1345
1346                if (t2 > 0) {
1347                    String sub = directive.substring(t2 + t5);
1348                    char c1 = sub.charAt(t3);
1349                    while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) {
1350                        c1 = sub.charAt(++t3);
1351                    }
1352                    t4 = t3;
1353                    while (c1 != '"') {
1354                        c1 = sub.charAt(++t4);
1355                    }
1356                    if (t4 > t3) {
1357                        argument = sub.substring(t3, t4);
1358                    }
1359                    if (LOG.isDebugEnabled()) {
1360                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument));
1361                    }
1362                }
1363
1364                if (argument != null) {
1365                    //  try to update the referenced file
1366                    String jspname = updateJsp(argument, controller, updatedFiles);
1367                    if (jspname != null) {
1368                        directive = jspname;
1369                        if (LOG.isDebugEnabled()) {
1370                            LOG.debug(
1371                                Messages.get().getBundle().key(
1372                                    Messages.LOG_DIRECTIVE_CHANGED_3,
1373                                    DIRECTIVE_START,
1374                                    directive,
1375                                    DIRECTIVE_END));
1376                        }
1377                    }
1378                    // cms directive was found
1379                    buf.append(content.substring(p0, i1));
1380                    buf.append(directive);
1381                    p0 = i2 + elen;
1382                    i1 = content.indexOf(DIRECTIVE_START, p0);
1383                } else {
1384                    // cms directive was not found
1385                    buf.append(content.substring(p0, i1 + slen));
1386                    buf.append(directive);
1387                    p0 = i2;
1388                    i1 = content.indexOf(DIRECTIVE_START, p0);
1389                }
1390            }
1391        }
1392        if (i2 > 0) {
1393            // the content of the JSP was changed
1394            buf.append(content.substring(p0, content.length()));
1395            content = buf.toString();
1396        }
1397        return content;
1398    }
1399
1400    /**
1401     * Parses the JSP content for the  <code>&lt;%page pageEncoding="..." %&gt;</code> tag
1402     * and ensures that the JSP page encoding is set according to the OpenCms
1403     * "content-encoding" property value of the JSP.<p>
1404     *
1405     * @param content the JSP content to parse
1406     * @param encoding the encoding to use for the JSP
1407     * @param isHardInclude indicated if this page is actually a "hard" include with <code>&lt;%@ include file="..." &gt;</code>
1408     *
1409     * @return the parsed JSP content
1410     */
1411    protected String parseJspEncoding(String content, String encoding, boolean isHardInclude) {
1412
1413        // check if a JSP directive occurs in the file
1414        int i1 = content.indexOf(DIRECTIVE_START);
1415        if (i1 < 0) {
1416            // no directive occurs
1417            if (isHardInclude) {
1418                return content;
1419            }
1420        }
1421
1422        StringBuffer buf = new StringBuffer(content.length() + 64);
1423        int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length();
1424        boolean found = false;
1425
1426        if (i1 < 0) {
1427            // no directive found at all, append content to buffer
1428            buf.append(content);
1429        }
1430
1431        while (i1 >= 0) {
1432            // parse the file and set/replace page encoding
1433            i2 = content.indexOf(DIRECTIVE_END, i1 + slen);
1434            if (i2 < 0) {
1435                // wrong syntax (missing end directive) - let the JSP compiler produce the error message
1436                return content;
1437            } else if (i2 > i1) {
1438                String directive = content.substring(i1 + slen, i2);
1439                if (LOG.isDebugEnabled()) {
1440                    LOG.debug(
1441                        Messages.get().getBundle().key(
1442                            Messages.LOG_DIRECTIVE_DETECTED_3,
1443                            DIRECTIVE_START,
1444                            directive,
1445                            DIRECTIVE_END));
1446                }
1447
1448                int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
1449                while (directive.charAt(t1) == ' ') {
1450                    t1++;
1451                }
1452                String argument = null;
1453                if (directive.startsWith("page", t1)) {
1454                    if (LOG.isDebugEnabled()) {
1455                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "page"));
1456                    }
1457                    t2 = directive.indexOf("pageEncoding", t1 + 4);
1458                    t5 = 12;
1459                    if (t2 > 0) {
1460                        found = true;
1461                    }
1462                }
1463
1464                if (t2 > 0) {
1465                    String sub = directive.substring(t2 + t5);
1466                    char c1 = sub.charAt(t3);
1467                    while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) {
1468                        c1 = sub.charAt(++t3);
1469                    }
1470                    t4 = t3;
1471                    while (c1 != '"') {
1472                        c1 = sub.charAt(++t4);
1473                    }
1474                    if (t4 > t3) {
1475                        argument = sub.substring(t3, t4);
1476                    }
1477                    if (LOG.isDebugEnabled()) {
1478                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument));
1479                    }
1480                }
1481
1482                if (argument != null) {
1483                    // a pageEncoding setting was found, changes have to be made
1484                    String pre = directive.substring(0, t2 + t3 + t5);
1485                    String suf = directive.substring(t2 + t3 + t5 + argument.length());
1486                    // change the encoding
1487                    directive = pre + encoding + suf;
1488                    if (LOG.isDebugEnabled()) {
1489                        LOG.debug(
1490                            Messages.get().getBundle().key(
1491                                Messages.LOG_DIRECTIVE_CHANGED_3,
1492                                DIRECTIVE_START,
1493                                directive,
1494                                DIRECTIVE_END));
1495                    }
1496                }
1497
1498                buf.append(content.substring(p0, i1 + slen));
1499                buf.append(directive);
1500                p0 = i2;
1501                i1 = content.indexOf(DIRECTIVE_START, p0);
1502            }
1503        }
1504        if (i2 > 0) {
1505            // the content of the JSP was changed
1506            buf.append(content.substring(p0, content.length()));
1507        }
1508        if (found) {
1509            content = buf.toString();
1510        } else if (!isHardInclude) {
1511            // encoding setting was not found
1512            // if this is not a "hard" include then add the encoding to the top of the page
1513            // checking for the hard include is important to prevent errors with
1514            // multiple page encoding settings if a template is composed from several hard included elements
1515            // this is an issue in Tomcat 4.x but not 5.x
1516            StringBuffer buf2 = new StringBuffer(buf.length() + 32);
1517            buf2.append("<%@ page pageEncoding=\"");
1518            buf2.append(encoding);
1519            buf2.append("\" %>");
1520            buf2.append(buf);
1521            content = buf2.toString();
1522        }
1523        return content;
1524    }
1525
1526    /**
1527     * Parses the JSP content for includes and replaces all OpenCms VFS
1528     * path information with information for the real FS.<p>
1529     *
1530     * @param content the JSP content to parse
1531     * @param controller the current JSP controller
1532     * @param updatedFiles a set of already updated files
1533     *
1534     * @return the parsed JSP content
1535     */
1536    protected String parseJspIncludes(String content, CmsFlexController controller, Set<String> updatedFiles) {
1537
1538        // check if a JSP directive occurs in the file
1539        int i1 = content.indexOf(DIRECTIVE_START);
1540        if (i1 < 0) {
1541            // no directive occurs
1542            return content;
1543        }
1544
1545        StringBuffer buf = new StringBuffer(content.length());
1546        int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length();
1547
1548        while (i1 >= 0) {
1549            // parse the file and replace JSP filename references
1550            i2 = content.indexOf(DIRECTIVE_END, i1 + slen);
1551            if (i2 < 0) {
1552                // wrong syntax (missing end directive) - let the JSP compiler produce the error message
1553                return content;
1554            } else if (i2 > i1) {
1555                String directive = content.substring(i1 + slen, i2);
1556                if (LOG.isDebugEnabled()) {
1557                    LOG.debug(
1558                        Messages.get().getBundle().key(
1559                            Messages.LOG_DIRECTIVE_DETECTED_3,
1560                            DIRECTIVE_START,
1561                            directive,
1562                            DIRECTIVE_END));
1563                }
1564
1565                int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
1566                while (directive.charAt(t1) == ' ') {
1567                    t1++;
1568                }
1569                String argument = null;
1570                if (directive.startsWith("include", t1)) {
1571                    if (LOG.isDebugEnabled()) {
1572                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "include"));
1573                    }
1574                    t2 = directive.indexOf("file", t1 + 7);
1575                    t5 = 6;
1576                } else if (directive.startsWith("page", t1)) {
1577                    if (LOG.isDebugEnabled()) {
1578                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "page"));
1579                    }
1580                    t2 = directive.indexOf("errorPage", t1 + 4);
1581                    t5 = 11;
1582                }
1583
1584                if (t2 > 0) {
1585                    String sub = directive.substring(t2 + t5);
1586                    char c1 = sub.charAt(t3);
1587                    while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) {
1588                        c1 = sub.charAt(++t3);
1589                    }
1590                    t4 = t3;
1591                    while (c1 != '"') {
1592                        c1 = sub.charAt(++t4);
1593                    }
1594                    if (t4 > t3) {
1595                        argument = sub.substring(t3, t4);
1596                    }
1597                    if (LOG.isDebugEnabled()) {
1598                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument));
1599                    }
1600                }
1601
1602                if (argument != null) {
1603                    // a file was found, changes have to be made
1604                    String pre = directive.substring(0, t2 + t3 + t5);
1605                    String suf = directive.substring(t2 + t3 + t5 + argument.length());
1606                    // now try to update the referenced file
1607                    String jspname = updateJsp(argument, controller, updatedFiles);
1608                    if (jspname != null) {
1609                        // only change something in case no error had occurred
1610                        directive = pre + jspname + suf;
1611                        if (LOG.isDebugEnabled()) {
1612                            LOG.debug(
1613                                Messages.get().getBundle().key(
1614                                    Messages.LOG_DIRECTIVE_CHANGED_3,
1615                                    DIRECTIVE_START,
1616                                    directive,
1617                                    DIRECTIVE_END));
1618                        }
1619                    }
1620                }
1621
1622                buf.append(content.substring(p0, i1 + slen));
1623                buf.append(directive);
1624                p0 = i2;
1625                i1 = content.indexOf(DIRECTIVE_START, p0);
1626            }
1627        }
1628        if (i2 > 0) {
1629            // the content of the JSP was changed
1630            buf.append(content.substring(p0, content.length()));
1631            content = buf.toString();
1632        }
1633        return content;
1634    }
1635
1636    /**
1637     * Parses all jsp link macros, and replace them by the right target path.<p>
1638     *
1639     * @param content the content to parse
1640     * @param controller the request controller
1641     *
1642     * @return the parsed content
1643     */
1644    protected String parseJspLinkMacros(String content, CmsFlexController controller) {
1645
1646        CmsJspLinkMacroResolver macroResolver = new CmsJspLinkMacroResolver(controller.getCmsObject(), null, true);
1647        return macroResolver.resolveMacros(content);
1648    }
1649
1650    /**
1651     * Returns the jsp resource identified by the given name, using the controllers cms context.<p>
1652     *
1653     * @param controller the flex controller
1654     * @param jspName the name of the jsp
1655     *
1656     * @return an OpenCms resource
1657     *
1658     * @throws CmsException if something goes wrong
1659     */
1660    protected CmsResource readJspResource(CmsFlexController controller, String jspName) throws CmsException {
1661
1662        // create an OpenCms user context that operates in the root site
1663        CmsObject cms = OpenCms.initCmsObject(controller.getCmsObject());
1664        // we only need to change the site, but not the project,
1665        // since the request has already the right project set
1666        cms.getRequestContext().setSiteRoot("");
1667        // try to read the resource
1668        return cms.readResource(jspName);
1669    }
1670
1671    /**
1672     * Delivers the plain uninterpreted resource with escaped XML.<p>
1673     *
1674     * This is intended for viewing historical versions.<p>
1675     *
1676     * @param cms the initialized CmsObject which provides user permissions
1677     * @param file the requested OpenCms VFS resource
1678     * @param req the servlet request
1679     * @param res the servlet response
1680     *
1681     * @throws IOException might be thrown by the servlet environment
1682     * @throws CmsException in case of errors accessing OpenCms functions
1683     */
1684    protected void showSource(CmsObject cms, CmsResource file, HttpServletRequest req, HttpServletResponse res)
1685    throws CmsException, IOException {
1686
1687        CmsResource historyResource = (CmsResource)CmsHistoryResourceHandler.getHistoryResource(req);
1688        if (historyResource == null) {
1689            historyResource = file;
1690        }
1691        CmsFile historyFile = cms.readFile(historyResource);
1692        String content = new String(historyFile.getContents());
1693        // change the content-type header so that browsers show plain text
1694        res.setContentLength(content.length());
1695        res.setContentType("text/plain");
1696
1697        Writer out = res.getWriter();
1698        out.write(content);
1699        out.close();
1700    }
1701
1702    /**
1703     * Updates a JSP page in the "real" file system in case the VFS resource has changed based on the resource name.<p>
1704     *
1705     * Generates a resource based on the provided name and calls {@link #updateJsp(CmsResource, CmsFlexController, Set)}.<p>
1706     *
1707     * @param vfsName the name of the JSP file resource in the VFS
1708     * @param controller the controller for the JSP integration
1709     * @param updatedFiles a Set containing all JSP pages that have been already updated
1710     *
1711     * @return the file name of the updated JSP in the "real" FS
1712     */
1713    protected String updateJsp(String vfsName, CmsFlexController controller, Set<String> updatedFiles) {
1714
1715        String jspVfsName = CmsLinkManager.getAbsoluteUri(vfsName, controller.getCurrentRequest().getElementRootPath());
1716        if (LOG.isDebugEnabled()) {
1717            LOG.debug(Messages.get().getBundle().key(Messages.LOG_UPDATE_JSP_1, jspVfsName));
1718        }
1719        String jspRfsName;
1720        try {
1721            CmsResource includeResource;
1722            try {
1723                // first try a root path
1724                includeResource = readJspResource(controller, jspVfsName);
1725            } catch (CmsVfsResourceNotFoundException e) {
1726                // if fails, try a site relative path
1727                includeResource = readJspResource(
1728                    controller,
1729                    controller.getCmsObject().getRequestContext().addSiteRoot(jspVfsName));
1730            }
1731            // make sure the jsp referenced file is generated
1732            jspRfsName = updateJsp(includeResource, controller, updatedFiles);
1733            if (LOG.isDebugEnabled()) {
1734                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NAME_REAL_FS_1, jspRfsName));
1735            }
1736        } catch (Exception e) {
1737            jspRfsName = null;
1738            if (LOG.isDebugEnabled()) {
1739                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_UPDATE_1, jspVfsName), e);
1740            }
1741        }
1742        return jspRfsName;
1743    }
1744
1745    /**
1746     * Updates all jsp files that include the given jsp file using the 'link.strong' macro.<p>
1747     *
1748     * @param resource the current updated jsp file
1749     * @param controller the controller for the jsp integration
1750     * @param updatedFiles the already updated files
1751     *
1752     * @return <code>true</code> if the given JSP file should be updated due to dirty included files
1753     *
1754     * @throws ServletException might be thrown in the process of including the JSP
1755     * @throws IOException might be thrown in the process of including the JSP
1756     * @throws CmsLoaderException if the resource type can not be read
1757     */
1758    protected boolean updateStrongLinks(CmsResource resource, CmsFlexController controller, Set<String> updatedFiles)
1759    throws CmsLoaderException, IOException, ServletException {
1760
1761        int numberOfUpdates = updatedFiles.size();
1762        CmsObject cms = controller.getCmsObject();
1763        CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType(CmsRelationType.JSP_STRONG);
1764        Iterator<CmsRelation> it;
1765        try {
1766            it = cms.getRelationsForResource(resource, filter).iterator();
1767        } catch (CmsException e) {
1768            // should never happen
1769            if (LOG.isErrorEnabled()) {
1770                LOG.error(e.getLocalizedMessage(), e);
1771            }
1772            return false;
1773        }
1774        while (it.hasNext()) {
1775            CmsRelation relation = it.next();
1776            CmsResource target = null;
1777            try {
1778                target = relation.getTarget(cms, CmsResourceFilter.DEFAULT);
1779            } catch (CmsException e) {
1780                // should never happen
1781                if (LOG.isErrorEnabled()) {
1782                    LOG.error(e.getLocalizedMessage(), e);
1783                }
1784                continue;
1785            }
1786            // prevent recursive update when including the same file
1787            if (resource.equals(target)) {
1788                continue;
1789            }
1790            // update the target
1791            updateJsp(target, controller, updatedFiles);
1792        }
1793        // the current jsp file should be updated only if one of the included jsp has been updated
1794        return numberOfUpdates < updatedFiles.size();
1795    }
1796
1797    /**
1798     * Returns the read-write-lock for the given jsp vfs name.<p>
1799     *
1800     * @param jspVfsName the jsp vfs name
1801     *
1802     * @return the read-write-lock
1803     */
1804    private ReentrantReadWriteLock getFileLock(String jspVfsName) {
1805
1806        ReentrantReadWriteLock lock = m_fileLocks.get(jspVfsName);
1807        if (lock == null) {
1808            // acquire the purge lock before adding new file lock entries
1809            // in case of a JSP repository purge, adding new file lock entries is blocked
1810            // and all present file locks will be locked for purge
1811            // @see #triggerPurge()
1812            m_purgeLock.readLock().lock();
1813            synchronized (m_fileLocks) {
1814                if (!m_fileLocks.containsKey(jspVfsName)) {
1815                    m_fileLocks.put(jspVfsName, new ReentrantReadWriteLock(true));
1816                }
1817                lock = m_fileLocks.get(jspVfsName);
1818            }
1819            m_purgeLock.readLock().unlock();
1820        }
1821        return lock;
1822    }
1823
1824    /**
1825     * Returns the RFS path for a JSP resource.<p>
1826     *
1827     * This does not check whether there actually exists a file at the returned path.
1828     *
1829     * @param resource the JSP resource
1830     * @param online true if the path for the online project should be returned
1831     *
1832     * @return the RFS path for the JSP
1833     *
1834     * @throws CmsLoaderException if accessing the resource loader fails
1835     */
1836    private String getJspRfsPath(CmsResource resource, boolean online) throws CmsLoaderException {
1837
1838        String jspVfsName = resource.getRootPath();
1839        String extension;
1840        int loaderId = OpenCms.getResourceManager().getResourceType(resource.getTypeId()).getLoaderId();
1841        if ((loaderId == CmsJspLoader.RESOURCE_LOADER_ID) && (!jspVfsName.endsWith(JSP_EXTENSION))) {
1842            // this is a true JSP resource that does not end with ".jsp"
1843            extension = JSP_EXTENSION;
1844        } else {
1845            // not a JSP resource or already ends with ".jsp"
1846            extension = "";
1847        }
1848        String jspPath = CmsFileUtil.getRepositoryName(m_jspRepository, jspVfsName + extension, online);
1849        return jspPath;
1850    }
1851}