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