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