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        if (null == locale) {
143            locale = OpenCms.getLocaleManager().getBestAvailableLocaleForXmlContent(cms, content.getFile(), content);
144        }
145        CmsMultiMessages message = new CmsMultiMessages(locale);
146        message.addMessages(OpenCms.getWorkplaceManager().getMessages(locale));
147        message.addMessages(content.getContentDefinition().getContentHandler().getMessages(locale));
148        setMessages(message);
149        m_content = content;
150        m_contentLocale = locale;
151    }
152
153    /**
154     * @see org.opencms.util.CmsMacroResolver#getMacroValue(java.lang.String)
155     */
156    @Override
157    public String getMacroValue(String macro) {
158
159        if (macro.startsWith(PREFIX_VALUE)) {
160            String path = macro.substring(PREFIX_VALUE.length());
161            I_CmsXmlContentValue contentValue = m_content.getValue(path, m_contentLocale);
162            String value = null;
163            if (contentValue != null) {
164                value = contentValue.getStringValue(m_cms);
165            }
166            if (value == null) {
167                value = "";
168            }
169            return value;
170        } else if (macro.equals(PAGE_TITLE)) {
171            return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_TITLE);
172        } else if (macro.equals(PAGE_NAV)) {
173            return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_NAVTEXT);
174        } else if (macro.equals(PAGE_ROOTPATH)) {
175            return getContainerPagePath(false);
176        } else if (macro.equals(PAGE_SITEPATH)) {
177            return getContainerPagePath(true);
178        } else if (macro.startsWith(PREFIX_STRINGTEMPLATE)) {
179            return resolveStringTemplate(macro.substring(PREFIX_STRINGTEMPLATE.length()));
180        } else if (macro.startsWith(PREFIX_ISDETAILPAGE)) {
181            return resolveIsDetailPage(macro.substring(PREFIX_ISDETAILPAGE.length()));
182        } else if (macro.startsWith(NO_PREFIX)) {
183            return "%(" + macro + ")";
184            // this is just to prevent the %(no_prefix:...) macro from being expanded to an empty string. We could call setKeepEmptyMacros(true) instead,
185            // but that would also affect other macros.
186        } else {
187            return super.getMacroValue(macro);
188        }
189    }
190
191    /**
192     * @see org.opencms.util.CmsMacroResolver#resolveMacros(java.lang.String)
193     */
194    @Override
195    public String resolveMacros(String input) {
196
197        if (input == null) {
198            return null;
199        }
200        // We are overriding this method to implement the no_prefix macro. This is because
201        // we only know what the no_prefix macro should expand to after resolving all other
202        // macros (there could be an arbitrary number of macros before it which might potentially
203        // all expand to the empty string).
204        String result = super.resolveMacros(input);
205        Matcher matcher = NO_PREFIX_PATTERN.matcher(result);
206        if (matcher.find()) {
207            StringBuffer resultBuffer = new StringBuffer();
208            matcher.appendReplacement(
209                resultBuffer,
210                matcher.start() == 0 ? "" : result.substring(matcher.start(1), matcher.end(1)));
211            matcher.appendTail(resultBuffer);
212            result = resultBuffer.toString();
213        }
214        return result;
215    }
216
217    public void setStringTemplateSource(Function<String, String> stringtemplateSource) {
218
219        if (stringtemplateSource == null) {
220            stringtemplateSource = m_defaultStringTemplateSource;
221        }
222        m_stringTemplateSource = stringtemplateSource;
223    }
224
225    /**
226     * Returns the site path of the page the resource is on, iff it is on exactly one page per locale.
227     * @return the site path of the page the resource is on, iff it is on exactly one page per locale.
228     */
229    protected String getContainerPagePath(boolean isSitePath) {
230
231        Collection<CmsResource> pages = getContainerPages();
232        if (pages.isEmpty()) {
233            return null;
234        }
235        Map<Locale, String> pagePathByLocale = Maps.newHashMap();
236        for (CmsResource page : pages) {
237            Locale pageLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, page);
238            String pagePathCandidate = page.getRootPath();
239            if (isSitePath) {
240                pagePathCandidate = OpenCms.getSiteManager().getSiteForRootPath(pagePathCandidate).getSitePath(
241                    pagePathCandidate);
242            }
243            if (pagePathCandidate != null) {
244                if (pagePathByLocale.get(pageLocale) == null) {
245                    pagePathByLocale.put(pageLocale, pagePathCandidate);
246                } else {
247                    return ""; // more than one container page per locale is referencing this content.
248                }
249            }
250        }
251        Locale matchingLocale = OpenCms.getLocaleManager().getBestMatchingLocale(
252            m_contentLocale,
253            OpenCms.getLocaleManager().getDefaultLocales(),
254            Lists.newArrayList(pagePathByLocale.keySet()));
255        String result = pagePathByLocale.get(matchingLocale);
256        if (result == null) {
257            result = "";
258        }
259        return result;
260    }
261
262    /**
263     * Gets the given property of the container page referencing this content.<p>
264     *
265     * If more than one container page with the same locale reference this content, the empty string will be returned.
266     *
267     * @param propName the property name to look up
268     *
269     * @return the value of the named property on the container page, or an empty string
270     */
271    protected String getContainerPageProperty(String propName) {
272
273        Collection<CmsResource> pages = getContainerPages();
274        if (pages.isEmpty()) {
275            return "";
276        }
277        try {
278            Map<Locale, String> pagePropsByLocale = Maps.newHashMap();
279            for (CmsResource page : pages) {
280                List<CmsProperty> pagePropertiesList = m_cms.readPropertyObjects(page, true);
281                Map<String, CmsProperty> pageProperties = CmsProperty.toObjectMap(pagePropertiesList);
282                Locale pageLocale = OpenCms.getLocaleManager().getDefaultLocale(m_cms, page);
283                CmsProperty pagePropCandidate = pageProperties.get(propName);
284                if (pagePropCandidate != null) {
285                    if (pagePropsByLocale.get(pageLocale) == null) {
286                        pagePropsByLocale.put(pageLocale, pagePropCandidate.getValue());
287                    } else {
288                        return ""; // more than one container page per locale is referencing this content.
289                    }
290                }
291            }
292            Locale matchingLocale = OpenCms.getLocaleManager().getBestMatchingLocale(
293                m_contentLocale,
294                OpenCms.getLocaleManager().getDefaultLocales(),
295                Lists.newArrayList(pagePropsByLocale.keySet()));
296            String result = pagePropsByLocale.get(matchingLocale);
297            if (result == null) {
298                result = "";
299            }
300            return result;
301        } catch (CmsException e) {
302            LOG.warn(e.getLocalizedMessage(), e);
303            return "";
304        }
305    }
306
307    /**
308     * Returns all container pages the content is placed on.
309     * @return all container pages the content is placed on.
310     */
311    Collection<CmsResource> getContainerPages() {
312
313        if (null != m_pages) {
314            return m_pages;
315        }
316        m_pages = new HashSet<CmsResource>();
317        try {
318            Collection<CmsRelation> relations = m_cms.readRelations(
319                CmsRelationFilter.relationsToStructureId(m_content.getFile().getStructureId()));
320            for (CmsRelation relation : relations) {
321                CmsResource source = relation.getSource(m_cms, CmsResourceFilter.IGNORE_EXPIRATION);
322                if (CmsResourceTypeXmlContainerPage.isContainerPage(source)) {
323                    m_pages.add(source);
324                }
325            }
326        } catch (CmsException e) {
327            LOG.warn(e.getLocalizedMessage(), e);
328            m_pages = Collections.emptySet();
329        }
330        return m_pages;
331    }
332
333    /**
334     * Checks if we are on a detail page (using the path from the CmsObject).
335     *
336     * @return true if we are on a detail page
337     */
338    private boolean isOnDetailPage() {
339
340        if (m_isOnDetailPage != null) {
341            return m_isOnDetailPage.booleanValue();
342        }
343        try {
344            String uri = m_cms.getRequestContext().getUri();
345            CmsResource page = m_cms.readResource(uri);
346            boolean result = OpenCms.getADEManager().isDetailPage(m_cms, page);
347            m_isOnDetailPage = Boolean.valueOf(result);
348            return result;
349        } catch (Exception e) {
350            LOG.error(e.getLocalizedMessage(), e);
351            return false;
352        }
353    }
354
355    /**
356     * Evaluates the content of the isDetailPage? macro.
357     *
358     * <p>
359     * The macro has the form %(isDetailPage?foo:bar), and in the case the current page (according to the URI of the CmsObject)
360     * is a detail page, the macro should evaluate to 'foo', and otherwise to 'bar'.
361     *
362     * @param content the macro content
363     * @return the macro value
364     */
365    private String resolveIsDetailPage(String content) {
366
367        int colonPos = content.indexOf(":");
368        if (colonPos != -1) {
369            String beforeColon = content.substring(0, colonPos);
370            String afterColon = content.substring(colonPos + 1);
371            return isOnDetailPage() ? beforeColon : afterColon;
372        } else {
373            LOG.error("Invalid body for isDetailPage? macro: " + content);
374            return content;
375        }
376    }
377
378    /**
379     * Evaluates the contents of a %(stringtemplate:...) macro by evaluating them as StringTemplate code.<p>
380     *
381     * @param stMacro the contents of the macro after the stringtemplate: prefix
382     * @return the StringTemplate evaluation result
383     */
384    private String resolveStringTemplate(String stMacro) {
385
386        String template = m_stringTemplateSource.apply(stMacro.trim());
387        if (template == null) {
388            return "";
389        }
390        CmsJspContentAccessBean jspContentAccess = new CmsJspContentAccessBean(m_cms, m_contentLocale, m_content);
391        Map<String, Object> params = Maps.newHashMap();
392        params.put(
393            CmsStringTemplateRenderer.KEY_FUNCTIONS,
394            CmsCollectionsGenericWrapper.createLazyMap(new CmsObjectFunctionTransformer(m_cms)));
395
396        // We don't necessarily need the page title / navigation, so instead of passing the computed values to the template, we pass objects whose
397        // toString methods compute the values
398        params.put(PAGE_TITLE, new Object() {
399
400            @Override
401            public String toString() {
402
403                return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_TITLE);
404            }
405        });
406
407        params.put("isDetailPage", Boolean.valueOf(isOnDetailPage()));
408
409        params.put(PAGE_NAV, new Object() {
410
411            @Override
412            public String toString() {
413
414                return getContainerPageProperty(CmsPropertyDefinition.PROPERTY_NAVTEXT);
415
416            }
417        });
418        String result = CmsStringTemplateRenderer.renderTemplate(m_cms, template, jspContentAccess, params);
419        return result;
420    }
421}