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