001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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.search.galleries;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProperty;
032import org.opencms.file.CmsPropertyDefinition;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
036import org.opencms.i18n.CmsMultiMessages;
037import org.opencms.jsp.util.CmsJspContentAccessBean;
038import org.opencms.jsp.util.CmsObjectFunctionTransformer;
039import org.opencms.jsp.util.CmsStringTemplateRenderer;
040import org.opencms.main.CmsException;
041import org.opencms.main.CmsLog;
042import org.opencms.main.OpenCms;
043import org.opencms.relations.CmsRelation;
044import org.opencms.relations.CmsRelationFilter;
045import org.opencms.util.CmsCollectionsGenericWrapper;
046import org.opencms.util.CmsMacroResolver;
047import org.opencms.xml.A_CmsXmlDocument;
048import org.opencms.xml.types.I_CmsXmlContentValue;
049
050import java.util.Collection;
051import java.util.List;
052import java.util.Locale;
053import java.util.Map;
054import java.util.function.Function;
055import java.util.regex.Matcher;
056import java.util.regex.Pattern;
057
058import org.apache.commons.logging.Log;
059
060import com.google.common.collect.Lists;
061import com.google.common.collect.Maps;
062
063/**
064 * Macro resolver used to resolve macros for the gallery name mapping.<p>
065 *
066 * This supports the following special macros:
067 * <ul>
068 * <li>%(no_prefix:some more text): This will expand to "some more text" if, after expanding all other macros in the input string,
069 *     there is at least one character before the occurence of this macro, and to an empty string otherwise.
070 * <li>%(value:/Some/XPath): This will expand to the value under the given XPath in the XML content and locale with
071 *     which the macro resolver was initialized. If no value is found under the XPath, the macro will expand to an empty string.
072 * <li>%(page_nav): This will expand to the NavText property of the container page in which this element is referenced.
073 *                  If this element is referenced from multiple container pages with the same locale, this macro is expanded
074 *                  to an empty string.
075 *<li>%(page_title): Same as %(page_nav), but uses the Title property instead of NavText.
076 *</ul>
077 */
078public class CmsGalleryNameMacroResolver extends CmsMacroResolver {
079
080    /** The logger instance for the class. */
081    private static final Log LOG = CmsLog.getLog(CmsGalleryNameMacroResolver.class);
082
083    /** Macro prefix. */
084    public static final String PREFIX_VALUE = "value:";
085
086    /** Macro name. */
087    public static final String PAGE_TITLE = "page_title";
088
089    /** Macro name. */
090    public static final String PAGE_NAV = "page_nav";
091
092    /** Macro prefix. */
093    public static final String NO_PREFIX = "no_prefix";
094
095    /** Pattern used to match the no_prefix macro. */
096    public static final Pattern NO_PREFIX_PATTERN = Pattern.compile("%\\(" + NO_PREFIX + ":(.*?)\\)");
097
098    /** Prefix for the stringtemplate macro. */
099    public static final String PREFIX_STRINGTEMPLATE = "stringtemplate:";
100
101    /** The XML content to use for the gallery name mapping. */
102    private A_CmsXmlDocument m_content;
103
104    /** The locale in the XML content. */
105    private Locale m_contentLocale;
106
107    /** The default string template source. */
108    private final Function<String, String> m_defaultStringTemplateSource = s -> {
109        return m_content.getHandler().getParameter(s);
110    };
111
112    /** The current string template source. */
113    private Function<String, String> m_stringTemplateSource = m_defaultStringTemplateSource;
114
115    /**
116     * Creates a new instance.<p>
117     *
118     * @param cms the CMS context to use for VFS operations
119     * @param content the content to use for macro value lookup
120     * @param locale the locale to use for macro value lookup
121     */
122    public CmsGalleryNameMacroResolver(CmsObject cms, A_CmsXmlDocument content, Locale locale) {
123
124        setCmsObject(cms);
125        CmsMultiMessages message = new CmsMultiMessages(locale);
126        message.addMessages(OpenCms.getWorkplaceManager().getMessages(locale));
127        message.addMessages(content.getContentDefinition().getContentHandler().getMessages(locale));
128        setMessages(message);
129        m_content = content;
130        m_contentLocale = locale;
131    }
132
133    /**
134     * @see org.opencms.util.CmsMacroResolver#getMacroValue(java.lang.String)
135     */
136    @Override
137    public String getMacroValue(String macro) {
138
139        if (macro.startsWith(PREFIX_VALUE)) {
140            String path = macro.substring(PREFIX_VALUE.length());
141            I_CmsXmlContentValue contentValue = m_content.getValue(path, m_contentLocale);
142            String value = null;
143            if (contentValue != null) {
144                value = contentValue.getStringValue(m_cms);
145            }
146            if (value == null) {
147                value = "";
148            }
149            return value;
150        } else if (macro.equals(PAGE_TITLE)) {
151            return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_TITLE);
152        } else if (macro.equals(PAGE_NAV)) {
153            return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_NAVTEXT);
154        } else if (macro.startsWith(PREFIX_STRINGTEMPLATE)) {
155            return resolveStringTemplate(macro.substring(PREFIX_STRINGTEMPLATE.length()));
156        } else if (macro.startsWith(NO_PREFIX)) {
157            return "%(" + macro + ")";
158            // this is just to prevent the %(no_prefix:...) macro from being expanded to an empty string. We could call setKeepEmptyMacros(true) instead,
159            // but that would also affect other macros.
160        } else {
161            return super.getMacroValue(macro);
162        }
163    }
164
165    /**
166     * @see org.opencms.util.CmsMacroResolver#resolveMacros(java.lang.String)
167     */
168    @Override
169    public String resolveMacros(String input) {
170
171        if (input == null) {
172            return null;
173        }
174        // We are overriding this method to implement the no_prefix macro. This is because
175        // we only know what the no_prefix macro should expand to after resolving all other
176        // macros (there could be an arbitrary number of macros before it which might potentially
177        // all expand to the empty string).
178        String result = super.resolveMacros(input);
179        Matcher matcher = NO_PREFIX_PATTERN.matcher(result);
180        if (matcher.find()) {
181            StringBuffer resultBuffer = new StringBuffer();
182            matcher.appendReplacement(
183                resultBuffer,
184                matcher.start() == 0 ? "" : result.substring(matcher.start(1), matcher.end(1)));
185            matcher.appendTail(resultBuffer);
186            result = resultBuffer.toString();
187        }
188        return result;
189    }
190
191    public void setStringTemplateSource(Function<String, String> stringtemplateSource) {
192
193        if (stringtemplateSource == null) {
194            stringtemplateSource = m_defaultStringTemplateSource;
195        }
196        m_stringTemplateSource = stringtemplateSource;
197    }
198
199    /**
200     * Gets the given property of the container page referencing this content.<p>
201     *
202     * If more than one container page with the same locale reference this content, the empty string will be returned.
203     *
204     * @param propName the property name to look up
205     *
206     * @return the value of the named property on the container page, or an empty string
207     */
208    protected String getContainerPageProperty(String propName) {
209
210        try {
211            Collection<CmsRelation> relations = m_cms.readRelations(
212                CmsRelationFilter.relationsToStructureId(m_content.getFile().getStructureId()));
213            Map<Locale, String> pagePropsByLocale = Maps.newHashMap();
214            for (CmsRelation relation : relations) {
215                CmsResource source = relation.getSource(m_cms, CmsResourceFilter.IGNORE_EXPIRATION);
216                if (CmsResourceTypeXmlContainerPage.isContainerPage(source)) {
217                    List<CmsProperty> pagePropertiesList = m_cms.readPropertyObjects(source, true);
218                    Map<String, CmsProperty> pageProperties = CmsProperty.toObjectMap(pagePropertiesList);
219                    Locale pageLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, source);
220                    CmsProperty pagePropCandidate = pageProperties.get(propName);
221                    if (pagePropCandidate != null) {
222                        if (pagePropsByLocale.get(pageLocale) == null) {
223                            pagePropsByLocale.put(pageLocale, pagePropCandidate.getValue());
224                        } else {
225                            return ""; // more than one container page per locale is referencing this content.
226                        }
227                    }
228                }
229            }
230            Locale matchingLocale = OpenCms.getLocaleManager().getBestMatchingLocale(
231                m_contentLocale,
232                OpenCms.getLocaleManager().getDefaultLocales(),
233                Lists.newArrayList(pagePropsByLocale.keySet()));
234            String result = pagePropsByLocale.get(matchingLocale);
235            if (result == null) {
236                result = "";
237            }
238            return result;
239        } catch (CmsException e) {
240            LOG.warn(e.getLocalizedMessage(), e);
241            return null;
242        }
243    }
244
245    /**
246     * Evaluates the contents of a %(stringtemplate:...) macro by evaluating them as StringTemplate code.<p>
247     *
248     * @param stMacro the contents of the macro after the stringtemplate: prefix
249     * @return the StringTemplate evaluation result
250     */
251    private String resolveStringTemplate(String stMacro) {
252
253        String template = m_stringTemplateSource.apply(stMacro.trim());
254        if (template == null) {
255            return "";
256        }
257        CmsJspContentAccessBean jspContentAccess = new CmsJspContentAccessBean(m_cms, m_contentLocale, m_content);
258        Map<String, Object> params = Maps.newHashMap();
259        params.put(
260            CmsStringTemplateRenderer.KEY_FUNCTIONS,
261            CmsCollectionsGenericWrapper.createLazyMap(new CmsObjectFunctionTransformer(m_cms)));
262
263        // We don't necessarily need the page title / navigation, so instead of passing the computed values to the template, we pass objects whose
264        // toString methods compute the values
265        params.put(PAGE_TITLE, new Object() {
266
267            @Override
268            public String toString() {
269
270                return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_TITLE);
271            }
272        });
273
274        params.put(PAGE_NAV, new Object() {
275
276            @Override
277            public String toString() {
278
279                return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_NAVTEXT);
280
281            }
282        });
283        String result = CmsStringTemplateRenderer.renderTemplate(m_cms, template, jspContentAccess, params);
284        return result;
285    }
286}