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}