001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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.loader;
029
030import org.opencms.ade.containerpage.Messages;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.flex.CmsFlexController;
036import org.opencms.gwt.shared.CmsClientVariantInfo;
037import org.opencms.gwt.shared.CmsGwtConstants;
038import org.opencms.gwt.shared.CmsTemplateContextInfo;
039import org.opencms.main.CmsException;
040import org.opencms.main.CmsLog;
041import org.opencms.main.OpenCms;
042import org.opencms.util.CmsDefaultSet;
043import org.opencms.util.CmsRequestUtil;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.xml.content.CmsXmlContentProperty;
046
047import java.util.ArrayList;
048import java.util.Collections;
049import java.util.HashMap;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.Locale;
053import java.util.Map;
054
055import javax.servlet.http.HttpServletRequest;
056
057import org.apache.commons.logging.Log;
058
059/**
060 * Manager class for template context providers.<p>
061 */
062public class CmsTemplateContextManager {
063
064    /** A bean containing information about the selected template. */
065    public static final String ATTR_TEMPLATE_BEAN = "ATTR_TEMPLATE_BEAN";
066
067    /** The request attribute in which the template context is stored. */
068    public static final String ATTR_TEMPLATE_CONTEXT = "templateContext";
069
070    /** Attribute name which contains the template name for non-dynamically selected templates. */
071    public static final String ATTR_TEMPLATE_NAME = "cmsTemplateName";
072
073    /** Attribute name for the template resource. */
074    public static final String ATTR_TEMPLATE_RESOURCE = "cmsTemplateResource";
075
076    /** The prefix used in the template property to activate dynamic template selection. */
077    public static final String DYNAMIC_TEMPLATE_PREFIX = "provider=";
078
079    /** Legacy prefix for property providers. */
080    private static final String DYNAMIC_TEMPLATE_LEGACY_PREFIX = "dynamic:";
081
082    /** The logger instance for this class. */
083    private static final Log LOG = CmsLog.getLog(CmsTemplateContextManager.class);
084
085    /** Request attribute used to set the template context during RPC calls. */
086    public static final String ATTR_RPC_CONTEXT_OVERRIDE = "ATTR_RPC_CONTEXT_OVERRIDE";
087
088    /** The CMS context. */
089    private CmsObject m_cms;
090
091    /** A cache in which the template context provider instances are stored, with their class name as the key. */
092    private Map<String, I_CmsTemplateContextProvider> m_providerInstances = new HashMap<String, I_CmsTemplateContextProvider>();
093
094    /**
095     * Creates a new instance.<p>
096     *
097     * @param cms the CMS context to use
098     */
099    public CmsTemplateContextManager(CmsObject cms) {
100
101        m_cms = cms;
102        CmsFlexController.registerUncacheableAttribute(ATTR_TEMPLATE_RESOURCE);
103        CmsFlexController.registerUncacheableAttribute(ATTR_TEMPLATE_CONTEXT);
104        CmsFlexController.registerUncacheableAttribute(ATTR_TEMPLATE_RESOURCE);
105    }
106
107    /**
108     * Checks if the property value starts with the prefix which marks a dynamic template provider.<p>
109     *
110     * @param propertyValue the property value to check
111     * @return true if the value has the format of a dynamic template provider
112     */
113    public static boolean hasPropertyPrefix(String propertyValue) {
114
115        return (propertyValue != null)
116            && (propertyValue.startsWith(DYNAMIC_TEMPLATE_PREFIX)
117                || propertyValue.startsWith(DYNAMIC_TEMPLATE_LEGACY_PREFIX));
118    }
119
120    /**
121     * Checks if a template property value refers to a  template context provider.<p>
122     *
123     * @param templatePath the template property value
124     * @return true if this value refers to a template context provider
125     */
126    public static boolean isProvider(String templatePath) {
127
128        if (CmsStringUtil.isEmptyOrWhitespaceOnly(templatePath)) {
129            return false;
130        }
131        templatePath = templatePath.trim();
132        return templatePath.startsWith(DYNAMIC_TEMPLATE_LEGACY_PREFIX)
133            || templatePath.startsWith(DYNAMIC_TEMPLATE_PREFIX);
134    }
135
136    /**
137     * Removes the prefix which marks a property value as a dynamic template provider.<p>
138     *
139     * @param propertyValue the value from which to remove the prefix
140     *
141     * @return the string with the prefix removed
142     */
143    public static String removePropertyPrefix(String propertyValue) {
144
145        if (propertyValue == null) {
146            return null;
147        }
148        if (propertyValue.startsWith(DYNAMIC_TEMPLATE_PREFIX)) {
149            return propertyValue.substring(DYNAMIC_TEMPLATE_PREFIX.length());
150        }
151        if (propertyValue.startsWith(DYNAMIC_TEMPLATE_LEGACY_PREFIX)) {
152            return propertyValue.substring(DYNAMIC_TEMPLATE_LEGACY_PREFIX.length());
153        }
154        return propertyValue;
155    }
156
157    /**
158     * Creates a bean with information about the current template context, for use in the client-side code.<p>
159     *
160     * @param cms the current CMS context
161     * @param request the current request
162     *
163     * @return the bean with the template context information
164     */
165    public CmsTemplateContextInfo getContextInfoBean(CmsObject cms, HttpServletRequest request) {
166
167        CmsTemplateContextInfo result = new CmsTemplateContextInfo();
168        CmsTemplateContext context = (CmsTemplateContext)request.getAttribute(ATTR_TEMPLATE_CONTEXT);
169        if (context != null) {
170            result.setCurrentContext(context.getKey());
171
172            I_CmsTemplateContextProvider provider = context.getProvider();
173            Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
174            CmsXmlContentProperty settingDefinition = createTemplateContextsPropertyDefinition(provider, locale);
175            result.setSettingDefinition(settingDefinition);
176            String cookieName = context.getProvider().getOverrideCookieName();
177            if (cookieName != null) {
178                String cookieValue = CmsRequestUtil.getCookieValue(request.getCookies(), cookieName);
179                result.setSelectedContext(cookieValue);
180            }
181            result.setCookieName(cookieName);
182            Map<String, String> niceNames = new LinkedHashMap<String, String>();
183            for (Map.Entry<String, CmsTemplateContext> entry : provider.getAllContexts().entrySet()) {
184                CmsTemplateContext otherContext = entry.getValue();
185                String niceName = otherContext.getLocalizedName(locale);
186                niceNames.put(otherContext.getKey(), niceName);
187                for (CmsClientVariant variant : otherContext.getClientVariants().values()) {
188                    CmsClientVariantInfo info = new CmsClientVariantInfo(
189                        variant.getName(),
190                        variant.getNiceName(locale),
191                        variant.getScreenWidth(),
192                        variant.getScreenHeight(),
193                        variant.getParameters());
194                    result.setClientVariant(otherContext.getKey(), variant.getName(), info);
195                }
196            }
197            result.setContextLabels(niceNames);
198            result.setContextProvider(provider.getClass().getName());
199        }
200        Map<String, CmsDefaultSet<String>> allowedContextMap = safeGetAllowedContextMap();
201        result.setAllowedContexts(allowedContextMap);
202        return result;
203    }
204
205    /**
206     * Gets the template context to use.<p>
207     *
208     * @param providerName the name of the template context provider
209     * @param cms the current CMS context
210     * @param request the current request
211     * @param resource the current resource
212     *
213     * @return the current template context
214     */
215    public CmsTemplateContext getTemplateContext(
216        String providerName,
217        CmsObject cms,
218        HttpServletRequest request,
219        CmsResource resource) {
220
221        I_CmsTemplateContextProvider provider = getTemplateContextProvider(providerName);
222        if (provider == null) {
223            return null;
224        }
225        String cookieName = provider.getOverrideCookieName();
226        String forcedValue = null;
227        if (request != null) {
228            String paramTemplateContext = request.getParameter(CmsGwtConstants.PARAM_TEMPLATE_CONTEXT);
229            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(paramTemplateContext)) {
230                forcedValue = paramTemplateContext;
231            } else if (cookieName != null) {
232                forcedValue = CmsRequestUtil.getCookieValue(request.getCookies(), cookieName);
233            }
234        }
235        if (forcedValue != null) {
236            Map<String, CmsTemplateContext> contextMap = provider.getAllContexts();
237            if (contextMap.containsKey(forcedValue)) {
238                CmsTemplateContext contextBean = contextMap.get(forcedValue);
239                return new CmsTemplateContext(
240                    contextBean.getKey(),
241                    contextBean.getTemplatePath(),
242                    contextBean.getMessageContainer(),
243                    contextBean.getProvider(),
244                    contextBean.getClientVariants().values(),
245                    true);
246
247            }
248        }
249        return provider.getTemplateContext(cms, request, resource);
250    }
251
252    /**
253     * Gets the template context provider for a given path.<p>
254     *
255     * @param cms the current CMS context
256     * @param path the path for which the template context provider should be determined
257     *
258     * @return the template context provider for the given path
259     *
260     * @throws CmsException if something goes wrong
261     */
262    public I_CmsTemplateContextProvider getTemplateContextProvider(CmsObject cms, String path) throws CmsException {
263
264        CmsResource resource = cms.readResource(path);
265        I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(resource);
266        if (loader instanceof A_CmsXmlDocumentLoader) {
267            String propertyName = ((A_CmsXmlDocumentLoader)loader).getTemplatePropertyDefinition();
268            List<CmsProperty> properties = cms.readPropertyObjects(resource, true);
269            CmsProperty property = CmsProperty.get(propertyName, properties);
270            if ((property != null) && !property.isNullProperty()) {
271                String propertyValue = property.getValue();
272                if (CmsTemplateContextManager.hasPropertyPrefix(propertyValue)) {
273                    return getTemplateContextProvider(removePropertyPrefix(propertyValue));
274                }
275            }
276            return null;
277        } else {
278            return null;
279        }
280    }
281
282    /**
283     * Retrieves an instance of a template context provider given its name (optionally prefixed by the 'dynamic:' prefix).<p>
284     *
285     * @param providerName the name of the provider
286     *
287     * @return an instance of the provider class
288     */
289    public I_CmsTemplateContextProvider getTemplateContextProvider(String providerName) {
290
291        providerName = providerName.trim();
292        providerName = removePropertyPrefix(providerName);
293        String providerClassName = providerName;
294        String providerConfig = "";
295
296        // get provider configuration string if available
297        int separatorIndex = providerName.indexOf(",");
298        if (separatorIndex > 0) {
299            providerClassName = providerName.substring(0, separatorIndex);
300            providerConfig = providerName.substring(separatorIndex + 1);
301        }
302
303        I_CmsTemplateContextProvider result = m_providerInstances.get(providerName);
304        if (result == null) {
305            try {
306                Class<?> providerClass = Class.forName(providerClassName);
307                if (I_CmsTemplateContextProvider.class.isAssignableFrom(providerClass)) {
308                    result = (I_CmsTemplateContextProvider)providerClass.newInstance();
309                    result.initialize(m_cms, providerConfig);
310                    //note: we use the provider name as a key here, which includes configuration parameters
311                    m_providerInstances.put(providerName, result);
312                }
313            } catch (Throwable t) {
314                LOG.error(t.getLocalizedMessage(), t);
315            }
316        }
317        return result;
318    }
319
320    /**
321     * Utility method which either reads a property from the template used for a specific resource, or from the template context provider used for the resource if available.<p>
322     *
323     * @param cms the CMS context to use
324     * @param res the resource from whose template or template context provider the property should be read
325     * @param propertyName the property name
326     * @param fallbackValue the fallback value
327     *
328     * @return the property value
329     */
330    public String readPropertyFromTemplate(CmsObject cms, CmsResource res, String propertyName, String fallbackValue) {
331
332        try {
333            CmsProperty templateProp = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_TEMPLATE, true);
334            String templatePath = templateProp.getValue().trim();
335            if (hasPropertyPrefix(templatePath)) {
336                I_CmsTemplateContextProvider provider = getTemplateContextProvider(templatePath);
337                return provider.readCommonProperty(cms, propertyName, fallbackValue);
338            } else {
339                return cms.readPropertyObject(templatePath, propertyName, false).getValue(fallbackValue);
340            }
341        } catch (Exception e) {
342            LOG.error(e);
343            return fallbackValue;
344        }
345    }
346
347    /**
348     * Helper method to check whether a given type should not be shown in a context.<p>
349     *
350     * @param contextKey the key of the template context
351     * @param typeName the type name
352     *
353     * @return true if the context does not prohibit showing the type
354     */
355    public boolean shouldShowType(String contextKey, String typeName) {
356
357        Map<String, CmsDefaultSet<String>> allowedContextMap = safeGetAllowedContextMap();
358        CmsDefaultSet<String> allowedContexts = allowedContextMap.get(typeName);
359        if (allowedContexts == null) {
360            return true;
361        }
362        return allowedContexts.contains(contextKey);
363    }
364
365    /**
366     * Creates the setting definition for the templateContexts setting.<p>
367     *
368     * @param contextProvider the context provider
369     * @param locale the current locale
370     *
371     * @return the setting definition
372     */
373    protected CmsXmlContentProperty createTemplateContextsPropertyDefinition(
374        I_CmsTemplateContextProvider contextProvider,
375        Locale locale) {
376
377        if (contextProvider == null) {
378            return null;
379        }
380        List<String> contextOptions = new ArrayList<String>();
381        for (CmsTemplateContext context : contextProvider.getAllContexts().values()) {
382            contextOptions.add(context.getKey() + ":" + context.getLocalizedName(locale));
383        }
384        String widgetConfig = CmsStringUtil.listAsString(contextOptions, "|");
385
386        String niceName = Messages.get().getBundle(locale).key(Messages.GUI_SETTING_TEMPLATE_CONTEXTS_NAME_0);
387        String description = Messages.get().getBundle(locale).key(Messages.GUI_SETTING_TEMPLATE_CONTEXTS_DESCRIPTION_0);
388        CmsXmlContentProperty propDef = new CmsXmlContentProperty(
389            CmsTemplateContextInfo.SETTING,
390            "string",
391            "multicheck",
392            widgetConfig,
393            null,
394            null,
395            "",
396            niceName,
397            description,
398            "",
399            "false");
400        return propDef;
401    }
402
403    /**
404     * Helper method for getting the forbidden contexts from the resource manager without a try-catch block.<p>
405     *
406     * @return the forbidden context map
407     */
408    protected Map<String, CmsDefaultSet<String>> safeGetAllowedContextMap() {
409
410        try {
411            return OpenCms.getResourceManager().getAllowedContextMap(m_cms);
412        } catch (Exception e) {
413            LOG.error(e.getLocalizedMessage(), e);
414            return Collections.emptyMap();
415        }
416    }
417}