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