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;
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    /** A bean containing information about the selected template. */
066    public static final String ATTR_TEMPLATE_BEAN = "ATTR_TEMPLATE_BEAN";
067
068    /** The request attribute in which the template context is stored. */
069    public static final String ATTR_TEMPLATE_CONTEXT = "templateContext";
070
071    /** Attribute name which contains the template name for non-dynamically selected templates. */
072    public static final String ATTR_TEMPLATE_NAME = "cmsTemplateName";
073
074    /** Attribute name for the template resource. */
075    public static final String ATTR_TEMPLATE_RESOURCE = "cmsTemplateResource";
076
077    /** The prefix used in the template property to activate dynamic template selection. */
078    public static final String DYNAMIC_TEMPLATE_PREFIX = "provider=";
079
080    /** Legacy prefix for property providers. */
081    private static final String DYNAMIC_TEMPLATE_LEGACY_PREFIX = "dynamic:";
082
083    /** The logger instance for this class. */
084    private static final Log LOG = CmsLog.getLog(CmsTemplateContextManager.class);
085
086    /** Request attribute used to set the template context during RPC calls. */
087    public static final String ATTR_RPC_CONTEXT_OVERRIDE = "ATTR_RPC_CONTEXT_OVERRIDE";
088
089    /** The CMS context. */
090    private CmsObject m_cms;
091
092    /** A cache in which the template context provider instances are stored, with their class name as the key. */
093    private Map<String, I_CmsTemplateContextProvider> m_providerInstances = new HashMap<String, I_CmsTemplateContextProvider>();
094
095    /** Cached allowed context map. */
096    private volatile Map<String, CmsDefaultSet<String>> m_cachedContextMap = null;
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            CmsXmlContentProperty settingDefinition = createTemplateContextsPropertyDefinition(provider, locale);
180            result.setSettingDefinition(settingDefinition);
181            String cookieName = context.getProvider().getOverrideCookieName();
182            if (cookieName != null) {
183                String cookieValue = CmsRequestUtil.getCookieValue(request.getCookies(), cookieName);
184                result.setSelectedContext(cookieValue);
185            }
186            result.setCookieName(cookieName);
187            Map<String, String> niceNames = new LinkedHashMap<String, String>();
188            for (Map.Entry<String, CmsTemplateContext> entry : provider.getAllContexts().entrySet()) {
189                CmsTemplateContext otherContext = entry.getValue();
190                String niceName = otherContext.getLocalizedName(locale);
191                niceNames.put(otherContext.getKey(), niceName);
192                for (CmsClientVariant variant : otherContext.getClientVariants().values()) {
193                    CmsClientVariantInfo info = new CmsClientVariantInfo(
194                        variant.getName(),
195                        variant.getNiceName(locale),
196                        variant.getScreenWidth(),
197                        variant.getScreenHeight(),
198                        variant.getParameters());
199                    result.setClientVariant(otherContext.getKey(), variant.getName(), info);
200                }
201            }
202            result.setContextLabels(niceNames);
203            result.setContextProvider(provider.getClass().getName());
204        }
205        Map<String, CmsDefaultSet<String>> allowedContextMap = safeGetAllowedContextMap();
206        result.setAllowedContexts(allowedContextMap);
207        return result;
208    }
209
210    /**
211     * Gets the template context to use.<p>
212     *
213     * @param providerName the name of the template context provider
214     * @param cms the current CMS context
215     * @param request the current request
216     * @param resource the current resource
217     *
218     * @return the current template context
219     */
220    public CmsTemplateContext getTemplateContext(
221        String providerName,
222        CmsObject cms,
223        HttpServletRequest request,
224        CmsResource resource) {
225
226        I_CmsTemplateContextProvider provider = getTemplateContextProvider(providerName);
227        if (provider == null) {
228            return null;
229        }
230        String cookieName = provider.getOverrideCookieName();
231        String forcedValue = null;
232        if (request != null) {
233            String paramTemplateContext = request.getParameter(CmsGwtConstants.PARAM_TEMPLATE_CONTEXT);
234            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(paramTemplateContext)) {
235                forcedValue = paramTemplateContext;
236            } else if (cookieName != null) {
237                forcedValue = CmsRequestUtil.getCookieValue(request.getCookies(), cookieName);
238            }
239        }
240        if (forcedValue != null) {
241            Map<String, CmsTemplateContext> contextMap = provider.getAllContexts();
242            if (contextMap.containsKey(forcedValue)) {
243                CmsTemplateContext contextBean = contextMap.get(forcedValue);
244                return new CmsTemplateContext(
245                    contextBean.getKey(),
246                    contextBean.getTemplatePath(),
247                    contextBean.getMessageContainer(),
248                    contextBean.getProvider(),
249                    contextBean.getClientVariants().values(),
250                    true);
251
252            }
253        }
254        return provider.getTemplateContext(cms, request, resource);
255    }
256
257    /**
258     * Gets the template context provider for a given path.<p>
259     *
260     * @param cms the current CMS context
261     * @param path the path for which the template context provider should be determined
262     *
263     * @return the template context provider for the given path
264     *
265     * @throws CmsException if something goes wrong
266     */
267    public I_CmsTemplateContextProvider getTemplateContextProvider(CmsObject cms, String path) throws CmsException {
268
269        CmsResource resource = cms.readResource(path);
270        I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(resource);
271        if (loader instanceof A_CmsXmlDocumentLoader) {
272            String propertyName = ((A_CmsXmlDocumentLoader)loader).getTemplatePropertyDefinition();
273            List<CmsProperty> properties = cms.readPropertyObjects(resource, true);
274            CmsProperty property = CmsProperty.get(propertyName, properties);
275            if ((property != null) && !property.isNullProperty()) {
276                String propertyValue = property.getValue();
277                if (CmsTemplateContextManager.hasPropertyPrefix(propertyValue)) {
278                    return getTemplateContextProvider(removePropertyPrefix(propertyValue));
279                }
280            }
281            return null;
282        } else {
283            return null;
284        }
285    }
286
287    /**
288     * Retrieves an instance of a template context provider given its name (optionally prefixed by the 'dynamic:' prefix).<p>
289     *
290     * @param providerName the name of the provider
291     *
292     * @return an instance of the provider class
293     */
294    public I_CmsTemplateContextProvider getTemplateContextProvider(String providerName) {
295
296        providerName = providerName.trim();
297        providerName = removePropertyPrefix(providerName);
298        String providerClassName = providerName;
299        String providerConfig = "";
300
301        // get provider configuration string if available
302        int separatorIndex = providerName.indexOf(",");
303        if (separatorIndex > 0) {
304            providerClassName = providerName.substring(0, separatorIndex);
305            providerConfig = providerName.substring(separatorIndex + 1);
306        }
307
308        I_CmsTemplateContextProvider result = m_providerInstances.get(providerName);
309        if (result == null) {
310            try {
311                Class<?> providerClass = Class.forName(providerClassName);
312                if (I_CmsTemplateContextProvider.class.isAssignableFrom(providerClass)) {
313                    result = (I_CmsTemplateContextProvider)providerClass.newInstance();
314                    result.initialize(m_cms, providerConfig);
315                    //note: we use the provider name as a key here, which includes configuration parameters
316                    m_providerInstances.put(providerName, result);
317                }
318            } catch (Throwable t) {
319                LOG.error(t.getLocalizedMessage(), t);
320            }
321        }
322        return result;
323    }
324
325    /**
326     * 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>
327     *
328     * @param cms the CMS context to use
329     * @param res the resource from whose template or template context provider the property should be read
330     * @param propertyName the property name
331     * @param fallbackValue the fallback value
332     *
333     * @return the property value
334     */
335    public String readPropertyFromTemplate(CmsObject cms, CmsResource res, String propertyName, String fallbackValue) {
336
337        try {
338            CmsProperty templateProp = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_TEMPLATE, true);
339            String templatePath = templateProp.getValue().trim();
340            if (hasPropertyPrefix(templatePath)) {
341                I_CmsTemplateContextProvider provider = getTemplateContextProvider(templatePath);
342                return provider.readCommonProperty(cms, propertyName, fallbackValue);
343            } else {
344                return cms.readPropertyObject(templatePath, propertyName, false).getValue(fallbackValue);
345            }
346        } catch (Exception e) {
347            LOG.error(e);
348            return fallbackValue;
349        }
350    }
351
352    /**
353     * Helper method to check whether a given type should not be shown in a context.<p>
354     *
355     * @param contextKey the key of the template context
356     * @param typeName the type name
357     *
358     * @return true if the context does not prohibit showing the type
359     */
360    public boolean shouldShowType(String contextKey, String typeName) {
361
362        Map<String, CmsDefaultSet<String>> allowedContextMap = safeGetAllowedContextMap();
363        CmsDefaultSet<String> allowedContexts = allowedContextMap.get(typeName);
364        if (allowedContexts == null) {
365            return true;
366        }
367        return allowedContexts.contains(contextKey);
368    }
369
370    /**
371     * Creates the setting definition for the templateContexts setting.<p>
372     *
373     * @param contextProvider the context provider
374     * @param locale the current locale
375     *
376     * @return the setting definition
377     */
378    protected CmsXmlContentProperty createTemplateContextsPropertyDefinition(
379        I_CmsTemplateContextProvider contextProvider,
380        Locale locale) {
381
382        if (contextProvider == null) {
383            return null;
384        }
385        List<String> contextOptions = new ArrayList<String>();
386        for (CmsTemplateContext context : contextProvider.getAllContexts().values()) {
387            contextOptions.add(context.getKey() + ":" + context.getLocalizedName(locale));
388        }
389        String widgetConfig = CmsStringUtil.listAsString(contextOptions, "|");
390
391        String niceName = Messages.get().getBundle(locale).key(Messages.GUI_SETTING_TEMPLATE_CONTEXTS_NAME_0);
392        String description = Messages.get().getBundle(locale).key(Messages.GUI_SETTING_TEMPLATE_CONTEXTS_DESCRIPTION_0);
393        CmsXmlContentProperty propDef = new CmsXmlContentProperty(
394            CmsTemplateContextInfo.SETTING,
395            "string",
396            "multicheck",
397            widgetConfig,
398            null,
399            null,
400            "",
401            niceName,
402            description,
403            "",
404            "false");
405        return propDef;
406    }
407
408    /**
409     * Helper method for getting the forbidden contexts from the resource manager without a try-catch block.<p>
410     *
411     * @return the forbidden context map
412     */
413    protected Map<String, CmsDefaultSet<String>> safeGetAllowedContextMap() {
414
415        Map<String, CmsDefaultSet<String>> result = m_cachedContextMap;
416        if (result != null) {
417            return result;
418        }
419        try {
420            return OpenCms.getResourceManager().getAllowedContextMap(m_cms);
421        } catch (Exception e) {
422            LOG.error(e.getLocalizedMessage(), e);
423            return Collections.emptyMap();
424        }
425    }
426
427    /**
428     * Updates the cached context map.
429     */
430    void updateContextMap() {
431
432        try {
433            LOG.debug("Updating cached 'allowed template contexts' map.");
434            m_cachedContextMap = OpenCms.getResourceManager().getAllowedContextMap(m_cms);
435            LOG.debug("Finished updating cached 'allowed template contexts' map.");
436        } catch (Exception e) {
437            LOG.error(e.getLocalizedMessage(), e);
438        }
439    }
440}