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                    if (res instanceof CmsFlexResponse) {
342                        encoding = ((CmsFlexResponse)res).getEncoding();
343                    } else {
344                        encoding = res.getCharacterEncoding();
345                    }
346                } else {
347                    // use the encoding from the property or the system default if not available
348                    encoding = cms.readPropertyObject(
349                        file,
350                        CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
351                        true).getValue(OpenCms.getSystemInfo().getDefaultEncoding());
352                }
353                // If the included target issued a redirect null will be returned from loader
354                if (result == null) {
355                    result = new byte[0];
356                }
357                content = new String(result, encoding);
358            }
359            // write the content String to the JSP output writer
360            context.getOut().print(content);
361
362        } catch (ServletException e) {
363            // store original Exception in controller in order to display it later
364            Throwable t = (e.getRootCause() != null) ? e.getRootCause() : e;
365            t = controller.setThrowable(t, target);
366            throw new JspException(t);
367        } catch (IOException 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        } catch (CmsException e) {
372            // store original Exception in controller in order to display it later
373            Throwable t = controller.setThrowable(e, target);
374            throw new JspException(t);
375        }
376    }
377
378    /**
379     * Includes the selected target using the Flex cache.<p>
380     *
381     * @param controller the current JSP controller
382     * @param context the current JSP page context
383     * @param target the target for the include, might be <code>null</code>
384     * @param parameterMap a map of parameters for the include
385     * @param attributeMap a map of request attributes for the include
386     * @param req the current request
387     * @param res the current response
388     *
389     * @throws JspException in case something goes wrong
390     */
391    private static void includeActionWithCache(
392        CmsFlexController controller,
393        PageContext context,
394        String target,
395        Map<String, String[]> parameterMap,
396        Map<String, Object> attributeMap,
397        ServletRequest req,
398        ServletResponse res)
399    throws JspException {
400
401        try {
402
403            // add the target to the include list (the list will be initialized if it is currently empty)
404            controller.getCurrentResponse().addToIncludeList(target, parameterMap, attributeMap);
405            // now use the Flex dispatcher to include the target (this will also work for targets in the OpenCms VFS)
406            try {
407                controller.getCurrentRequest().getRequestDispatcher(target).include(req, res);
408            } finally {
409                // write out a FLEX_CACHE_DELIMITER char on the page, this is used as a parsing delimiter later
410                context.getOut().print(CmsFlexResponse.FLEX_CACHE_DELIMITER);
411            }
412        } catch (ServletException e) {
413            // store original Exception in controller in order to display it later
414            Throwable t = (e.getRootCause() != null) ? e.getRootCause() : e;
415            t = controller.setThrowable(t, target);
416            throw new JspException(t);
417        } catch (IOException e) {
418            // store original Exception in controller in order to display it later
419            Throwable t = controller.setThrowable(e, target);
420            throw new JspException(t);
421        }
422    }
423
424    /**
425     * This methods adds parameters to the current request.<p>
426     *
427     * Parameters added here will be treated like parameters from the
428     * HttpRequest on included pages.<p>
429     *
430     * Remember that the value for a parameter in a HttpRequest is a
431     * String array, not just a simple String. If a parameter added here does
432     * not already exist in the HttpRequest, it will be added. If a parameter
433     * exists, another value will be added to the array of values. If the
434     * value already exists for the parameter, nothing will be added, since a
435     * value can appear only once per parameter.<p>
436     *
437     * @param name the name to add
438     * @param value the value to add
439     * @see org.opencms.jsp.I_CmsJspTagParamParent#addParameter(String, String)
440     */
441    public void addParameter(String name, String value) {
442
443        // No null values allowed in parameters
444        if ((name == null) || (value == null)) {
445            return;
446        }
447
448        // Check if internal map exists, create new one if not
449        if (m_parameterMap == null) {
450            m_parameterMap = new HashMap<String, String[]>();
451        }
452
453        addParameter(m_parameterMap, name, value, false);
454    }
455
456    /**
457     * @return <code>EVAL_PAGE</code>
458     *
459     * @see javax.servlet.jsp.tagext.Tag#doEndTag()
460     *
461     * @throws JspException by interface default
462     */
463    @Override
464    public int doEndTag() throws JspException {
465
466        ServletRequest req = pageContext.getRequest();
467        ServletResponse res = pageContext.getResponse();
468
469        if (CmsFlexController.isCmsRequest(req)) {
470            // this will always be true if the page is called through OpenCms
471            CmsObject cms = CmsFlexController.getCmsObject(req);
472            String target = null;
473
474            // try to find out what to do
475            if (m_target != null) {
476                // option 1: target is set with "page" or "file" parameter
477                target = m_target + getSuffix();
478            } else if (m_property != null) {
479                // option 2: target is set with "property" parameter
480                try {
481                    String prop = cms.readPropertyObject(cms.getRequestContext().getUri(), m_property, true).getValue();
482                    if (prop != null) {
483                        target = prop + getSuffix();
484                    }
485                } catch (RuntimeException e) {
486                    // target must be null
487                    target = null;
488                } catch (Exception e) {
489                    // target will be null
490                    e = null;
491                }
492            } else if (m_attribute != null) {
493                // option 3: target is set in "attribute" parameter
494                try {
495                    String attr = (String)req.getAttribute(m_attribute);
496                    if (attr != null) {
497                        target = attr + getSuffix();
498                    }
499                } catch (RuntimeException e) {
500                    // target must be null
501                    target = null;
502                } catch (Exception e) {
503                    // target will be null
504                    e = null;
505                }
506            } else {
507                // option 4: target might be set in body
508                String body = null;
509                if (getBodyContent() != null) {
510                    body = getBodyContent().getString();
511                    if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(body)) {
512                        // target IS set in body
513                        target = body + getSuffix();
514                    }
515                    // else target is not set at all, default will be used
516                }
517            }
518
519            // now perform the include action
520            includeTagAction(
521                pageContext,
522                target,
523                m_element,
524                null,
525                m_editable,
526                m_cacheable,
527                m_parameterMap,
528                CmsRequestUtil.getAtrributeMap(req),
529                req,
530                res);
531
532            release();
533        }
534
535        return EVAL_PAGE;
536    }
537
538    /**
539     * Returns <code>{@link #EVAL_BODY_BUFFERED}</code>.<p>
540     *
541     * @return <code>{@link #EVAL_BODY_BUFFERED}</code>
542     *
543     * @see javax.servlet.jsp.tagext.Tag#doStartTag()
544     */
545    @Override
546    public int doStartTag() {
547
548        return EVAL_BODY_BUFFERED;
549    }
550
551    /**
552     * Returns the attribute.<p>
553     *
554     * @return the attribute
555     */
556    public String getAttribute() {
557
558        return m_attribute != null ? m_attribute : "";
559    }
560
561    /**
562     * Returns the cacheable flag.<p>
563     *
564     * @return the cacheable flag
565     */
566    public String getCacheable() {
567
568        return String.valueOf(m_cacheable);
569    }
570
571    /**
572     * Returns the editable flag.<p>
573     *
574     * @return the editable flag
575     */
576    public String getEditable() {
577
578        return String.valueOf(m_editable);
579    }
580
581    /**
582     * Returns the element.<p>
583     *
584     * @return the element
585     */
586    public String getElement() {
587
588        return m_element;
589    }
590
591    /**
592     * Returns the value of <code>{@link #getPage()}</code>.<p>
593     *
594     * @return the value of <code>{@link #getPage()}</code>
595     * @see #getPage()
596     */
597    public String getFile() {
598
599        return getPage();
600    }
601
602    /**
603     * Returns the include page target.<p>
604     *
605     * @return the include page target
606     */
607    public String getPage() {
608
609        return m_target != null ? m_target : "";
610    }
611
612    /**
613     * Returns the property.<p>
614     *
615     * @return the property
616     */
617    public String getProperty() {
618
619        return m_property != null ? m_property : "";
620    }
621
622    /**
623     * Returns the suffix.<p>
624     *
625     * @return the suffix
626     */
627    public String getSuffix() {
628
629        return m_suffix != null ? m_suffix : "";
630    }
631
632    /**
633     * @see javax.servlet.jsp.tagext.Tag#release()
634     */
635    @Override
636    public void release() {
637
638        super.release();
639        m_target = null;
640        m_suffix = null;
641        m_property = null;
642        m_element = null;
643        m_parameterMap = null;
644        m_editable = false;
645        m_cacheable = true;
646    }
647
648    /**
649     * Sets the attribute.<p>
650     *
651     * @param attribute the attribute to set
652     */
653    public void setAttribute(String attribute) {
654
655        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(attribute)) {
656            m_attribute = attribute;
657        }
658    }
659
660    /**
661     * Sets the cacheable flag.<p>
662     *
663     * Cachable is <code>true</code> by default.<p>
664     *
665     * @param cacheable the flag to set
666     */
667    public void setCacheable(String cacheable) {
668
669        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(cacheable)) {
670            m_cacheable = Boolean.valueOf(cacheable).booleanValue();
671        }
672    }
673
674    /**
675     * Sets the editable flag.<p>
676     *
677     * Editable is <code>false</code> by default.<p>
678     *
679     * @param editable the flag to set
680     */
681    public void setEditable(String editable) {
682
683        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(editable)) {
684            m_editable = Boolean.valueOf(editable).booleanValue();
685        }
686    }
687
688    /**
689     * Sets the element.<p>
690     *
691     * @param element the element to set
692     */
693    public void setElement(String element) {
694
695        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(element)) {
696            m_element = element;
697        }
698    }
699
700    /**
701     * Sets the file, same as using <code>setPage()</code>.<p>
702     *
703     * @param file the file to set
704     * @see #setPage(String)
705     */
706    public void setFile(String file) {
707
708        setPage(file);
709    }
710
711    /**
712     * Sets the include page target.<p>
713     *
714     * @param target the target to set
715     */
716    public void setPage(String target) {
717
718        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(target)) {
719            m_target = target;
720        }
721    }
722
723    /**
724     * Sets the property.<p>
725     *
726     * @param property the property to set
727     */
728    public void setProperty(String property) {
729
730        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(property)) {
731            m_property = property;
732        }
733    }
734
735    /**
736     * Sets the suffix.<p>
737     *
738     * @param suffix the suffix to set
739     */
740    public void setSuffix(String suffix) {
741
742        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(suffix)) {
743            m_suffix = suffix.toLowerCase();
744        }
745    }
746}