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