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}