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