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.collectors.I_CmsResourceCollector;
032import org.opencms.flex.CmsFlexController;
033import org.opencms.i18n.CmsEncoder;
034import org.opencms.i18n.CmsLocaleManager;
035import org.opencms.jsp.util.CmsJspContentLoadBean;
036import org.opencms.loader.CmsDefaultFileNameGenerator;
037import org.opencms.main.CmsException;
038import org.opencms.main.CmsIllegalArgumentException;
039import org.opencms.main.OpenCms;
040import org.opencms.util.CmsMacroResolver;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.workplace.editors.directedit.CmsDirectEditButtonSelection;
043import org.opencms.workplace.editors.directedit.CmsDirectEditMode;
044import org.opencms.workplace.editors.directedit.CmsDirectEditParams;
045import org.opencms.xml.I_CmsXmlDocument;
046import org.opencms.xml.content.CmsXmlContentFactory;
047
048import java.util.Iterator;
049import java.util.Locale;
050
051import javax.servlet.jsp.JspException;
052import javax.servlet.jsp.PageContext;
053import javax.servlet.jsp.tagext.Tag;
054
055/**
056 * Implementation of the <code>&lt;cms:contentload/&gt;</code> tag,
057 * used to access and display XML content item information from the VFS.<p>
058 *
059 * Since version 7.0.2 it is also possible to store the results of the content load in the JSP context
060 * using a {@link CmsJspContentLoadBean}. Using this bean the loaded XML content objects can be accessed
061 * directly using the JSP EL and the JSTL. To use this feature, you need to add the <code>var</code> (and optionally
062 * the <code>scope</code>) parameter to the content load tag. For example, if a parameter like
063 * <code>var="myVarName"</code> is provided, then the result of the content load is stored in the JSP
064 * context variable <code>myVarName</code> with an instance of a {@link CmsJspContentLoadBean}.<p>
065 *
066 * @since 6.0.0
067 */
068public class CmsJspTagContentLoad extends CmsJspTagResourceLoad implements I_CmsXmlContentContainer {
069
070    /** Serial version UID required for safe serialization. */
071    private static final long serialVersionUID = 981176995635225294L;
072
073    /** Reference to the last loaded content element. */
074    private transient I_CmsXmlDocument m_content;
075
076    /**
077     * The locale to use for displaying the current content.<p>
078     *
079     * Initially, this is equal to the locale set using <code>{@link #setLocale(String)}</code>.
080     * However, the content locale may change in case a loaded XML content does not have the selected locale available.
081     * In this case the next default locale that is available in the content will be used as content locale.<p>
082     */
083    private Locale m_contentLocale;
084
085    /** The "direct edit" button selection to use for the 2nd to the last element. */
086    private CmsDirectEditButtonSelection m_directEditFollowButtons;
087
088    /** The link for creation of a new element, specified by the selected collector. */
089    private String m_directEditLinkForNew;
090
091    /** The direct edit mode. */
092    private CmsDirectEditMode m_directEditMode;
093
094    /** Indicates if the last element was direct editable. */
095    private boolean m_directEditOpen;
096
097    /** The edit empty tag attribute. */
098    private boolean m_editEmpty;
099
100    /** Indicates if this is the first content iteration loop. */
101    private boolean m_isFirstLoop;
102
103    /** Reference to the currently selected locale. */
104    private Locale m_locale;
105
106    /** Post-create handler class. */
107    private String m_postCreateHandler;
108
109    /**
110     * Empty constructor, required for JSP tags.<p>
111     */
112    public CmsJspTagContentLoad() {
113
114        super();
115    }
116
117    /**
118     * Constructor used when using <code>contentload</code> from scriptlet code.<p>
119     *
120     * @param container the parent content container (could be a preloader)
121     * @param context the JSP page context
122     * @param collectorName the collector name to use
123     * @param collectorParam the collector param to use
124     * @param locale the locale to use
125     * @param editable indicates if "direct edit" support is wanted
126     *
127     * @throws JspException in case something goes wrong
128     */
129    public CmsJspTagContentLoad(
130        I_CmsXmlContentContainer container,
131        PageContext context,
132        String collectorName,
133        String collectorParam,
134        Locale locale,
135        boolean editable)
136        throws JspException {
137
138        this(container, context, collectorName, collectorParam, null, null, locale, editable);
139    }
140
141    /**
142     * Constructor used when using <code>contentload</code> from scriptlet code.<p>
143     *
144     * @param container the parent content container (could be a preloader)
145     * @param context the JSP page context
146     * @param collectorName the collector name to use
147     * @param collectorParam the collector param to use
148     * @param pageIndex the display page index (may contain macros)
149     * @param pageSize the display page size (may contain macros)
150     * @param locale the locale to use
151     * @param editable indicates if "direct edit" support is wanted
152     *
153     * @throws JspException in case something goes wrong
154     */
155    public CmsJspTagContentLoad(
156        I_CmsXmlContentContainer container,
157        PageContext context,
158        String collectorName,
159        String collectorParam,
160        String pageIndex,
161        String pageSize,
162        Locale locale,
163        boolean editable)
164        throws JspException {
165
166        this(
167            container,
168            context,
169            collectorName,
170            collectorParam,
171            pageIndex,
172            pageSize,
173            locale,
174            CmsDirectEditMode.valueOf(editable));
175    }
176
177    /**
178     * Constructor used when using <code>contentload</code> from scriptlet code.<p>
179     *
180     * @param container the parent content container (could be a preloader)
181     * @param context the JSP page context
182     * @param collectorName the collector name to use
183     * @param collectorParam the collector param to use
184     * @param pageIndex the display page index (may contain macros)
185     * @param pageSize the display page size (may contain macros)
186     * @param locale the locale to use
187     * @param editMode indicates which "direct edit" mode is wanted
188     *
189     * @throws JspException in case something goes wrong
190     */
191    public CmsJspTagContentLoad(
192        I_CmsXmlContentContainer container,
193        PageContext context,
194        String collectorName,
195        String collectorParam,
196        String pageIndex,
197        String pageSize,
198        Locale locale,
199        CmsDirectEditMode editMode)
200        throws JspException {
201
202        setCollector(collectorName);
203        setParam(collectorParam);
204        setPageIndex(pageIndex);
205        setPageSize(pageSize);
206        m_locale = locale;
207        m_contentLocale = locale;
208        m_directEditMode = editMode;
209        m_preload = false;
210
211        setPageContext(context);
212        init(container);
213    }
214
215    /**
216     * @see javax.servlet.jsp.tagext.Tag#doStartTag()
217     */
218    @Override
219    public int doStartTag() throws JspException, CmsIllegalArgumentException {
220
221        // get a reference to the parent "content container" class (if available)
222        Tag ancestor = findAncestorWithClass(this, I_CmsXmlContentContainer.class);
223        I_CmsXmlContentContainer container = null;
224        if (ancestor != null) {
225            // parent content container available, use preloaded values from this container
226            container = (I_CmsXmlContentContainer)ancestor;
227            // check if container really is a preloader
228            if (!container.isPreloader()) {
229                // don't use ancestor if not a preloader
230                container = null;
231            }
232        }
233
234        // initialize the content load tag
235        init(container);
236
237        hasMoreResources();
238
239        return isScopeVarSet() ? SKIP_BODY : EVAL_BODY_INCLUDE;
240    }
241
242    /**
243     * Returns the editable flag.<p>
244     *
245     * @return the editable flag
246     */
247    public String getEditable() {
248
249        return m_directEditMode != null ? m_directEditMode.toString() : "";
250    }
251
252    /**
253     * Returns the locale.<p>
254     *
255     * @return the locale
256     */
257    public String getLocale() {
258
259        return (m_locale != null) ? m_locale.toString() : "";
260    }
261
262    /**
263     * @see org.opencms.jsp.I_CmsXmlContentContainer#getXmlDocument()
264     */
265    public I_CmsXmlDocument getXmlDocument() {
266
267        return m_content;
268    }
269
270    /**
271     * @see org.opencms.jsp.I_CmsXmlContentContainer#getXmlDocumentElement()
272     */
273    public String getXmlDocumentElement() {
274
275        // value must be set in "loop" or "show" class
276        return null;
277    }
278
279    /**
280     * @see org.opencms.jsp.I_CmsXmlContentContainer#getXmlDocumentLocale()
281     */
282    public Locale getXmlDocumentLocale() {
283
284        return m_contentLocale;
285    }
286
287    /**
288     * @see org.opencms.jsp.I_CmsXmlContentContainer#hasMoreResources()
289     */
290    @Override
291    public boolean hasMoreResources() throws JspException {
292
293        // check if there are more files to iterate
294        boolean hasMoreContent = m_collectorResult.size() > 0;
295        if (m_isFirstLoop) {
296
297            if (!m_cms.getRequestContext().getCurrentProject().isOnlineProject() && (m_collector != null)) {
298                CmsContentLoadCollectorInfo info = new CmsContentLoadCollectorInfo();
299                info.setCollectorName(m_collectorName);
300                info.setCollectorParams(m_collectorParam);
301                info.setId(m_contentInfoBean.getId());
302                if (CmsJspTagEditable.getDirectEditProvider(pageContext) != null) {
303                    CmsJspTagEditable.getDirectEditProvider(pageContext).insertDirectEditListMetadata(
304                        pageContext,
305                        info);
306                }
307            }
308            m_isFirstLoop = false;
309            if (!hasMoreContent && m_editEmpty && (m_directEditLinkForNew != null)) {
310                try {
311                    CmsJspTagEditable.insertEditEmpty(pageContext, this, m_directEditMode, m_contentInfoBean.getId());
312                } catch (CmsException e) {
313                    throw new JspException(e);
314                }
315            }
316        } else {
317            if (m_directEditOpen) {
318                // last element was direct editable, close it
319                CmsJspTagEditable.endDirectEdit(pageContext);
320                m_directEditOpen = false;
321            }
322        }
323
324        if (isPreloader()) {
325            // if in preload mode, no result is required
326            return false;
327        }
328
329        if (hasMoreContent) {
330            // there are more results available...
331            try {
332                doLoadNextFile();
333            } catch (CmsException e) {
334                m_controller.setThrowable(e, m_resourceName);
335                throw new JspException(e);
336            }
337
338            // check "direct edit" support
339            if (m_directEditMode.isEnabled() && (m_resourceName != null)) {
340
341                // check options for first element
342                CmsDirectEditButtonSelection directEditButtons;
343                if (m_directEditFollowButtons == null) {
344                    // this is the first call, calculate the options
345                    if (m_directEditLinkForNew == null) {
346                        // if create link is null, show only "edit" and "delete" button for first element
347                        directEditButtons = CmsDirectEditButtonSelection.EDIT_DELETE;
348                        m_directEditFollowButtons = directEditButtons;
349                    } else {
350                        // if create link is not null, show "edit", "delete" and "new" buttons
351                        directEditButtons = CmsDirectEditButtonSelection.EDIT_DELETE_NEW;
352                        m_directEditFollowButtons = CmsDirectEditButtonSelection.EDIT_DELETE_NEW;
353                    }
354                } else {
355                    // re-use pre calculated options
356                    directEditButtons = m_directEditFollowButtons;
357                }
358
359                CmsDirectEditParams params = new CmsDirectEditParams(
360                    m_resourceName,
361                    directEditButtons,
362                    m_directEditMode,
363                    m_directEditLinkForNew);
364                params.setPostCreateHandler(m_postCreateHandler);
365                params.setId(m_contentInfoBean.getId());
366                params.setCollectorName(m_collectorName);
367                params.setCollectorParams(m_param);
368                m_directEditOpen = CmsJspTagEditable.startDirectEdit(pageContext, params);
369            }
370
371        } else {
372            // no more results in the collector, reset locale (just to make sure...)
373            m_locale = null;
374            m_editEmpty = false;
375        }
376
377        return hasMoreContent;
378    }
379
380    /**
381     * Returns the edit empty attribute.<p>
382     *
383     * @return the edit empty attribute
384     */
385    public boolean isEditEmpty() {
386
387        return m_editEmpty;
388    }
389
390    /**
391     * @see javax.servlet.jsp.tagext.Tag#release()
392     */
393    @Override
394    public void release() {
395
396        m_content = null;
397        m_contentLocale = null;
398        m_directEditLinkForNew = null;
399        m_directEditFollowButtons = null;
400        m_directEditOpen = false;
401        m_directEditMode = null;
402        m_isFirstLoop = false;
403        m_locale = null;
404        super.release();
405    }
406
407    /**
408     * Sets the editable mode.<p>
409     *
410     * @param mode the mode to set
411     */
412    public void setEditable(String mode) {
413
414        m_directEditMode = CmsDirectEditMode.valueOf(mode);
415    }
416
417    /**
418     * Sets the edit empty attribute.<p>
419     *
420     * @param editEmpty the edit empty attribute to set
421     */
422    public void setEditEmpty(boolean editEmpty) {
423
424        m_editEmpty = editEmpty;
425    }
426
427    /**
428     * Sets the locale.<p>
429     *
430     * @param locale the locale to set
431     */
432    public void setLocale(String locale) {
433
434        if (CmsStringUtil.isEmpty(locale)) {
435            m_locale = null;
436            m_contentLocale = null;
437        } else {
438            m_locale = CmsLocaleManager.getLocale(locale);
439            m_contentLocale = m_locale;
440        }
441    }
442
443    /**
444     * Sets the post-create handler class name.<p>
445     *
446     * @param postCreateHandler the post-create handler class name
447     */
448    public void setPostCreateHandler(String postCreateHandler) {
449
450        m_postCreateHandler = postCreateHandler;
451    }
452
453    /**
454     * Load the next file name from the initialized list of file names.<p>
455     *
456     * @throws CmsException if something goes wrong
457     */
458    protected void doLoadNextFile() throws CmsException {
459
460        super.doLoadNextResource();
461        if (m_resource == null) {
462            return;
463        }
464
465        // upgrade the resource to a file
466        CmsFile file = m_cms.readFile(m_resource);
467
468        // unmarshal the XML content from the resource, don't use unmarshal(CmsObject, CmsResource)
469        // as no support for getting the historic version that has been cached by a CmsHistoryResourceHandler
470        // will come from there!
471        m_content = CmsXmlContentFactory.unmarshal(m_cms, file, pageContext.getRequest());
472
473        // check if locale is available
474        m_contentLocale = m_locale;
475        if (!m_content.hasLocale(m_contentLocale)) {
476            Iterator<Locale> it = OpenCms.getLocaleManager().getDefaultLocales().iterator();
477            while (it.hasNext()) {
478                Locale locale = it.next();
479                if (m_content.hasLocale(locale)) {
480                    // found a matching locale
481                    m_contentLocale = locale;
482                    break;
483                }
484            }
485        }
486    }
487
488    /**
489     * Initializes this content load tag.<p>
490     *
491     * @param container the parent container (could be a preloader)
492     *
493     * @throws JspException in case something goes wrong
494     */
495    protected void init(I_CmsXmlContentContainer container) throws JspException {
496
497        // check if the tag contains a pageSize, pageIndex and pageNavLength attribute, or none of them
498        int pageAttribCount = 0;
499        pageAttribCount += CmsStringUtil.isNotEmpty(m_pageSize) ? 1 : 0;
500        pageAttribCount += CmsStringUtil.isNotEmpty(m_pageIndex) ? 1 : 0;
501
502        if ((pageAttribCount > 0) && (pageAttribCount < 2)) {
503            throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_TAG_CONTENTLOAD_INDEX_SIZE_0));
504        }
505
506        I_CmsXmlContentContainer usedContainer;
507        if (container == null) {
508            // no preloading ancestor has been found
509            usedContainer = this;
510            if (CmsStringUtil.isEmpty(m_collector)) {
511                // check if the tag contains a collector attribute
512                throw new CmsIllegalArgumentException(
513                    Messages.get().container(Messages.ERR_TAG_CONTENTLOAD_MISSING_COLLECTOR_0));
514            }
515            if (CmsStringUtil.isEmpty(m_param)) {
516                // check if the tag contains a param attribute
517                throw new CmsIllegalArgumentException(
518                    Messages.get().container(Messages.ERR_TAG_CONTENTLOAD_MISSING_PARAM_0));
519            }
520        } else {
521            // use provided container (preloading ancestor)
522            usedContainer = container;
523        }
524
525        if (isPreloader()) {
526            // always disable direct edit for preload
527            m_directEditMode = CmsDirectEditMode.FALSE;
528        } else if (m_directEditMode == null) {
529            // direct edit mode must not be null
530            m_directEditMode = CmsDirectEditMode.FALSE;
531        }
532
533        // initialize OpenCms access objects
534        m_controller = CmsFlexController.getController(pageContext.getRequest());
535        m_cms = m_controller.getCmsObject();
536
537        // get the resource name from the selected container
538        String resourcename = getResourceName(m_cms, usedContainer);
539
540        // initialize a string mapper to resolve EL like strings in tag attributes
541        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(m_cms).setJspPageContext(
542            pageContext).setResourceName(resourcename).setKeepEmptyMacros(true);
543
544        // resolve the collector name
545        if (container == null) {
546            // no preload parent container, initialize new values
547            m_collectorName = resolver.resolveMacros(getCollector());
548            // resolve the parameter
549            m_collectorParam = resolver.resolveMacros(getParam());
550            m_collectorResult = null;
551        } else {
552            // preload parent content container available, use values from this container
553            m_collectorName = usedContainer.getCollectorName();
554            m_collectorParam = usedContainer.getCollectorParam();
555            m_collectorResult = usedContainer.getCollectorResult();
556            if (m_locale == null) {
557                // use locale from ancestor if available
558                m_locale = usedContainer.getXmlDocumentLocale();
559            }
560        }
561
562        if (m_locale == null) {
563            // no locale set, use locale from users request context
564            m_locale = m_cms.getRequestContext().getLocale();
565        }
566
567        try {
568            // now collect the resources
569            I_CmsResourceCollector collector = OpenCms.getResourceManager().getContentCollector(m_collectorName);
570            if (collector == null) {
571                throw new CmsException(Messages.get().container(Messages.ERR_COLLECTOR_NOT_FOUND_1, m_collectorName));
572            }
573            // execute the collector if not already done in parent tag
574            if (m_collectorResult == null) {
575                m_collectorResult = collector.getResults(m_cms, m_collectorName, m_collectorParam);
576            }
577
578            m_contentInfoBean = new CmsContentInfoBean();
579            m_contentInfoBean.setPageSizeAsString(resolver.resolveMacros(m_pageSize));
580            m_contentInfoBean.setPageIndexAsString(resolver.resolveMacros(m_pageIndex));
581            m_contentInfoBean.setPageNavLengthAsString(resolver.resolveMacros(m_pageNavLength));
582            m_contentInfoBean.setResultSize(m_collectorResult.size());
583            m_contentInfoBean.setLocale(m_locale.toString());
584            m_contentInfoBean.initResultIndex();
585
586            if (!isPreloader()) {
587                // not required when only preloading
588                m_collectorResult = CmsJspTagResourceLoad.limitCollectorResult(m_contentInfoBean, m_collectorResult);
589                m_contentInfoBean.initPageNavIndexes();
590
591                String createParam = collector.getCreateParam(m_cms, m_collectorName, m_collectorParam);
592                if ((createParam != null) && CmsDefaultFileNameGenerator.hasNumberMacro(createParam)) {
593                    // use "create link" only if collector supports it and it contains the number macro for new file names
594                    m_directEditLinkForNew = CmsEncoder.encode(m_collectorName + "|" + createParam);
595                }
596            } else if (isScopeVarSet()) {
597                // scope variable is set, store content load bean in JSP context
598                CmsJspContentLoadBean bean = new CmsJspContentLoadBean(m_cms, m_locale, m_collectorResult);
599                storeAttribute(bean);
600            }
601
602        } catch (CmsException e) {
603            m_controller.setThrowable(e, m_cms.getRequestContext().getUri());
604            throw new JspException(e);
605        }
606
607        // reset the direct edit options (required because of re-used tags)
608        m_directEditOpen = false;
609        m_directEditFollowButtons = null;
610
611        // the next loop is the first loop
612        m_isFirstLoop = true;
613    }
614}