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