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.Collections;
052import java.util.HashSet;
053import java.util.List;
054import java.util.Locale;
055import java.util.Map;
056import java.util.function.Function;
057import java.util.regex.Matcher;
058import java.util.regex.Pattern;
059
060import org.apache.commons.logging.Log;
061
062import com.google.common.collect.Lists;
063import com.google.common.collect.Maps;
064
065/**
066 * Macro resolver used to resolve macros for the gallery name mapping.<p>
067 *
068 * This supports the following special macros:
069 * <ul>
070 * <li>%(no_prefix:some more text): This will expand to "some more text" if, after expanding all other macros in the input string,
071 *     there is at least one character before the occurence of this macro, and to an empty string otherwise.
072 * <li>%(value:/Some/XPath): This will expand to the value under the given XPath in the XML content and locale with
073 *     which the macro resolver was initialized. If no value is found under the XPath, the macro will expand to an empty string.
074 * <li>%(page_nav): This will expand to the NavText property of the container page in which this element is referenced.
075 *                  If this element is referenced from multiple container pages with the same locale, this macro is expanded
076 *                  to an empty string.
077 *<li>%(page_title): Same as %(page_nav), but uses the Title property instead of NavText.
078 *</ul>
079 */
080public class CmsGalleryNameMacroResolver extends CmsMacroResolver {
081
082    /** Macro prefix. */
083    public static final String NO_PREFIX = "no_prefix";
084
085    /** Pattern used to match the no_prefix macro. */
086    public static final Pattern NO_PREFIX_PATTERN = Pattern.compile("%\\(" + NO_PREFIX + ":(.*?)\\)");
087
088    /** Macro name. */
089    public static final String PAGE_NAV = "page_nav";
090
091    /** Macro name. */
092    public static final String PAGE_ROOTPATH = "page_rootpath";
093
094    /** Macro name. */
095    public static final String PAGE_SITEPATH = "page_sitepath";
096
097    /** Macro name. */
098    public static final String PAGE_TITLE = "page_title";
099
100    /** Prefix for the isDetailPage? macro. */
101    public static final String PREFIX_ISDETAILPAGE = "isDetailPage?";
102
103    /** Prefix for the stringtemplate macro. */
104    public static final String PREFIX_STRINGTEMPLATE = "stringtemplate:";
105
106    /** Macro prefix. */
107    public static final String PREFIX_VALUE = "value:";
108
109    /** The logger instance for the class. */
110    private static final Log LOG = CmsLog.getLog(CmsGalleryNameMacroResolver.class);
111
112    /** The XML content to use for the gallery name mapping. */
113    private A_CmsXmlDocument m_content;
114
115    /** The locale in the XML content. */
116    private Locale m_contentLocale;
117
118    /** The default string template source. */
119    private final Function<String, String> m_defaultStringTemplateSource = s -> {
120        return m_content.getHandler().getParameter(s);
121    };
122
123    /** Collection of pages the content is placed on. */
124    private Collection<CmsResource> m_pages;
125
126    /** The current string template source. */
127    private Function<String, String> m_stringTemplateSource = m_defaultStringTemplateSource;
128
129    /** Cached detail page status. */
130    private Boolean m_isOnDetailPage;
131
132    /**
133     * Creates a new instance.<p>
134     *
135     * @param cms the CMS context to use for VFS operations
136     * @param content the content to use for macro value lookup
137     * @param locale the locale to use for macro value lookup
138     */
139    public CmsGalleryNameMacroResolver(CmsObject cms, A_CmsXmlDocument content, Locale locale) {
140
141        setCmsObject(cms);
142        CmsMultiMessages message = new CmsMultiMessages(locale);
143        message.addMessages(OpenCms.getWorkplaceManager().getMessages(locale));
144        message.addMessages(content.getContentDefinition().getContentHandler().getMessages(locale));
145        setMessages(message);
146        m_content = content;
147        m_contentLocale = locale;
148    }
149
150    /**
151     * @see org.opencms.util.CmsMacroResolver#getMacroValue(java.lang.String)
152     */
153    @Override
154    public String getMacroValue(String macro) {
155
156        if (macro.startsWith(PREFIX_VALUE)) {
157            String path = macro.substring(PREFIX_VALUE.length());
158            I_CmsXmlContentValue contentValue = m_content.getValue(path, m_contentLocale);
159            String value = null;
160            if (contentValue != null) {
161                value = contentValue.getStringValue(m_cms);
162            }
163            if (value == null) {
164                value = "";
165            }
166            return value;
167        } else if (macro.equals(PAGE_TITLE)) {
168            return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_TITLE);
169        } else if (macro.equals(PAGE_NAV)) {
170            return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_NAVTEXT);
171        } else if (macro.equals(PAGE_ROOTPATH)) {
172            return getContainerPagePath(false);
173        } else if (macro.equals(PAGE_SITEPATH)) {
174            return getContainerPagePath(true);
175        } else if (macro.startsWith(PREFIX_STRINGTEMPLATE)) {
176            return resolveStringTemplate(macro.substring(PREFIX_STRINGTEMPLATE.length()));
177        } else if (macro.startsWith(PREFIX_ISDETAILPAGE)) {
178            return resolveIsDetailPage(macro.substring(PREFIX_ISDETAILPAGE.length()));
179        } else if (macro.startsWith(NO_PREFIX)) {
180            return "%(" + macro + ")";
181            // this is just to prevent the %(no_prefix:...) macro from being expanded to an empty string. We could call setKeepEmptyMacros(true) instead,
182            // but that would also affect other macros.
183        } else {
184            return super.getMacroValue(macro);
185        }
186    }
187
188    /**
189     * @see org.opencms.util.CmsMacroResolver#resolveMacros(java.lang.String)
190     */
191    @Override
192    public String resolveMacros(String input) {
193
194        if (input == null) {
195            return null;
196        }
197        // We are overriding this method to implement the no_prefix macro. This is because
198        // we only know what the no_prefix macro should expand to after resolving all other
199        // macros (there could be an arbitrary number of macros before it which might potentially
200        // all expand to the empty string).
201        String result = super.resolveMacros(input);
202        Matcher matcher = NO_PREFIX_PATTERN.matcher(result);
203        if (matcher.find()) {
204            StringBuffer resultBuffer = new StringBuffer();
205            matcher.appendReplacement(
206                resultBuffer,
207                matcher.start() == 0 ? "" : result.substring(matcher.start(1), matcher.end(1)));
208            matcher.appendTail(resultBuffer);
209            result = resultBuffer.toString();
210        }
211        return result;
212    }
213
214    public void setStringTemplateSource(Function<String, String> stringtemplateSource) {
215
216        if (stringtemplateSource == null) {
217            stringtemplateSource = m_defaultStringTemplateSource;
218        }
219        m_stringTemplateSource = stringtemplateSource;
220    }
221
222    /**
223     * Returns the site path of the page the resource is on, iff it is on exactly one page per locale.
224     * @return the site path of the page the resource is on, iff it is on exactly one page per locale.
225     */
226    protected String getContainerPagePath(boolean isSitePath) {
227
228        Collection<CmsResource> pages = getContainerPages();
229        if (pages.isEmpty()) {
230            return null;
231        }
232        Map<Locale, String> pagePathByLocale = Maps.newHashMap();
233        for (CmsResource page : pages) {
234            Locale pageLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, page);
235            String pagePathCandidate = page.getRootPath();
236            if (isSitePath) {
237                pagePathCandidate = OpenCms.getSiteManager().getSiteForRootPath(pagePathCandidate).getSitePath(
238                    pagePathCandidate);
239            }
240            if (pagePathCandidate != null) {
241                if (pagePathByLocale.get(pageLocale) == null) {
242                    pagePathByLocale.put(pageLocale, pagePathCandidate);
243                } else {
244                    return ""; // more than one container page per locale is referencing this content.
245                }
246            }
247        }
248        Locale matchingLocale = OpenCms.getLocaleManager().getBestMatchingLocale(
249            m_contentLocale,
250            OpenCms.getLocaleManager().getDefaultLocales(),
251            Lists.newArrayList(pagePathByLocale.keySet()));
252        String result = pagePathByLocale.get(matchingLocale);
253        if (result == null) {
254            result = "";
255        }
256        return result;
257    }
258
259    /**
260     * Gets the given property of the container page referencing this content.<p>
261     *
262     * If more than one container page with the same locale reference this content, the empty string will be returned.
263     *
264     * @param propName the property name to look up
265     *
266     * @return the value of the named property on the container page, or an empty string
267     */
268    protected String getContainerPageProperty(String propName) {
269
270        Collection<CmsResource> pages = getContainerPages();
271        if (pages.isEmpty()) {
272            return "";
273        }
274        try {
275            Map<Locale, String> pagePropsByLocale = Maps.newHashMap();
276            for (CmsResource page : pages) {
277                List<CmsProperty> pagePropertiesList = m_cms.readPropertyObjects(page, true);
278                Map<String, CmsProperty> pageProperties = CmsProperty.toObjectMap(pagePropertiesList);
279                Locale pageLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, page);
280                CmsProperty pagePropCandidate = pageProperties.get(propName);
281                if (pagePropCandidate != null) {
282                    if (pagePropsByLocale.get(pageLocale) == null) {
283                        pagePropsByLocale.put(pageLocale, pagePropCandidate.getValue());
284                    } else {
285                        return ""; // more than one container page per locale is referencing this content.
286                    }
287                }
288            }
289            Locale matchingLocale = OpenCms.getLocaleManager().getBestMatchingLocale(
290                m_contentLocale,
291                OpenCms.getLocaleManager().getDefaultLocales(),
292                Lists.newArrayList(pagePropsByLocale.keySet()));
293            String result = pagePropsByLocale.get(matchingLocale);
294            if (result == null) {
295                result = "";
296            }
297            return result;
298        } catch (CmsException e) {
299            LOG.warn(e.getLocalizedMessage(), e);
300            return "";
301        }
302    }
303
304    /**
305     * Returns all container pages the content is placed on.
306     * @return all container pages the content is placed on.
307     */
308    Collection<CmsResource> getContainerPages() {
309
310        if (null != m_pages) {
311            return m_pages;
312        }
313        m_pages = new HashSet<CmsResource>();
314        try {
315            Collection<CmsRelation> relations = m_cms.readRelations(
316                CmsRelationFilter.relationsToStructureId(m_content.getFile().getStructureId()));
317            for (CmsRelation relation : relations) {
318                CmsResource source = relation.getSource(m_cms, CmsResourceFilter.IGNORE_EXPIRATION);
319                if (CmsResourceTypeXmlContainerPage.isContainerPage(source)) {
320                    m_pages.add(source);
321                }
322            }
323        } catch (CmsException e) {
324            LOG.warn(e.getLocalizedMessage(), e);
325            m_pages = Collections.emptySet();
326        }
327        return m_pages;
328    }
329
330    /**
331     * Checks if we are on a detail page (using the path from the CmsObject).
332     *
333     * @return true if we are on a detail page
334     */
335    private boolean isOnDetailPage() {
336
337        if (m_isOnDetailPage != null) {
338            return m_isOnDetailPage.booleanValue();
339        }
340        try {
341            String uri = m_cms.getRequestContext().getUri();
342            CmsResource page = m_cms.readResource(uri);
343            boolean result = OpenCms.getADEManager().isDetailPage(m_cms, page);
344            m_isOnDetailPage = Boolean.valueOf(result);
345            return result;
346        } catch (Exception e) {
347            LOG.error(e.getLocalizedMessage(), e);
348            return false;
349        }
350    }
351
352    /**
353     * Evaluates the content of the isDetailPage? macro.
354     *
355     * <p>
356     * The macro has the form %(isDetailPage?foo:bar), and in the case the current page (according to the URI of the CmsObject)
357     * is a detail page, the macro should evaluate to 'foo', and otherwise to 'bar'.
358     *
359     * @param content the macro content
360     * @return the macro value
361     */
362    private String resolveIsDetailPage(String content) {
363
364        int colonPos = content.indexOf(":");
365        if (colonPos != -1) {
366            String beforeColon = content.substring(0, colonPos);
367            String afterColon = content.substring(colonPos + 1);
368            return isOnDetailPage() ? beforeColon : afterColon;
369        } else {
370            LOG.error("Invalid body for isDetailPage? macro: " + content);
371            return content;
372        }
373    }
374
375    /**
376     * Evaluates the contents of a %(stringtemplate:...) macro by evaluating them as StringTemplate code.<p>
377     *
378     * @param stMacro the contents of the macro after the stringtemplate: prefix
379     * @return the StringTemplate evaluation result
380     */
381    private String resolveStringTemplate(String stMacro) {
382
383        String template = m_stringTemplateSource.apply(stMacro.trim());
384        if (template == null) {
385            return "";
386        }
387        CmsJspContentAccessBean jspContentAccess = new CmsJspContentAccessBean(m_cms, m_contentLocale, m_content);
388        Map<String, Object> params = Maps.newHashMap();
389        params.put(
390            CmsStringTemplateRenderer.KEY_FUNCTIONS,
391            CmsCollectionsGenericWrapper.createLazyMap(new CmsObjectFunctionTransformer(m_cms)));
392
393        // We don't necessarily need the page title / navigation, so instead of passing the computed values to the template, we pass objects whose
394        // toString methods compute the values
395        params.put(PAGE_TITLE, new Object() {
396
397            @Override
398            public String toString() {
399
400                return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_TITLE);
401            }
402        });
403
404        params.put("isDetailPage", Boolean.valueOf(isOnDetailPage()));
405
406        params.put(PAGE_NAV, new Object() {
407
408            @Override
409            public String toString() {
410
411                return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_NAVTEXT);
412
413            }
414        });
415        String result = CmsStringTemplateRenderer.renderTemplate(m_cms, template, jspContentAccess, params);
416        return result;
417    }
418}