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, 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.util;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.formatters.CmsFormatterBeanParser;
032import org.opencms.ade.configuration.formatters.CmsFormatterConfigurationCache;
033import org.opencms.file.CmsFile;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.types.I_CmsResourceType;
038import org.opencms.flex.CmsFlexController;
039import org.opencms.i18n.CmsLocaleManager;
040import org.opencms.jsp.CmsJspTagDisplay;
041import org.opencms.jsp.Messages;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.CmsRuntimeException;
045import org.opencms.main.OpenCms;
046import org.opencms.util.CmsStringUtil;
047import org.opencms.util.CmsUUID;
048import org.opencms.util.I_CmsMacroResolver;
049import org.opencms.xml.containerpage.CmsContainerElementBean;
050import org.opencms.xml.containerpage.CmsMacroFormatterBean;
051import org.opencms.xml.containerpage.I_CmsFormatterBean;
052import org.opencms.xml.content.CmsXmlContent;
053import org.opencms.xml.content.CmsXmlContentFactory;
054import org.opencms.xml.types.CmsXmlVfsFileValue;
055import org.opencms.xml.types.I_CmsXmlContentValue;
056
057import java.io.IOException;
058import java.util.HashMap;
059import java.util.List;
060import java.util.Map;
061
062import javax.servlet.http.HttpServletRequest;
063import javax.servlet.http.HttpServletResponse;
064import javax.servlet.jsp.PageContext;
065
066import org.apache.commons.beanutils.BeanUtilsBean;
067import org.apache.commons.beanutils.PropertyUtilsBean;
068import org.apache.commons.logging.Log;
069
070/**
071 * Resolver for macro formatters.<p>
072 */
073public class CmsMacroFormatterResolver {
074
075    /** The parent macro key. */
076    public static final String KEY_CMS = "cms.";
077
078    /** The element macro key. */
079    public static final String KEY_ELEMENT = "element.";
080
081    /** The parent macro key. */
082    public static final String KEY_PARENT = "parent.";
083
084    /** The settings macro key. */
085    public static final String KEY_SETTINGS = "settings.";
086
087    /** Node name. */
088    public static final String N_FORMATTER = "Formatter";
089
090    /** Node name. */
091    public static final String N_FORMATTERS = "Formatters";
092
093    /** Node name. */
094    public static final String N_MACRO = "Macro";
095
096    /** Node name. */
097    public static final String N_MACRO_NAME = "MacroName";
098
099    /** The log object for this class. */
100    private static final Log LOG = CmsLog.getLog(CmsMacroFormatterResolver.class);
101
102    /** The current cms context. */
103    private CmsObject m_cms;
104
105    /** The page context. */
106    private PageContext m_context;
107
108    /** The JSP context bean. */
109    private CmsJspStandardContextBean m_contextBean;
110
111    /** The element to render. */
112    private CmsContainerElementBean m_element;
113
114    /** The formatter references. */
115    private Map<String, CmsUUID> m_formatterReferences;
116
117    /** The macro input string. */
118    private String m_input;
119
120    /** The request. */
121    private HttpServletRequest m_request;
122
123    /** The response. */
124    private HttpServletResponse m_response;
125
126    /**
127     * Constructor.<p>
128     *
129     * @param context the page context
130     * @param req the request
131     * @param res the response
132     */
133    public CmsMacroFormatterResolver(PageContext context, HttpServletRequest req, HttpServletResponse res) {
134
135        m_context = context;
136        m_request = req;
137        m_response = res;
138        CmsFlexController controller = CmsFlexController.getController(req);
139        if (controller == null) {
140            handleMissingFlexController();
141            return;
142        }
143        m_cms = controller.getCmsObject();
144        m_contextBean = CmsJspStandardContextBean.getInstance(m_request);
145        m_element = m_contextBean.getElement();
146    }
147
148    /**
149     * Resolves the macro.<p>
150     *
151     * @throws IOException in case writing to the page context output stream fails
152     * @throws CmsException in case reading the macro settings fails
153     */
154    @SuppressWarnings("resource")
155    public void resolve() throws IOException, CmsException {
156
157        initMacroContent();
158        String input = getMacroInput();
159        if (input == null) {
160            return;
161        }
162        if (input.length() < 3) {
163            // macro must have at last 3 chars "${}" or "%()"
164            m_context.getOut().print(input);
165            return;
166        }
167
168        int newDelimPos = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER);
169        int oldDelomPos = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD);
170
171        if ((oldDelomPos == -1) && (newDelimPos == -1)) {
172            // no macro delimiter found in input
173            m_context.getOut().print(input);
174            return;
175        }
176
177        int len = input.length();
178        int nextDelimPos, delimPos1, delimPos2, endPos;
179        String macro;
180        char startChar, endChar;
181        int delimPos;
182
183        if ((oldDelomPos == -1) || ((newDelimPos > -1) && (newDelimPos < oldDelomPos))) {
184            delimPos = newDelimPos;
185            startChar = I_CmsMacroResolver.MACRO_START;
186            endChar = I_CmsMacroResolver.MACRO_END;
187        } else {
188            delimPos = oldDelomPos;
189            startChar = I_CmsMacroResolver.MACRO_START_OLD;
190            endChar = I_CmsMacroResolver.MACRO_END_OLD;
191        }
192
193        // append chars before the first delimiter found
194        m_context.getOut().print(input.substring(0, delimPos));
195        do {
196            delimPos1 = delimPos + 1;
197            delimPos2 = delimPos1 + 1;
198            if (delimPos2 >= len) {
199                // remaining chars can't be a macro (minimum size is 3)
200                m_context.getOut().print(input.substring(delimPos, len));
201                break;
202            }
203            // get the next macro delimiter
204            if ((newDelimPos > -1) && (newDelimPos < delimPos1)) {
205                newDelimPos = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER, delimPos1);
206            }
207            if ((oldDelomPos > -1) && (oldDelomPos < delimPos1)) {
208                oldDelomPos = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD, delimPos1);
209            }
210            if ((oldDelomPos == -1) && (newDelimPos == -1)) {
211                // none found, make sure remaining chars in this segment are appended
212                nextDelimPos = len;
213            } else {
214                // check if the next delimiter is old or new style
215                if ((oldDelomPos == -1) || ((newDelimPos > -1) && (newDelimPos < oldDelomPos))) {
216                    nextDelimPos = newDelimPos;
217                } else {
218                    nextDelimPos = oldDelomPos;
219                }
220            }
221            // check if the next char is a "macro start"
222            char start = input.charAt(delimPos1);
223            if (start == startChar) {
224                // we have a starting macro sequence "${" or "%(", now check if this segment contains a "}" or ")"
225                endPos = input.indexOf(endChar, delimPos);
226                if ((endPos > 0) && (endPos < nextDelimPos)) {
227                    // this segment contains a closing macro delimiter "}" or "]", so we may have found a macro
228                    macro = input.substring(delimPos2, endPos);
229                    // resolve macro
230                    try {
231                        printMacroValue(macro);
232                    } catch (Exception ex) {
233                        LOG.error("Writing value for macro '" + macro + "' failed.", ex);
234                    }
235                    endPos++;
236                } else {
237                    // no complete macro "${...}" or "%(...)" in this segment
238                    endPos = delimPos;
239                }
240            } else {
241                // no macro start char after the "$" or "%"
242                endPos = delimPos;
243            }
244            // set macro style for next delimiter found
245            if (nextDelimPos == newDelimPos) {
246                startChar = I_CmsMacroResolver.MACRO_START;
247                endChar = I_CmsMacroResolver.MACRO_END;
248            } else {
249                startChar = I_CmsMacroResolver.MACRO_START_OLD;
250                endChar = I_CmsMacroResolver.MACRO_END_OLD;
251            }
252            // append the remaining chars after the macro to the start of the next macro
253            m_context.getOut().print(input.substring(endPos, nextDelimPos));
254            delimPos = nextDelimPos;
255        } while (delimPos < len);
256    }
257
258    /**
259     * Returns the formatter bean for the given macro string, or <code>null</code> if none available.<p>
260     *
261     * @param macro the macro
262     *
263     * @return the formatter bean
264     */
265    protected I_CmsFormatterBean getFormatterForMacro(String macro) {
266
267        CmsADEConfigData config = OpenCms.getADEManager().lookupConfigurationWithCache(
268            m_cms,
269            m_cms.getRequestContext().getRootUri());
270
271        CmsUUID formatterId = null;
272        if (m_formatterReferences.containsKey(macro)) {
273            formatterId = m_formatterReferences.get(macro);
274        } else {
275            try {
276                I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(
277                    CmsFormatterConfigurationCache.TYPE_FORMATTER_CONFIG);
278                CmsResourceFilter filter = CmsResourceFilter.DEFAULT.addRequireType(type);
279                if (m_cms.existsResource(macro, filter)) {
280                    CmsResource res = m_cms.readResource(macro);
281                    formatterId = res.getStructureId();
282                }
283            } catch (CmsException e) {
284                LOG.error("Failed to read formatter configuration.", e);
285            }
286        }
287        if (formatterId != null) {
288            return config.findFormatter(formatterId);
289        }
290        return null;
291    }
292
293    /**
294     * Returns the property value read from the given JavaBean.
295     *
296     * @param bean the JavaBean to read the property from
297     * @param property the property to read
298     *
299     * @return the property value read from the given JavaBean
300     */
301    protected Object getMacroBeanValue(Object bean, String property) {
302
303        Object result = null;
304        if ((bean != null) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(property)) {
305            try {
306                PropertyUtilsBean propBean = BeanUtilsBean.getInstance().getPropertyUtils();
307                result = propBean.getProperty(bean, property);
308            } catch (Exception e) {
309                LOG.error("Unable to access property '" + property + "' of '" + bean + "'.", e);
310            }
311        } else {
312            LOG.info("Invalid parameters: property='" + property + "' bean='" + bean + "'.");
313        }
314        return result;
315    }
316
317    /**
318     * Returns the macro input string.<p>
319     *
320     * @return the macro input string
321     */
322    protected String getMacroInput() {
323
324        return m_input;
325    }
326
327    /**
328     * Prints the macro value to the output stream.<p>
329     *
330     * @param macro the macro string
331     *
332     * @throws IOException in case writing to the page context output stream fails
333     */
334    @SuppressWarnings("resource")
335    protected void printMacroValue(String macro) throws IOException {
336
337        if (macro.startsWith(KEY_CMS)) {
338            Object result = getMacroBeanValue(m_contextBean, macro.substring(KEY_CMS.length()));
339            if (result != null) {
340                m_context.getOut().print(result);
341            }
342        } else if (macro.startsWith(KEY_ELEMENT)) {
343            Object result = getMacroBeanValue(m_contextBean.getElement(), macro.substring(KEY_ELEMENT.length()));
344            if (result != null) {
345                m_context.getOut().print(result);
346            }
347        } else if (macro.startsWith(KEY_PARENT)) {
348            Object result = getMacroBeanValue(
349                m_contextBean.getParentElement(m_element),
350                macro.substring(KEY_PARENT.length()));
351            if (result != null) {
352                m_context.getOut().print(result);
353            }
354        } else if (macro.startsWith(KEY_SETTINGS)) {
355            String settingValue = m_element.getSettings().get(macro.substring(KEY_SETTINGS.length()));
356            if (settingValue != null) {
357                m_context.getOut().print(settingValue);
358            }
359        } else {
360
361            I_CmsFormatterBean formatter = getFormatterForMacro(macro);
362            if (formatter != null) {
363                CmsContainerElementBean copy = CmsContainerElementBean.cloneWithFormatter(
364                    m_element,
365                    formatter.getJspStructureId());
366                copy.setDoNotCache(true);
367                try {
368                    CmsJspTagDisplay.displayAction(copy, formatter, m_context, m_request, m_response);
369                } catch (Exception e) {
370                    LOG.error("Failed to display formatted content.", e);
371                }
372            }
373        }
374    }
375
376    /**
377     * This method is called when the flex controller can not be found during initialization.<p>
378     *
379     * Override this if you are reusing old workplace classes in a context where no flex controller is available.
380     */
381    private void handleMissingFlexController() {
382
383        // controller not found - this request was not initialized properly
384        throw new CmsRuntimeException(
385            Messages.get().container(Messages.ERR_MISSING_CMS_CONTROLLER_1, CmsMacroFormatterResolver.class.getName()));
386    }
387
388    /**
389     * Initializes settings from the macro content.<p>
390     *
391     * @throws CmsException in case reading the settings fails
392     */
393    private void initMacroContent() throws CmsException {
394
395        CmsADEConfigData adeConfig = OpenCms.getADEManager().lookupConfigurationWithCache(
396            m_cms,
397            m_cms.getRequestContext().getRootUri());
398        I_CmsFormatterBean formatterConfig = adeConfig.findFormatter(m_element.getFormatterId());
399        if (formatterConfig instanceof CmsMacroFormatterBean) {
400            CmsMacroFormatterBean config = (CmsMacroFormatterBean)formatterConfig;
401            m_input = config.getMacroInput();
402            m_formatterReferences = config.getReferencedFormatters();
403            if (m_element.isInMemoryOnly()) {
404                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(config.getPlaceholderMacroInput())) {
405                    m_input = config.getPlaceholderMacroInput();
406                }
407                if (config.getDefaultContentStructureId() != null) {
408                    try {
409                        CmsResource defaultContent = m_cms.readResource(
410                            ((CmsMacroFormatterBean)formatterConfig).getDefaultContentStructureId());
411                        CmsFile defaultFile = m_cms.readFile(defaultContent);
412                        m_element = new CmsContainerElementBean(
413                            defaultFile,
414                            m_element.getFormatterId(),
415                            m_element.getIndividualSettings(),
416                            true,
417                            m_element.editorHash(),
418                            m_element.isCreateNew());
419                    } catch (CmsException e) {
420                        LOG.error("Error reading default content for new resource", e);
421                    }
422                }
423            }
424        } else {
425            // only as a fall back, should not be used
426            m_formatterReferences = new HashMap<String, CmsUUID>();
427            CmsResource macroContent = m_cms.readResource(m_element.getFormatterId());
428            CmsXmlContent xmlContent = CmsXmlContentFactory.unmarshal(m_cms, macroContent, m_request);
429            m_input = xmlContent.getStringValue(m_cms, CmsFormatterBeanParser.N_MACRO, CmsLocaleManager.MASTER_LOCALE);
430            List<I_CmsXmlContentValue> formatters = xmlContent.getValues(
431                CmsFormatterBeanParser.N_FORMATTERS,
432                CmsLocaleManager.MASTER_LOCALE);
433            for (I_CmsXmlContentValue formatterValue : formatters) {
434                CmsXmlVfsFileValue file = (CmsXmlVfsFileValue)xmlContent.getValue(
435                    formatterValue.getPath() + "/" + CmsFormatterBeanParser.N_FORMATTER,
436                    CmsLocaleManager.MASTER_LOCALE);
437                String macroName = xmlContent.getStringValue(
438                    m_cms,
439                    formatterValue.getPath() + "/" + CmsFormatterBeanParser.N_MACRO_NAME,
440                    CmsLocaleManager.MASTER_LOCALE);
441                m_formatterReferences.put(macroName, file.getLink(m_cms).getStructureId());
442            }
443        }
444    }
445}