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.jsp;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsPropertyDefinition;
033import org.opencms.flex.CmsFlexController;
034import org.opencms.flex.CmsFlexResponse;
035import org.opencms.loader.CmsJspLoader;
036import org.opencms.loader.CmsLoaderException;
037import org.opencms.loader.I_CmsResourceLoader;
038import org.opencms.loader.I_CmsResourceStringDumpLoader;
039import org.opencms.main.CmsException;
040import org.opencms.main.OpenCms;
041import org.opencms.staticexport.CmsLinkManager;
042import org.opencms.util.CmsCollectionsGenericWrapper;
043import org.opencms.util.CmsRequestUtil;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.workplace.editors.directedit.CmsDirectEditParams;
046
047import java.io.IOException;
048import java.util.HashMap;
049import java.util.Locale;
050import java.util.Map;
051import java.util.Set;
052
053import javax.servlet.ServletException;
054import javax.servlet.ServletRequest;
055import javax.servlet.ServletResponse;
056import javax.servlet.http.HttpServletRequest;
057import javax.servlet.http.HttpServletResponse;
058import javax.servlet.jsp.JspException;
059import javax.servlet.jsp.PageContext;
060import javax.servlet.jsp.tagext.BodyTagSupport;
061
062import com.google.common.collect.Maps;
063
064/**
065 * Implementation of the <code>&lt;cms:include/&gt;</code> tag,
066 * used to include another OpenCms managed resource in a JSP.<p>
067 *
068 * @since 6.0.0
069 */
070public class CmsJspTagInclude extends BodyTagSupport implements I_CmsJspTagParamParent {
071
072    /** Serial version UID required for safe serialization. */
073    private static final long serialVersionUID = 705978510743164951L;
074
075    /** The value of the "attribute" attribute. */
076    private String m_attribute;
077
078    /** The value of the "cacheable" attribute. */
079    private boolean m_cacheable;
080
081    /** The value of the "editable" attribute. */
082    private boolean m_editable;
083
084    /** The value of the "element" attribute. */
085    private String m_element;
086
087    /** Map to save parameters to the include in. */
088    private Map<String, String[]> m_parameterMap;
089
090    /** The value of the "property" attribute. */
091    private String m_property;
092
093    /** The value of the "suffix" attribute. */
094    private String m_suffix;
095
096    /** The value of the "page" attribute. */
097    private String m_target;
098
099    /**
100     * Empty constructor, required for attribute value initialization.<p>
101     */
102    public CmsJspTagInclude() {
103
104        super();
105        m_cacheable = true;
106    }
107
108    /**
109     * Adds parameters to a parameter Map that can be used for a http request.<p>
110     *
111     * @param parameters the Map to add the parameters to
112     * @param name the name to add
113     * @param value the value to add
114     * @param overwrite if <code>true</code>, a parameter in the map will be overwritten by
115     *      a parameter with the same name, otherwise the request will have multiple parameters
116     *      with the same name (which is possible in http requests)
117     */
118    public static void addParameter(Map<String, String[]> parameters, String name, String value, boolean overwrite) {
119
120        // No null values allowed in parameters
121        if ((parameters == null) || (name == null) || (value == null)) {
122            return;
123        }
124
125        // Check if the parameter name (key) exists
126        if (parameters.containsKey(name) && (!overwrite)) {
127            // Yes: Check name values if value exists, if so do nothing, else add new value
128            String[] values = parameters.get(name);
129            String[] newValues = new String[values.length + 1];
130            System.arraycopy(values, 0, newValues, 0, values.length);
131            newValues[values.length] = value;
132            parameters.put(name, newValues);
133        } else {
134            // No: Add new parameter name / value pair
135            String[] values = new String[] {value};
136            parameters.put(name, values);
137        }
138    }
139
140    /**
141     * Includes the selected target.<p>
142     *
143     * @param context the current JSP page context
144     * @param target the target for the include, might be <code>null</code>
145     * @param element the element to select form the target might be <code>null</code>
146     * @param editable flag to indicate if the target is editable
147     * @param paramMap a map of parameters for the include, will be merged with the request
148     *      parameters, might be <code>null</code>
149     * @param attrMap a map of attributes for the include, will be merged with the request
150     *      attributes, might be <code>null</code>
151     * @param req the current request
152     * @param res the current response
153     *
154     * @throws JspException in case something goes wrong
155     */
156    public static void includeTagAction(
157        PageContext context,
158        String target,
159        String element,
160        boolean editable,
161        Map<String, String[]> paramMap,
162        Map<String, Object> attrMap,
163        ServletRequest req,
164        ServletResponse res)
165    throws JspException {
166
167        // no locale and no cachable parameter are used by default
168        includeTagAction(context, target, element, null, editable, true, paramMap, attrMap, req, res);
169    }
170
171    /**
172     * Includes the selected target.<p>
173     *
174     * @param context the current JSP page context
175     * @param target the target for the include, might be <code>null</code>
176     * @param element the element to select form the target, might be <code>null</code>
177     * @param locale the locale to use for the selected element, might be <code>null</code>
178     * @param editable flag to indicate if the target is editable
179     * @param cacheable flag to indicate if the target should be cacheable in the Flex cache
180     * @param paramMap a map of parameters for the include, will be merged with the request
181     *      parameters, might be <code>null</code>
182     * @param attrMap a map of attributes for the include, will be merged with the request
183     *      attributes, might be <code>null</code>
184     * @param req the current request
185     * @param res the current response
186     *
187     * @throws JspException in case something goes wrong
188     */
189    public static void includeTagAction(
190        PageContext context,
191        String target,
192        String element,
193        Locale locale,
194        boolean editable,
195        boolean cacheable,
196        Map<String, String[]> paramMap,
197        Map<String, Object> attrMap,
198        ServletRequest req,
199        ServletResponse res)
200    throws JspException {
201
202        // the Flex controller provides access to the internal OpenCms structures
203        CmsFlexController controller = CmsFlexController.getController(req);
204
205        if (target == null) {
206            // set target to default
207            target = controller.getCmsObject().getRequestContext().getUri();
208        }
209
210        // resolve possible relative URI
211        target = CmsLinkManager.getAbsoluteUri(target, controller.getCurrentRequest().getElementUri());
212
213        try {
214            // check if the target actually exists in the OpenCms VFS
215            controller.getCmsObject().readResource(target);
216        } catch (CmsException e) {
217            // store exception in controller and discontinue
218            controller.setThrowable(e, target);
219            throw new JspException(e);
220        }
221
222        // include direct edit "start" element (if enabled)
223        boolean directEditOpen = editable
224            && CmsJspTagEditable.startDirectEdit(context, new CmsDirectEditParams(target, element));
225
226        // save old parameters from request
227        Map<String, String[]> oldParameterMap = CmsCollectionsGenericWrapper.map(req.getParameterMap());
228        try {
229            // each include will have it's unique map of parameters
230            Map<String, String[]> parameterMap = (paramMap == null)
231            ? new HashMap<String, String[]>()
232            : new HashMap<String, String[]>(paramMap);
233            if (cacheable && (element != null)) {
234                // add template element selector for JSP templates (only required if cacheable)
235                addParameter(parameterMap, I_CmsResourceLoader.PARAMETER_ELEMENT, element, true);
236            }
237            // add parameters to set the correct element
238            controller.getCurrentRequest().addParameterMap(parameterMap);
239            // each include will have it's unique map of attributes
240            Map<String, Object> attributeMap = (attrMap == null)
241            ? new HashMap<String, Object>()
242            : new HashMap<String, Object>(attrMap);
243            // add attributes to set the correct element
244            controller.getCurrentRequest().addAttributeMap(attributeMap);
245            Set<String> dynamicParams = controller.getCurrentRequest().getDynamicParameters();
246            Map<String, String[]> extendedParameterMap = null;
247            if (!dynamicParams.isEmpty()) {
248                // We want to store the parameters from the request with keys in dynamicParams in the flex response's include list, but only if they're set
249                extendedParameterMap = Maps.newHashMap();
250                extendedParameterMap.putAll(parameterMap);
251                for (String dynamicParam : dynamicParams) {
252                    String[] val = req.getParameterMap().get(dynamicParam);
253                    if (val != null) {
254                        extendedParameterMap.put(dynamicParam, val);
255                    }
256                }
257            }
258            if (cacheable) {
259                // use include with cache
260                includeActionWithCache(
261                    controller,
262                    context,
263                    target,
264                    extendedParameterMap != null ? extendedParameterMap : parameterMap,
265                    attributeMap,
266                    req,
267                    res);
268            } else {
269                // no cache required
270                includeActionNoCache(controller, context, target, element, locale, req, res);
271            }
272        } finally {
273            // restore old parameter map (if required)
274            if (oldParameterMap != null) {
275                controller.getCurrentRequest().setParameterMap(oldParameterMap);
276            }
277        }
278
279        // include direct edit "end" element (if required)
280        if (directEditOpen) {
281            CmsJspTagEditable.endDirectEdit(context);
282        }
283    }
284
285    /**
286     * Includes the selected target without caching.<p>
287     *
288     * @param controller the current JSP controller
289     * @param context the current JSP page context
290     * @param target the target for the include
291     * @param element the element to select form the target
292     * @param locale the locale to select from the target
293     * @param req the current request
294     * @param res the current response
295     *
296     * @throws JspException in case something goes wrong
297     */
298    private static void includeActionNoCache(
299        CmsFlexController controller,
300        PageContext context,
301        String target,
302        String element,
303        Locale locale,
304        ServletRequest req,
305        ServletResponse res)
306    throws JspException {
307
308        try {
309            // include is not cachable
310            CmsFile file = controller.getCmsObject().readFile(target);
311            CmsObject cms = controller.getCmsObject();
312            if (locale == null) {
313                locale = cms.getRequestContext().getLocale();
314            }
315            // get the loader for the requested file
316            I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(file);
317            String content;
318            if (loader instanceof I_CmsResourceStringDumpLoader) {
319                // loader can provide content as a String
320                I_CmsResourceStringDumpLoader strLoader = (I_CmsResourceStringDumpLoader)loader;
321                content = strLoader.dumpAsString(cms, file, element, locale, req, res);
322            } else {
323                if (!(req instanceof HttpServletRequest) || !(res instanceof HttpServletResponse)) {
324                    // http type is required for loader (no refactoring to avoid changes to interface)
325                    CmsLoaderException e = new CmsLoaderException(
326                        Messages.get().container(Messages.ERR_BAD_REQUEST_RESPONSE_0));
327                    throw new JspException(e);
328                }
329                // get the bytes from the loader and convert them to a String
330                byte[] result = loader.dump(
331                    cms,
332                    file,
333                    element,
334                    locale,
335                    (HttpServletRequest)req,
336                    (HttpServletResponse)res);
337
338                String encoding;
339                if (loader instanceof CmsJspLoader) {
340                    // in case of JSPs use the response encoding
341                    encoding = res.getCharacterEncoding();
342                } else {
343                    // use the encoding from the property or the system default if not available
344                    encoding = cms.readPropertyObject(
345                        file,
346                        CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
347                        true).getValue(OpenCms.getSystemInfo().getDefaultEncoding());
348                }
349                // If the included target issued a redirect null will be returned from loader
350                if (result == null) {
351                    result = new byte[0];
352                }
353                content = new String(result, encoding);
354            }
355            // write the content String to the JSP output writer
356            context.getOut().print(content);
357
358        } catch (ServletException e) {
359            // store original Exception in controller in order to display it later
360            Throwable t = (e.getRootCause() != null) ? e.getRootCause() : e;
361            t = controller.setThrowable(t, target);
362            throw new JspException(t);
363        } catch (IOException e) {
364            // store original Exception in controller in order to display it later
365            Throwable t = controller.setThrowable(e, target);
366            throw new JspException(t);
367        } catch (CmsException e) {
368            // store original Exception in controller in order to display it later
369            Throwable t = controller.setThrowable(e, target);
370            throw new JspException(t);
371        }
372    }
373
374    /**
375     * Includes the selected target using the Flex cache.<p>
376     *
377     * @param controller the current JSP controller
378     * @param context the current JSP page context
379     * @param target the target for the include, might be <code>null</code>
380     * @param parameterMap a map of parameters for the include
381     * @param attributeMap a map of request attributes for the include
382     * @param req the current request
383     * @param res the current response
384     *
385     * @throws JspException in case something goes wrong
386     */
387    private static void includeActionWithCache(
388        CmsFlexController controller,
389        PageContext context,
390        String target,
391        Map<String, String[]> parameterMap,
392        Map<String, Object> attributeMap,
393        ServletRequest req,
394        ServletResponse res)
395    throws JspException {
396
397        try {
398
399            // add the target to the include list (the list will be initialized if it is currently empty)
400            controller.getCurrentResponse().addToIncludeList(target, parameterMap, attributeMap);
401            // now use the Flex dispatcher to include the target (this will also work for targets in the OpenCms VFS)
402            controller.getCurrentRequest().getRequestDispatcher(target).include(req, res);
403            // write out a FLEX_CACHE_DELIMITER char on the page, this is used as a parsing delimiter later
404            context.getOut().print(CmsFlexResponse.FLEX_CACHE_DELIMITER);
405        } catch (ServletException e) {
406            // store original Exception in controller in order to display it later
407            Throwable t = (e.getRootCause() != null) ? e.getRootCause() : e;
408            t = controller.setThrowable(t, target);
409            throw new JspException(t);
410        } catch (IOException e) {
411            // store original Exception in controller in order to display it later
412            Throwable t = controller.setThrowable(e, target);
413            throw new JspException(t);
414        }
415    }
416
417    /**
418     * This methods adds parameters to the current request.<p>
419     *
420     * Parameters added here will be treated like parameters from the
421     * HttpRequest on included pages.<p>
422     *
423     * Remember that the value for a parameter in a HttpRequest is a
424     * String array, not just a simple String. If a parameter added here does
425     * not already exist in the HttpRequest, it will be added. If a parameter
426     * exists, another value will be added to the array of values. If the
427     * value already exists for the parameter, nothing will be added, since a
428     * value can appear only once per parameter.<p>
429     *
430     * @param name the name to add
431     * @param value the value to add
432     * @see org.opencms.jsp.I_CmsJspTagParamParent#addParameter(String, String)
433     */
434    public void addParameter(String name, String value) {
435
436        // No null values allowed in parameters
437        if ((name == null) || (value == null)) {
438            return;
439        }
440
441        // Check if internal map exists, create new one if not
442        if (m_parameterMap == null) {
443            m_parameterMap = new HashMap<String, String[]>();
444        }
445
446        addParameter(m_parameterMap, name, value, false);
447    }
448
449    /**
450     * @return <code>EVAL_PAGE</code>
451     *
452     * @see javax.servlet.jsp.tagext.Tag#doEndTag()
453     *
454     * @throws JspException by interface default
455     */
456    @Override
457    public int doEndTag() throws JspException {
458
459        ServletRequest req = pageContext.getRequest();
460        ServletResponse res = pageContext.getResponse();
461
462        if (CmsFlexController.isCmsRequest(req)) {
463            // this will always be true if the page is called through OpenCms
464            CmsObject cms = CmsFlexController.getCmsObject(req);
465            String target = null;
466
467            // try to find out what to do
468            if (m_target != null) {
469                // option 1: target is set with "page" or "file" parameter
470                target = m_target + getSuffix();
471            } else if (m_property != null) {
472                // option 2: target is set with "property" parameter
473                try {
474                    String prop = cms.readPropertyObject(cms.getRequestContext().getUri(), m_property, true).getValue();
475                    if (prop != null) {
476                        target = prop + getSuffix();
477                    }
478                } catch (RuntimeException e) {
479                    // target must be null
480                    target = null;
481                } catch (Exception e) {
482                    // target will be null
483                    e = null;
484                }
485            } else if (m_attribute != null) {
486                // option 3: target is set in "attribute" parameter
487                try {
488                    String attr = (String)req.getAttribute(m_attribute);
489                    if (attr != null) {
490                        target = attr + getSuffix();
491                    }
492                } catch (RuntimeException e) {
493                    // target must be null
494                    target = null;
495                } catch (Exception e) {
496                    // target will be null
497                    e = null;
498                }
499            } else {
500                // option 4: target might be set in body
501                String body = null;
502                if (getBodyContent() != null) {
503                    body = getBodyContent().getString();
504                    if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(body)) {
505                        // target IS set in body
506                        target = body + getSuffix();
507                    }
508                    // else target is not set at all, default will be used
509                }
510            }
511
512            // now perform the include action
513            includeTagAction(
514                pageContext,
515                target,
516                m_element,
517                null,
518                m_editable,
519                m_cacheable,
520                m_parameterMap,
521                CmsRequestUtil.getAtrributeMap(req),
522                req,
523                res);
524
525            release();
526        }
527
528        return EVAL_PAGE;
529    }
530
531    /**
532     * Returns <code>{@link #EVAL_BODY_BUFFERED}</code>.<p>
533     *
534     * @return <code>{@link #EVAL_BODY_BUFFERED}</code>
535     *
536     * @see javax.servlet.jsp.tagext.Tag#doStartTag()
537     */
538    @Override
539    public int doStartTag() {
540
541        return EVAL_BODY_BUFFERED;
542    }
543
544    /**
545     * Returns the attribute.<p>
546     *
547     * @return the attribute
548     */
549    public String getAttribute() {
550
551        return m_attribute != null ? m_attribute : "";
552    }
553
554    /**
555     * Returns the cacheable flag.<p>
556     *
557     * @return the cacheable flag
558     */
559    public String getCacheable() {
560
561        return String.valueOf(m_cacheable);
562    }
563
564    /**
565     * Returns the editable flag.<p>
566     *
567     * @return the editable flag
568     */
569    public String getEditable() {
570
571        return String.valueOf(m_editable);
572    }
573
574    /**
575     * Returns the element.<p>
576     *
577     * @return the element
578     */
579    public String getElement() {
580
581        return m_element;
582    }
583
584    /**
585     * Returns the value of <code>{@link #getPage()}</code>.<p>
586     *
587     * @return the value of <code>{@link #getPage()}</code>
588     * @see #getPage()
589     */
590    public String getFile() {
591
592        return getPage();
593    }
594
595    /**
596     * Returns the include page target.<p>
597     *
598     * @return the include page target
599     */
600    public String getPage() {
601
602        return m_target != null ? m_target : "";
603    }
604
605    /**
606     * Returns the property.<p>
607     *
608     * @return the property
609     */
610    public String getProperty() {
611
612        return m_property != null ? m_property : "";
613    }
614
615    /**
616     * Returns the suffix.<p>
617     *
618     * @return the suffix
619     */
620    public String getSuffix() {
621
622        return m_suffix != null ? m_suffix : "";
623    }
624
625    /**
626     * @see javax.servlet.jsp.tagext.Tag#release()
627     */
628    @Override
629    public void release() {
630
631        super.release();
632        m_target = null;
633        m_suffix = null;
634        m_property = null;
635        m_element = null;
636        m_parameterMap = null;
637        m_editable = false;
638        m_cacheable = true;
639    }
640
641    /**
642     * Sets the attribute.<p>
643     *
644     * @param attribute the attribute to set
645     */
646    public void setAttribute(String attribute) {
647
648        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(attribute)) {
649            m_attribute = attribute;
650        }
651    }
652
653    /**
654     * Sets the cacheable flag.<p>
655     *
656     * Cachable is <code>true</code> by default.<p>
657     *
658     * @param cacheable the flag to set
659     */
660    public void setCacheable(String cacheable) {
661
662        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(cacheable)) {
663            m_cacheable = Boolean.valueOf(cacheable).booleanValue();
664        }
665    }
666
667    /**
668     * Sets the editable flag.<p>
669     *
670     * Editable is <code>false</code> by default.<p>
671     *
672     * @param editable the flag to set
673     */
674    public void setEditable(String editable) {
675
676        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(editable)) {
677            m_editable = Boolean.valueOf(editable).booleanValue();
678        }
679    }
680
681    /**
682     * Sets the element.<p>
683     *
684     * @param element the element to set
685     */
686    public void setElement(String element) {
687
688        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(element)) {
689            m_element = element;
690        }
691    }
692
693    /**
694     * Sets the file, same as using <code>setPage()</code>.<p>
695     *
696     * @param file the file to set
697     * @see #setPage(String)
698     */
699    public void setFile(String file) {
700
701        setPage(file);
702    }
703
704    /**
705     * Sets the include page target.<p>
706     *
707     * @param target the target to set
708     */
709    public void setPage(String target) {
710
711        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(target)) {
712            m_target = target;
713        }
714    }
715
716    /**
717     * Sets the property.<p>
718     *
719     * @param property the property to set
720     */
721    public void setProperty(String property) {
722
723        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(property)) {
724            m_property = property;
725        }
726    }
727
728    /**
729     * Sets the suffix.<p>
730     *
731     * @param suffix the suffix to set
732     */
733    public void setSuffix(String suffix) {
734
735        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(suffix)) {
736            m_suffix = suffix.toLowerCase();
737        }
738    }
739}