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